[
  {
    "path": ".ai-rulez/config.toml",
    "content": "# AI-Rulez Configuration (migrated to V4 TOML format)\n# Documentation: https://github.com/Goldziher/ai-rulez\n\nversion = '4.0'\nname = 'html-to-markdown'\ndescription = 'High-performance HTML to Markdown converter with Rust core and polyglot bindings (Python, TypeScript, Ruby, PHP, Go, Java, C#, Elixir, R, WebAssembly, C FFI).'\ngitignore = true\npresets = ['claude', 'copilot', 'cursor', 'antigravity', 'codex']\nbuiltins = ['rust', 'python', 'typescript', 'go', 'java', 'ruby', 'php', 'csharp', 'elixir', 'r', 'default-commands']\n\n[header]\nstyle = 'minimal'\n\n[[includes]]\nname = 'kreuzberg-core'\nsource = 'https://github.com/kreuzberg-dev/ai-rulez.git'\npath = 'modules/core'\nmerge_strategy = 'local-override'\n\n[[includes]]\nname = 'kreuzberg-languages'\nsource = 'https://github.com/kreuzberg-dev/ai-rulez.git'\npath = 'modules/languages'\nmerge_strategy = 'local-override'\n\n[[includes]]\nname = 'kreuzberg-cicd'\nsource = 'https://github.com/kreuzberg-dev/ai-rulez.git'\npath = 'modules/cicd'\nmerge_strategy = 'local-override'\n\n[[includes]]\nname = 'kreuzberg-infrastructure'\nsource = 'https://github.com/kreuzberg-dev/ai-rulez.git'\npath = 'modules/infrastructure'\nmerge_strategy = 'local-override'\n\n[[includes]]\nname = 'kreuzberg-e2e-generator'\nsource = 'https://github.com/kreuzberg-dev/ai-rulez.git'\npath = 'modules/e2e-generator'\nmerge_strategy = 'local-override'\n\n[[installed_skills]]\nname = 'alef'\nsource = 'https://github.com/kreuzberg-dev/alef.git'\n\n[[mcp_servers]]\nname = 'playwright'\ndescription = 'Playwright browser automation for E2E testing and docs verification'\ncommand = 'npx'\nargs = ['-y', '@playwright/mcp@latest']\n\n[defaults]\neffort = 'medium'\n"
  },
  {
    "path": ".ai-rulez/context/crate-structure.md",
    "content": "---\npriority: high\n---\n\n# Crate & Package Structure\n\n## Workspace crates (`crates/`)\n\n- `html-to-markdown` — core library, primary Rust API, `unsafe_code = \"forbid\"` at workspace level\n- `html-to-markdown-cli` — CLI binary (clap)\n- `html-to-markdown-ffi` — C FFI bridge, cbindgen headers, **only crate that overrides unsafe_code lint**\n- `html-to-markdown-py` — PyO3 Python binding\n- `html-to-markdown-node` — NAPI-RS Node/TypeScript binding\n- `html-to-markdown-php` — ext-php-rs PHP binding\n- `html-to-markdown-wasm` — wasm-bindgen WebAssembly binding\n\n## Out-of-workspace packages (`packages/`)\n\n- `csharp/`, `elixir/`, `go/`, `java/`, `r/`, `ruby/` — language-native packages wrapping the FFI crate\n- `php/`, `python/`, `typescript/`, `wasm/` — distribution packages\n\n## Primary API\n\n- `convert(&str, Option<ConversionOptions>) -> Result<ConversionResult, ConversionError>`\n- `ConversionResult`: `content`, `warnings`, optionally `metadata` and `inline_images` (feature-gated)\n- Feature flags: `inline-images`, `metadata`, `visitor` (custom traversal), `serde`\n- Dual parser: html5ever (spec-compliance) and astral-tl (performance), selectable via `ConversionOptions`\n"
  },
  {
    "path": ".ai-rulez/domains/conversion-algorithms/DOMAIN.md",
    "content": "# Conversion Algorithms Domain\n\n## Purpose\n\nCore HTML-to-Markdown transformation logic. Converts parsed DOM trees into well-formatted Markdown output for 60+ HTML element types.\n\n## Key Areas\n\n- **Block elements**: headings, paragraphs, blockquotes, lists, tables, code blocks, horizontal rules, semantic HTML5 elements\n- **Inline elements**: bold, italic, strikethrough, inline code, links, images, abbreviations\n- **Tables**: GFM pipe tables with alignment, colspan/rowspan handling, complex table fallbacks\n- **Lists**: ordered, unordered, nested, task lists, definition lists, tight vs loose detection\n- **Forms & media**: input fields, textareas, selects, audio, video, iframes, embeds\n- **Special elements**: line breaks, comments, SVG text extraction, ruby annotations\n\n## Architecture\n\nVisitor pattern in `visitor.rs` dispatches to per-element converter functions. Conversion behavior is controlled by `ConversionOptions` (heading style, list indent, code block style, newline style, table format).\n\n## Dependencies\n\n- Upstream: HTML Parsing domain (DOM tree), Safety-Sanitization domain (attribute validation)\n- Downstream: Output formatting, metadata extraction\n"
  },
  {
    "path": ".ai-rulez/domains/html-parsing/DOMAIN.md",
    "content": "# HTML Parsing Domain\n\n## Purpose\n\nFoundation of the conversion pipeline: HTML parser selection, DOM tree construction, and tree traversal infrastructure.\n\n## Key Areas\n\n- **Parser backends**: html5ever (HTML5 spec compliance, malformed HTML recovery) and tl/astral-tl (lightweight, fast)\n- **DOM traversal**: depth-first tree walking via visitor pattern, parent/child/sibling navigation\n- **Node types**: element nodes (60+ tags), text nodes, comment nodes, document/fragment nodes\n- **Text extraction**: text content from subtrees, configurable whitespace handling (preserve, minimal, collapse)\n- **Attribute access**: by name, iteration, class checking, case-insensitive per HTML spec\n- **Safety constraints**: depth limits, size limits, binary data rejection, encoding detection\n\n## Architecture\n\nParser infrastructure in `converter.rs` and `wrapper.rs`. DOM traversal via `DomWalker` trait in `visitor.rs`. Element classification into Block, Inline, Void, FormControl, Semantic categories. Configuration through `ConversionOptions` (parser type, encoding, whitespace mode, max depth, max size).\n\n## Dependencies\n\n- Upstream: html5ever, astral-tl, encoding_rs\n- Downstream: Conversion Algorithms domain, Safety-Sanitization domain\n"
  },
  {
    "path": ".ai-rulez/domains/safety-sanitization/DOMAIN.md",
    "content": "# Safety & Sanitization Domain\n\n## Purpose\n\nProtects the conversion pipeline from malicious or malformed input. Ensures converted Markdown output cannot be exploited for XSS, code injection, or data exfiltration.\n\n## Key Areas\n\n- **Input validation**: binary data detection (magic numbers, null byte ratios, control char ratios), encoding detection, size/depth limits\n- **XSS prevention**: dangerous element removal (script, style, iframe, object, embed), event handler stripping, javascript:/data:/vbscript: URL blocking\n- **URL sanitization**: scheme whitelist (http, https, mailto, ftp), protocol normalization, URL-encoded payload detection, case-insensitive scheme matching\n- **Attribute filtering**: event handler removal, safe attribute whitelist (id, class, title, alt, href, src), style sanitization\n- **SVG handling**: script/style removal within SVG, event handler stripping, xlink:href validation, text extraction fallback\n- **Runtime safety**: stack overflow prevention (max nesting depth), memory bounds enforcement, ReDoS prevention\n\n## Architecture\n\nMulti-layer defense: validate_input() -> sanitize -> parse -> convert with URL/attribute sanitization at each element. Configuration via `SafetyConfig` (max document size, max nesting depth, allowed tags/attributes/schemes, strip options).\n\n## Dependencies\n\n- Upstream: url, encoding_rs\n- Downstream: HTML Parsing domain (operates on validated input), Conversion Algorithms domain (safe elements only)\n"
  },
  {
    "path": ".ai-rulez/rules/alef-generated-bindings.md",
    "content": "---\npriority: critical\n---\n\n- Files in `packages/*/` and binding crates are generated or managed by Alef — check `alef.toml` before editing\n- `alef.toml` defines: output paths, module names, rename mappings, e2e call overrides, README templates\n- Run `alef generate` after changing `alef.toml` — commit both source and generated files\n- Never hand-edit generated files; modify `alef.toml` or the Rust source instead\n- Fixtures under `fixtures/` feed `tools/e2e-generator/` — never add tests to `e2e/` directly\n"
  },
  {
    "path": ".cargo/config.toml",
    "content": "\n[build]\nincremental = true\n\n[target.wasm32-unknown-unknown]\nrustflags = [\"-C\", \"target-feature=+bulk-memory\", \"--cfg\", \"getrandom_backend=\\\"wasm_js\\\"\"]\n\n[net]\ngit-fetch-with-cli = true\n\n[registries.crates-io]\nprotocol = \"sparse\"\n\n[target.'cfg(target_os = \"macos\")']\nrustflags = [\"-C\", \"link-arg=-Wl,-undefined,dynamic_lookup\"]\n\n[target.x86_64-pc-windows-msvc]\nlinker = \"rust-lld\"\n\n[target.i686-pc-windows-msvc]\nlinker = \"rust-lld\"\n\n[target.x86_64-unknown-linux-musl]\nlinker = \"musl-gcc\"\n\n[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\n\n[env]\nRUBY = { value = \"scripts/preferred-ruby.sh\", relative = true }\n"
  },
  {
    "path": ".clang-format",
    "content": "---\nBasedOnStyle: LLVM\nIndentWidth: 4\nColumnLimit: 100\nBreakBeforeBraces: Attach\nAllowShortFunctionsOnASingleLine: Empty\nAllowShortIfStatementsOnASingleLine: false\nSortIncludes: true\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# All files\n[*]\ncharset = utf-8\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\n\n# Code files\n[*.{cs,go,rs,py,js,ts,tsx,jsx,php,rb}]\nindent_style = space\n\n# C# files\n[*.cs]\nindent_size = 4\n\n# Organize usings\ndotnet_sort_system_directives_first = true\ndotnet_separate_import_directive_groups = false\n\n# this. and Me. preferences\ndotnet_style_qualification_for_field = false:warning\ndotnet_style_qualification_for_property = false:warning\ndotnet_style_qualification_for_method = false:warning\ndotnet_style_qualification_for_event = false:warning\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true:warning\ndotnet_style_predefined_type_for_member_access = true:warning\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = always:warning\ndotnet_style_readonly_field = true:warning\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion\n\n# Expression-level preferences\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_explicit_tuple_names = true:warning\ndotnet_style_prefer_inferred_tuple_names = true:suggestion\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\ndotnet_style_prefer_auto_properties = true:suggestion\ndotnet_style_prefer_conditional_expression_over_assignment = true:silent\ndotnet_style_prefer_conditional_expression_over_return = true:silent\ndotnet_style_prefer_compound_assignment = true:suggestion\ndotnet_style_prefer_simplified_interpolation = true:suggestion\ndotnet_style_prefer_simplified_boolean_expressions = true:suggestion\n\n# Null-checking preferences\ndotnet_style_coalesce_expression = true:warning\ndotnet_style_null_propagation = true:warning\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning\n\n# C# Code Style Rules\n# var preferences\ncsharp_style_var_for_built_in_types = true:suggestion\ncsharp_style_var_when_type_is_apparent = true:suggestion\ncsharp_style_var_elsewhere = true:suggestion\n\n# Expression-bodied members\ncsharp_style_expression_bodied_methods = when_on_single_line:suggestion\ncsharp_style_expression_bodied_constructors = false:silent\ncsharp_style_expression_bodied_operators = when_on_single_line:suggestion\ncsharp_style_expression_bodied_properties = when_on_single_line:suggestion\ncsharp_style_expression_bodied_indexers = when_on_single_line:suggestion\ncsharp_style_expression_bodied_accessors = when_on_single_line:suggestion\ncsharp_style_expression_bodied_lambdas = when_on_single_line:suggestion\ncsharp_style_expression_bodied_local_functions = when_on_single_line:suggestion\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_is_with_cast_check = true:warning\ncsharp_style_pattern_matching_over_as_with_null_check = true:warning\ncsharp_style_prefer_switch_expression = true:suggestion\ncsharp_style_prefer_pattern_matching = true:suggestion\ncsharp_style_prefer_not_pattern = true:suggestion\n\n# Null-checking preferences\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:warning\n\n# Code block preferences\ncsharp_prefer_braces = true:warning\ncsharp_prefer_simple_using_statement = true:suggestion\n\n# Expression preferences\ncsharp_prefer_simple_default_expression = true:suggestion\ncsharp_style_pattern_local_over_anonymous_function = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_deconstructed_variable_declaration = true:suggestion\ncsharp_style_prefer_index_operator = true:suggestion\ncsharp_style_prefer_range_operator = true:suggestion\ncsharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion\n\n# C# Formatting Rules\n# New line preferences\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation preferences\ncsharp_indent_case_contents = true\ncsharp_indent_switch_labels = true\ncsharp_indent_labels = no_change\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents_when_block = false\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_between_parentheses = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_square_brackets = false\n\n# Wrap preferences\ncsharp_preserve_single_line_statements = false\ncsharp_preserve_single_line_blocks = true\n\n# Using directive preferences\ncsharp_using_directive_placement = outside_namespace:warning\n\n# Go files\n[*.go]\nindent_style = tab\nindent_size = 4\n\n# Rust files\n[*.rs]\nindent_size = 4\n\n# Python files\n[*.py]\nindent_size = 4\n\n# JavaScript/TypeScript files\n[*.{js,ts,tsx,jsx}]\nindent_size = 2\n\n# Ruby files\n[*.rb]\nindent_size = 2\n\n# PHP files\n[*.php]\nindent_size = 4\n\n# YAML files\n[*.{yml,yaml}]\nindent_size = 2\n\n# Markdown files\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Default owner — everything\n* @Goldziher\n\n# Zensical config and documentation\n/zensical.toml @Goldziher @pratik-mahalle @v-tan\n/docs/ @Goldziher @pratik-mahalle @v-tan\n*.md @Goldziher @pratik-mahalle @v-tan\n\n# Rust crates\n/crates/ @Goldziher @kh3rld\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Report a bug or unexpected behavior\ntitle: \"bug: \"\nlabels: [\"bug\"]\nprojects: [\"kreuzberg-dev/1\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: What happened? What did you expect to happen?\n    validations:\n      required: true\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: Steps to reproduce\n      description: Minimal steps to reproduce the issue.\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction-files\n    attributes:\n      label: Relevant files and configuration\n      description: >-\n        Any configuration files, input files, or code snippets needed to\n        reproduce the issue.\n      render: text\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.yml",
    "content": "name: Documentation Issue\ndescription: Report missing, unclear, or incorrect documentation\ntitle: \"docs: \"\nlabels: [\"documentation\"]\nprojects: [\"kreuzberg-dev/1\"]\nbody:\n  - type: textarea\n    id: what\n    attributes:\n      label: What\n      description: What documentation is missing, unclear, or incorrect?\n    validations:\n      required: true\n  - type: textarea\n    id: why\n    attributes:\n      label: Why\n      description: Why does this need to change?\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest a new feature or improvement\ntitle: \"feat: \"\nlabels: [\"enhancement\"]\nprojects: [\"kreuzberg-dev/1\"]\nbody:\n  - type: textarea\n    id: what\n    attributes:\n      label: What is the proposed feature?\n    validations:\n      required: true\n  - type: textarea\n    id: why\n    attributes:\n      label: Why would this be a good addition?\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Related\n\n<!-- Link issues or discussions if applicable -->\n\n## Description\n\n<!-- What does this PR do? -->\n\n## Checklist\n\n- [ ] CI passing\n- [ ] Tests added where applicable\n"
  },
  {
    "path": ".github/actions/build-typescript/action.yml",
    "content": "name: Build TypeScript package\ndescription: Builds TypeScript package (requires Node bindings to be built first)\n\nruns:\n  using: composite\n  steps:\n    - name: Build TypeScript package\n      shell: bash\n      working-directory: packages/typescript\n      run: pnpm run build\n"
  },
  {
    "path": ".github/actions/smoke-pie/action.yml",
    "content": "name: Smoke test PIE install\ndescription: Tests PHP extension installation via PIE\n\ninputs:\n  pie-artifacts-dir:\n    description: Directory containing PIE source artifacts\n    required: true\n\nruns:\n  using: composite\n  steps:\n    - name: Smoke PIE install\n      shell: bash\n      env:\n        COMPOSER_ALLOW_SUPERUSER: 1\n      run: |\n        set -euo pipefail\n        # Download PIE\n        curl -fsSL https://github.com/php/pie/releases/latest/download/pie.phar -o /tmp/pie.phar\n\n        # Find the PIE source archive\n        pie_archive=$(find \"${{ inputs.pie-artifacts-dir }}\" -name \"php_html_to_markdown-*.tgz\" | head -n 1)\n        if [ -z \"$pie_archive\" ]; then\n          echo \"PIE source archive not found\" >&2\n          exit 1\n        fi\n\n        # Extract to temp dir and install via PIE\n        tmp=$(mktemp -d)\n        tar -xzf \"$pie_archive\" -C \"$tmp\"\n\n        # Add as local repository and build\n        php /tmp/pie.phar repository:add path \"$tmp\"\n        CARGO_BIN=$(command -v cargo)\n        php /tmp/pie.phar build kreuzberg-dev/html-to-markdown:*@dev --working-dir \"$tmp\" --with-cargo-bin=\"$CARGO_BIN\"\n\n        # Find the built extension\n        ext_so=$(find \"$tmp\" -name \"*.so\" -path \"*/html_to_markdown.so\" | head -n 1)\n        if [ -z \"$ext_so\" ]; then\n          echo \"Extension .so file not found after PIE build\" >&2\n          exit 1\n        fi\n\n        # Test the extension (placeholder for smoke test)\n        # Note: PHP smoke example directory was removed\n        # Consider implementing integration tests via packages/php/tests\n\n        echo \"✓ PIE install smoke test passed\"\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    ignore:\n      # Pin artifact actions to v4 until GitHub Actions runners support v6/v7\n      # v6 and v7 require Actions Runner 2.327.1+ (released Dec 12, 2025)\n      - dependency-name: \"actions/upload-artifact\"\n        update-types: [\"version-update:semver-major\"]\n      - dependency-name: \"actions/download-artifact\"\n        update-types: [\"version-update:semver-major\"]\n\n  - package-ecosystem: \"cargo\"\n    # Explicitly list root only — packages/ruby/ext has a standalone workspace\n    # with path deps to vendored crates that only exist at build time\n    directories:\n      - \"/\"\n    schedule:\n      interval: \"weekly\"\n    ignore:\n      - dependency-name: \"html-to-markdown-rs\"\n\n  - package-ecosystem: \"pip\"\n    directories:\n      - \"/\"\n      - \"/packages/python\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"npm\"\n    directories:\n      - \"/\"\n      - \"/crates/html-to-markdown-node\"\n      - \"/crates/html-to-markdown-wasm\"\n      - \"/packages/typescript\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"bundler\"\n    directory: \"/packages/ruby\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"composer\"\n    directories:\n      - \"/\"\n      - \"/packages/php\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/packages/go/v3\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"maven\"\n    directory: \"/packages/java\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"nuget\"\n    directory: \"/packages/csharp\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"mix\"\n    directory: \"/packages/elixir\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"crates/**\"\n      - \"packages/**\"\n      - \"e2e/**\"\n      - \"tools/**\"\n      - \"scripts/**\"\n      - \"fixtures/**\"\n      - \".github/**\"\n      - \".cargo/config.toml\"\n      - \".pre-commit-config.yaml\"\n      - \".golangci.yml\"\n      - \"alef.toml\"\n      - \"pyproject.toml\"\n      - \"uv.lock\"\n      - \"uv.toml\"\n      - \"pnpm-lock.yaml\"\n      - \"pnpm-workspace.yaml\"\n      - \"package.json\"\n      - \"Cargo.toml\"\n      - \"Cargo.lock\"\n      - \"Taskfile.yaml\"\n      - \".task/**\"\n      - \"rustfmt.toml\"\n      - \"rust-toolchain.toml\"\n      - \"Gemfile\"\n      - \"Gemfile.lock\"\n      - \"composer.json\"\n      - \"composer.lock\"\n      - \"go.mod\"\n      - \"go.sum\"\n  pull_request:\n    branches: [main]\n    paths:\n      - \"crates/**\"\n      - \"packages/**\"\n      - \"e2e/**\"\n      - \"tools/**\"\n      - \"scripts/**\"\n      - \"fixtures/**\"\n      - \".github/**\"\n      - \".cargo/config.toml\"\n      - \".pre-commit-config.yaml\"\n      - \".golangci.yml\"\n      - \"alef.toml\"\n      - \"pyproject.toml\"\n      - \"uv.lock\"\n      - \"uv.toml\"\n      - \"pnpm-lock.yaml\"\n      - \"pnpm-workspace.yaml\"\n      - \"package.json\"\n      - \"Cargo.toml\"\n      - \"Cargo.lock\"\n      - \"Taskfile.yaml\"\n      - \".task/**\"\n      - \"rustfmt.toml\"\n      - \"rust-toolchain.toml\"\n      - \"Gemfile\"\n      - \"Gemfile.lock\"\n      - \"composer.json\"\n      - \"composer.lock\"\n      - \"go.mod\"\n      - \"go.sum\"\n  workflow_dispatch: {}\n\nconcurrency:\n  group: ci-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\nenv:\n  CARGO_TERM_COLOR: always\n  CARGO_INCREMENTAL: 0\n  RUST_BACKTRACE: short\n  BUILD_PROFILE: \"ci\"\n  GO_VERSION: \"1.26.0\"\n  GO_TOOLCHAIN: \"go1.26.0\"\n  GOLANGCI_LINT_VERSION: \"latest\"\n\npermissions:\n  contents: read\n\n# --- Stage 1: Validate ---\n\njobs:\n  validate:\n    name: \"Validate\"\n    runs-on: ubuntu-24.04-arm\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n\n      - name: Setup Python\n        uses: kreuzberg-dev/actions/setup-python-env@v1 # v1\n        with:\n          python-version: \"3.13\"\n\n      - name: Setup Node Workspace\n        uses: kreuzberg-dev/actions/setup-node-workspace@v1 # v1\n\n      - name: Setup Go\n        uses: actions/setup-go@v6 # v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          cache-dependency-path: packages/go/go.sum\n\n      - name: Install golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          install-only: true\n\n      - name: Setup Java\n        uses: actions/setup-java@v5 # v5\n        with:\n          distribution: temurin\n          java-version: \"25\"\n\n      - name: Setup Elixir\n        uses: kreuzberg-dev/actions/setup-elixir@v1 # v1\n\n      - name: Setup Ruby\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: \"3.4\"\n          bundler-cache: false\n\n      - name: Setup PHP\n        uses: kreuzberg-dev/actions/setup-php@v1 # v1\n\n      - name: Setup R\n        uses: kreuzberg-dev/actions/setup-r@v1 # v1\n        with:\n          install-deps-script: scripts/ci/r/install-deps.sh\n\n      - name: Install C/C++ tools\n        run: |\n          sudo apt-get update -qq\n          sudo apt-get install -y --no-install-recommends cppcheck clang-format\n\n      - name: Install Alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Install All Binding Dependencies\n        run: alef setup\n        shell: bash\n\n      - name: Run Lint Checks\n        run: task lint:check\n        shell: bash\n\n      - name: Check Code Formatting\n        run: task format:check\n        shell: bash\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Run Pre-commit Hooks\n        uses: j178/prek-action@v2 # v2\n        with:\n          extra-args: --all-files\n\n      - name: Install Python README Dependencies\n        run: pip install pyyaml jinja2\n        shell: bash\n\n      - name: Validate READMEs\n        run: task docs:generate-readme:check\n        shell: bash\n\n  validate-rust:\n    name: \"Validate: Rust\"\n    runs-on: ubuntu-24.04-arm\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Check Rust Formatting\n        run: task rust:lint:check\n        shell: bash\n\n      - name: Run Clippy\n        run: task rust:lint:check\n        shell: bash\n\n      - name: Check feature flags (html-to-markdown)\n        shell: bash\n        env:\n          RUSTFLAGS: \"-D warnings\"\n        run: |\n          cargo check -p html-to-markdown-rs --no-default-features\n          cargo check -p html-to-markdown-rs --no-default-features --features visitor\n          cargo check -p html-to-markdown-rs --no-default-features --features metadata\n          cargo check -p html-to-markdown-rs --no-default-features --features inline-images\n\n  changes:\n    name: \"Detect Changes\"\n    runs-on: ubuntu-24.04-arm\n    outputs:\n      core: ${{ steps.filter.outputs.core }}\n      rust: ${{ steps.filter.outputs.rust }}\n      ffi: ${{ steps.filter.outputs.ffi }}\n      python: ${{ steps.filter.outputs.python }}\n      node: ${{ steps.filter.outputs.node }}\n      ruby: ${{ steps.filter.outputs.ruby }}\n      php: ${{ steps.filter.outputs.php }}\n      go: ${{ steps.filter.outputs.go }}\n      java: ${{ steps.filter.outputs.java }}\n      elixir: ${{ steps.filter.outputs.elixir }}\n      r: ${{ steps.filter.outputs.r }}\n      wasm: ${{ steps.filter.outputs.wasm }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Detect changes\n        uses: dorny/paths-filter@v4 # v3\n        id: filter\n        with:\n          filters: |\n            core:\n              - 'crates/html-to-markdown/**'\n              - 'Cargo.toml'\n              - 'Cargo.lock'\n              - 'rust-toolchain.toml'\n              - '.cargo/config.toml'\n              - 'fixtures/**'\n              - 'tools/e2e-generator/**'\n            rust:\n              - 'crates/html-to-markdown/**'\n              - 'crates/html-to-markdown-cli/**'\n              - 'e2e/rust/**'\n              - 'Cargo.toml'\n              - 'Cargo.lock'\n              - 'rustfmt.toml'\n            ffi:\n              - 'crates/html-to-markdown-ffi/**'\n              - 'crates/html-to-markdown/**'\n              - 'Cargo.toml'\n              - 'Cargo.lock'\n            python:\n              - 'crates/html-to-markdown-py/**'\n              - 'packages/python/**'\n              - 'e2e/python/**'\n              - 'pyproject.toml'\n              - 'uv.lock'\n              - 'uv.toml'\n              - 'fixtures/**'\n            node:\n              - 'crates/html-to-markdown-node/**'\n              - 'packages/typescript/**'\n              - 'e2e/node/**'\n              - 'package.json'\n              - 'pnpm-lock.yaml'\n              - 'pnpm-workspace.yaml'\n              - 'fixtures/**'\n            ruby:\n              - 'packages/ruby/**'\n              - 'e2e/ruby/**'\n              - 'Gemfile'\n              - 'Gemfile.lock'\n              - 'fixtures/**'\n            php:\n              - 'crates/html-to-markdown-php/**'\n              - 'packages/php/**'\n              - 'packages/php-ext/**'\n              - 'e2e/php/**'\n              - 'composer.json'\n              - 'composer.lock'\n              - 'fixtures/**'\n            go:\n              - 'packages/go/**'\n              - 'e2e/go/**'\n              - 'crates/html-to-markdown-ffi/**'\n              - 'go.mod'\n              - 'go.sum'\n              - 'fixtures/**'\n            java:\n              - 'packages/java/**'\n              - 'e2e/java/**'\n              - 'crates/html-to-markdown-ffi/**'\n              - 'fixtures/**'\n            elixir:\n              - 'packages/elixir/**'\n              - 'e2e/elixir/**'\n              - 'crates/html-to-markdown-ffi/**'\n              - 'fixtures/**'\n            r:\n              - 'packages/r/**'\n              - 'e2e/r/**'\n              - 'fixtures/**'\n            wasm:\n              - 'crates/html-to-markdown-wasm/**'\n              - 'crates/html-to-markdown-wasm-wasi/**'\n              - 'packages/wasm/**'\n              - 'e2e/wasm/**'\n              - 'fixtures/**'\n\n  # --- Stage 2: Core Builds ---\n\n  build-ffi:\n    needs: [validate, validate-rust, changes]\n    name: \"Build: FFI (${{ matrix.runner }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.ffi == 'true'\n    runs-on: ${{ matrix.runner }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        runner:\n          - ubuntu-latest\n          - ubuntu-24.04-arm\n          - macos-latest\n          - windows-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Free disk space\n        if: startsWith(matrix.runner, 'ubuntu')\n        uses: kreuzberg-dev/actions/free-disk-space-linux@v1 # v1\n        with:\n          show-initial: \"false\"\n          show-final: \"true\"\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: ffi-${{ matrix.runner }}\n          use-sccache: false\n\n      - name: Build html-to-markdown-ffi (release, Unix)\n        if: matrix.runner != 'windows-latest'\n        run: cargo build --release -p html-to-markdown-ffi\n        shell: bash\n        env:\n          CARGO_TERM_COLOR: always\n          CARGO_INCREMENTAL: \"0\"\n          RUST_BACKTRACE: short\n\n      - name: Build html-to-markdown-ffi (debug, Windows)\n        if: matrix.runner == 'windows-latest'\n        run: cargo build -p html-to-markdown-ffi\n        shell: bash\n        env:\n          CARGO_TERM_COLOR: always\n\n      - name: Verify header exists\n        shell: bash\n        run: |\n          HEADER=\"crates/html-to-markdown-ffi/include/html_to_markdown.h\"\n          test -f \"$HEADER\"\n          echo \"Header verified: $HEADER\"\n\n      - name: Upload FFI artifacts\n        if: matrix.runner != 'windows-latest'\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: ffi-${{ matrix.runner }}\n          path: |\n            target/release/libhtml_to_markdown_ffi.*\n            target/release/html_to_markdown_ffi.*\n            crates/html-to-markdown-ffi/include/html_to_markdown.h\n          retention-days: 7\n          if-no-files-found: warn\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  rust-tests:\n    needs: [validate, validate-rust, changes]\n    name: \"Test: Rust (${{ matrix.os }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.rust == 'true'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: \"3.13\"\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n          cache-key-prefix: rust-tests-${{ matrix.os }}\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Run Rust Tests\n        env:\n          RUST_BACKTRACE: full\n        run: task rust:test:ci\n        shell: bash\n\n      - name: Run E2E Tests\n        run: task rust:e2e:test\n        shell: bash\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  rust-coverage:\n    needs: [validate, validate-rust, changes]\n    name: \"Coverage: Rust\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.rust == 'true'\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Generate Rust Coverage\n        run: task rust:coverage\n        shell: bash\n\n      - name: Upload Coverage Artifacts\n        if: always()\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: coverage-report-${{ github.sha }}\n          path: rust-coverage.lcov\n          retention-days: 7\n\n  # --- Stage 3: Language Builds ---\n\n  build-python:\n    needs: [rust-tests, changes]\n    name: \"Build: Python (${{ matrix.os }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.python == 'true'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 45\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python: [\"3.10\", \"3.12\", \"3.14\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7 # v7\n        with:\n          enable-cache: true\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: ${{ matrix.python }}\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n          cache-key-prefix: python-${{ matrix.os }}-${{ matrix.python }}\n\n      - name: Install Python Dependencies\n        uses: nick-fields/retry@v4 # v4\n        with:\n          timeout_minutes: 5\n          max_attempts: 3\n          retry_wait_seconds: 30\n          command: |\n            if [[ \"${{ runner.os }}\" == \"Windows\" ]] && [[ -d \".venv\" ]]; then\n              echo \"Removing existing .venv directory on Windows\"\n              rm -rf .venv\n            fi\n            uv sync --all-extras --no-install-workspace\n          shell: bash\n\n      - name: Build Python Bindings\n        run: |\n          uv pip install maturin\n          cd packages/python && uv run maturin develop --release\n        shell: bash\n\n      - name: Build CLI binary\n        run: cargo build --release -p html-to-markdown-cli\n        shell: bash\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  build-node:\n    needs: [rust-tests, changes]\n    name: \"Build: Node (${{ matrix.os }}, ${{ matrix.runtime }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.node == 'true'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        runtime: [node, bun]\n        exclude:\n          - os: windows-latest\n            runtime: bun\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: node-${{ matrix.os }}-${{ matrix.runtime }}\n\n      - name: Setup Node.js workspace\n        if: matrix.runtime == 'node'\n        uses: kreuzberg-dev/actions/setup-node-workspace@v1 # v1\n\n      - name: Setup Bun\n        if: matrix.runtime == 'bun'\n        uses: oven-sh/setup-bun@v2 # v2\n        with:\n          bun-version: latest\n\n      - name: Build NAPI-RS Bindings (Node.js)\n        if: matrix.runtime == 'node'\n        uses: kreuzberg-dev/actions/build-node-napi@v1 # v1\n        with:\n          crate-dir: crates/html-to-markdown-node\n\n      - name: Install workspace dependencies (Bun)\n        if: matrix.runtime == 'bun'\n        run: bun install\n        shell: bash\n\n      - name: Build NAPI-RS Bindings (Bun)\n        if: matrix.runtime == 'bun'\n        working-directory: crates/html-to-markdown-node\n        run: bun run build\n        shell: bash\n\n      - name: Build TypeScript package (Node.js)\n        if: matrix.runtime == 'node'\n        uses: ./.github/actions/build-typescript\n\n      - name: Build TypeScript package (Bun)\n        if: matrix.runtime == 'bun'\n        working-directory: packages/typescript\n        run: bun x tsc --project tsconfig.json\n        shell: bash\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  build-ruby:\n    needs: [rust-tests, changes]\n    name: \"Build: Ruby (${{ matrix.os }}, ruby-${{ matrix.ruby }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.ruby == 'true'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        ruby: [\"3.2\", \"3.3\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: ruby-${{ matrix.os }}-${{ matrix.ruby }}\n\n      - name: Setup Ruby (Unix)\n        if: runner.os != 'Windows'\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler: \"4.0.3\"\n          bundler-cache: false\n          working-directory: packages/ruby\n\n      - name: Setup Ruby (Windows)\n        if: runner.os == 'Windows'\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler: \"4.0.3\"\n          bundler-cache: false\n          working-directory: packages/ruby\n          windows-toolchain: UCRT64\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: \"3.12\"\n\n      - name: Vendor core crate\n        run: python3 scripts/ci/ruby/vendor-core-crate.py\n        shell: bash\n\n      - name: Build CLI binary\n        uses: kreuzberg-dev/actions/build-rust-cli@v1 # v1\n        with:\n          package-name: html-to-markdown-cli\n          binary-name: html-to-markdown\n\n      - name: Build Ruby extension\n        uses: kreuzberg-dev/actions/build-ruby-gem@v1 # v1\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  build-php:\n    needs: [rust-tests, changes]\n    name: \"Build: PHP\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.php == 'true'\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@2.37.0 # 2\n        with:\n          php-version: \"8.4\"\n          tools: composer:2.9.1\n          coverage: none\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: php\n\n      - name: Capture php-config path\n        run: scripts/ci/php/set-php-config.sh\n        shell: bash\n\n      - name: Install root Composer dependencies\n        uses: ramsey/composer-install@4.0.0 # 3\n        with:\n          dependency-versions: locked\n        env:\n          COMPOSER_AUTH: '{\"github-oauth\":{\"github.com\":\"${{ secrets.GITHUB_TOKEN }}\"}}'\n\n      - name: Install PHP package Composer dependencies\n        uses: ramsey/composer-install@4.0.0 # 3\n        with:\n          dependency-versions: locked\n          working-directory: packages/php\n        env:\n          COMPOSER_AUTH: '{\"github-oauth\":{\"github.com\":\"${{ secrets.GITHUB_TOKEN }}\"}}'\n\n      - name: Build PHP extension\n        id: build-php-extension\n        uses: kreuzberg-dev/actions/build-php-extension@v1 # v1\n        with:\n          crate-name: html-to-markdown-php\n          lib-name: html_to_markdown_php\n\n      - name: Upload PHP extension artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: php-extension-ubuntu\n          path: ${{ steps.build-php-extension.outputs.extension-path }}\n          retention-days: 7\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  build-java:\n    needs: [rust-tests, changes]\n    name: \"Build: Java (${{ matrix.os }})\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.java == 'true'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        java: [\"25\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: java-${{ matrix.os }}\n\n      - name: Test Java Panama FFI bindings\n        uses: kreuzberg-dev/actions/test-java-ffi@v1 # v1\n        with:\n          ffi-crate-name: html-to-markdown-ffi\n          ffi-lib-name: html_to_markdown_ffi\n          java-version: ${{ matrix.java }}\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  build-wasm:\n    needs: [rust-tests, changes]\n    name: \"Build: WASM\"\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      needs.changes.outputs.core == 'true' ||\n      needs.changes.outputs.wasm == 'true'\n    runs-on: ubuntu-latest\n    timeout-minutes: 120\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        id: checkout\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          target: wasm32-unknown-unknown\n          use-sccache: false\n          cache-key-prefix: wasm\n\n      - name: Ensure wasm target installed\n        run: scripts/common/ensure-wasm-target.sh\n        shell: bash\n\n      - name: Install wasm-pack\n        run: scripts/common/install-wasm-pack.sh\n        shell: bash\n\n      - name: Setup Node workspace\n        uses: kreuzberg-dev/actions/setup-node-workspace@v1 # v1\n\n      - name: Build WASM (all targets)\n        uses: kreuzberg-dev/actions/build-wasm-package@v1 # v1\n        with:\n          crate-dir: crates/html-to-markdown-wasm\n\n      - name: Cleanup Rust cache\n        if: always() && steps.checkout.outcome == 'success'\n        uses: kreuzberg-dev/actions/cleanup-rust-cache@v1 # v1\n\n  # --- Stage 4: Language Tests ---\n\n  test-python:\n    needs: [build-python]\n    name: \"Test: Python (${{ matrix.os }}, py-${{ matrix.python }})\"\n    if: always() && !cancelled() && needs.build-python.result != 'skipped'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 45\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python: [\"3.10\", \"3.12\", \"3.14\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7 # v7\n        with:\n          enable-cache: true\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: ${{ matrix.python }}\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          components: rustfmt, clippy, llvm-tools-preview\n\n      - name: Install Python Dependencies\n        uses: nick-fields/retry@v4 # v4\n        with:\n          timeout_minutes: 5\n          max_attempts: 3\n          retry_wait_seconds: 30\n          command: |\n            if [[ \"${{ runner.os }}\" == \"Windows\" ]] && [[ -d \".venv\" ]]; then\n              echo \"Removing existing .venv directory on Windows\"\n              rm -rf .venv\n            fi\n            uv sync --all-extras --no-install-workspace\n          shell: bash\n\n      - name: Build Python Bindings\n        run: |\n          uv pip install maturin\n          cd packages/python && uv run maturin develop --release\n        shell: bash\n\n      - name: Build CLI binary\n        run: cargo build --release -p html-to-markdown-cli\n        shell: bash\n\n      - name: Run E2E tests\n        run: alef test --e2e --lang python\n        shell: bash\n\n  test-node:\n    needs: [build-node]\n    name: \"Test: Node (${{ matrix.os }}, ${{ matrix.runtime }})\"\n    if: always() && !cancelled() && needs.build-node.result != 'skipped'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        runtime: [node, bun]\n        exclude:\n          - os: windows-latest\n            runtime: bun\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Setup Node.js workspace\n        if: matrix.runtime == 'node'\n        uses: kreuzberg-dev/actions/setup-node-workspace@v1 # v1\n\n      - name: Setup Bun\n        if: matrix.runtime == 'bun'\n        uses: oven-sh/setup-bun@v2 # v2\n        with:\n          bun-version: latest\n\n      - name: Build NAPI-RS Bindings (Node.js)\n        if: matrix.runtime == 'node'\n        uses: kreuzberg-dev/actions/build-node-napi@v1 # v1\n        with:\n          crate-dir: crates/html-to-markdown-node\n\n      - name: Install workspace dependencies (Bun)\n        if: matrix.runtime == 'bun'\n        run: bun install\n        shell: bash\n\n      - name: Build NAPI-RS Bindings (Bun)\n        if: matrix.runtime == 'bun'\n        working-directory: crates/html-to-markdown-node\n        run: bun run build\n        shell: bash\n\n      - name: Run Rust Tests (Node.js only)\n        if: matrix.runtime == 'node'\n        run: task rust:test\n        shell: bash\n\n      - name: Build TypeScript package (Node.js)\n        if: matrix.runtime == 'node'\n        uses: ./.github/actions/build-typescript\n\n      - name: Build TypeScript package (Bun)\n        if: matrix.runtime == 'bun'\n        working-directory: packages/typescript\n        run: bun x tsc --project tsconfig.json\n        shell: bash\n\n      - name: Run E2E tests (Node.js only)\n        if: matrix.runtime == 'node'\n        run: alef test --e2e --lang node\n        shell: bash\n\n  test-ruby:\n    needs: [build-ruby]\n    name: \"Test: Ruby (${{ matrix.os }}, ruby-${{ matrix.ruby }})\"\n    if: always() && !cancelled() && needs.build-ruby.result != 'skipped'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        ruby: [\"3.2\", \"3.3\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Setup Ruby (Unix)\n        if: runner.os != 'Windows'\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler: \"4.0.3\"\n          bundler-cache: false\n          working-directory: packages/ruby\n\n      - name: Setup Ruby (Windows)\n        if: runner.os == 'Windows'\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler: \"4.0.3\"\n          bundler-cache: false\n          working-directory: packages/ruby\n          windows-toolchain: UCRT64\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: \"3.12\"\n\n      - name: Vendor core crate\n        run: python3 scripts/ci/ruby/vendor-core-crate.py\n        shell: bash\n\n      - name: Build CLI binary\n        uses: kreuzberg-dev/actions/build-rust-cli@v1 # v1\n        with:\n          package-name: html-to-markdown-cli\n          binary-name: html-to-markdown\n\n      - name: Build Ruby extension\n        uses: kreuzberg-dev/actions/build-ruby-gem@v1 # v1\n\n      - name: Run Rubocop (Ubuntu/ruby-3.3 only)\n        if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'\n        run: ./scripts/ci/ruby/run-rubocop.sh\n        shell: bash\n\n      - name: Validate RBS signatures (Ubuntu/ruby-3.3 only)\n        if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'\n        run: ./scripts/ci/ruby/run-rbs-validate.sh\n        shell: bash\n\n      - name: Run Steep type checking (Ubuntu/ruby-3.3 only)\n        if: runner.os != 'Windows' && matrix.os == 'ubuntu-latest' && matrix.ruby == '3.3'\n        working-directory: packages/ruby\n        run: ../../scripts/ci/ruby/run-steep.sh\n        shell: bash\n\n      - name: Run Ruby specs (Unix)\n        if: runner.os != 'Windows'\n        working-directory: packages/ruby\n        run: ../../scripts/ci/ruby/run-rspec-unix.sh\n        shell: bash\n\n      - name: Run Ruby specs (Windows)\n        if: runner.os == 'Windows'\n        working-directory: packages/ruby\n        shell: pwsh\n        run: ../../scripts/ci/ruby/run-rspec-windows.ps1\n\n      - name: Install Task\n        if: runner.os != 'Windows'\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Run E2E tests (Unix only)\n        if: runner.os != 'Windows'\n        run: alef test --e2e --lang ruby\n        shell: bash\n\n  test-php:\n    needs: [build-php]\n    name: \"Test: PHP\"\n    if: always() && !cancelled() && needs.build-php.result != 'skipped'\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@2.37.0 # 2\n        with:\n          php-version: \"8.4\"\n          tools: composer:2.9.1\n          coverage: none\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Capture php-config path\n        run: scripts/ci/php/set-php-config.sh\n        shell: bash\n\n      - name: Install root Composer dependencies\n        uses: ramsey/composer-install@4.0.0 # 3\n        with:\n          dependency-versions: locked\n        env:\n          COMPOSER_AUTH: '{\"github-oauth\":{\"github.com\":\"${{ secrets.GITHUB_TOKEN }}\"}}'\n\n      - name: Install PHP package Composer dependencies\n        uses: ramsey/composer-install@4.0.0 # 3\n        with:\n          dependency-versions: locked\n          working-directory: packages/php\n        env:\n          COMPOSER_AUTH: '{\"github-oauth\":{\"github.com\":\"${{ secrets.GITHUB_TOKEN }}\"}}'\n\n      - name: Build PHP extension\n        id: build-php-extension\n        uses: kreuzberg-dev/actions/build-php-extension@v1 # v1\n        with:\n          crate-name: html-to-markdown-php\n          lib-name: html_to_markdown_php\n\n      - name: Run PHP static analysis\n        run: scripts/ci/php/run-phpstan.sh\n        shell: bash\n\n      - name: Run PHP tests\n        run: scripts/ci/php/run-php-tests.sh\n        shell: bash\n        env:\n          EXTENSION_PATH: ${{ steps.build-php-extension.outputs.extension-path }}\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Run E2E tests\n        run: alef test --e2e --lang php\n        shell: bash\n        env:\n          EXTENSION_PATH: ${{ steps.build-php-extension.outputs.extension-path }}\n\n  test-go:\n    needs: [build-ffi, changes]\n    name: \"Test: Go\"\n    if: |\n      always() && !cancelled() && needs.build-ffi.result != 'skipped' &&\n      (github.event_name == 'workflow_dispatch' ||\n       needs.changes.outputs.go == 'true' ||\n       needs.changes.outputs.ffi == 'true' ||\n       needs.changes.outputs.core == 'true')\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Setup Go\n        uses: actions/setup-go@v6 # v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          check-latest: true\n\n      - name: Build FFI library\n        run: cargo build --release -p html-to-markdown-ffi\n        shell: bash\n\n      - name: Detect Go modules\n        id: set-modules\n        shell: bash\n        run: scripts/ci/go/detect-go-modules.sh\n\n      - name: Install golangci-lint\n        if: steps.set-modules.outputs.modules != '[]'\n        env:\n          GOTOOLCHAIN: ${{ env.GO_TOOLCHAIN }}\n        run: scripts/ci/go/install-golangci-lint.sh\n        shell: bash\n\n      - name: Run golangci-lint (all modules)\n        if: steps.set-modules.outputs.modules != '[]'\n        shell: bash\n        run: |\n          for module in $(echo '${{ steps.set-modules.outputs.modules }}' | jq -r '.[]'); do\n            echo \"=== Linting $module ===\"\n            (cd \"$module\" && \"${{ github.workspace }}/scripts/ci/go/run-golangci-lint.sh\")\n          done\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1 # v1\n\n      - name: Run E2E tests\n        run: alef test --e2e --lang go\n        shell: bash\n\n  test-java:\n    needs: [build-java]\n    name: \"Test: Java (${{ matrix.os }})\"\n    if: always() && !cancelled() && needs.build-java.result != 'skipped'\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        java: [\"25\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Test Java Panama FFI bindings\n        uses: kreuzberg-dev/actions/test-java-ffi@v1 # v1\n        with:\n          ffi-crate-name: html-to-markdown-ffi\n          ffi-lib-name: html_to_markdown_ffi\n          java-version: ${{ matrix.java }}\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Run E2E tests (Ubuntu only)\n        if: matrix.os == 'ubuntu-latest'\n        run: alef test --e2e --lang java\n        shell: bash\n\n  test-elixir:\n    needs: [build-ffi, changes]\n    name: \"Test: Elixir\"\n    if: |\n      always() && !cancelled() && needs.build-ffi.result != 'skipped' &&\n      (github.event_name == 'workflow_dispatch' ||\n       needs.changes.outputs.elixir == 'true' ||\n       needs.changes.outputs.ffi == 'true' ||\n       needs.changes.outputs.core == 'true')\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Elixir\n        uses: erlef/setup-beam@v1 # v1\n        with:\n          elixir-version: \"1.19\"\n          otp-version: \"28.1\"\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install Hex/Rebar\n        run: scripts/ci/elixir/install-hex-rebar.sh\n        shell: bash\n\n      - name: Install dependencies\n        working-directory: packages/elixir\n        run: ../../scripts/ci/elixir/install-deps.sh\n        shell: bash\n\n      - name: Run tests\n        working-directory: packages/elixir\n        run: ../../scripts/ci/elixir/run-tests.sh\n        shell: bash\n\n      - name: Credo lint\n        working-directory: packages/elixir\n        run: ../../scripts/ci/elixir/run-credo.sh\n        shell: bash\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1 # v1\n\n      - name: Run E2E tests\n        run: alef test --e2e --lang elixir\n        shell: bash\n\n  test-r:\n    needs: [rust-tests, changes]\n    name: \"Test: R\"\n    if: |\n      always() && !cancelled() && needs.rust-tests.result != 'skipped' &&\n      (github.event_name == 'workflow_dispatch' ||\n       needs.changes.outputs.r == 'true' ||\n       needs.changes.outputs.core == 'true')\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup R\n        uses: kreuzberg-dev/actions/setup-r@v1 # v1\n        with:\n          install-deps-script: scripts/ci/r/install-deps.sh\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Run tests\n        working-directory: packages/r\n        run: ../../scripts/ci/r/run-tests.sh\n        shell: bash\n\n      - name: Run lintr\n        working-directory: packages/r\n        run: ../../scripts/ci/r/run-lintr.sh\n        shell: bash\n\n      - name: Install Task\n        uses: kreuzberg-dev/actions/install-task@v1 # v1\n\n      - name: Run E2E tests\n        run: alef test --e2e --lang r\n        shell: bash\n\n  test-c-ffi:\n    needs: [build-ffi, changes]\n    name: \"Test: C FFI (${{ matrix.runner }})\"\n    if: |\n      always() && !cancelled() && needs.build-ffi.result != 'skipped' &&\n      (github.event_name == 'workflow_dispatch' ||\n       needs.changes.outputs.ffi == 'true' ||\n       needs.changes.outputs.core == 'true')\n    runs-on: ${{ matrix.runner }}\n    timeout-minutes: 60\n    strategy:\n      fail-fast: false\n      matrix:\n        runner:\n          - ubuntu-latest\n          - ubuntu-24.04-arm\n          - macos-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: c-ffi-${{ matrix.runner }}\n          use-sccache: false\n\n      - name: Build html-to-markdown-ffi\n        shell: bash\n        run: cargo build --release -p html-to-markdown-ffi\n\n      - name: Run C e2e tests\n        shell: bash\n        env:\n          LD_LIBRARY_PATH: ${{ github.workspace }}/target/release\n          DYLD_LIBRARY_PATH: ${{ github.workspace }}/target/release\n        run: cd e2e/c && make test\n\n      - name: Verify header exists\n        shell: bash\n        run: |\n          HEADER=\"crates/html-to-markdown-ffi/include/html_to_markdown.h\"\n          test -f \"$HEADER\"\n          echo \"Header verified: $HEADER\"\n\n      - name: Verify pkg-config output\n        shell: bash\n        run: |\n          PC_DIR=\"$(pwd)/target/release/build\"\n          PC_FILE=$(find \"$PC_DIR\" -name 'html-to-markdown.pc' -path '*/html-to-markdown-ffi-*/out/*' 2>/dev/null | head -1)\n          if [ -z \"$PC_FILE\" ]; then\n            echo \"Warning: html-to-markdown.pc not found in build output\"\n            find \"$PC_DIR\" -name '*.pc' 2>/dev/null || echo \"No .pc files found\"\n          else\n            echo \"Found pkg-config file: $PC_FILE\"\n            cat \"$PC_FILE\"\n          fi\n\n  test-c-ffi-windows:\n    needs: [build-ffi, changes]\n    name: \"Test: C FFI (windows-latest)\"\n    if: |\n      always() && !cancelled() && needs.build-ffi.result != 'skipped' &&\n      (github.event_name == 'workflow_dispatch' ||\n       needs.changes.outputs.ffi == 'true' ||\n       needs.changes.outputs.core == 'true')\n    runs-on: windows-latest\n    timeout-minutes: 60\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: c-ffi-windows\n          use-sccache: false\n\n      - name: Build html-to-markdown-ffi\n        shell: bash\n        run: cargo build -p html-to-markdown-ffi\n\n      - name: Verify header generated\n        shell: bash\n        run: |\n          test -f crates/html-to-markdown-ffi/include/html_to_markdown.h\n          echo \"Header verified on Windows.\"\n\n  test-wasm:\n    needs: [build-wasm]\n    name: \"Test: WASM\"\n    if: always() && !cancelled() && needs.build-wasm.result != 'skipped'\n    runs-on: ubuntu-latest\n    timeout-minutes: 120\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          target: wasm32-unknown-unknown\n          use-sccache: false\n\n      - name: Ensure wasm target installed\n        run: scripts/common/ensure-wasm-target.sh\n        shell: bash\n\n      - name: Install wasm-pack\n        run: scripts/common/install-wasm-pack.sh\n        shell: bash\n\n      - name: Setup Node workspace\n        uses: kreuzberg-dev/actions/setup-node-workspace@v1 # v1\n\n      - name: Build WASM (all targets)\n        uses: kreuzberg-dev/actions/build-wasm-package@v1 # v1\n        with:\n          crate-dir: crates/html-to-markdown-wasm\n\n      - name: Test WASM bundle\n        working-directory: crates/html-to-markdown-wasm\n        run: ../../scripts/ci/wasm/test-wasm-bundle.sh\n        shell: bash\n\n      - name: Run Rust WASM tests\n        working-directory: crates/html-to-markdown-wasm\n        run: ../../scripts/ci/wasm/test-wasm-rust.sh\n        shell: bash\n"
  },
  {
    "path": ".github/workflows/deploy-docs.yaml",
    "content": "name: Deploy Documentation\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'docs/**'\n      - 'zensical.toml'\n      - 'pyproject.toml'\n      - '.github/workflows/deploy-docs.yaml'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6 # v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: '3.13'\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7 # v7\n        with:\n          enable-cache: true\n\n      - name: Install dependencies and build docs\n        run: |\n          uv sync --group doc --no-editable --no-install-workspace --no-install-project\n          uv run --no-sync zensical build --clean\n\n      - name: Upload Pages artifact\n        uses: actions/upload-pages-artifact@v5 # v4\n        with:\n          path: site\n\n  deploy:\n    needs: build\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v5 # v4\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: Publish Release\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Release tag to build (e.g., v2.6.0)\"\n        required: true\n        type: string\n      dry_run:\n        description: \"Prepare artifacts without publishing\"\n        required: false\n        type: boolean\n        default: false\n      ref:\n        description: \"Git ref (branch, tag, or commit) to build; defaults to the tag\"\n        required: false\n        type: string\n      force_republish_java:\n        description: \"Force republish Java artifacts even if the version exists\"\n        required: false\n        type: boolean\n        default: false\n      force_republish_wasm:\n        description: \"Force republish WASM package even if the version exists\"\n        required: false\n        type: boolean\n        default: false\n      republish:\n        description: \"Delete and re-create the tag on current HEAD before publishing (retag + full republish)\"\n        required: false\n        type: boolean\n        default: false\n  release:\n    types: [published]\n  repository_dispatch:\n    types: [publish-release]\n\npermissions:\n  contents: write\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' && (github.event.inputs.ref || github.event.inputs.tag)) || github.ref || github.run_id }}\n  cancel-in-progress: false\n\njobs:\n  prepare:\n    name: Prepare metadata\n    runs-on: ubuntu-latest\n    outputs:\n      tag: ${{ steps.meta.outputs.tag }}\n      version: ${{ steps.meta.outputs.version }}\n      ref: ${{ steps.meta.outputs.ref }}\n      dry_run: ${{ steps.meta.outputs.dry_run }}\n      checkout_ref: ${{ steps.meta.outputs.checkout_ref }}\n      target_sha: ${{ steps.meta.outputs.target_sha }}\n      matrix_ref: ${{ steps.meta.outputs.matrix_ref }}\n      is_tag: ${{ steps.meta.outputs.is_tag }}\n      force_republish_java: ${{ steps.republish.outputs.force_republish_java }}\n      force_republish_wasm: ${{ steps.republish.outputs.force_republish_wasm }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ (inputs.republish == true && (inputs.ref || github.event.repository.default_branch)) || inputs.ref || inputs.tag || github.ref }}\n          fetch-depth: 0\n\n      - name: Retag for republish\n        if: ${{ inputs.republish == true || github.event.client_payload.republish == true }}\n        env:\n          TAG: ${{ inputs.tag || github.event.client_payload.tag }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          if [[ -z \"${TAG}\" ]]; then\n            echo \"::error::republish requires a tag input\"\n            exit 1\n          fi\n          sha=\"$(git rev-parse HEAD)\"\n          echo \"::notice::Republish requested — deleting and re-creating tag ${TAG} on ${sha:0:8}\"\n          # Delete via API (avoids workflows permission issue with git push)\n          gh api \"repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}\" -X DELETE 2>/dev/null || true\n          # Create via API\n          gh api \"repos/${GITHUB_REPOSITORY}/git/refs\" \\\n            -f \"ref=refs/tags/${TAG}\" \\\n            -f \"sha=${sha}\" --silent\n          # Update local state\n          git tag -d \"${TAG}\" 2>/dev/null || true\n          git tag \"${TAG}\" \"${sha}\"\n\n      - name: Validate tag and compute version\n        id: meta\n        env:\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          GITHUB_REF_NAME: ${{ github.ref_name }}\n          INPUT_TAG: ${{ inputs.tag }}\n          INPUT_DRY_RUN: ${{ inputs.dry_run }}\n          INPUT_REF: ${{ inputs.republish == true && format('refs/tags/{0}', inputs.tag) || inputs.ref }}\n          EVENT_RELEASE_TAG: ${{ github.event.release.tag_name }}\n          EVENT_DISPATCH_TAG: ${{ github.event.client_payload.tag }}\n          EVENT_DISPATCH_DRY_RUN: ${{ github.event.client_payload.dry_run }}\n          EVENT_DISPATCH_REF: ${{ github.event.client_payload.ref }}\n        run: scripts/publish/validate-and-compute-metadata.sh\n      - name: Resolve republish flags\n        id: republish\n        env:\n          INPUT_FORCE_REPUBLISH_JAVA: ${{ inputs.force_republish_java }}\n          INPUT_FORCE_REPUBLISH_WASM: ${{ inputs.force_republish_wasm }}\n          EVENT_DISPATCH_FORCE_REPUBLISH_JAVA: ${{ github.event.client_payload.force_republish_java }}\n          EVENT_DISPATCH_FORCE_REPUBLISH_WASM: ${{ github.event.client_payload.force_republish_wasm }}\n        run: |\n          force_java=\"${INPUT_FORCE_REPUBLISH_JAVA:-${EVENT_DISPATCH_FORCE_REPUBLISH_JAVA:-false}}\"\n          force_wasm=\"${INPUT_FORCE_REPUBLISH_WASM:-${EVENT_DISPATCH_FORCE_REPUBLISH_WASM:-false}}\"\n          echo \"force_republish_java=${force_java}\" >>\"$GITHUB_OUTPUT\"\n          echo \"force_republish_wasm=${force_wasm}\" >>\"$GITHUB_OUTPUT\"\n      - name: Install Task\n        uses: go-task/setup-task@v2 # v2\n        with:\n          version: 3.46.4\n\n      - name: Upload release metadata\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: release-metadata\n          path: release-metadata.json\n          retention-days: 14\n\n  check-pypi:\n    name: Check PyPI for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check PyPI version\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: pypi\n          package: html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-npm:\n    name: Check npm for existing versions\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      node_exists: ${{ steps.check.outputs.exists }}\n      wasm_exists: ${{ steps.check.outputs.wasm_exists }}\n      ts_exists: ${{ steps.check.outputs.ts_exists }}\n    steps:\n      - name: Check npm packages\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown\"\n          version: ${{ needs.prepare.outputs.version }}\n          extra-packages: |\n            wasm_exists=@kreuzberg/html-to-markdown-wasm\n            ts_exists=@kreuzberg/html-to-markdown\n\n  check-rubygems:\n    name: Check RubyGems for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check RubyGems version\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: rubygems\n          package: html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-hex:\n    name: Check Hex.pm for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check Hex version\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: hex\n          package: html_to_markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-maven:\n    name: Check Maven Central for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check Maven version\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: maven\n          package: \"dev.kreuzberg:html-to-markdown\"\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-nuget:\n    name: Check NuGet for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check NuGet package\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: nuget\n          package: KreuzbergDev.HtmlToMarkdown\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-packagist:\n    name: Check Packagist for existing version\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check Packagist version\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: packagist\n          package: kreuzberg-dev/html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n  check-cratesio:\n    name: Check crates.io for existing versions\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    outputs:\n      rs_exists: ${{ steps.check.outputs.exists }}\n      cli_exists: ${{ steps.check.outputs.cli_exists }}\n      all_exist: ${{ steps.derive.outputs.all_exist }}\n    steps:\n      - name: Query crates.io\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: cratesio\n          package: html-to-markdown-rs\n          version: ${{ needs.prepare.outputs.version }}\n          extra-packages: |\n            cli_exists=html-to-markdown-cli\n\n      - name: Derive all_exist\n        id: derive\n        run: |\n          if [[ \"${{ steps.check.outputs.exists }}\" == \"true\" && \"${{ steps.check.outputs.cli_exists }}\" == \"true\" ]]; then\n            echo \"all_exist=true\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"all_exist=false\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n  check-homebrew:\n    name: Check if Homebrew formula already published\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    outputs:\n      exists: ${{ steps.check.outputs.exists }}\n    steps:\n      - name: Check Homebrew tap for formula\n        id: check\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: homebrew\n          package: html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n          tap-repo: kreuzberg-dev/homebrew-tap\n\n  python-wheels:\n    name: Build Python wheels (${{ matrix.os }})\n    needs: prepare\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Build wheels\n        uses: kreuzberg-dev/actions/build-python-wheels@v1 # v1\n        with:\n          python-version: \"3.13\"\n          package-dir: packages/python\n          cibw-before-build-linux: >\n            yum install -y openssl-devel &&\n            (test -x /usr/bin/aarch64-linux-gnu-gcc ||\n              ln -sf \"$(command -v gcc)\" /usr/local/bin/aarch64-linux-gnu-gcc 2>/dev/null || true) &&\n            pip install maturin uv &&\n            source ~/.cargo/env &&\n            python scripts/prepare_wheel.py\n          cibw-before-build-macos: >\n            pip install maturin uv &&\n            source ~/.cargo/env &&\n            python scripts/prepare_wheel.py\n          cibw-before-build-windows: >\n            pip install maturin uv &&\n            set PATH=%USERPROFILE%\\.cargo\\bin;%PATH% &&\n            python scripts\\prepare_wheel.py\n          upload-artifact: \"false\"\n\n      - name: Upload wheels\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: python-wheels-${{ matrix.os }}\n          path: wheelhouse/*.whl\n          retention-days: 14\n\n  python-sdist:\n    name: Build Python sdist\n    needs: prepare\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Set up Python\n        uses: actions/setup-python@v6 # v6\n        with:\n          python-version: \"3.13\"\n\n      - name: Install build dependencies\n        run: scripts/publish/python/install-build-deps.sh\n        shell: bash\n\n      - name: Build CLI binary for sdist\n        run: scripts/publish/python/build-cli-for-sdist.sh\n        shell: bash\n\n      - name: Prepare sdist with CLI\n        run: scripts/publish/python/prepare-sdist-with-cli.sh\n        shell: bash\n\n      - name: Build sdist\n        run: scripts/publish/python/build-sdist.sh\n        shell: bash\n\n      - name: Upload sdist\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: python-sdist\n          path: packages/python/dist/*.tar.gz\n          retention-days: 14\n\n  php-package:\n    name: Build PHP PIE binary (php${{ matrix.php }} ${{ matrix.platform.label }})\n    needs: prepare\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ${{ matrix.platform.os }}\n    timeout-minutes: 60\n    permissions:\n      contents: read\n    strategy:\n      fail-fast: false\n      matrix:\n        php: [\"8.2\", \"8.3\", \"8.4\", \"8.5\"]\n        platform:\n          - os: ubuntu-latest\n            label: linux-x86_64\n            target: x86_64-unknown-linux-gnu\n          - os: ubuntu-24.04-arm\n            label: linux-arm64\n            target: aarch64-unknown-linux-gnu\n          - os: macos-latest\n            label: macos-arm64\n            target: aarch64-apple-darwin\n          - os: windows-latest\n            label: windows-x86_64\n            target: x86_64-pc-windows-msvc\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup PHP\n        uses: kreuzberg-dev/actions/setup-php@v1 # v1\n        with:\n          php-version: ${{ matrix.php }}\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n        with:\n          cache-key-prefix: publish-php-${{ matrix.platform.label }}-php${{ matrix.php }}\n          toolchain: stable\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1 # v1\n\n      - name: Build PHP extension\n        uses: kreuzberg-dev/actions/build-php-extension@v1 # v1\n        with:\n          crate-name: html-to-markdown-php\n          lib-name: html_to_markdown_php\n          php-version: ${{ matrix.php }}\n          php-ts: nts\n\n      - name: Determine Windows compiler\n        if: runner.os == 'Windows'\n        id: wincompiler\n        shell: pwsh\n        run: |\n          $compiler = switch ('${{ matrix.php }}') {\n            '8.2' { 'vs16' }\n            '8.3' { 'vs16' }\n            '8.4' { 'vs17' }\n            '8.5' { 'vs17' }\n            default { 'vs17' }\n          }\n          \"compiler=$compiler\" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append\n\n      - name: Package PIE archive\n        uses: kreuzberg-dev/actions/package-php-pie@v1 # v1\n        with:\n          php-version: ${{ matrix.php }}\n          php-ts: nts\n          target: ${{ matrix.platform.target }}\n          windows-compiler: ${{ steps.wincompiler.outputs.compiler }}\n          version: ${{ needs.prepare.outputs.version }}\n          output-dir: dist/php-package\n\n      - name: Upload PHP PIE package artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: php-package-${{ matrix.platform.label }}-php${{ matrix.php }}\n          path: |\n            dist/php-package/php_*.tgz\n            dist/php-package/php_*.tgz.sha256\n            dist/php-package/php_*.zip\n            dist/php-package/php_*.zip.sha256\n          retention-days: 14\n\n  node-typescript-defs:\n    name: Generate Node TypeScript definitions\n    needs: prepare\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Setup Node\n        uses: actions/setup-node@v6 # v6\n        with:\n          node-version: 24\n          check-latest: true\n\n      - name: Enable corepack\n        run: scripts/common/enable-corepack.sh\n        shell: bash\n\n      - name: Install Node dependencies\n        run: scripts/publish/node/install-node-deps.sh\n        shell: bash\n\n      - name: Generate TypeScript definitions\n        run: scripts/publish/node/generate-typescript-defs.sh\n        shell: bash\n\n      - name: Upload TypeScript definitions\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: node-typescript-defs\n          path: typescript-defs/\n          retention-days: 14\n\n  node-bindings:\n    name: Build Node bindings (${{ matrix.target }})\n    needs: prepare\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            rust_target: \"\"\n            use_cross: false\n            use_napi_cross: false\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            rust_target: \"\"\n            use_cross: false\n            use_napi_cross: false\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            rust_target: x86_64-unknown-linux-musl\n            use_cross: true\n            use_napi_cross: false\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-gnu\n            rust_target: aarch64-unknown-linux-gnu\n            use_cross: false\n            use_napi_cross: true\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-musl\n            rust_target: aarch64-unknown-linux-musl\n            use_cross: true\n            use_napi_cross: false\n          - os: ubuntu-latest\n            target: armv7-unknown-linux-gnueabihf\n            rust_target: armv7-unknown-linux-gnueabihf\n            use_cross: false\n            use_napi_cross: true\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            rust_target: \"\"\n            use_cross: false\n            use_napi_cross: false\n          - os: windows-latest\n            target: aarch64-pc-windows-msvc\n            rust_target: aarch64-pc-windows-msvc\n            use_cross: false\n            use_napi_cross: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Add Rust target\n        if: ${{ matrix.rust_target != '' }}\n        env:\n          RUST_TARGET: ${{ matrix.rust_target }}\n        run: scripts/publish/common/add-rust-target.sh\n        shell: bash\n\n      - name: Install cross\n        if: ${{ matrix.use_cross }}\n        run: scripts/publish/cli/install-cross.sh\n        shell: bash\n\n      - name: Setup Node\n        uses: actions/setup-node@v6 # v6\n        with:\n          node-version: 24\n          check-latest: true\n\n      - name: Enable corepack\n        run: scripts/common/enable-corepack.sh\n        shell: bash\n\n      - name: Install Node dependencies\n        run: scripts/publish/node/install-node-deps.sh\n        shell: bash\n\n      - name: Clean npm directory\n        if: runner.os != 'Windows'\n        run: scripts/publish/node/clean-npm-dir.sh\n        shell: bash\n\n      - name: Clean npm directory (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: scripts/publish/node/clean-npm-dir.ps1\n\n      - name: Create npm package structure\n        run: scripts/publish/node/create-npm-package-structure.sh\n        shell: bash\n\n      - name: Build native module\n        if: runner.os != 'Windows'\n        env:\n          TARGET: ${{ matrix.target }}\n          USE_CROSS: ${{ matrix.use_cross }}\n          USE_NAPI_CROSS: ${{ matrix.use_napi_cross }}\n        shell: bash\n        run: scripts/publish/node/build-native-module.sh\n\n      - name: Build native module (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        env:\n          TARGET: ${{ matrix.target }}\n          USE_CROSS: ${{ matrix.use_cross }}\n          USE_NAPI_CROSS: ${{ matrix.use_napi_cross }}\n        run: scripts/publish/node/build-native-module.ps1\n\n      - name: Package artifacts\n        if: runner.os != 'Windows'\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/node/package-artifacts.sh\n        shell: bash\n\n      - name: Package artifacts (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/node/package-artifacts.ps1\n\n      - name: Upload Node artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: node-bindings-${{ matrix.target }}\n          path: node-bindings-${{ matrix.target }}.tar.gz\n          retention-days: 14\n\n  wasm-bindings:\n    name: Build WASM bindings\n    needs: prepare\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Add wasm32 target\n        run: scripts/common/ensure-wasm-target.sh\n        shell: bash\n\n      - name: Install wasm-pack\n        run: scripts/common/install-wasm-pack.sh\n        shell: bash\n\n      - name: Setup Node\n        uses: actions/setup-node@v6 # v6\n        with:\n          node-version: 24\n          check-latest: true\n\n      - name: Enable corepack\n        run: scripts/common/enable-corepack.sh\n        shell: bash\n\n      - name: Install dependencies\n        run: scripts/publish/wasm/install-deps.sh\n        shell: bash\n\n      - name: Build WASM bundles\n        run: scripts/publish/wasm/build-bundles.sh\n        shell: bash\n\n      - name: Package WASM artifacts\n        run: scripts/publish/wasm/package-artifacts.sh\n        shell: bash\n\n      - name: Upload WASM artifacts\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: wasm-bundles\n          path: wasm-artifacts/*\n          retention-days: 14\n\n  cli-binaries:\n    name: Build CLI binaries (${{ matrix.target }})\n    needs: prepare\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            use_cross: false\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            use_cross: false\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-gnu\n            use_cross: false\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            use_cross: false\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            use_cross: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Add compilation target\n        env:\n          RUST_TARGET: ${{ matrix.target }}\n        run: scripts/publish/common/add-rust-target.sh\n        shell: bash\n\n      - name: Install build dependencies\n        if: runner.os == 'Linux'\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/cli/install-build-deps-linux.sh\n        shell: bash\n\n      - name: Configure cross linker\n        if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/cli/configure-cross-linker.sh\n        shell: bash\n\n      - name: Install cross\n        if: ${{ matrix.use_cross }}\n        run: scripts/publish/cli/install-cross.sh\n        shell: bash\n\n      - name: Build CLI\n        shell: bash\n        env:\n          TARGET: ${{ matrix.target }}\n          USE_CROSS: ${{ matrix.use_cross }}\n        run: scripts/publish/cli/build-cli.sh\n\n      - name: Package CLI artifact\n        if: runner.os != 'Windows'\n        shell: bash\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/cli/package-cli-artifact.sh\n\n      - name: Package CLI artifact (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        env:\n          TARGET: ${{ matrix.target }}\n        run: scripts/publish/cli/package-cli-artifact.ps1\n\n      - name: Upload CLI artifact\n        if: runner.os != 'Windows'\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: cli-${{ matrix.target }}\n          path: cli-${{ matrix.target }}.tar.gz\n          retention-days: 14\n\n      - name: Upload CLI artifact (Windows)\n        if: runner.os == 'Windows'\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: cli-${{ matrix.target }}\n          path: cli-${{ matrix.target }}.zip\n          retention-days: 14\n\n  ruby-gem:\n    name: Build Ruby gem (${{ matrix.label }})\n    needs: prepare\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            label: linux\n          - os: ubuntu-24.04-arm\n            label: linux-aarch64\n          - os: macos-latest\n            label: macos-arm64\n          - os: windows-latest\n            label: windows-x64\n    runs-on: ${{ matrix.os }}\n    env:\n      RB_SYS_CARGO_PROFILE: release\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Remove cached CLI binaries\n        shell: bash\n        run: scripts/publish/ruby/remove-cached-cli.sh\n\n      - name: Install MSYS2 toolchain\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: scripts/publish/ruby/install-msys2-toolchain.ps1\n\n      - name: Install Rust (GNU on Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: scripts/publish/ruby/install-rust-gnu.ps1\n\n      - name: Configure bindgen sysroot (Windows)\n        if: runner.os == 'Windows'\n        shell: bash\n        run: scripts/publish/ruby/configure-bindgen-windows.sh\n\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: \"3.3\"\n          bundler: \"4.0.3\"\n          bundler-cache: false\n\n      - name: Install Ruby dependencies (Unix)\n        if: runner.os != 'Windows'\n        run: scripts/publish/ruby/install-deps-unix.sh\n        shell: bash\n\n      - name: Install Ruby dependencies (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: scripts/publish/ruby/install-deps-windows.ps1\n\n      - name: Build gem artifacts (Unix)\n        if: runner.os != 'Windows'\n        shell: bash\n        run: scripts/publish/ruby/build-gem-unix.sh\n\n      - name: Build gem artifacts (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: scripts/publish/ruby/build-gem-windows.ps1\n\n      # Only the canonical `linux` builder ships the source gem; other matrix\n      # entries would emit byte-different source gems (line endings, ext-rb\n      # contents, vendor layout) that overwrite each other under merge-multiple\n      # and produce an invalid .gem at publish time. Their native platform\n      # gems (.gem with platform suffix) are still uploaded.\n      - name: Drop source gem on non-canonical builders\n        if: ${{ matrix.label != 'linux' }}\n        shell: bash\n        run: |\n          shopt -s nullglob\n          for f in packages/ruby/pkg/*.gem; do\n            base=\"$(basename \"$f\")\"\n            case \"$base\" in\n              *-x86_64-linux.gem|*-aarch64-linux.gem|*-arm64-darwin.gem|*-x86_64-darwin.gem|*-x64-mingw32.gem|*-x64-mingw-ucrt.gem) ;;\n              *) echo \"Removing non-canonical source gem $base\"; rm -f \"$f\" ;;\n            esac\n          done\n\n      - name: Upload gem artifacts\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: rubygems-${{ matrix.label }}\n          path: packages/ruby/pkg/*.gem\n          retention-days: 14\n\n  elixir-natives:\n    name: Build Elixir native libs (${{ matrix.settings.label }})\n    needs: [prepare]\n    if: ${{ needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ${{ matrix.settings.os }}\n    timeout-minutes: 180\n    strategy:\n      fail-fast: false\n      matrix:\n        settings:\n          - os: ubuntu-24.04-arm\n            label: linux-aarch64\n            target: aarch64-unknown-linux-gnu\n          - os: ubuntu-latest\n            label: linux-x86_64\n            target: x86_64-unknown-linux-gnu\n          - os: macos-latest\n            label: macos-arm64\n            target: aarch64-apple-darwin\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1\n        with:\n          target: ${{ matrix.settings.target }}\n\n      - name: Build Elixir NIF\n        env:\n          CARGO_BUILD_TARGET: ${{ matrix.settings.target }}\n        run: cargo build --release --target ${{ matrix.settings.target }} --manifest-path packages/elixir/native/html_to_markdown_nif/Cargo.toml\n\n      - name: Package NIF (NIF 2.16)\n        shell: bash\n        run: |\n          VERSION=\"${{ needs.prepare.outputs.version }}\"\n          TARGET=\"${{ matrix.settings.target }}\"\n          NIF_VERSION=\"2.16\"\n          NIF_DIR=\"packages/elixir/native/html_to_markdown_nif\"\n          if [[ \"${{ runner.os }}\" == \"macOS\" ]]; then\n            LIB_NAME=\"libhtml_to_markdown_nif.dylib\"; EXT=\"so\"\n          else\n            LIB_NAME=\"libhtml_to_markdown_nif.so\"; EXT=\"so\"\n          fi\n          mkdir -p dist/elixir\n          ARTIFACT=\"libhtml_to_markdown_nif-v${VERSION}-nif-${NIF_VERSION}-${TARGET}.${EXT}\"\n          LIB_PATH=\"${NIF_DIR}/target/${TARGET}/release/${LIB_NAME}\"\n          [[ ! -f \"$LIB_PATH\" ]] && LIB_PATH=\"${NIF_DIR}/target/release/${LIB_NAME}\"\n          cp \"$LIB_PATH\" \"${ARTIFACT}\"\n          tar -czf \"dist/elixir/${ARTIFACT}.tar.gz\" \"${ARTIFACT}\"\n\n      - name: Package NIF (NIF 2.17)\n        shell: bash\n        run: |\n          VERSION=\"${{ needs.prepare.outputs.version }}\"\n          TARGET=\"${{ matrix.settings.target }}\"\n          NIF_VERSION=\"2.17\"\n          NIF_DIR=\"packages/elixir/native/html_to_markdown_nif\"\n          if [[ \"${{ runner.os }}\" == \"macOS\" ]]; then\n            LIB_NAME=\"libhtml_to_markdown_nif.dylib\"; EXT=\"so\"\n          else\n            LIB_NAME=\"libhtml_to_markdown_nif.so\"; EXT=\"so\"\n          fi\n          ARTIFACT=\"libhtml_to_markdown_nif-v${VERSION}-nif-${NIF_VERSION}-${TARGET}.${EXT}\"\n          LIB_PATH=\"${NIF_DIR}/target/${TARGET}/release/${LIB_NAME}\"\n          [[ ! -f \"$LIB_PATH\" ]] && LIB_PATH=\"${NIF_DIR}/target/release/${LIB_NAME}\"\n          cp \"$LIB_PATH\" \"${ARTIFACT}\"\n          tar -czf \"dist/elixir/${ARTIFACT}.tar.gz\" \"${ARTIFACT}\"\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: elixir-${{ matrix.settings.label }}\n          path: dist/elixir/*.tar.gz\n          if-no-files-found: error\n          retention-days: 1\n\n  upload-elixir-release:\n    name: Upload Elixir NIF binaries to GitHub Release\n    needs: [prepare, elixir-natives]\n    if: ${{ always() && needs.prepare.outputs.is_tag == 'true' && needs.elixir-natives.result == 'success' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download Elixir NIF artifacts\n        uses: actions/download-artifact@v8\n        with:\n          pattern: elixir-*\n          path: dist/elixir\n          merge-multiple: true\n\n      - name: Upload to GitHub Release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          TAG=\"${{ needs.prepare.outputs.tag }}\"\n          for file in dist/elixir/*.tar.gz; do\n            echo \"Uploading $(basename \"$file\")...\"\n            gh release upload \"$TAG\" \"$file\" --clobber\n          done\n\n  elixir-package:\n    name: Build Elixir Hex package (${{ matrix.label }})\n    needs: prepare\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            label: linux\n            build_hex: true\n          - os: macos-latest\n            label: macos\n            build_hex: false\n    runs-on: ${{ matrix.os }}\n    env:\n      MIX_ENV: dev\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Elixir\n        uses: erlef/setup-beam@v1 # v1\n        with:\n          elixir-version: \"1.19\"\n          otp-version: \"28.1\"\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install Hex and Rebar\n        run: scripts/publish/elixir/install-hex-rebar.sh\n        shell: bash\n\n      - name: Install dependencies\n        run: scripts/publish/elixir/install-deps.sh\n        shell: bash\n\n      - name: Run Elixir tests\n        run: scripts/publish/elixir/run-tests.sh\n        shell: bash\n\n      - name: Build Hex package\n        if: ${{ matrix.build_hex }}\n        run: scripts/publish/elixir/build-hex-package.sh\n        shell: bash\n\n      - name: Upload Hex artifact\n        if: ${{ matrix.build_hex }}\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: elixir-hex-package\n          path: packages/elixir/html_to_markdown-*.tar\n          retention-days: 14\n\n  csharp-package:\n    name: Build C# NuGet package\n    needs: [prepare, check-nuget, csharp-ffi]\n    if: ${{ needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-nuget.outputs.exists != 'true' }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    permissions:\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v5 # v5\n        with:\n          dotnet-version: \"8.0.x\"\n\n      - name: Download C# native FFI libraries\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: csharp-ffi-*\n          path: dist/csharp-ffi\n          merge-multiple: true\n\n      - name: Install dependencies\n        run: scripts/publish/csharp/restore.sh packages/csharp/HtmlToMarkdown.csproj\n        shell: bash\n\n      - name: Pack NuGet package\n        run: scripts/publish/csharp/pack.sh\n        shell: bash\n\n      - name: Upload NuGet artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: csharp-nuget\n          path: artifacts/csharp/*.nupkg\n          retention-days: 14\n\n  csharp-ffi:\n    name: Build C# native FFI libraries\n    needs: [prepare, check-nuget]\n    if: ${{ needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-nuget.outputs.exists != 'true' }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            rid: linux-x64\n            target: x86_64-unknown-linux-gnu\n          - os: ubuntu-24.04-arm\n            rid: linux-arm64\n            target: aarch64-unknown-linux-gnu\n          - os: windows-latest\n            rid: win-x64\n            target: x86_64-pc-windows-msvc\n          - os: macos-latest\n            rid: osx-arm64\n            target: aarch64-apple-darwin\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    permissions:\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Build and stage FFI library\n        shell: bash\n        run: |\n          alef publish build --lang ffi --target ${{ matrix.target }}\n          mkdir -p dist/csharp-ffi/${{ matrix.rid }}/native\n          find target/release -maxdepth 1 -type f \\( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \\) -name '*html_to_markdown_ffi*' -exec cp {} dist/csharp-ffi/${{ matrix.rid }}/native/ \\;\n\n      - name: Upload FFI artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: csharp-ffi-${{ matrix.rid }}\n          path: dist/csharp-ffi\n          retention-days: 14\n\n  go-ffi:\n    name: Build Go native FFI libraries (${{ matrix.platform }})\n    needs: prepare\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            platform: linux-x64\n            target: x86_64-unknown-linux-gnu\n          - os: ubuntu-24.04-arm\n            platform: linux-arm64\n            target: aarch64-unknown-linux-gnu\n          - os: windows-latest\n            platform: windows-x64\n            target: x86_64-pc-windows-msvc\n          - os: macos-latest\n            platform: darwin-arm64\n            target: aarch64-apple-darwin\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Build and package Go FFI library\n        shell: bash\n        run: |\n          alef publish build --lang ffi --target ${{ matrix.target }}\n          alef publish package --lang go --target ${{ matrix.target }} -o dist/go-ffi\n\n      - name: Upload Go FFI artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: go-ffi-${{ matrix.platform }}\n          path: dist/go-ffi\n          retention-days: 14\n\n  c-ffi-libraries:\n    name: Build C FFI distribution packages (${{ matrix.platform }})\n    needs: prepare\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            platform: linux-x64\n            target: x86_64-unknown-linux-gnu\n          - os: ubuntu-24.04-arm\n            platform: linux-arm64\n            target: aarch64-unknown-linux-gnu\n          - os: windows-latest\n            platform: windows-x64\n            target: x86_64-pc-windows-msvc\n          - os: macos-latest\n            platform: darwin-arm64\n            target: aarch64-apple-darwin\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Build and package C FFI distribution\n        shell: bash\n        run: |\n          alef publish build --lang ffi --target ${{ matrix.target }}\n          alef publish package --lang ffi --target ${{ matrix.target }} -o dist/c-ffi\n\n      - name: Upload C FFI artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: c-ffi-${{ matrix.platform }}\n          path: dist/c-ffi\n          retention-days: 14\n\n  cargo-packages:\n    name: Package Rust crates\n    needs: prepare\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Add Windows Rust target\n        if: runner.os == 'Windows'\n        run: rustup target add x86_64-pc-windows-msvc\n        shell: bash\n\n      - name: Package crates\n        env:\n          RELEASE_VERSION: ${{ needs.prepare.outputs.version }}\n        run: scripts/publish/crates/package-crates.sh\n        shell: bash\n\n      - name: Upload crate packages\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: cargo-crates\n          path: crate-artifacts/*.crate\n          retention-days: 14\n\n  upload-release-artifacts:\n    name: Upload Release Artifacts\n    needs:\n      [\n        prepare,\n        python-wheels,\n        python-sdist,\n        php-package,\n        node-typescript-defs,\n        node-bindings,\n        wasm-bindings,\n        cli-binaries,\n        ruby-gem,\n        go-ffi,\n        c-ffi-libraries,\n        cargo-packages,\n      ]\n    if: ${{ always() && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.tag }}\n          fetch-depth: 0\n\n      - name: Download PHP package artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: php-package-*\n          path: dist/php-package\n          merge-multiple: true\n\n      - name: Download CLI artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: cli-*\n          path: dist/cli\n          merge-multiple: false\n\n      - name: Download Go FFI artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: go-ffi-*\n          path: dist/go-ffi\n          merge-multiple: false\n\n      - name: Download C FFI artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: c-ffi-*\n          path: dist/c-ffi\n          merge-multiple: false\n\n      - name: Upload PHP PIE packages\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: scripts/publish/upload-php-pie.sh\n        shell: bash\n\n      - name: Upload CLI binaries\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: scripts/publish/upload-cli-artifacts.sh\n        shell: bash\n\n      - name: Upload Go FFI artifacts\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: scripts/publish/upload-go-ffi-artifacts.sh\n        shell: bash\n\n      - name: Upload C FFI artifacts\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: scripts/publish/upload-c-ffi-artifacts.sh\n        shell: bash\n\n      - name: Create Go module tag\n        env:\n          VERSION: ${{ needs.prepare.outputs.version }}\n        run: scripts/publish/go/create-module-tag.sh \"v${VERSION}\"\n        shell: bash\n\n  publish-crates:\n    name: Publish crates.io packages\n    needs: [prepare, cargo-packages, check-cratesio]\n    if: ${{ always() && needs.cargo-packages.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-cratesio.outputs.all_exist != 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Verify Cargo.toml version matches tag\n        env:\n          TAG_VERSION: ${{ needs.prepare.outputs.version }}\n        run: scripts/publish/crates/verify-cargo-version.sh\n        shell: bash\n\n      - name: Re-check crates.io before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: cratesio\n          package: html-to-markdown-rs\n          version: ${{ needs.prepare.outputs.version }}\n          extra-packages: |\n            cli_exists=html-to-markdown-cli\n\n      - name: Publish html-to-markdown-rs\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        env:\n          CARGO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}\n        run: scripts/publish/crates/publish-rs.sh\n        shell: bash\n\n      - name: Wait for indexing\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        run: scripts/publish/crates/wait-for-indexing.sh\n        shell: bash\n\n      - name: Publish html-to-markdown-cli\n        if: ${{ steps.recheck.outputs.cli_exists != 'true' }}\n        env:\n          CARGO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}\n        run: scripts/publish/crates/publish-cli.sh\n        shell: bash\n\n  publish-pypi:\n    name: Publish Python packages to PyPI\n    needs: [prepare, python-wheels, python-sdist, check-pypi]\n    if: ${{ always() && needs.python-wheels.result == 'success' && needs.python-sdist.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-pypi.outputs.exists != 'true' }}\n    runs-on: ubuntu-latest\n    environment: pypi\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download wheel artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: python-wheels-*\n          path: dist\n          merge-multiple: true\n\n      - name: Download sdist artifact\n        uses: actions/download-artifact@v8 # v8\n        with:\n          name: python-sdist\n          path: dist\n\n      - name: List packages to publish\n        run: |\n          echo \"Packages in dist:\"\n          ls -lh dist/ 2>/dev/null || echo \"No packages found\"\n        shell: bash\n\n      - name: Re-check PyPI before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: pypi\n          package: html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Publish to PyPI\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          packages-dir: dist\n          skip-existing: true\n\n  publish-rubygems:\n    name: Publish Ruby gems\n    needs: [prepare, ruby-gem, check-rubygems]\n    if: ${{ always() && needs.ruby-gem.result == 'success' && needs.prepare.outputs.is_tag == 'true' && (needs.prepare.outputs.dry_run == 'true' || needs.check-rubygems.outputs.exists != 'true') }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download Ruby gem artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: rubygems-*\n          path: dist\n          merge-multiple: true\n\n      - name: Re-check RubyGems before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: rubygems\n          package: html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Setup Ruby\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: ruby/setup-ruby@v1 # v1\n        with:\n          ruby-version: \"3.3\"\n          bundler-cache: false\n\n      - name: Update RubyGems\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        run: gem update --system\n        shell: bash\n\n      - name: Configure trusted publishing credentials\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: rubygems/configure-rubygems-credentials@v2.0.0 # v1.0.0\n\n      - name: Publish gems\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-rubygems@v1 # v1\n        with:\n          gems-dir: dist\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n\n      - name: RubyGems already published summary\n        if: ${{ steps.recheck.outputs.exists == 'true' }}\n        run: echo \"Gem html-to-markdown@${{ needs.prepare.outputs.version }} already published on RubyGems — skipped.\" >> \"$GITHUB_STEP_SUMMARY\"\n        shell: bash\n\n  publish-hex:\n    name: Publish Hex package\n    needs: [prepare, elixir-package, check-hex, upload-elixir-release]\n    if: ${{ always() && needs.elixir-package.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-hex.outputs.exists != 'true' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download Hex artifact\n        uses: actions/download-artifact@v8 # v8\n        with:\n          name: elixir-hex-package\n          path: dist/elixir\n\n      - name: Upload Elixir package to GitHub Release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: scripts/publish/upload-elixir-package.sh\n        shell: bash\n\n      - name: Setup Elixir\n        uses: erlef/setup-beam@v1 # v1\n        with:\n          elixir-version: \"1.19\"\n          otp-version: \"28.1\"\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install Hex/Rebar\n        run: scripts/publish/elixir/install-hex-rebar.sh\n        shell: bash\n\n      - name: Generate NIF checksums from GitHub release\n        run: scripts/publish/generate_elixir_checksums.sh \"${{ needs.prepare.outputs.version }}\"\n\n      - name: Install dependencies\n        run: scripts/publish/elixir/install-deps.sh\n        shell: bash\n\n      - name: Stage Rust core and generate lockfile\n        shell: bash\n        run: |\n          scripts/publish/elixir/stage-rust-core.sh\n          pushd packages/elixir/native/html_to_markdown_elixir >/dev/null\n          cargo generate-lockfile\n          popd >/dev/null\n\n      - name: Re-check Hex.pm before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: hex\n          package: html_to_markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Publish to Hex.pm\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-hex@v1 # v1\n        with:\n          package-dir: packages/elixir\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          HEX_API_KEY: ${{ secrets.HEX_API_KEY }}\n\n      - name: Hex.pm already published summary\n        if: ${{ steps.recheck.outputs.exists == 'true' }}\n        run: echo \"Package html_to_markdown@${{ needs.prepare.outputs.version }} already published on Hex.pm — skipped.\" >> \"$GITHUB_STEP_SUMMARY\"\n        shell: bash\n\n  publish-nuget:\n    name: Publish NuGet package\n    needs: [prepare, csharp-package, check-nuget]\n    if: ${{ always() && needs.csharp-package.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-nuget.outputs.exists != 'true' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download NuGet artifact\n        uses: actions/download-artifact@v8 # v8\n        with:\n          name: csharp-nuget\n          path: dist\n\n      - name: Re-check NuGet before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: nuget\n          package: KreuzbergDev.HtmlToMarkdown\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Publish to NuGet\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-nuget@v1 # v1\n        with:\n          packages-dir: dist\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}\n\n  publish-packagist:\n    name: Publish to Packagist\n    runs-on: ubuntu-latest\n    needs: [prepare, check-packagist]\n    if: |\n      always() &&\n      needs.prepare.result == 'success' &&\n      needs.prepare.outputs.is_tag == 'true' &&\n      needs.prepare.outputs.dry_run != 'true' &&\n      needs.check-packagist.outputs.exists != 'true'\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Re-check Packagist before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: packagist\n          package: kreuzberg-dev/html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Trigger Packagist Update\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-packagist@v1 # v1\n        with:\n          packagist-username: kreuzberg-dev\n          package-name: kreuzberg/html-to-markdown\n          version: ${{ needs.prepare.outputs.version }}\n          repository-url: https://github.com/kreuzberg-dev/html-to-markdown\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }}\n\n  java-ffi:\n    name: Build Java native FFI libraries\n    needs: [prepare, check-maven]\n    if: ${{ needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && (needs.check-maven.outputs.exists != 'true' || needs.prepare.outputs.force_republish_java == 'true') }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            platform: linux-x86_64\n            target: x86_64-unknown-linux-gnu\n          - os: ubuntu-24.04-arm\n            platform: linux-aarch64\n            target: aarch64-unknown-linux-gnu\n          - os: windows-latest\n            platform: windows-x86_64\n            target: x86_64-pc-windows-msvc\n          - os: macos-latest\n            platform: osx-aarch64\n            target: aarch64-apple-darwin\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    permissions:\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        env:\n          TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}\n        run: scripts/publish/common/ensure-target-commit.sh\n        shell: bash\n\n      - name: Setup Rust\n        uses: kreuzberg-dev/actions/setup-rust@v1 # v1\n\n      - name: Install alef\n        uses: kreuzberg-dev/actions/install-alef@v1\n\n      - name: Build and stage FFI library\n        shell: bash\n        run: |\n          alef publish build --lang ffi --target ${{ matrix.target }}\n          mkdir -p dist/java-ffi/${{ matrix.platform }}/native\n          find target/release -maxdepth 1 -type f \\( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \\) -name '*html_to_markdown_ffi*' -exec cp {} dist/java-ffi/${{ matrix.platform }}/native/ \\;\n\n      - name: Upload FFI artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: java-ffi-${{ matrix.platform }}\n          path: dist/java-ffi\n          retention-days: 14\n\n  publish-maven:\n    name: Publish Maven package\n    needs: [prepare, check-maven, java-ffi]\n    if: |\n      always() &&\n      needs.prepare.outputs.dry_run != 'true' &&\n      needs.prepare.outputs.is_tag == 'true' &&\n      (needs.check-maven.outputs.exists != 'true' || needs.prepare.outputs.force_republish_java == 'true') &&\n      needs.java-ffi.result == 'success'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Check Maven Central for existing release\n        id: maven_check\n        uses: kreuzberg-dev/actions/check-registry@v1\n        with:\n          registry: maven\n          package: \"dev.kreuzberg:html-to-markdown\"\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Download Java FFI artifacts\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        uses: actions/download-artifact@v8\n        with:\n          pattern: java-ffi-*\n          path: java-ffi-artifacts\n          merge-multiple: true\n\n      - name: Setup Rust\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/setup-rust@v1\n\n      - name: Setup Java\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        env:\n          MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }}\n          MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}\n          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n        uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: '25'\n          cache: maven\n          server-id: ossrh\n          server-username: MAVEN_USERNAME\n          server-password: MAVEN_PASSWORD\n          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}\n          gpg-passphrase: MAVEN_GPG_PASSPHRASE\n\n      - name: Setup Maven\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/setup-maven@v1\n\n      - name: Prefer gpg2 binary\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        run: scripts/publish/maven/prefer-gpg2.sh\n        shell: bash\n\n      - name: Copy native libraries into resources\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        shell: bash\n        run: scripts/publish/java/copy-native-libs.sh java-ffi-artifacts\n\n      - name: Release Maven package\n        if: ${{ steps.maven_check.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-maven@v1\n        with:\n          pom-file: packages/java/pom.xml\n          maven-profile: publish\n          extra-args: -DskipTests\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }}\n          MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}\n          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n\n      - name: Maven already published summary\n        if: ${{ steps.maven_check.outputs.exists == 'true' }}\n        run: echo \"Maven package version ${{ needs.prepare.outputs.version }} already published; skipping.\" >> \"$GITHUB_STEP_SUMMARY\"\n\n  publish-node:\n    name: Publish Node packages\n    needs: [prepare, node-bindings, node-typescript-defs, check-npm]\n    if: ${{ always() && needs.node-bindings.result == 'success' && needs.node-typescript-defs.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && needs.check-npm.outputs.node_exists != 'true' }}\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download Node artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: node-bindings-*\n          path: node-artifacts\n          merge-multiple: true\n\n      - name: Download TypeScript definitions\n        uses: actions/download-artifact@v8 # v8\n        with:\n          name: node-typescript-defs\n          path: typescript-defs\n\n      - name: Setup Node\n        uses: actions/setup-node@v6 # v6\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org/\n\n      - name: Update NPM\n        run: npm install -g npm@latest\n        shell: bash\n\n      - name: Enable corepack\n        run: scripts/common/enable-corepack.sh\n        shell: bash\n\n      - name: Prepare artifact directory\n        run: scripts/publish/node/prepare-artifact-directory.sh\n        shell: bash\n\n      - name: Install workspace dependencies\n        if: ${{ needs.prepare.outputs.dry_run != 'true' }}\n        run: scripts/publish/node/install-node-deps.sh\n        shell: bash\n\n      - name: Pack platform packages\n        run: scripts/publish/node/pack-platform-packages.sh\n        shell: bash\n\n      - name: Re-check npm before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown\"\n          version: ${{ needs.prepare.outputs.version }}\n          extra-packages: |\n            ts_exists=@kreuzberg/html-to-markdown\n\n      - name: Publish native binary packages\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-npm@v1 # v1\n        with:\n          packages-dir: crates/html-to-markdown-node/npm\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Wait for npm indexing (x64)\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/wait-for-package@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown-linux-x64-gnu\"\n          version: ${{ needs.prepare.outputs.version }}\n          max-attempts: \"25\"\n\n      - name: Wait for npm indexing (arm64)\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/wait-for-package@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown-linux-arm64-gnu\"\n          version: ${{ needs.prepare.outputs.version }}\n          max-attempts: \"25\"\n\n      - name: Prepare main Node package metadata\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        run: scripts/publish/node/prepublish-main-package.sh crates/html-to-markdown-node\n        shell: bash\n\n      - name: Publish main Node package\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-npm@v1 # v1\n        with:\n          package-dir: crates/html-to-markdown-node\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Wait for main Node package indexing\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.exists != 'true' && steps.recheck.outputs.ts_exists != 'true' }}\n        uses: kreuzberg-dev/actions/wait-for-package@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown\"\n          version: ${{ needs.prepare.outputs.version }}\n          max-attempts: \"25\"\n\n      - name: Install TypeScript wrapper dependencies from npm\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.ts_exists != 'true' }}\n        working-directory: packages/typescript\n        run: pnpm install --no-frozen-lockfile\n        shell: bash\n\n      - name: Build TypeScript wrapper package\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.ts_exists != 'true' }}\n        run: scripts/publish/typescript/build-package.sh\n        shell: bash\n\n      - name: Publish TypeScript wrapper package\n        if: ${{ needs.prepare.outputs.dry_run != 'true' && steps.recheck.outputs.ts_exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-npm@v1 # v1\n        with:\n          package-dir: packages/typescript\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  publish-wasm:\n    name: Publish WASM package\n    needs: [prepare, wasm-bindings, check-npm]\n    if: ${{ always() && needs.wasm-bindings.result == 'success' && needs.prepare.outputs.dry_run != 'true' && needs.prepare.outputs.is_tag == 'true' && (needs.check-npm.outputs.wasm_exists != 'true' || needs.prepare.outputs.force_republish_wasm == 'true') }}\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Download WASM artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          name: wasm-bundles\n          path: wasm-artifacts\n\n      - name: Extract WASM artifacts\n        run: scripts/publish/wasm/extract-artifacts.sh\n        shell: bash\n\n      - name: Remove .gitignore files from dist directories\n        run: |\n          rm -f crates/html-to-markdown-wasm/dist/.gitignore\n          rm -f crates/html-to-markdown-wasm/dist-node/.gitignore\n          rm -f crates/html-to-markdown-wasm/dist-web/.gitignore\n        shell: bash\n\n      - name: Setup Node\n        uses: actions/setup-node@v6 # v6\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org/\n\n      - name: Update NPM\n        run: npm install -g npm@latest\n        shell: bash\n\n      - name: Re-check npm before publish\n        id: recheck\n        uses: kreuzberg-dev/actions/check-registry@v1 # v1\n        with:\n          registry: npm\n          package: \"@kreuzberg/html-to-markdown-wasm\"\n          version: ${{ needs.prepare.outputs.version }}\n\n      - name: Publish WASM package\n        if: ${{ steps.recheck.outputs.exists != 'true' }}\n        uses: kreuzberg-dev/actions/publish-npm@v1 # v1\n        with:\n          package-dir: crates/html-to-markdown-wasm\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  homebrew-bottles:\n    name: Build Homebrew bottles (${{ matrix.bottle_tag }})\n    needs: [prepare, check-homebrew]\n    if: |\n      needs.prepare.outputs.is_tag == 'true' &&\n      (needs.check-homebrew.outputs.exists != 'true')\n    runs-on: ${{ matrix.runner }}\n    timeout-minutes: 180\n    permissions:\n      contents: write\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - runner: macos-latest\n            bottle_tag: arm64_sequoia\n          - runner: macos-15-intel\n            bottle_tag: sequoia\n          - runner: ubuntu-latest\n            bottle_tag: x86_64_linux\n          - runner: ubuntu-24.04-arm\n            bottle_tag: arm64_linux\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n          fetch-depth: 0\n\n      - name: Ensure target commit\n        if: ${{ needs.prepare.outputs.target_sha != '' }}\n        run: git checkout --progress --force ${{ needs.prepare.outputs.target_sha }}\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Setup Homebrew\n        run: |\n          brew tap homebrew/core\n          brew update\n\n      - name: Extract version\n        id: version\n        env:\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: |\n          VERSION=\"${TAG#v}\"\n          echo \"version=${VERSION}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Build CLI for bottle\n        run: |\n          cargo build --release \\\n            -p html-to-markdown-cli\n\n      - name: Create Homebrew bottle\n        id: bottle\n        env:\n          VERSION: ${{ steps.version.outputs.version }}\n          TAG: ${{ needs.prepare.outputs.tag }}\n        run: |\n          # Homebrew bottles require {formula}/{version}/ prefix in the tarball\n          bottle_root=\"/tmp/html-to-markdown-bottle\"\n          bottle_dir=\"${bottle_root}/html-to-markdown/${VERSION}\"\n          mkdir -p \"${bottle_dir}/bin\"\n\n          # Copy the built binary\n          cp target/release/html-to-markdown \"${bottle_dir}/bin/\"\n\n          # Create bottle tarball with correct prefix\n          cd \"${bottle_root}\"\n          bottle_filename=\"html-to-markdown-${VERSION}.${{ matrix.bottle_tag }}.bottle.tar.gz\"\n          tar -czf \"${bottle_filename}\" html-to-markdown/\n\n          # Calculate SHA256\n          sha256=$(shasum -a 256 \"${bottle_filename}\" | cut -d' ' -f1)\n          echo \"sha256=${sha256}\" >> \"$GITHUB_OUTPUT\"\n          echo \"filename=${bottle_filename}\" >> \"$GITHUB_OUTPUT\"\n\n          # Copy to workspace for artifact upload\n          cp \"${bottle_filename}\" \"${{ github.workspace }}/\"\n          echo \"Bottle created: ${bottle_filename}\"\n          echo \"SHA256: ${sha256}\"\n\n      - name: Verify bottle file in workspace\n        run: |\n          cd \"${{ github.workspace }}\"\n          ls -lh html-to-markdown-*.bottle.tar.gz\n          echo \"Files in workspace:\"\n          ls -la\n        shell: bash\n\n      - name: Upload bottle artifact\n        uses: actions/upload-artifact@v7 # v7\n        with:\n          name: homebrew-bottle-${{ matrix.bottle_tag }}\n          path: html-to-markdown-${{ steps.version.outputs.version }}.${{ matrix.bottle_tag }}.bottle.tar.gz\n          retention-days: 14\n          if-no-files-found: error\n\n  upload-homebrew-bottles:\n    name: Upload Homebrew bottles to GitHub Release\n    needs: [prepare, check-homebrew, homebrew-bottles]\n    if: |\n      always() &&\n      needs.prepare.outputs.dry_run != 'true' &&\n      needs.prepare.outputs.is_tag == 'true' &&\n      (needs.check-homebrew.outputs.exists != 'true') &&\n      needs.homebrew-bottles.result == 'success'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.ref }}\n\n      - name: Ensure GitHub release exists\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: scripts/publish/ensure-github-release-exists.sh \"${{ needs.prepare.outputs.tag }}\"\n\n      - name: Download bottle artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: homebrew-bottle-*\n          path: dist/homebrew\n          merge-multiple: true\n\n      - name: Verify downloaded artifacts\n        run: |\n          echo \"Contents of dist/homebrew:\"\n          ls -laR dist/homebrew || echo \"dist/homebrew not found\"\n          echo \"All dist contents:\"\n          ls -laR dist || echo \"dist not found\"\n        shell: bash\n\n      - name: Upload bottles (idempotent)\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: scripts/publish/upload-homebrew-bottles.sh \"${{ needs.prepare.outputs.tag }}\" dist/homebrew\n\n  publish-homebrew:\n    name: Update Homebrew formula\n    needs: [prepare, check-homebrew, upload-homebrew-bottles]\n    if: |\n      always() &&\n      needs.prepare.outputs.dry_run != 'true' &&\n      needs.prepare.outputs.is_tag == 'true' &&\n      (needs.check-homebrew.outputs.exists != 'true') &&\n      needs.upload-homebrew-bottles.result == 'success'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6 # v6\n        with:\n          ref: ${{ needs.prepare.outputs.checkout_ref }}\n\n      - name: Download bottle artifacts\n        uses: actions/download-artifact@v8 # v8\n        with:\n          pattern: homebrew-bottle-*\n          path: dist/homebrew\n          merge-multiple: true\n\n      - name: Setup Git credentials\n        env:\n          GH_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}\n        run: |\n          git config --global credential.helper store\n          echo \"https://x-access-token:${GH_TOKEN}@github.com\" > ~/.git-credentials\n          git config --global user.name \"html-to-markdown-bot\"\n          git config --global user.email \"bot@kreuzberg.dev\"\n\n      - name: Update Homebrew formula with bottles\n        uses: kreuzberg-dev/actions/publish-homebrew@v1 # v1\n        with:\n          bottles-dir: dist/homebrew\n          formula-name: html-to-markdown\n          tap-repo: kreuzberg-dev/homebrew-tap\n          tag: ${{ needs.prepare.outputs.tag }}\n          version: ${{ needs.prepare.outputs.version }}\n          github-repo: kreuzberg-dev/html-to-markdown\n          dry-run: ${{ needs.prepare.outputs.dry_run }}\n"
  },
  {
    "path": ".github/workflows/validate-issues.yml",
    "content": "name: Validate Issues\n\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  validate:\n    uses: kreuzberg-dev/actions/.github/workflows/reusable-validate-issues.yml@v1\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/validate-pr.yml",
    "content": "name: Validate PR\n\non:\n  pull_request:\n    types: [opened, edited, synchronize]\n\njobs:\n  validate:\n    uses: kreuzberg-dev/actions/.github/workflows/reusable-validate-pr.yml@v1\n    secrets: inherit\n"
  },
  {
    "path": ".gitignore",
    "content": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n.pytest_cache/\n.mypy_cache/\n.ruff_cache/\nhtmlcov/\n.coverage\n.coverage.*\ncoverage.lcov\n\n# Rust\ntarget/\nCargo.lock\nrust-coverage.lcov\n*.node\nexamples/**/.cargo/config.toml\n\n# Node.js / TypeScript\nnode_modules/\n**/node_modules/\ndist/\ndist-node/\ndist-web/\n*.tsbuildinfo\n.pnpm-debug.log\npackages/html-to-markdown-ts/bin/\n\n# Ruby gem build outputs\n!packages/ruby/lib/\n!packages/ruby/lib/**/*.rb\npackages/ruby/lib/bin/\npackages/ruby/lib/*.bundle\npackages/ruby/tmp/\npackages/ruby/vendor/html-to-markdown-rs/\npackages/ruby/vendor/Cargo.toml\npackages/php-ext/workspace/\n!packages/elixir/lib/\n!packages/elixir/lib/**/*.ex\nerl_crash.dump\n\n# R package build artifacts\n!packages/r/R/\n!packages/r/R/**/*.R\npackages/r/src/*.o\npackages/r/src/*.so\npackages/r/src/*.dll\npackages/r/src/*.dylib\npackages/r/*.tar.gz\n\n# Elixir test application dependencies and builds\ntests/test_apps/elixir/deps/\ntests/test_apps/elixir/_build/\n\n# Example dependency directories\nvendor/\n**/vendor/\n**/vendor/bundle/\n.wrangler/\n\n# IDEs & AI tool configs\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n.cursorrules\n.windsurfrules\n\n# MkDocs\nsite/\n\n# OS\n.DS_Store\nThumbs.db\n\n# Benchmarks\n.benchmarks/\nbenchmark-harness-results-*/\ntools/runtime-bench/results/\ntools/benchmark-harness/results/\ntools/benchmark-harness/artifacts/\ntools/benchmark-harness/artifacts-*/\ntools/benchmark-harness/results-consolidated/\ntools/benchmark-harness/results-local-*/\nartifacts/\n\n# Cache files\n*.cache\n.cache/\npackages/php/.php-cs-fixer.cache\n\n# Temporary files\n.tmp/\n[Tt][Oo][Dd][Oo]*\n\n# Environment & virtualenvs\n.env\n.env.local\n.venv/\n**/.venv/\n\n# C# / .NET\nbin/\nobj/\n*.dll\n*.exe\n*.pdb\n\n# Allow benchmark harness entrypoint under packages/go/v2/bin\n!packages/go/v2/bin/\n!packages/go/v2/bin/benchmark.go\n\n# C FFI test binaries and build artifacts\ncrates/html-to-markdown-ffi/tests/c/test_*\n!crates/html-to-markdown-ffi/tests/c/test_*.c\ncrates/html-to-markdown-ffi/tests/c/*.o\ncrates/html-to-markdown-ffi/tests/c/*.dSYM/\n\n\n# Additional generated artifacts\n.remote-cache/\n.alef/\n.gemini/\nGEMINI.md\n\n*.pyd\nvendor/bundle/\n*.h.bak\n*.test\n*.class\n*.nupkg\npkg/\n.gems/\n\n# BEGIN ai-rulez (DO NOT EDIT - managed by ai-rulez)\n.agents/\n.claude/\n.codex/\n.cursor/\n.github/agents/\n.github/commands/\n.github/copilot-instructions.md\n.github/skills/\n.mcp.json\nAGENTS.md\nCLAUDE.md\n# END ai-rulez\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"homebrew-tap\"]\n\tpath = homebrew-tap\n\turl = https://github.com/Goldziher/homebrew-tap.git\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nrun:\n  timeout: 5m\n  issues-exit-code: 1\n  tests: true\n  concurrency: 4\n  modules-download-mode: readonly\n  allow-serial-runners: false\n  allow-parallel-runners: true\n\nlinters:\n  default: none\n  enable:\n    - errcheck\n    - govet\n    - ineffassign\n    - staticcheck\n    - unused\n    - revive\n    - gocyclo\n    - goconst\n    - gocritic\n    - gosec\n    - misspell\n    - nakedret\n  settings:\n    errcheck:\n      check-type-assertions: true\n      check-blank: true\n      exclude-functions:\n        - (net/http.ResponseWriter).Write\n        - (io.Closer).Close\n        - fmt.Fprintf\n        - fmt.Printf\n        - fmt.Println\n        - os.Setenv\n        - os.Unsetenv\n    goconst:\n      min-len: 3\n      min-occurrences: 3\n    gocyclo:\n      min-complexity: 25\n    gosec:\n      excludes:\n        - G101 # ~keep hardcoded credentials check (too many false positives)\n    govet:\n      enable-all: true\n      disable:\n        - shadow\n    misspell:\n      locale: US\n    nakedret:\n      max-func-lines: 30\n    revive:\n      confidence: 0.8\n      severity: warning\n      enable-all-rules: false\n      rules:\n        - name: blank-imports\n        - name: context-keys-type\n        - name: time-naming\n        - name: var-declaration\n        - name: unexported-return\n        - name: errorf\n        - name: context-as-argument\n        - name: dot-imports\n        - name: error-return\n        - name: error-strings\n        - name: error-naming\n        - name: if-return\n        - name: increment-decrement\n        - name: var-naming\n        - name: range\n        - name: receiver-naming\n        - name: indent-error-flow\n        - name: exported\n          disabled: true\n        - name: package-comments\n          disabled: true\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - goconst\n        path: _test\\.go\n      - linters:\n          - gocyclo\n        path: _test\\.go\n      - linters:\n          - gosec\n        path: _test\\.go\n      - linters:\n          - revive\n        path: _test\\.go\n        text: \"context-as-argument\"\n      - linters:\n          - goconst\n          - revive\n          - errcheck\n          - govet\n        path: _test\\.go\n        text: \"unusedwrite:\"\n      - linters:\n          - govet\n        text: \"fieldalignment:\"\n      - linters:\n          - errcheck\n        path: _test\\.go\n    paths:\n      - vendor\n      - build\n      - deployments\n      - third_party$\n      - builtin$\n      - examples$\n\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\n  uniq-by-line: true\n  new: false\n  exclude:\n    - \"Error return value of `\\\\(\\\\*github\\\\.com/goccy/go-json\\\\.Encoder\\\\)\\\\.Encode` is not checked\"\n    - \"Error return value of `w\\\\.Write` is not checked\"\n    - \"Error return value of `resp\\\\.Body\\\\.Close` is not checked\"\n    - \"Error return value of `res\\\\.Body\\\\.Close` is not checked\"\n    - \"Error return value of `r\\\\.Body\\\\.Read` is not checked\"\n    - \"Error return value of `os\\\\.Setenv` is not checked\"\n    - \"Error return value of `os\\\\.Unsetenv` is not checked\"\n    - 'shadow: declaration of \"err\" shadows declaration'\n    - \"unusedwrite: unused write to field\"\n    - \"Error return value of `c\\\\.provider\\\\.Delete` is not checked\"\n    - \"Error return value of `provider\\\\.Close` is not checked\"\n    - \"Error return value of `natsClient\\\\.Close` is not checked\"\n    - \"Error return value of `cacheProvider\\\\.Close` is not checked\"\n    - \"Error return value of `processor\\\\.Close` is not checked\"\n    - \"Error return value of `sub\\\\.Unsubscribe` is not checked\"\n    - \"Error return value of `json\\\\.Marshal` is not checked\"\n    - \"Error return value of `strconv\\\\.\"\n    - \"Error return value of `fmt\\\\.Sscanf` is not checked\"\n    - \"Error return value is not checked\"\n\nformatters:\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".mailmap",
    "content": "Na'aman Hirschfeld <nhirschfeld@gmail.com> Na'aman Hischfeld <nhirschfeld@gmail.com>\nNa'aman Hirschfeld <nhirschfeld@gmail.com> Test User <nhirschfeld@gmail.com>\n"
  },
  {
    "path": ".markdownlint.yaml",
    "content": "default: true\nMD007:\n  indent: 4\nMD033: false\nMD041: false\nMD013: false\nMD014: false\nMD024:\n  siblings_only: true\nMD046: false\n"
  },
  {
    "path": ".mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\n * Copyright 2007-present the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport java.net.*;\nimport java.io.*;\nimport java.nio.channels.*;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    private static final String WRAPPER_VERSION = \"3.3.4\";\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL =\n            \"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/\"\n                    + WRAPPER_VERSION\n                    + \"/maven-wrapper-\"\n                    + WRAPPER_VERSION\n                    + \".jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if(mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if(mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if(!outputFile.getParentFile().exists()) {\n            if(!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output directory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        if (System.getenv(\"MVNW_USERNAME\") != null && System.getenv(\"MVNW_PASSWORD\") != null) {\n            String username = System.getenv(\"MVNW_USERNAME\");\n            char[] password = System.getenv(\"MVNW_PASSWORD\").toCharArray();\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password);\n                }\n            });\n        }\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar\nmaven.mainClass=org.apache.maven.cli.MavenCli\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\nreturn (new Config())\n    ->setRiskyAllowed(false)\n    ->setRules([\n        '@auto' => true\n    ])\n    // 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config\n    ->setFinder(\n        (new Finder())\n            // 💡 root folder to check\n            ->in(__DIR__)\n            // 💡 additional files, eg bin entry file\n            // ->append([__DIR__.'/bin-entry-file'])\n            // 💡 folders to exclude, if any\n            // ->exclude([/* ... */])\n            // 💡 path patterns to exclude, if any\n            // ->notPath([/* ... */])\n            // 💡 extra configs\n            // ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode\n            // ->ignoreVCS(true) // true by default\n    )\n;\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "default_install_hook_types:\n  - pre-commit\n  - commit-msg\nexclude: ^docs/snippets/|vendor/|node_modules/|target/|dist/|artifacts/|scripts/ci/|\\.cache/|rust-vendor/|\\.venv/\n\nrepos:\n  # AI-Rulez: auto-generate AI assistant configuration files\n  - repo: https://github.com/Goldziher/ai-rulez\n    rev: v4.1.5\n    hooks:\n      - id: ai-rulez-generate\n\n  # Commit message linting\n  - repo: https://github.com/Goldziher/gitfluff\n    rev: v0.8.0\n    hooks:\n      - id: gitfluff-lint\n        args: [\"--write\"]\n        stages: [commit-msg]\n\n  # General file checks\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: trailing-whitespace\n        exclude: \\.github/copilot-instructions\\.md\n      - id: end-of-file-fixer\n        exclude: \\.github/copilot-instructions\\.md\n      - id: check-merge-conflict\n      - id: check-added-large-files\n        exclude: uv.lock\n      - id: detect-private-key\n      - id: check-json\n        exclude: tsconfig\\.base\\.json\n      - id: check-yaml\n        args: [\"--allow-multiple-documents\", \"--unsafe\"]\n      - id: check-toml\n      - id: check-case-conflict\n\n  # TOML formatting\n  - repo: https://github.com/tox-dev/pyproject-fmt\n    rev: \"v2.21.1\"\n    hooks:\n      - id: pyproject-fmt\n\n  - repo: https://github.com/DevinR528/cargo-sort\n    rev: \"v2.1.4\"\n    hooks:\n      - id: cargo-sort\n        args: [-w]\n\n  # Python: ruff (linting + formatting) + mypy (type checking)\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.12\n    hooks:\n      - id: ruff\n        args: [--fix]\n      - id: ruff-format\n\n  # Rust: formatting and linting (core workspace only — bindings handled by alef)\n  - repo: https://github.com/AndrejOrsula/pre-commit-cargo\n    rev: 0.5.0\n    hooks:\n      - id: cargo-fmt\n        args: [\"--all\"]\n      - id: cargo-clippy\n        args:\n          [\n            \"--fix\",\n            \"--allow-dirty\",\n            \"--allow-staged\",\n            \"--workspace\",\n            \"--exclude=html-to-markdown-php\",\n            \"--exclude=html-to-markdown-py\",\n            \"--exclude=html-to-markdown-node\",\n            \"--exclude=html-to-markdown-e2e-rust\",\n            \"--all-features\",\n            \"--all-targets\",\n            \"--\",\n            \"-D\",\n            \"warnings\",\n          ]\n\n  - repo: https://github.com/bnjbvr/cargo-machete\n    rev: v0.9.2\n    hooks:\n      - id: cargo-machete\n        args: [crates/]\n        exclude: ^(e2e/|test_apps/)\n\n  - repo: https://github.com/EmbarkStudios/cargo-deny\n    rev: 0.19.4\n    hooks:\n      - id: cargo-deny\n        args: [\"check\"]\n\n  # Node/TS/WASM: oxlint\n  - repo: https://github.com/oxc-project/mirrors-oxlint\n    rev: v1.62.0\n    hooks:\n      - id: oxlint\n        args: [\"--fix\"]\n        exclude: ^(docs/demo/|e2e/)\n\n  # cppcheck — kept upstream until shared repo ships it\n  - repo: https://github.com/pocc/pre-commit-hooks\n    rev: v1.3.5\n    hooks:\n      - id: cppcheck\n        args:\n          [\n            \"--std=c11\",\n            \"--enable=warning,style,performance\",\n            \"--suppress=missingIncludeSystem\",\n            \"--suppress=unusedStructMember\",\n            \"--suppress=normalCheckLevelMaxBranches\",\n          ]\n        files: ^crates/html-to-markdown-ffi/tests/c/\n\n  # Markdown linting\n  - repo: https://github.com/rvben/rumdl-pre-commit\n    rev: \"v0.1.86\"\n    hooks:\n      - id: rumdl-fmt\n        exclude: 'test_documents/|\\.ai-rulez/|\\.remote-cache/|e2e/|fixtures/|test_apps/|\\.github/copilot-instructions\\.md|CLAUDE\\.md|\\.claude/|\\.agents/|\\.codex/'\n\n  # Shared kreuzberg-dev polyglot hooks (shell, C/C++, Java checkstyle, Go, Python, Ruby, C#, PHP, Elixir)\n  - repo: https://github.com/kreuzberg-dev/pre-commit-hooks\n    rev: v0.1.0\n    hooks:\n      - id: shfmt\n        args: [\"-w\", \"-i\", \"2\"]\n      - id: shellcheck\n      - id: clang-format\n        args: [\"--style=file\"]\n        files: ^crates/html-to-markdown-ffi/tests/c/\n      - id: clang-tidy\n        files: ^crates/html-to-markdown-ffi/tests/c/\n      - id: checkstyle\n        args: [\"-c\", \"packages/java/checkstyle.xml\", \"-p\", \"packages/java/checkstyle.properties\"]\n        exclude: ^(\\.mvn/wrapper/|e2e/|test_apps/|packages/java/src/)\n      - id: mypy\n        exclude: \"e2e/|tests/|scripts/\"\n      - id: go-fmt\n        exclude: ^(e2e/|test_apps/)\n      - id: golangci-lint\n        exclude: ^(e2e/|test_apps/)\n        env:\n          KREUZBERG_GO_MOD_DIRS: \"packages/go\"\n      - id: govulncheck\n        exclude: ^(e2e/|test_apps/)\n      - id: rubocop\n        files: ^packages/ruby/.*\\.rb$\n        exclude: ^packages/ruby/ext/\n      - id: rubocop-lint\n        files: ^packages/ruby/.*\\.(rb|rbs)$\n        exclude: ^packages/ruby/ext/\n      - id: steep\n        files: ^packages/ruby/.*\\.(rb|rbs)$\n        exclude: ^packages/ruby/ext/\n      - id: dotnet-format\n        files: ^packages/csharp/.*\\.cs$\n      - id: dotnet-format-check\n        files: ^packages/csharp/.*\\.cs$\n      - id: php-cs-fixer\n        files: ^packages/php/.*\\.php$\n      - id: phpstan\n        files: ^packages/php/(src|tests|stubs)/.*\\.php$\n        args: [\"analyse\", \"--no-progress\", \"--configuration\", \"packages/php/phpstan.neon\"]\n      - id: mix-format\n        files: ^packages/elixir/\n      - id: mix-credo\n        files: ^packages/elixir/\n      - id: java-verify\n        files: ^packages/java/\n\n  # Alef: verify bindings and sync versions\n  - repo: https://github.com/kreuzberg-dev/alef\n    rev: v0.13.6\n    hooks:\n      - id: alef-verify\n      - id: alef-sync-versions\n\n  # GitHub Actions: linting\n  - repo: https://github.com/rhysd/actionlint\n    rev: v1.7.12\n    hooks:\n      - id: actionlint\n\n  # Java cpd — kept upstream (not yet in shared repo)\n  - repo: https://github.com/gherynos/pre-commit-java\n    rev: v0.6.37\n    hooks:\n      - id: cpd\n        exclude: ^(\\.mvn/wrapper/|e2e/|test_apps/|packages/java/src/)\n\n  # Spelling (last — runs after all formatters and generators)\n  - repo: https://github.com/crate-ci/typos\n    rev: v1.46.0\n    hooks:\n      - id: typos\n        args: [--force-exclude]\n"
  },
  {
    "path": ".ruby-version",
    "content": "3.4.8\n"
  },
  {
    "path": ".rumdl.toml",
    "content": "# rumdl — Rust-based markdown linter\n# https://github.com/rvben/rumdl\n\nrespect-gitignore = true\nexclude = [\"node_modules\", \"target\", \"dist\", \"vendor\"]\n\n# MD013: Disable line-length enforcement (tables and code blocks can be long)\n# MD041: Don't require first line to be an H1\n# MD046: Disable code block style — MkDocs tabs/admonitions indent fenced\n#        blocks, which rumdl misidentifies as indented code blocks\n# MD051: Disable cross-file link fragment checking (incompatible with MkDocs\n#        HTML processing — MkDocs strips <span> tags from heading IDs)\n# MD013: Line length (tables/code can be long)\n# MD033: Inline HTML (MkDocs uses HTML extensively)\n# MD036: Emphasis as heading (intentional style in docs/READMEs)\n# MD041: First line H1 not required\n# MD046: Code block style (MkDocs tabs indent fenced blocks)\n# MD051: Link fragment checking (incompatible with MkDocs anchor generation)\n# MD076: Blank lines between list items (intentional formatting in READMEs)\ndisable = [\n  \"MD012\",\n  \"MD013\",\n  \"MD024\",\n  \"MD033\",\n  \"MD036\",\n  \"MD041\",\n  \"MD046\",\n  \"MD051\",\n  \"MD076\",\n]\n\n# MD024: Allow duplicate heading names if they are not siblings\n[MD024]\nsiblings_only = true\n"
  },
  {
    "path": ".sdkmanrc",
    "content": "java=25.0.2-tem\nmaven=3.9.9\n"
  },
  {
    "path": ".task/README.md",
    "content": "# .task/ Directory - Modular Task Organization\n\nThis directory contains the modular Task configuration for the html-to-markdown project, following the **Kreuzberg pattern** for maintainable, scalable build automation.\n\n## Purpose\n\nThe `.task/` directory structure reduces the root `Taskfile.yml` from 838 lines to ~250 lines (66% reduction) by organizing tasks into logical modules. This approach:\n\n- **Improves Maintainability**: Each language/workflow lives in its own file\n- **Enables Reusability**: Common patterns defined once, reused everywhere\n- **Simplifies Testing**: Test individual modules independently\n- **Supports Cross-Platform**: Platform-specific logic isolated in config/\n- **Scales Gracefully**: Adding new languages doesn't bloat the root Taskfile\n\n## Directory Structure\n\n```text\n.task/\n├── config/\n│   ├── vars.yml           # Global variables, version detection, paths\n│   └── platforms.yml      # OS/arch detection, library extensions, target triples\n│\n├── languages/             # Language-specific task modules (11 total)\n│   ├── rust.yml          # Rust core library tasks\n│   ├── python.yml        # PyO3 Python bindings\n│   ├── node.yml          # NAPI-RS Node.js bindings\n│   ├── typescript.yml    # TypeScript wrapper package\n│   ├── wasm.yml          # WebAssembly bindings\n│   ├── ruby.yml          # Magnus Ruby bindings\n│   ├── php.yml           # ext-php-rs PHP extension\n│   ├── go.yml            # Go FFI wrapper\n│   ├── java.yml          # Java JNI bindings\n│   ├── csharp.yml        # C# P/Invoke wrapper\n│   └── elixir.yml        # Elixir NIF bindings\n│\n├── workflows/            # Aggregated workflow tasks (internal)\n│   ├── build.yml        # Build all languages with profile support\n│   ├── test.yml         # Test all languages (parallel/sequential)\n│   └── lint.yml         # Lint all languages with auto-fix\n│\n└── tools/               # Utility and automation tasks\n    ├── version-sync.yml # Version synchronization across manifests\n    ├── general.yml      # TOML formatting, shell linting\n    └── pre-commit.yml   # Prek pre-commit hook management (future)\n```\n\n## Configuration Files\n\n### `config/vars.yml`\n\n**Purpose**: Global variables shared across all task modules.\n\n**Key Variables**:\n\n```yaml\nVERSION:           # Extracted from Cargo.toml\nBUILD_PROFILE:     # dev/release/ci (default: release)\nOS:                # darwin/linux/windows\nARCH:              # x86_64/arm64/armv7\nNUM_CPUS:          # Detected CPU count for parallel builds\nROOT:              # Project root directory\nCRATES_DIR:        # crates/ directory\nPACKAGES_DIR:      # packages/ directory\nTARGET_DIR:        # target/ directory (Rust build outputs)\n```\n\n**Example Usage**:\n\n```yaml\n# In any language module:\ndir: \"{{.PACKAGES_DIR}}/python\"\ncmds:\n  - cargo build --profile {{.BUILD_PROFILE}}\n```\n\n### `config/platforms.yml`\n\n**Purpose**: Platform-specific detection and configuration.\n\n**Key Variables**:\n\n```yaml\nEXE_EXT:           # .exe on Windows, empty on Unix\nLIB_EXT:           # dylib/so/dll based on OS\nLIB_PREFIX:        # lib on Unix, empty on Windows\nRUST_TARGET:       # Target triple (x86_64-apple-darwin, etc.)\nRUBY_FULL_PATH:    # Full path to Ruby binary (handles Homebrew ARM64)\nIS_WINDOWS:        # Boolean: true on Windows\nIS_MACOS:          # Boolean: true on macOS\nIS_LINUX:          # Boolean: true on Linux\n```\n\n**Example Usage**:\n\n```yaml\n# Cross-platform library path configuration:\nenv:\n  LD_LIBRARY_PATH: '{{if ne .OS \"windows\"}}{{.TARGET_DIR}}/release{{end}}'\n  PATH: '{{if eq .OS \"windows\"}}{{.TARGET_DIR}}/release;{{end}}{{.PATH}}'\n```\n\n## Language Modules\n\nEach language module follows a **consistent pattern**:\n\n### Standard Tasks (All Languages)\n\n```yaml\ninstall:           # Install dependencies/toolchain\nbuild:             # Build with profile support (uses BUILD_PROFILE)\nbuild:dev:         # Debug build (fast, unoptimized)\nbuild:release:     # Release build (optimized)\nbuild:ci:          # CI build (release + debug symbols)\ntest:              # Run tests\ntest:ci:           # Run tests with coverage (CI mode)\ncoverage:          # Generate coverage reports (lcov format)\nlint:              # Lint + auto-fix (format + linters)\nlint:check:        # Check-only (no modifications, for CI)\nformat:            # Format code\nformat:check:      # Check formatting without changes\nupdate:            # Update dependencies\nclean:             # Remove build artifacts\n```\n\n### Example: `languages/python.yml`\n\n```yaml\nversion: \"3\"\ninternal: true\n\nincludes:\n  platforms: ../config/platforms.yml\n\nvars:\n  BUILD_PROFILE: \"{{.BUILD_PROFILE | default \\\"release\\\"}}\"\n  PYTHON_WORK_DIR: \"{{.PACKAGES_DIR}}/python\"\n\ntasks:\n  install:\n    desc: \"Install Python dependencies with uv\"\n    dir: \"{{.PYTHON_WORK_DIR}}\"\n    cmds:\n      - uv sync\n      - uv pip install -e .\n\n  build:\n    desc: \"Build Python bindings with maturin ({{.BUILD_PROFILE}} profile)\"\n    dir: \"{{.PYTHON_WORK_DIR}}\"\n    cmds:\n      - maturin develop{{if eq .BUILD_PROFILE \"release\"}} --release{{end}}\n\n  test:\n    desc: \"Run Python tests with pytest\"\n    dir: \"{{.PYTHON_WORK_DIR}}\"\n    cmds:\n      - pytest -v tests/\n\n  # ... additional tasks\n```\n\n### Cross-Platform Patterns\n\n**DO Use** (Cross-Platform Compatible):\n\n```yaml\n# Python for file operations:\n- cmd: |\n    python -c \"\n    import shutil, glob\n    for d in ['build', 'dist']:\n        shutil.rmtree(d, ignore_errors=True)\n    \"\n\n# Conditional environment variables:\nenv:\n  LD_LIBRARY_PATH: '{{if ne .OS \"windows\"}}{{.TARGET_DIR}}/release{{end}}'\n  PATH: '{{if eq .OS \"windows\"}}{{.TARGET_DIR}}/release;{{end}}{{.PATH}}'\n\n# Task's built-in ignore_error:\n- cmd: some-command-that-might-fail\n  ignore_error: true\n```\n\n**DON'T Use** (Platform-Specific):\n\n```yaml\n# ❌ Unix-only commands:\n- rm -rf build/ dist/\n- find . -name \"*.pyc\" -delete\n\n# ❌ Hardcoded paths:\n- /opt/homebrew/bin/ruby\n- C:\\Program Files\\Tool\\bin\n\n# ❌ Bash-specific syntax:\n- cmd: test -d .venv && source .venv/bin/activate\n```\n\n## Workflow Modules\n\nWorkflow modules aggregate language tasks into unified operations. These are **internal** (not exposed to users directly).\n\n### `workflows/build.yml`\n\n```yaml\nversion: \"3\"\ninternal: true\n\ntasks:\n  all:\n    desc: \"Build all language bindings\"\n    cmds:\n      - task: rust:build\n      - task: python:build\n      - task: node:build\n      # ... (11 languages)\n\n  all:dev:\n    desc: \"Build all in debug mode\"\n    cmds:\n      - task: rust:build:dev\n      - task: python:build:dev\n      # ...\n\n  core:\n    desc: \"Build Rust core only\"\n    cmds:\n      - task: rust:build\n\n  bindings:\n    desc: \"Build all bindings (skip core)\"\n    cmds:\n      - task: python:build\n      - task: node:build\n      # ... (exclude rust)\n```\n\n### `workflows/test.yml`\n\n```yaml\nversion: \"3\"\ninternal: true\n\ntasks:\n  all:\n    desc: \"Run all tests\"\n    cmds:\n      - task: rust:test\n      - task: python:test\n      # ... (sequential)\n\n  all:parallel:\n    desc: \"Run tests in parallel\"\n    deps:\n      - rust:test\n      - python:test\n      # ... (parallel execution)\n\n  all:ci:\n    desc: \"Run CI tests with coverage\"\n    cmds:\n      - task: rust:test:ci\n      - task: python:test:ci\n      # ...\n```\n\n## Tools Modules\n\n### `tools/version-sync.yml`\n\n**Purpose**: Synchronize version across all package manifests.\n\n```yaml\nversion: \"3\"\n\ntasks:\n  sync:\n    desc: \"Sync version from Cargo.toml to all manifests\"\n    cmds:\n      - python {{.ROOT}}/scripts/sync_versions.py\n```\n\n**Updates**:\n\n- Cargo workspace members (crates/*/Cargo.toml)\n- Python (packages/python/pyproject.toml)\n- Node.js (packages/typescript/package.json)\n- Ruby (packages/ruby/lib/html_to_markdown/version.rb)\n- PHP (packages/php/composer.json)\n- Go (packages/go/v3/version.go)\n- Java (packages/java/pom.xml)\n- C# (packages/csharp/HtmlToMarkdown.csproj)\n- Elixir (packages/elixir/mix.exs)\n- **test_apps manifests** (tests/test_apps/*/pyproject.toml, package.json, etc.)\n\n### `tools/general.yml`\n\n**Purpose**: General-purpose linting and validation tasks.\n\n```yaml\nversion: \"3\"\n\ntasks:\n  toml:format:\n    desc: \"Format TOML files\"\n    cmds:\n      - taplo format **/*.toml\n\n  toml:format:check:\n    desc: \"Check TOML formatting\"\n    cmds:\n      - taplo format --check **/*.toml\n```\n\n## How to Add a New Language\n\nLet's add **Swift** as an example:\n\n### Step 1: Create Language Module\n\n**File**: `.task/languages/swift.yml`\n\n```yaml\nversion: \"3\"\ninternal: true\n\nincludes:\n  platforms: ../config/platforms.yml\n\nvars:\n  BUILD_PROFILE: \"{{.BUILD_PROFILE | default \\\"release\\\"}}\"\n  SWIFT_WORK_DIR: \"{{.PACKAGES_DIR}}/swift\"\n\ntasks:\n  install:\n    desc: \"Install Swift dependencies\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift package resolve\n\n  build:\n    desc: \"Build Swift package ({{.BUILD_PROFILE}} profile)\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - cmd: swift build{{if eq .BUILD_PROFILE \"release\"}} -c release{{else}} -c debug{{end}}\n        ignore_error: false\n\n  build:dev:\n    desc: \"Build Swift package in debug mode\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift build -c debug\n\n  build:release:\n    desc: \"Build Swift package in release mode\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift build -c release\n\n  test:\n    desc: \"Run Swift tests\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift test\n\n  test:ci:\n    desc: \"Run Swift tests with coverage (CI mode)\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift test --enable-code-coverage\n\n  lint:\n    desc: \"Lint Swift code with auto-fix\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swiftlint --fix\n      - swiftformat .\n\n  lint:check:\n    desc: \"Lint Swift code without auto-fix\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swiftlint\n      - swiftformat --lint .\n\n  format:\n    desc: \"Format Swift code\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swiftformat .\n\n  format:check:\n    desc: \"Check Swift formatting\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swiftformat --lint .\n\n  update:\n    desc: \"Update Swift dependencies\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift package update\n\n  clean:\n    desc: \"Clean Swift build artifacts\"\n    dir: \"{{.SWIFT_WORK_DIR}}\"\n    cmds:\n      - swift package clean\n```\n\n### Step 2: Include in Root Taskfile\n\n**File**: `Taskfile.yml`\n\n```yaml\nincludes:\n  # ... existing includes\n  swift:\n    taskfile: .task/languages/swift.yml\n```\n\n### Step 3: Add to Workflow Aggregators\n\n**File**: `.task/workflows/build.yml`\n\n```yaml\ntasks:\n  all:\n    cmds:\n      - task: rust:build\n      # ... existing languages\n      - task: swift:build  # ADD THIS\n```\n\n**File**: `.task/workflows/test.yml`\n\n```yaml\ntasks:\n  all:\n    cmds:\n      - task: rust:test\n      # ... existing languages\n      - task: swift:test  # ADD THIS\n```\n\n**File**: `.task/workflows/lint.yml`\n\n```yaml\ntasks:\n  all:\n    cmds:\n      - task: rust:lint\n      # ... existing languages\n      - task: swift:lint  # ADD THIS\n```\n\n### Step 4: Update Root Taskfile Aggregates\n\n**File**: `Taskfile.yml`\n\n```yaml\ntasks:\n  setup:\n    cmds:\n      - task: rust:install\n      # ... existing installs\n      - task: swift:install  # ADD THIS\n```\n\nNow users can run:\n\n```bash\ntask swift:build\ntask swift:test\ntask swift:lint\n```\n\n## Internal vs Public Tasks\n\n### Internal Tasks\n\nDefined with `internal: true` at the file level:\n\n```yaml\nversion: \"3\"\ninternal: true  # This file's tasks are not listed in `task --list`\n```\n\n**Characteristics**:\n\n- Not visible in `task --list`\n- Only callable from other tasks\n- Used for: config files, workflow aggregators\n\n**Examples**:\n\n- `.task/config/vars.yml` (internal)\n- `.task/workflows/build.yml` (internal)\n- `.task/workflows/test.yml` (internal)\n\n### Public Tasks\n\nIncluded without `internal: true` or via root Taskfile:\n\n```yaml\nincludes:\n  rust:\n    taskfile: .task/languages/rust.yml\n    # No internal flag = public\n```\n\n**Characteristics**:\n\n- Visible in `task --list`\n- Directly callable by users\n- Used for: language modules, tool modules\n\n**Examples**:\n\n- `rust:build` (public)\n- `python:test` (public)\n- `version:sync` (public)\n\n## Best Practices\n\n### 1. Always Use Template Variables\n\n```yaml\n# ✅ Good:\ndir: \"{{.PACKAGES_DIR}}/python\"\ncmds:\n  - cargo build --profile {{.BUILD_PROFILE}}\n\n# ❌ Bad:\ndir: \"packages/python\"\ncmds:\n  - cargo build --release\n```\n\n### 2. Support All Build Profiles\n\n```yaml\n# ✅ Good: Profile-aware command\n- cmd: maturin develop{{if eq .BUILD_PROFILE \"release\"}} --release{{end}}\n\n# ❌ Bad: Hardcoded profile\n- cmd: maturin develop --release\n```\n\n### 3. Use Cross-Platform Commands\n\n```yaml\n# ✅ Good: Python for file operations\n- cmd: |\n    python -c \"\n    import shutil\n    shutil.rmtree('build', ignore_errors=True)\n    \"\n\n# ❌ Bad: Unix-only command\n- cmd: rm -rf build/\n```\n\n### 4. Include Platform Config\n\n```yaml\n# ✅ Good: Include platforms for cross-platform logic\nincludes:\n  platforms: ../config/platforms.yml\n\nenv:\n  LD_LIBRARY_PATH: '{{if ne .OS \"windows\"}}{{.TARGET_DIR}}/release{{end}}'\n\n# ❌ Bad: Hardcoded Unix assumption\nenv:\n  LD_LIBRARY_PATH: \"{{.TARGET_DIR}}/release\"\n```\n\n### 5. Consistent Task Naming\n\n```yaml\n# ✅ Good: Consistent naming with colons\ninstall:\nbuild:\nbuild:dev:\nbuild:release:\nbuild:ci:\ntest:\ntest:ci:\nlint:\nlint:check:\nformat:\nformat:check:\n\n# ❌ Bad: Inconsistent naming\ninstall_deps:\nmake_build:\nrun_tests:\ncheck-format:\n```\n\n### 6. Document Descriptions\n\n```yaml\n# ✅ Good: Clear, actionable description\ninstall:\n  desc: \"Install Python dependencies with uv\"\n\n# ❌ Bad: Vague or missing description\ninstall:\n  desc: \"Install stuff\"\n```\n\n### 7. Error Handling\n\n```yaml\n# ✅ Good: Explicit error handling\n- cmd: pytest -v tests/\n  ignore_error: false  # Fail on errors\n\n- cmd: rm -rf .cache/\n  ignore_error: true   # OK to fail (file may not exist)\n\n# ❌ Bad: Implicit behavior\n- cmd: pytest -v tests/\n```\n\n## Troubleshooting\n\n### Task Not Found\n\n**Error**: `Task \"foo:bar\" not found`\n\n**Solution**: Ensure the include is in root `Taskfile.yml`:\n\n```yaml\nincludes:\n  foo:\n    taskfile: .task/languages/foo.yml\n```\n\n### Variable Not Defined\n\n**Error**: `template: :1:2: executing \"\" at <.SOME_VAR>: map has no entry for key \"SOME_VAR\"`\n\n**Solution**: Define variable in `.task/config/vars.yml` or include platforms:\n\n```yaml\nincludes:\n  platforms: ../config/platforms.yml\n```\n\n### Cross-Platform Failures\n\n**Error**: Task works on macOS but fails on Windows\n\n**Solution**: Use conditional environment variables and cross-platform commands:\n\n```yaml\nenv:\n  PATH: '{{if eq .OS \"windows\"}}{{.TARGET_DIR}}/release;{{end}}{{.PATH}}'\ncmds:\n  - cmd: |\n      python -c \"import shutil; shutil.rmtree('build', ignore_errors=True)\"\n```\n\n### Circular Dependencies\n\n**Error**: `task: import cycle not allowed`\n\n**Solution**: Avoid including files that include each other. Use internal workflow aggregators instead.\n\n## References\n\n- **Task Documentation**: <https://taskfile.dev>\n- **Kreuzberg Pattern**: ../kreuzberg/ (sibling project)\n- **Root Taskfile**: ../Taskfile.yml\n- **Platform Config**: config/platforms.yml\n- **Global Variables**: config/vars.yml\n\n---\n\n**Last Updated**: 2025-12-28\n**Maintainers**: html-to-markdown contributors\n"
  },
  {
    "path": ".task/checksum/_lint-typescript-lint",
    "content": "5185d264d62b8f691570c5e0c226b22\n"
  },
  {
    "path": ".task/checksum/_test-typescript-test",
    "content": "b93fe0d03a54250e90b23f1a50fb35ec\n"
  },
  {
    "path": ".task/checksum/typescript-typecheck",
    "content": "99aa06d3014798d86001c324468d497f\n"
  },
  {
    "path": ".task/config/platforms.yml",
    "content": "version: \"3\"\ninternal: true\n\nincludes:\n  vars: ./vars.yml\n\nvars:\n  # Executable extension - empty for Unix, .exe for Windows\n  EXE_EXT:\n    sh: |\n      if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \".exe\"\n      else\n        echo \"\"\n      fi\n\n  # Library extension - platform specific shared library suffix\n  LIB_EXT:\n    sh: |\n      if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n        echo \"dylib\"\n      elif [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \"dll\"\n      else\n        echo \"so\"\n      fi\n\n  # Library prefix - lib for Unix-like systems, empty for Windows\n  LIB_PREFIX:\n    sh: |\n      if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \"\"\n      else\n        echo \"lib\"\n      fi\n\n  # Platform string for Rust targets\n  RUST_TARGET:\n    sh: |\n      ARCH=$(uname -m)\n      OS_TYPE=\"$OSTYPE\"\n      case \"$ARCH\" in\n        x86_64|x64)\n          ARCH_STR=\"x86_64\"\n          ;;\n        aarch64|arm64)\n          ARCH_STR=\"aarch64\"\n          ;;\n        armv7l|armv7)\n          ARCH_STR=\"armv7\"\n          ;;\n        *)\n          ARCH_STR=\"$ARCH\"\n          ;;\n      esac\n\n      if [[ \"$OS_TYPE\" == \"darwin\"* ]]; then\n        echo \"${ARCH_STR}-apple-darwin\"\n      elif [[ \"$OS_TYPE\" == \"linux-gnu\"* ]] || [[ \"$OS_TYPE\" == \"linux\"* ]]; then\n        echo \"${ARCH_STR}-unknown-linux-gnu\"\n      elif [[ \"$OS_TYPE\" == \"msys\" ]] || [[ \"$OS_TYPE\" == \"cygwin\" ]] || [[ \"$OS_TYPE\" == \"win32\" ]]; then\n        echo \"${ARCH_STR}-pc-windows-msvc\"\n      else\n        echo \"${ARCH_STR}-unknown-unknown\"\n      fi\n\n  # Boolean platform checks (imported from vars.yml)\n  IS_WINDOWS: \"{{.IS_WINDOWS}}\"\n  IS_MACOS: \"{{.IS_MACOS}}\"\n  IS_LINUX: \"{{.IS_LINUX}}\"\n\n  # Ruby path detection - handles Homebrew ARM64 and standard installations\n  RUBY_FULL_PATH:\n    sh: |\n      if command -v ruby >/dev/null 2>&1; then\n        command -v ruby\n      elif [[ \"$OSTYPE\" == \"darwin\"* ]] && [[ -f \"/opt/homebrew/opt/ruby/bin/ruby\" ]]; then\n        echo \"/opt/homebrew/opt/ruby/bin/ruby\"\n      else\n        echo \"ruby\"\n      fi\n\n  # Convenient binary paths for platform-specific tools\n  CARGO_BIN:\n    sh: command -v cargo 2>/dev/null || echo \"cargo\"\n\n  RUSTC_BIN:\n    sh: command -v rustc 2>/dev/null || echo \"rustc\"\n\n  # Shell script extension for platform-specific scripts\n  SHELL_EXT:\n    sh: |\n      if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \".ps1\"\n      else\n        echo \".sh\"\n      fi\n"
  },
  {
    "path": ".task/config/vars.yml",
    "content": "version: \"3\"\ninternal: true\n\nvars:\n  # Version extraction from Cargo.toml (workspace.package.version)\n  VERSION:\n    sh: grep -m 1 'version = ' Cargo.toml | sed 's/version = \"\\(.*\\)\"/\\1/'\n\n  # Build profile (dev/release/ci) - default to release\n  BUILD_PROFILE: \"{{.BUILD_PROFILE | default \\\"release\\\"}}\"\n\n  # Toolchain versions\n  GOLANGCI_LINT_VERSION: \"latest\"\n  GO_TOOLCHAIN: \"go1.26.0\"\n  BUNDLER_VERSION: \"4.0.0\"\n  RUBY_BIN:\n    sh: |\n      if command -v ruby >/dev/null 2>&1; then\n        dirname \"$(command -v ruby)\"\n      elif [[ \"$OSTYPE\" == \"darwin\"* ]] && [[ -d \"/opt/homebrew/opt/ruby/bin\" ]]; then\n        echo \"/opt/homebrew/opt/ruby/bin\"\n      else\n        echo \"ruby\"\n      fi\n\n  # Logging\n  RUST_LOG: \"info\"\n\n  # Root project directories (absolute paths)\n  ROOT: \"{{.ROOT_DIR}}\"\n  CRATES_DIR: \"{{.ROOT_DIR}}/crates\"\n  PACKAGES_DIR: \"{{.ROOT_DIR}}/packages\"\n  SCRIPTS_DIR: \"{{.ROOT_DIR}}/scripts\"\n  TOOLS_DIR: \"{{.ROOT_DIR}}/tools\"\n  TARGET_DIR: \"{{.ROOT_DIR}}/target\"\n  EXAMPLES_DIR: \"{{.ROOT_DIR}}/examples\"\n\n  # OS Detection - determine operating system\n  OS:\n    sh: |\n      case \"$(uname -s 2>/dev/null || echo 'unknown')\" in\n        Darwin*)\n          echo \"darwin\"\n          ;;\n        Linux*)\n          echo \"linux\"\n          ;;\n        MINGW*|MSYS*|CYGWIN*)\n          echo \"windows\"\n          ;;\n        *)\n          if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n            echo \"darwin\"\n          elif [[ \"$OSTYPE\" == \"linux-gnu\"* ]] || [[ \"$OSTYPE\" == \"linux\"* ]]; then\n            echo \"linux\"\n          elif [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n            echo \"windows\"\n          else\n            echo \"unknown\"\n          fi\n          ;;\n      esac\n\n  # OS Boolean helpers\n  IS_WINDOWS:\n    sh: |\n      if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \"true\"\n      else\n        echo \"false\"\n      fi\n\n  IS_MACOS:\n    sh: |\n      if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n        echo \"true\"\n      else\n        echo \"false\"\n      fi\n\n  IS_LINUX:\n    sh: |\n      if [[ \"$OSTYPE\" == \"linux-gnu\"* ]] || [[ \"$OSTYPE\" == \"linux\"* ]]; then\n        echo \"true\"\n      else\n        echo \"false\"\n      fi\n\n  # Architecture detection - determine CPU architecture\n  ARCH:\n    sh: |\n      ARCH=$(uname -m)\n      case \"$ARCH\" in\n        x86_64|x64)\n          echo \"x86_64\"\n          ;;\n        aarch64|arm64)\n          echo \"arm64\"\n          ;;\n        armv7l|armv7)\n          echo \"armv7\"\n          ;;\n        armv6l|armv6)\n          echo \"armv6\"\n          ;;\n        i686|i386)\n          echo \"i386\"\n          ;;\n        *)\n          echo \"$ARCH\"\n          ;;\n      esac\n\n  # Number of CPUs available\n  NUM_CPUS:\n    sh: |\n      if command -v nproc >/dev/null 2>&1; then\n        nproc\n      elif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n        sysctl -n hw.ncpu\n      elif [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"cygwin\" ]] || [[ \"$OSTYPE\" == \"win32\" ]]; then\n        echo \"${NUMBER_OF_PROCESSORS:-4}\"\n      else\n        echo \"4\"\n      fi\n\n  # GNU Make parallel flag for optimal builds\n  MAKE_JOBS: \"{{.NUM_CPUS}}\"\n"
  },
  {
    "path": ".task/languages/python.yml",
    "content": "version: \"3\"\ninternal: true\n\nvars:\n  BUILD_PROFILE: \"{{.BUILD_PROFILE | default \\\"release\\\"}}\"\n  PYTHON_PKG: \"packages/python\"\n\ntasks:\n  install:\n    desc: \"Install Python dependencies with uv\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && uv sync\n      - cd {{.PYTHON_PKG}} && uv pip install -e .\n\n  build:\n    desc: \"Build Python bindings with maturin ({{.BUILD_PROFILE}} profile)\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && maturin develop{{if eq .BUILD_PROFILE \"release\"}} --release{{end}}\n\n  build:dev:\n    desc: \"Build Python bindings in debug mode\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && maturin develop\n\n  build:release:\n    desc: \"Build Python bindings in release mode\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && maturin develop --release\n\n  build:ci:\n    desc: \"Build Python bindings for CI (release with debug info)\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && maturin develop --release\n\n  wheel:\n    desc: \"Build Python wheel distribution\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && maturin build --release\n\n  coverage:\n    desc: \"Generate Python code coverage report (lcov format)\"\n    cmds:\n      - cd {{.PYTHON_PKG}} && uv run pytest -v --cov=. --cov-report=lcov:coverage.lcov tests/\n\n  clean:\n    desc: \"Clean Python build artifacts\"\n    cmds:\n      - cmd: |\n          cd {{.PYTHON_PKG}} && python -c \"\n          import shutil, glob\n          dirs = ['__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', 'dist', 'build', '.maturin']\n          for d in dirs:\n              shutil.rmtree(d, ignore_errors=True)\n          for f in glob.glob('*.egg-info'):\n              shutil.rmtree(f, ignore_errors=True)\n          \"\n        ignore_error: true\n"
  },
  {
    "path": ".task/languages/rust.yml",
    "content": "version: \"3\"\ninternal: true\n\nincludes:\n  platforms: ../config/platforms.yml\n\nvars:\n  RUST_LOG: \"{{.RUST_LOG | default \\\"info\\\"}}\"\n  BUILD_PROFILE: \"{{.BUILD_PROFILE | default \\\"release\\\"}}\"\n  RUST_BACKTRACE: \"{{.RUST_BACKTRACE | default \\\"1\\\"}}\"\n  CARGO_TERM_COLOR: \"always\"\n\ntasks:\n  install:\n    desc: \"Install Rust toolchain and components (rustup, cargo)\"\n    silent: false\n    cmds:\n      - rustup update stable\n      - rustup component add rustfmt clippy\n      - rustup component add llvm-tools-preview\n      - cargo install cargo-llvm-cov --locked\n      - cargo install cargo-upgrades --locked\n      - cargo --version\n      - rustc --version\n\n  build:\n    desc: \"Build all Rust crates with {{.BUILD_PROFILE}} profile\"\n    silent: false\n    cmds:\n      - cmd: |\n          cargo build --workspace --profile {{.BUILD_PROFILE}} -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  build:dev:\n    desc: \"Build all Rust crates in debug mode\"\n    silent: false\n    cmds:\n      - cmd: |\n          cargo build --workspace -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  build:release:\n    desc: \"Build all Rust crates in release mode\"\n    silent: false\n    cmds:\n      - cmd: |\n          cargo build --workspace --release -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  build:ci:\n    desc: \"Build for CI with debug info enabled (no strip)\"\n    silent: false\n    cmds:\n      - cmd: |\n          CARGO_PROFILE_RELEASE_DEBUG=2 CARGO_PROFILE_RELEASE_STRIP=none cargo build --workspace --exclude html-to-markdown-php --release -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  test:\n    desc: \"Run Rust test suite\"\n    silent: false\n    cmds:\n      - cmd: |\n          RUST_LOG={{.RUST_LOG}} RUST_BACKTRACE={{.RUST_BACKTRACE}} cargo test --release --no-default-features --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  test:ci:\n    desc: \"Run tests with coverage for CI (generates lcov)\"\n    silent: false\n    cmds:\n      - cmd: |\n          {{if eq OS \"windows\"}}\n          RUST_LOG={{.RUST_LOG}} RUST_BACKTRACE={{.RUST_BACKTRACE}} cargo llvm-cov --features metadata,visitor,inline-images --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php --exclude benchmark-harness --lcov --output-path rust-coverage.lcov -j {{.NUM_CPUS}}\n          {{else}}\n          RUST_LOG={{.RUST_LOG}} RUST_BACKTRACE={{.RUST_BACKTRACE}} cargo llvm-cov --all-features --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php --exclude benchmark-harness --lcov --output-path rust-coverage.lcov -j {{.NUM_CPUS}}\n          {{end}}\n        ignore_error: false\n      - cmd: |\n          {{if eq OS \"windows\"}}\n          cargo llvm-cov --features metadata,visitor,inline-images --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php --exclude benchmark-harness --summary-only\n          {{else}}\n          cargo llvm-cov --all-features --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php --exclude benchmark-harness --summary-only\n          {{end}}\n        ignore_error: false\n\n  coverage:\n    desc: \"Generate code coverage report (lcov format)\"\n    silent: false\n    cmds:\n      - cmd: |\n          RUST_LOG={{.RUST_LOG}} cargo llvm-cov --all-features --workspace --exclude html-to-markdown-py --exclude html-to-markdown-php --exclude benchmark-harness --exclude html-to-markdown-wasm-wasmtime-tests --lcov --output-path rust-coverage.lcov -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  lint:\n    desc: \"Lint Rust code WITH auto-fix (cargo fmt + cargo clippy --fix)\"\n    silent: false\n    cmds:\n      - cmd: cargo fmt --all\n        ignore_error: false\n      - cmd: |\n          cargo clippy --workspace --fix --allow-dirty --allow-staged -j {{.NUM_CPUS}}\n        ignore_error: false\n\n  lint:check:\n    desc: \"Lint Rust code WITHOUT auto-fix (check-only)\"\n    silent: false\n    cmds:\n      - cmd: cargo fmt --all --check\n        ignore_error: false\n      - cmd: |\n          cargo clippy -j {{.NUM_CPUS}} --workspace -- -D warnings\n        ignore_error: false\n\n  format:\n    desc: \"Format Rust code (with modifications)\"\n    silent: false\n    cmds:\n      - cmd: cargo fmt --all\n        ignore_error: false\n\n  format:check:\n    desc: \"Check Rust formatting without modifications\"\n    silent: false\n    cmds:\n      - cmd: cargo fmt --all --check\n        ignore_error: false\n\n  update:\n    desc: \"Update Rust dependencies within major versions (cargo update)\"\n    silent: false\n    cmds:\n      - cmd: cargo update\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/ruby/ext/html_to_markdown_rb/Cargo.toml\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/elixir/native/html_to_markdown_nif/Cargo.toml\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/r/src/rust/Cargo.toml\n        ignore_error: false\n\n  upgrade:\n    desc: \"Upgrade Rust dependencies to latest including breaking changes (cargo upgrade --incompatible + cargo update)\"\n    silent: false\n    cmds:\n      - cmd: cargo upgrade --incompatible\n        ignore_error: false\n      - cmd: cargo update\n        ignore_error: false\n      - cmd: cargo upgrade --incompatible --manifest-path packages/ruby/ext/html_to_markdown_rb/Cargo.toml\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/ruby/ext/html_to_markdown_rb/Cargo.toml\n        ignore_error: false\n      - cmd: cargo upgrade --incompatible --manifest-path packages/elixir/native/html_to_markdown_nif/Cargo.toml\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/elixir/native/html_to_markdown_nif/Cargo.toml\n        ignore_error: false\n      - cmd: cargo upgrade --incompatible --manifest-path packages/r/src/rust/Cargo.toml\n        ignore_error: false\n      - cmd: cargo update --manifest-path packages/r/src/rust/Cargo.toml\n        ignore_error: false\n\n  clean:\n    desc: \"Clean Rust build artifacts\"\n    silent: false\n    cmds:\n      - cmd: cargo clean\n        ignore_error: false\n\n  doc:\n    desc: \"Generate and open Rust documentation\"\n    silent: false\n    cmds:\n      - cmd: |\n          cargo doc --workspace --all-features --no-deps --open\n        ignore_error: false\n\n  e2e:generate:\n    desc: \"Generate E2E tests from fixtures using alef\"\n    silent: false\n    cmds:\n      - cmd: alef e2e generate --lang rust\n        ignore_error: false\n\n  e2e:test:\n    desc: \"Run Rust E2E tests in e2e/rust directory\"\n    silent: false\n    cmds:\n      - cmd: cargo test --manifest-path e2e/rust/Cargo.toml\n        ignore_error: false\n"
  },
  {
    "path": ".task/tools/docs.yml",
    "content": "version: '3'\n\ntasks:\n  generate-readme:\n    desc: Generate package READMEs using alef\n    cmds:\n      - alef readme\n\n  generate-readme:check:\n    desc: Validate READMEs match generated output (CI mode)\n    cmds:\n      - alef readme\n      - git diff --exit-code -- packages/*/README.md crates/*/README.md\n\n  generate-docs:\n    desc: Generate API reference documentation using alef\n    cmds:\n      - alef docs\n\n  generate-docs:check:\n    desc: Validate API docs match generated output (CI mode)\n    cmds:\n      - alef docs\n      - git diff --exit-code -- docs/reference/\n"
  },
  {
    "path": ".task/tools/general.yml",
    "content": "version: \"3\"\ninternal: true\n\nincludes:\n  platforms: ../config/platforms.yml\n\nvars:\n  SCRIPTS_DIR: \"{{.TASKFILE_DIR}}/../../scripts\"\n\ntasks:\n  pre-commit:install:\n    desc: \"Install prek pre-commit hooks for commit and commit-msg\"\n    silent: false\n    cmds:\n      - cmd: prek install\n        ignore_error: false\n      - cmd: prek install --hook-type commit-msg\n        ignore_error: false\n\n  pre-commit:run:\n    desc: \"Run prek pre-commit hooks on all files\"\n    silent: false\n    cmds:\n      - cmd: prek run --all-files\n        ignore_error: false\n\n  pre-commit:uninstall:\n    desc: \"Uninstall prek hooks\"\n    silent: false\n    cmds:\n      - cmd: prek uninstall\n        ignore_error: true\n      - cmd: prek uninstall --hook-type commit-msg\n        ignore_error: true\n\n  validate:config:\n    desc: \"Validate YAML task configuration files\"\n    silent: false\n    cmds:\n      - cmd: |\n          for file in {{.TASKFILE_DIR}}/**/*.yml; do\n            echo \"Validating $file...\"\n            if ! command -v yamllint &> /dev/null; then\n              echo \"yamllint not found, skipping validation\"\n              break\n            fi\n            yamllint \"$file\" || exit 1\n          done\n        ignore_error: false\n\n  validate:all:\n    desc: \"Validate all project configurations\"\n    silent: false\n    cmds:\n      - task: validate:config\n"
  },
  {
    "path": ".task/tools/version-sync.yml",
    "content": "version: \"3\"\ninternal: true\n\nincludes:\n  platforms: ../config/platforms.yml\n\nvars:\n  # Use installed alef binary. Install via: cargo binstall alef-cli\n  # For local dev with sibling repo: cargo run --manifest-path ../alef/Cargo.toml --\n  ALEF: \"alef\"\n\ntasks:\n  sync:\n    desc: \"Synchronize version across all package manifests and regenerate everything\"\n    cmds:\n      - \"{{.ALEF}} sync-versions\"\n      - \"{{.ALEF}} readme\"\n      - \"{{.ALEF}} docs\"\n      - \"{{.ALEF}} generate --clean\"\n      - \"{{.ALEF}} stubs\"\n      - \"{{.ALEF}} e2e generate\"\n\n  check:\n    desc: \"Check if versions are synchronized (dry-run)\"\n    cmds:\n      - cmd: grep -m 1 'version = ' Cargo.toml | sed 's/version = \"\\(.*\\)\"/\\1/'\n        silent: false\n\n  bump:major:\n    desc: \"Bump major version (X.0.0) and sync\"\n    cmds:\n      - \"{{.ALEF}} sync-versions --bump major\"\n      - task: sync\n\n  bump:minor:\n    desc: \"Bump minor version (0.X.0) and sync\"\n    cmds:\n      - \"{{.ALEF}} sync-versions --bump minor\"\n      - task: sync\n\n  bump:patch:\n    desc: \"Bump patch version (0.0.X) and sync\"\n    cmds:\n      - \"{{.ALEF}} sync-versions --bump patch\"\n      - task: sync\n\n  show:\n    desc: \"Show current version from Cargo.toml\"\n    cmds:\n      - cmd: grep -m 1 'version = ' Cargo.toml | sed 's/version = \"\\(.*\\)\"/\\1/'\n        silent: false\n"
  },
  {
    "path": ".task/workflows/e2e.yml",
    "content": "version: \"3\"\n\ntasks:\n  generate:all:\n    desc: Generate all E2E tests from fixtures across all supported languages\n    cmds:\n      - alef e2e generate\n\n  test:all:\n    desc: Run all E2E tests across all supported languages\n    cmds:\n      - alef test --e2e\n\n  lint:all:\n    desc: Lint all generated E2E test code\n    cmds:\n      - alef lint\n\n  verify:all:\n    desc: Full E2E pipeline - generate, lint, and test all suites\n    cmds:\n      - alef e2e generate\n      - alef lint\n      - alef test --e2e\n\n  generate:rust:\n    desc: Generate Rust E2E tests from fixtures\n    cmds:\n      - alef e2e generate --lang rust\n\n  test:rust:\n    desc: Run Rust E2E tests\n    cmds:\n      - task: rust:e2e:test\n\n  quick:\n    desc: Run quick E2E tests (Rust only)\n    cmds:\n      - task: rust:e2e:test\n"
  },
  {
    "path": ".typos.toml",
    "content": "[files]\nextend-exclude = [\"target/\", \".alef/\", \"*.lock\", \"*.min.js\"]\n\n[default.extend-words]\n# Add project-specific words here\n# crate_name = \"crate_name\"\n"
  },
  {
    "path": "ATTRIBUTIONS.md",
    "content": "# Attributions\n\nThis project includes vendored code from third-party libraries. This file\nprovides the required attribution and license information.\n\n## markup5ever_rcdom\n\n- **Version vendored**: 0.36.0+unofficial\n- **Original authors**: The html5ever Project Developers\n- **Repository**: <https://github.com/servo/html5ever>\n- **Vendored into**: `crates/html-to-markdown/src/rcdom.rs`\n- **License**: MIT OR Apache-2.0\n\n### MIT License\n\n```text\nCopyright (c) 2014 The html5ever Project Developers\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n```\n\n### Apache License, Version 2.0\n\n```text\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n```\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to html-to-markdown will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Fixed\n\n- **Visitor element start/end sequence for hyphenated tags (#331)** — the `repair_with_html5ever` fallback (triggered when input contains custom-element / hyphenated tag names) re-parsed the document under HTML5 semantics, which discard XML-style self-closing on unknown elements. As a result, `<ac:parameter ... />` was treated as an open tag and subsequent siblings nested inside it, breaking the visitor's pre-order/post-order start/end pairing. The repair path now pre-expands XML-style self-closing tags on non-void elements to explicit open+close pairs before the HTML5 parse, so visitor events remain correctly paired for hyphenated/namespaced custom tags.\n\n- **`default-features = false` build broken (#332)** — bare `#[serde(...)]` and `#[derive(Serialize, Deserialize)]` on core types in `src/types/{document,tables,result,warnings}.rs` and `src/options/conversion.rs` are now feature-gated behind `#[cfg_attr(feature = \"serde\", ...)]`. CI now runs `cargo check --no-default-features` matrix to prevent regressions.\n\n- **Ruby `TypeError` on `convert()` with options** (#334) — `HtmlToMarkdown.convert(html, options)`\n  raised `TypeError` on every call that supplied options (including `3.4.0.pre.rc.15`). The Ruby\n  wrapper was passing a `ConversionOptions` object to the FFI, but the generated Rust function\n  expects `Option<String>` (JSON). The wrapper now serialises the options hash to JSON before\n  crossing the FFI boundary, matching what `serde_json::from_str` expects on the Rust side.\n\n## [3.3.3] - 2026-04-23\n\n### Fixed\n\n- **Python enum KeyError** (#324) — `ConversionOptions()` with default enums no longer crashes; PyO3 enum fields are passed directly instead of broken `str()` + map lookup.\n- **Ruby Magnus binding** — fixed 65 compilation errors: `funcall` API, visitor bridge args, `Vec` conversion, optional flattening, sanitized field serde round-trip.\n- **Elixir `.formatter.exs`** — 120-char line length, generated code now passes `mix format --check-formatted`.\n- **Unused deps** — removed `serde_json` from Node and WASM binding crates.\n- **Checkstyle** — excluded `test_apps/` from pre-commit checkstyle hook.\n\n## [3.3.2] - 2026-04-23\n\n### Fixed\n\n- **Elixir visitor bridge** — implemented async thread-based visitor protocol using `rustler::thread::spawn` + `OwnedEnv::send_and_clear` + `mpsc` channels, replacing the impossible synchronous `env.call()` approach.\n- **Elixir NIF rustler 0.37** — replaced removed `SavedTerm`, `is_nil()`, `Pid::spawn_monitor`, `.encode()` APIs with 0.37-compatible equivalents.\n- **Elixir type conversions** — fixed double-optional wrapping (`map(Some)`) and ambiguous `From` impl in generated `_from` methods.\n- **Java checkstyle** — added `maven-checkstyle-plugin` to pom.xml pointing to project `checkstyle.xml` (120-char limit), so `mvn checkstyle:check` uses our config instead of default Sun checks.\n- **Ruby Rakefile** — explicit `Bundler::GemHelper.install_tasks name:` for Bundler 4 compatibility.\n\n## [3.3.1] - 2026-04-23\n\n### Fixed\n\n- **Java checkstyle** — switched to 120-char line limit, added Spotless auto-formatting with Eclipse JDT formatter, added `final` params and javadoc to all generated code.\n- **Elixir `list` type collision** — `NodeContent::List` variant no longer redefines Elixir's built-in `list/0` type (now emits `list_variant`).\n- **Elixir NIF missing `serde`** — added `serde` with derive feature as direct dependency to the NIF crate.\n- **C# `VisitResult.Continue`** — default visitor methods now use `new VisitResult.Continue()` instead of non-invocable `VisitResult.Continue()`.\n- **Node `convert` export** — restored the missing `#[napi] pub fn convert` function dropped during binding regeneration.\n- **Ruby CI** — updated Bundler from 2.7.2 to 4.0.3 to match `Gemfile.lock`.\n\n## [3.3.0] - 2026-04-23\n\n### Added\n\n- **`exclude_selectors` option** — CSS selector-based element exclusion. Unlike `strip_tags` (which removes the wrapper but keeps children), excluded elements and all descendants are dropped entirely. Supports any CSS selector: `.class`, `#id`, `[attribute]`, compound selectors. Works in both markdown and plain text output modes.\n- **CLI flags** — `--preserve-tags`, `--skip-images`, `--max-depth` for full ConversionOptions parity.\n- **Visitor pattern for all bindings** (#314, #313) — restored visitor support across Python, TypeScript, Ruby, PHP, Go, Java, C#, Elixir, R, WASM, and C FFI.\n- **R visitor support** — added visitor callbacks for the R binding.\n- **E2E test fixtures** — 78 new fixtures for 100% ConversionOptions field coverage (35/35 fields). Added fixtures for `exclude_selectors`, `ConversionResult.tables`, and `ConversionResult.warnings`.\n- **Ruby RBS type stubs** — auto-generated via alef from the Rust IR, including `VERSION` constant. Gemspec now includes `sig/**/*`.\n- **Alef pre-commit hook** — `alef-verify` hook added to `.pre-commit-config.yaml` to check generated code freshness. CI installs alef v0.5.3 binary.\n\n### Fixed\n\n- **`<h1>` inside `<header>` not exported** (#321) — top-level `<header>` elements were unconditionally dropped during preprocessing; now only `<header>` with navigation hints (e.g. `class=\"site-header\"`, `role=\"navigation\"`) is removed.\n- **`PreprocessingPreset` not wired into preprocessing logic** — the `preset` field on `PreprocessingOptions` was defined but never checked. Now Minimal/Standard/Aggressive presets have distinct behavior.\n- **`remove_forms` flag was dead code** — `<form>` elements are now dropped when `remove_forms: true` and preset is Standard or Aggressive.\n- **Aggressive preset** — now drops navigation-hinted elements of any tag type, `<noscript>` elements, and noise-hinted elements (cookie banners, ad containers).\n- **Python `NodeContent` type** — binding wrapper now implements `Default`, `Serialize`, `Deserialize` via forwarding to core type, fixing compilation when `DocumentNode` (which contains `NodeContent`) derives these traits.\n- **Python `dict[str, Any]` for data enums** — `NodeContent`, `AnnotationKind`, `VisitResult` now use TypedDicts with `Literal` discriminators instead of untyped dicts.\n- **Python `__init__.py` exports** — all public types now exported from the package.\n- **`ImageMetadata.dimensions`** — tuple type `(u32, u32)` correctly maps to `Vec<u32>` / `number[]` / `[]uint32` in all bindings via serde round-trip conversion.\n- **FFI doc comments** — multi-line doc strings on VTable struct fields now properly prefixed with `///` on every line (was breaking `cargo fmt`).\n- **FFI optional parameters** — visitor methods with optional string params (`title`, `id`, `lang`) correctly generate `Option<&str>` instead of `&str`.\n- **FFI duplicate imports** — removed duplicate `use std::ffi::...` in trait bridge output.\n- **Go `htm_convert` stub** — FFI function was always returning \"Not implemented\"; now delegates to `core::convert(html, options, None)`.\n- **Go lint errors** — fixed CGO preamble types, removed invalid `?` syntax, fixed parameter syntax, `NodeType` mapping, variable shadowing, gofmt indentation.\n- **Java FFI broken on all platforms** (#315) — native libraries were bundled under wrong JAR path.\n- **Java/C# visitor type conflicts** — fixed by skipping gen_bindings types when visitor bridge is active.\n- **Ruby `convert()` TypeError** (#319) — options type mismatch and wrong return type.\n- **PHP binding panics** — `from_update` and `from` methods now emit safe return values instead of `panic!()`.\n- **R `ConversionOptionsBuilder`** — fixed `todo!()` panics in opaque type delegation.\n- **CLI `autolinks` default** — replaced `--autolinks` with `--no-autolinks` so defaults match library.\n- **CLI dead metadata flags** — removed flags that were parsed but never wired through.\n- **Python 3.14 wheels** (#322) — enabled `abi3-py310` stable ABI for PyO3 crate, so a single wheel works on Python 3.10 through 3.14+ without per-version builds.\n\n### Changed\n\n- **Public API surface restricted** — internal Rust modules changed from `pub` to `pub(crate)`. API docs now document only the public API (down from 233 to 66 items).\n- **alef.toml simplified** — switched from 20-item include whitelist to minimal 3-type + 1-function include. Transitive dependencies expand automatically.\n- **Generated code lint-clean** — all generated bindings pass their respective language linters (`cargo fmt`, `cargo clippy`, `mypy`, `ruff`, `gofmt`, `dotnet format`, `mix format`, `steep check`, `rubocop`, `phpstan`, `biome`).\n- **Dead code removed** — deleted unused dispatch functions, duplicate text utilities, and unused safety module from core crate.\n- **CI consolidated** (#317) — 13 workflows merged into single `ci.yaml`.\n\n## [3.2.6] - 2026-04-20\n\n### Fixed\n\n- **Python type mismatch** — `convert()` return type annotation now uses the public `ConversionResult` instead of `_rust.ConversionResult`, fixing Pylance type errors when annotating with the re-exported type (#310).\n- **Maturin build failure** — removed `readme = \"README.md\"` from binding crate Cargo.toml files (ffi, node, php, py, wasm) since no README exists for these internal crates (#309).\n\n### Changed\n\n- **PHPUnit** — bumped from `^12.5` to `^13.1` across root, e2e, and test_apps.\n- **Pre-commit shfmt hook** — fixed rev from non-existent `v3.14.2-1` to `v3.9.0-1`.\n- **Pre-commit ai-rulez hook** — updated from `v3.14.0` to `v3.14.2`.\n\n## [3.2.5] - 2026-04-18\n\n### Fixed\n\n- **Silent truncation on large HTML inputs** (#277) — fixed preprocessing pipeline gap where `strip_script_and_style_tags` was not called after `repair_with_html5ever`, causing documents with custom elements and scripts containing literal `<script>` strings to be silently truncated. Also fixed `preprocess_html` fallback to skip only the problematic tag instead of consuming the rest of the document.\n- **WASM plain-object options** (#303) — WASM package now exports a wrapper `convert()` function that accepts plain JavaScript objects for options, eliminating the need to construct `WasmConversionOptions` class instances. The raw WASM classes remain available for advanced use.\n\n### Added\n\n- **`max_depth` option** — new `max_depth` field on `ConversionOptions` to limit DOM traversal depth, preventing stack overflow on deeply nested or malicious HTML. Default is `None` (unlimited). When set, subtrees beyond the limit are silently truncated.\n\n### Removed\n\n- Phantom type references (`MetadataResult`, `HeadingInfo`, `LinkInfo`, `ImageInfo`) from alef configuration that did not correspond to any Rust types.\n\n## [3.2.4] - 2026-04-17\n\n### Fixed\n\n- **Elixir Hex package** — fixed precompiled NIF pipeline: correct build output paths, removed Rust source from Hex files list, removed Windows target (no build job), simplified build script for `rustler_precompiled`.\n- **C# NuGet package** — republished with `htm_conversion_options_from_json` and related FFI functions.\n\n## [3.2.3] - 2026-04-17\n\n### Fixed\n\n- **Java/C#/Go FFI functions** — `htm_conversion_options_from_json`, `htm_preprocessing_options_from_json`, and related `to_json` functions now generated. Fixed alef IR extraction to detect serde derives inside `#[cfg_attr(...)]` attributes.\n- **Node.js native binding loader** — regenerated `index.js` with correct NAPI platform-aware loader (was referencing old `html-to-markdown-rs.node` binary name).\n- **Go module path** — fixed from non-existent `github.com/kreuzberg-dev/html-to-markdown-go` to monorepo path `github.com/kreuzberg-dev/html-to-markdown/packages/go/v3`.\n- **Elixir precompiled NIFs** — switched from `Rustler` (compile-from-source) to `RustlerPrecompiled` with CI jobs for building and uploading platform-specific NIF binaries to GitHub releases.\n\n### Removed\n\n- Stale hand-written test files superseded by alef-generated e2e tests (comprehensive_test, feature_test, smoke_test duplicates across Go, Python, Ruby, PHP, Node, WASM, Elixir, R).\n- Empty placeholder crate directories (`html-to-markdown-rs-ffi`, `html-to-markdown-rs-wasm`).\n- Duplicate Ruby extension directory (`html-to-markdown_rb` with wrong naming).\n\n## [3.2.2] - 2026-04-16\n\n### Fixed\n\n- **Ruby binding compilation** — fixed serde derive errors and Default trait conflicts by conditionally deriving serde traits only when all field types support it, and generating Default derives for kwargs constructors.\n- **Ruby deprecated Magnus API** — replaced `magnus::exception::type_error()` / `runtime_error()` with `Ruby::exception_type_error()` / `Ruby::exception_runtime_error()` (Magnus 0.7+ API).\n- **PHP e2e tests** — fixed missing class import causing \"Class not found\" fatal error.\n- **Cargo.toml metadata** — added missing `readme`, `keywords`, `categories`, `description` fields to binding crate Cargo.toml files.\n- **C# dotnet format** — auto-formatted generated C# bindings.\n\n## [3.2.1] - 2026-04-16\n\n### Fixed\n\n- **Node.js Docker/cross-platform installs** (#273) — platform-specific native packages (`@kreuzberg/html-to-markdown-node-linux-x64-gnu`, etc.) are now correctly published with `optionalDependencies` via NAPI prepublish, resolving cross-platform lockfile issues.\n- **Homebrew formula** (#304) — formula updated with correct source tarball SHA and bottle configuration.\n- **Ruby gem build failure** — fixed `NodeContent::MetadataBlock` type mismatch (`Vec<(String, String)>` → `String`) in binding-to-core conversion by deserializing sanitized fields from JSON.\n- **Maven Central publish** — aligned `pom.xml` with kreuzberg: GPG plugin in main build section, developer email, correct `groupId` (`dev.kreuzberg`), `pluginManagement` with pinned plugin versions.\n- **All binding compilation failures** — fixed private module path (`convert_api`) in generated bindings, missing `From` impls for function return types, glob import conflicts in PyO3/FFI backends, and cbindgen compatibility (removed `const extern fn`, updated to cbindgen 0.29).\n- **Elixir NIF compilation** — added `compilers: [:rustler] ++ Mix.compilers()` to `mix.exs` so Rustler compiles the NIF during `mix compile`.\n- **FFI `from_json`/`to_json` functions** — `htm_conversion_options_from_json`, `htm_conversion_result_to_json`, etc. now generated for all serde-compatible types, fixing Java (Panama FFM) and Go (cgo) bindings.\n- **PHP e2e tests** — fixed function call generation to use correct `HtmlToMarkdownRs::convert()` pattern.\n- **Python `pyproject.toml`** — corrected `module-name` and `python-packages` to match `html-to-markdown` pip package name.\n- **`docs/llms.txt`** metadata defaults corrected from `false` to `true` for `extract_metadata`, `extract_document`, `extract_headers`, `extract_links`, `extract_images`, `extract_structured_data` (#276).\n\n- **WASM type prefix restored** (#303) — `WasmConversionOptions` (not `JsConversionOptions`) via configurable `type_prefix` in alef. No breaking change for WASM users.\n\n### Known Issues\n\n- **Python silent output cap** (#277) — `convert()` silently truncates output at ~439 KB on certain large HTML inputs. Under investigation.\n\n## [3.2.0] - 2026-04-14\n\n### Breaking Changes\n\n#### Core Defaults Changed\n\n- **`code_block_style`** default changed from `Indented` to `Backticks` — code blocks now use triple-backtick fences by default instead of 4-space indentation.\n- **`bullets`** default changed from `\"-\"` to `\"-*+\"` — nested unordered lists now cycle through `-`, `*`, `+` at successive nesting levels.\n- **`preprocessing.enabled`** default changed from `false` to `true` — HTML preprocessing (navigation removal, form stripping) is now on by default.\n- **Rust serde field names** changed from `camelCase` to `snake_case` — affects JSON serialization/deserialization of the Rust core `ConversionOptions` struct (`heading_style` instead of `headingStyle`). Language bindings are not affected — each binding uses its language-native naming convention (camelCase for JS/TS/Java/C#, snake_case for Python/Ruby/Elixir/R).\n\n#### Package Renames\n\n- **TypeScript/Node.js**: npm package renamed from `@kreuzberg/html-to-markdown` to `@kreuzberg/html-to-markdown-node`.\n- **PHP**: Namespace changed from `HtmlToMarkdown\\` to `Html\\To\\Markdown\\Rs\\`. Main class renamed from `HtmlToMarkdown` to `HtmlToMarkdownRs`.\n- **Java**: Maven coordinates remain `dev.kreuzberg:html-to-markdown`. Internal package namespace changed to `dev.kreuzberg.htmltomarkdown`.\n- **C FFI**: Function prefix changed from `html_to_markdown_` to `htm_` (e.g., `htm_convert`, `htm_last_error_code`). Header moved to `include/html_to_markdown.h`.\n- **C#**: Main class renamed from `HtmlToMarkdownConverter` to `HtmlToMarkdownRs`.\n\n#### Python Exception Hierarchy\n\n- Old exceptions (`HtmlToMarkdownError`, `EmptyHtmlError`, `InvalidParserError`, etc.) replaced with new hierarchy: `ConversionError`, `ParseError`, `SanitizationError`, `ConfigError`, `IoError`, `InvalidInputError`, `PanicError`, `OtherError`.\n\n### Added\n\n- **`ConversionOptionsBuilder`** is now public in the Rust API — use `ConversionOptions::builder()` for ergonomic option construction.\n- **`TableData`** exported from crate root — no longer requires importing from submodules.\n- **Per-language API reference documentation** — generated `docs/reference/api-{lang}.md` pages for Python, TypeScript, Go, Java, C#, Ruby, PHP, Elixir, WASM, and C with full type mappings, signatures, and docstrings.\n- **Alef codegen** — all 12 language bindings (Rust, Python, TypeScript, Go, Java, C#, Ruby, PHP, Elixir, WASM, C, R) are now auto-generated from a single IR via [alef](https://github.com/kreuzberg-dev/alef), configured in `alef.toml`.\n- **E2E test suite** — 130 fixture-driven tests across all 12 languages, generated from shared JSON fixtures in `fixtures/`.\n- **`AnnotationKind`** and **`NodeContent`** now implement `Default`.\n- **`ConversionResult`** now derives `Serialize`/`Deserialize`.\n\n### Changed\n\n- **Docs platform**: Switched from MkDocs to [Zensical](https://zensical.dev) (`zensical.toml` replaces `mkdocs.yaml`).\n- **READMEs**: Now generated by `alef readme` from minijinja templates with inline configuration in `alef.toml`.\n- **Version sync**: `task version:sync` now uses `alef` binary for all operations (replaces `sync_versions.py`).\n- **CI workflows**: Simplified to e2e-only testing per language — removed per-binding unit test jobs.\n\n### Removed\n\n- **`crates/html-to-markdown-bindings-common`** — shared bindings helper crate replaced by alef codegen.\n- **`tools/e2e-generator`** — replaced by `alef e2e generate`.\n- **`tools/snippet-runner`** — snippet validation removed.\n- **`scripts/generate_readme.py`**, **`scripts/sync_versions.py`**, **`scripts/readme_filters.py`** — replaced by alef.\n- **Hand-written binding code** — all per-language binding implementations replaced by alef-generated code.\n\n### Fixed\n\n- **Preserve metadata when using AST visitors** (#279).\n- **`deny_unknown_fields`** added to serde option structs — invalid JSON fields now produce errors instead of being silently ignored.\n- **Ruby e2e generator** — fixed camelCase→snake_case field name conversion.\n- **WASM test options** — added missing `link_style` field.\n\n## [3.1.0] - 2026-04-01\n\n### Added\n\n- **Reference-style links**: New `link_style` option (`\"inline\"` default, `\"reference\"`) renders links as `[text][1]` with numbered `[1]: url \"title\"` definitions appended at the end of the output. Supports URL+title deduplication, images (`![alt][1]`), and media elements (audio, video, iframe). Available across all bindings (Python, Node.js, WASM, PHP, CLI `--link-style`, FFI via JSON).\n\n## [3.0.2] - 2026-04-01\n\n### Fixed\n\n- **Structure collector in tables**: Suppressed `StructureCollector` calls for headings and lists inside table cells, preventing spurious document-structure nodes from table content.\n- **Char boundary safety**: Fixed potential panics from slicing at non-UTF-8-char boundaries in `generate_id` hash truncation and list item text extraction.\n- **Dead feature gates removed**: Cleaned up unused `document-structure` feature gates that were no longer wired to any Cargo feature.\n- **Structure collector coverage**: Added missing `StructureCollector` calls for lists, images, and code blocks so document structure captures all block-level elements.\n\n## [3.0.1] - 2026-03-31\n\n### Fixed\n\n- **WASM TypeScript types**: `convert()` now returns typed `WasmConversionResult` instead of `any`. All `WasmConversionTable`, `WasmGridCell`, `WasmTableGrid`, `WasmConversionWarning`, and `WasmInlineImage` interfaces are now emitted in generated `.d.ts` files. Added missing options fields (`skipImages`, `outputFormat`, `includeDocumentStructure`, `extractImages`, `maxImageSize`, `captureSvg`, `inferDimensions`). Fixes #265.\n- **Python type stubs**: Synced crate `.pyi` stub with package stub — added keyword-only (`*`) parameter markers and `visitor` parameter to `convert()`.\n- **PHP type stubs**: Expanded PHPStan stubs with full `ConversionResult`, `ConversionOptions`, and all nested type shapes. Wired stubs into `composer.json` PHPStan config.\n\n## [3.0.0] - 2026-03-30\n\n### Added\n\n- **Single `convert()` API**: One entry point across all 12 language bindings returning `ConversionResult` with content, document, metadata, tables, images, and warnings.\n- **`ConversionResult` type**: Structured result with `content` (markdown/djot/plain), `document` (optional `DocumentStructure`), `metadata` (`HtmlMetadata`), `tables` (grid-based), `images` (inline image data), and `warnings`.\n- **`DocumentStructure`**: Structured document tree with flat node array, index-based parent/child references, and `TextAnnotation` for inline formatting.\n- **Options support in all bindings**: Go, Java, C# now accept options. All generators wire fixture options into e2e tests.\n- **GFM defaults**: Code blocks default to backtick fences (was indented). ATX headings remain default.\n- **E2E contract validation**: Generators produce tests validating ConversionResult structure (metadata, tables, warnings) across all 12 languages.\n- **New options**: `includeDocumentStructure`, `extractImages`, `maxImageSize`, `captureSvg`, `inferDimensions`, `outputFormat` (markdown/djot/plain).\n- **`<q>` element**: Wraps content in quotation marks.\n- **`<figure>`/`<figcaption>` elements**: Routed to semantic handler with caption separation.\n- **`hidden` attribute**: Elements with `hidden` stripped before parsing.\n\n### Changed\n\n- **`convert()` returns `ConversionResult`** instead of `String` in all bindings (Go, Java, C#, Node, Python, PHP, Ruby, Elixir, R, WASM, C FFI).\n- **`ExtendedMetadata` renamed to `HtmlMetadata`** across all crates and bindings.\n- **Go `Convert()`** returns `*ConversionResult` with `Content`, `Metadata`, `Tables`, `Images`, `Warnings` fields. Accepts optional JSON options via variadic parameter.\n- **Table data** uses grid-based schema (`TableGrid` with `GridCell`) instead of flat `cells [][]string`.\n- **`serde(deny_unknown_fields)`** on `MetadataConfig`, `MetadataConfigUpdate`, `InlineImageConfigUpdate`.\n- **Go 1.26**, golangci-lint@latest.\n\n### Removed\n\n- **All `convert_with_*` functions**: `convert_with_metadata`, `convert_with_inline_images`, `convert_with_visitor` (standalone), `convert_with_tables`, `convert_with_async_visitor` removed from public API. Single `convert()` replaces all.\n- **Async visitor**: Feature removed entirely (`async-visitor` Cargo feature, `AsyncHtmlVisitor` trait, async bridge/dispatch code).\n- **Profiling**: All profiling infrastructure removed (8 binding crates, CI workflow, C tests, `start_profiling`/`stop_profiling` APIs).\n- **Benchmarks**: All benchmark scripts and harness removed.\n- **hOCR support**: Entire `hocr` module deleted. The `hocr_spatial_tables` option removed.\n- **Python v1 compatibility**: `convert_to_markdown()` and `markdownify()` removed.\n- **Redundant binding tests**: Tests covered by e2e generators removed from Python, Ruby, Elixir, R.\n\n## [2.30.0] - 2026-03-27\n\n### Deprecated\n\n- **hOCR support**: The `hocr_spatial_tables` option and all hOCR-related APIs are deprecated and will be removed in v3. All hOCR functionality continues to work but emits deprecation warnings. This is the final v2 release.\n\n### Fixed\n\n- **PHP PHPStan CI errors**: Removed redundant `@var` annotations and `is_array()` runtime checks in `ExtensionBridge.php` that PHPStan flagged as always-true due to stub-defined return types. Removed redundant `array_values()` calls in `ConversionOptions.php` on properties already typed as `list<string>`. Updated PHPStan baseline count for callable invocations.\n\n## [2.29.0] - 2026-03-22\n\n### Added\n\n- **`full` feature group**: Added a `full` feature to core crate and all binding crates (PHP, Python, Node, WASM, FFI, Elixir, bindings-common) that enables all available features. All bindings now default to `full`.\n- **Dublin Core metadata extraction**: `DC.*` and `DCTERMS.*` meta tags now map to dedicated `DocumentMetadata` fields (title, description, author, keywords). Other DC/DCTERMS fields stored in `meta_tags` with `dc_`/`dcterms_` prefix.\n- **Extended keyword variants**: Keywords now extracted from `news_keywords`, `citation_keywords`, `DC.subject`, `DC.keywords`, `DCTERMS.subject`, `subject`, `topic`, `category`, and `classification` meta tags.\n- **`cargo-sort` pre-commit hook**: Added for consistent Cargo.toml key ordering.\n- **`checkmake` pre-commit hook**: Added for Makefile linting.\n- **`typescript-typecheck` pre-commit hook**: Added TypeScript type checking via `tsc --noEmit`.\n- **`typecheck` npm script**: Added to `packages/typescript/package.json`.\n\n### Fixed\n\n- **Case-insensitive meta tag matching** ([#251](https://github.com/kreuzberg-dev/html-to-markdown/issues/251)): All meta tag name matching is now case-insensitive per the HTML spec. `<meta name=\"Keywords\">` and `<meta name=\"DC.keywords\">` are now correctly captured.\n- **PHP `convertWithTables()` not found** ([#250](https://github.com/kreuzberg-dev/html-to-markdown/issues/250)): The `visitor` feature was not enabled by default in the PHP binding crate, causing `html_to_markdown_convert_with_tables` to be missing from the extension.\n- **PHP binding defaults**: PHP crate now defaults to `[\"full\"]` (was `[\"metadata\"]`), enabling visitor support.\n- **Python binding defaults**: Python crate now defaults to `[\"full\"]` (was `[]`), enabling metadata, visitor, async-visitor, and inline-images.\n- **PHPStan 2.x compatibility**: Fixed 40+ PHPStan errors from the 1.x→2.x upgrade (type narrowing, property access on mixed, redundant assertions). Added `--memory-limit=512M` to prevent OOM.\n- **Makefile `test` target**: Added missing `.PHONY: test` target to FFI test Makefile.\n\n### Changed\n\n- **Pre-commit config aligned with kreuzberg**: Added `cargo-sort`, `checkmake`, `typescript-typecheck`. Updated `taplo-format` to exclude `Cargo.toml`. Excluded `Makefile.frag` from checkmake.\n- **Cargo.toml formatting**: All workspace Cargo.toml files sorted via `cargo-sort`.\n- **pyproject.toml formatting**: All pyproject.toml files formatted via `pyproject-fmt`.\n\n## [2.28.6] - 2026-03-20\n\n### Changed\n\n- **Ruby gem vendoring**: Replaced bash+embedded-Python vendoring script with a standalone Python vendoring script adapted from kreuzberg, using `vendor/` directory instead of `rust-vendor/` for core crate vendoring.\n- **Ruby gem build**: Added `build-native-gem.rb` for platform-specific pre-compiled gem builds, following kreuzberg patterns.\n- **Pre-commit hooks**: Switched Ruby hooks (rubocop, rbs-validate, steep-check) from inline bash commands to task-based delegation matching kreuzberg.\n- **Dependabot config**: Expanded from GitHub Actions only to full multi-ecosystem coverage (Cargo, pip, npm, bundler, composer, gomod, maven, nuget, mix).\n- **Task update commands**: Aligned all language update tasks with kreuzberg's comprehensive approach (outdated checks, aggressive updates).\n- **C# update**: Switched from slow `dotnet list --outdated` Python script to `dotnet-outdated-tool` for faster dependency updates.\n\n### Fixed\n\n- **CI Validate shfmt failure**: Fixed `packages/r/configure.win` tab indentation to match shfmt 2-space requirement.\n- **Java linting**: Added PMD plugin (3.28.0), JaCoCo coverage (0.8.14), and pinned checkstyle runtime (13.3.0). Bumped maven-compiler-plugin to 3.15.0, maven-surefire-plugin to 3.5.5, spotless to 3.4.0, central-publishing to 0.10.0.\n\n### Updated\n\n- **GitHub Actions**: Bumped `go-task/setup-task` from v1 to v2, `nick-fields/retry` from v3 to v4.\n- **Dependencies**: Updated all language dependencies via `task update`.\n\n## [2.28.5] - 2026-03-19\n\n### Fixed\n\n- **Table colspan parsing** ([#233](https://github.com/kreuzberg-dev/html-to-markdown/issues/233)): Fixed column count calculation to accurately use colspan values instead of incrementing by 1, and refined layout table heuristics to exempt simple data tables with colspans while correctly catching layout tables.\n- **Ruby version.rb formatting**: Fixed missing space around `=` operator in `version.rb` that caused Rubocop lint failures in CI.\n- **CI tooling alignment**: Aligned tooling and documentation with kreuzberg standards, fixing CI failures.\n\n## [2.28.4] - 2026-03-13\n\n### Fixed\n\n- **Panic with cid image followed by italic paragraph** ([#222](https://github.com/kreuzberg-dev/html-to-markdown/issues/222)): Confirmed fix for panic (\"byte index 53 is out of bounds of ``\") when converting HTML containing `cid:` image paragraphs followed by italicized text. This was resolved in v2.28.0 via the block_content_start bounds fix (#216, #217) and multi-byte UTF-8 character boundary fix (#218).\n- **Ruby gem installation on macOS** ([#219](https://github.com/kreuzberg-dev/html-to-markdown/issues/219)): Confirmed fix for `Cargo.lock` missing from published gem causing `magnus` dependency load failure. Resolved in v2.28.3 with native platform gem builds.\n\n## [2.28.3] - 2026-03-10\n\n### Fixed\n\n- **Java visitor FFI struct return type**: Fixed `IllegalArgumentException: Wrong method handle type` when using visitors in Java. The Panama FFI callback descriptors incorrectly used `JAVA_LONG` (8 bytes) as the return type instead of the actual `HtmlToMarkdownVisitResult` C struct (24 bytes: enum + 2 pointers). All 14 callback descriptors now use a proper `StructLayout` matching the C ABI.\n- **Homebrew bottle tarball structure**: Fixed bottle tarballs missing the required `html-to-markdown/{version}/` prefix directory. Homebrew expects this prefix for proper cellar installation.\n- **Ruby gem publishing**: Added native platform gem builds (`rake native gem`) alongside source gems so precompiled extensions are available for Linux, macOS, and Windows.\n\n## [2.28.2] - 2026-03-08\n\n### Fixed\n\n- **Publish workflow republish flag**: Fixed republish mode skipping all publish jobs because `INPUT_REF` resolved to a branch name instead of the tag ref.\n- **Definition list fixture**: Aligned real-world test fixture for `<dl>`/`<dt>`/`<dd>` with actual converter output (plain text, no Pandoc-style `:` prefix).\n\n## [2.28.1] - 2026-03-06\n\n### Fixed\n\n- **Panic with multi-byte UTF-8 and visitor** ([#218](https://github.com/kreuzberg-dev/html-to-markdown/issues/218)): Fixed a panic (\"byte index N is not a char boundary\") when converting HTML containing multi-byte UTF-8 characters (Cyrillic, CJK, emoji, etc.) with tabs between block elements and any visitor. The stale byte position captured before whitespace trimming could land inside a multi-byte character when new content was appended.\n- **Java formatting**: Fixed spotless formatting violations in `HtmlToMarkdown.java`, `TableData.java`, and `TableExtractionResult.java`.\n\n## [2.28.0] - 2026-03-05\n\n### Added\n\n- **Table extraction API**: New `convert_with_tables` function that extracts structured table data during HTML-to-Markdown conversion. Returns `TableData` structs containing cell contents as `Vec<Vec<String>>`, rendered markdown output, and per-row header flags. Uses the visitor pattern internally with a built-in `TableCollector` to capture table structure in a single pass. Available across all language bindings:\n  - **Rust**: `convert_with_tables(html, options, metadata_config)` returning `ConversionWithTables`\n  - **Python**: `convert_with_tables(html, options, preprocessing, metadata_config)` returning `TableExtractionResult`\n  - **TypeScript/Node.js**: `convertWithTables(html, options?, metadataConfig?)` returning `TableExtraction`\n  - **Ruby**: `HtmlToMarkdown.convert_with_tables(html, options, metadata_config)` returning a Hash\n  - **PHP**: `HtmlToMarkdown::convertWithTables($html, $options, $metadataConfig)` returning `TableExtractionResult`\n  - **Go**: `ConvertWithTables(html)` returning `TableExtractionResult`\n  - **Java**: `HtmlToMarkdown.convertWithTables(html)` returning `TableExtractionResult`\n  - **C#**: `HtmlToMarkdownConverter.ConvertWithTables(html)` returning `TableExtractionResult`\n  - **Elixir**: `HtmlToMarkdown.convert_with_tables(html, options, metadata_config)` returning `{:ok, content, tables, metadata}`\n  - **R**: `convert_with_tables(html, options, metadata_config)` returning a list\n  - **C (FFI)**: `html_to_markdown_convert_with_tables(html, options_json, metadata_json)` returning JSON\n  - **WASM**: `convertWithTables(html, options?, metadataConfig?)` returning a JS object\n\n### Fixed\n\n- **Plain text fast path skipping visitor callbacks**: When `OutputFormat::Plain` was used with `convert_with_tables`, the plain text fast path returned before the visitor could extract table data, resulting in empty tables. The conversion pipeline now runs the full visitor walk before returning plain text content.\n\n## [2.27.3] - 2026-03-05\n\n### Fixed\n\n- **Panic on block_content_start out of bounds**: Fixed a crash (`byte index N is out of bounds`) in text node processing when inline handlers (e.g. `<strong>`, `<em>`) collected children into a fresh buffer while inheriting a parent paragraph context. The `block_content_start` index pointed into the wrong buffer, causing a panic on certain HTML structures — notably `<details>` containing `<p>` with inline formatting. (Issues #216, #217)\n\n## [2.27.2] - 2026-03-02\n\n### Fixed\n\n- **Plain text list items missing markers**: `<ul>` and `<ol>` list items in `OutputFormat::Plain` were output without any bullet or number prefix. Now emits `-` for unordered lists and sequential `N.` for ordered lists, respecting the `start` attribute on `<ol>`.\n\n## [2.27.1] - 2026-03-01\n\n### Fixed\n\n- **Colon introduced into definition list text**: `<dd>` elements inside `<dl>` were incorrectly prefixed with `:` (Pandoc definition list syntax), introducing spurious colons into converted text. Standard Markdown and GFM do not support definition list syntax, so `<dd>` content is now output as plain blocks. (Issue #214, thanks @smoyerx)\n- **Go test app go.sum out of sync**: Updated `tests/test_apps/go/go.sum` to match the v2.27.0 module version, fixing the CI Go lint job.\n\n## [2.27.0] - 2026-03-01\n\n### Added\n\n- **Plain text output format**: New `OutputFormat::Plain` option that strips all markup and returns only visible text content. Set `output_format` to `\"plain\"` (also accepts `\"plaintext\"` or `\"text\"`). This fast-path bypasses the full Markdown/Djot conversion pipeline — after DOM parsing, a lightweight text extractor walks the tree collecting only visible text with structural whitespace. Useful for search indexing, text extraction, and feeding content to LLMs.\n\n## [2.26.3] - 2026-02-28\n\n### Fixed\n\n- **Subscript/superscript content silently dropped**: When `sub_symbol` or `sup_symbol` was empty (the default), text inside `<sub>` and `<sup>` tags was discarded entirely — e.g. `H<sub>2</sub>O` produced `HO` instead of `H2O`.\n- **Missing whitespace between newline-separated inline elements**: Whitespace-only text nodes containing newlines between adjacent inline elements (e.g. `<a>…</a>\\n<a>…</a>`) were dropped, causing links and other inline markup to merge without a word boundary. Now collapses to a single space per HTML white-space normalization rules.\n\n## [2.26.2] - 2026-02-28\n\n### Fixed\n\n- **Inconsistent whitespace before inline elements across paragraphs**: Fixed a stateful bug where `\\n` before `<a>`, `<strong>`, `<em>`, and other inline elements inside `<p>` tags was handled differently depending on the paragraph's position in the document. The second and subsequent paragraphs would drop the space before inline elements, producing `text[link](url)` instead of `text [link](url)`. (Issue #212, thanks @haroldparis)\n\n## [2.26.1] - 2026-02-27\n\n### Fixed\n\n- **YAML frontmatter in `convert_with_metadata` output**: `convert_with_metadata` no longer prepends YAML frontmatter to the markdown string. Since metadata is returned as a structured `ExtendedMetadata` object, embedding it in the content string was redundant and polluted the output.\n\n## [2.26.0] - 2026-02-26\n\n### Added\n\n- **C FFI distribution infrastructure**: Distribution-grade C FFI library with CMake/pkg-config integration, installation scripts, and packaging for system-level consumption.\n- **C FFI test coverage**: Comprehensive C test suite covering conversion, metadata extraction, error handling, visitor pattern, profiling, and version queries.\n- **C documentation and examples**: C API reference, getting-started snippets, and example programs for basic conversion, metadata extraction, and visitor pattern usage.\n\n### Fixed\n\n- **R package r-universe build**: Configure scripts now download the source archive from GitHub when the monorepo is unavailable, enabling r-universe and standalone source installs to vendor crates automatically.\n\n## [2.25.2] - 2026-02-25\n\n### Fixed\n\n- **Visitor panic with metadata extraction**: Fixed an out-of-bounds slice panic when using visitors (e.g. image visitors returning `Custom`) combined with metadata extraction on minified HTML. The issue occurred because parent element output offsets became stale after child visitor truncations. (PR #204, thanks @gmalette)\n\n### Added\n\n- **R language bindings**: Full-parity R bindings via extendr framework with support for `convert()`, `convert_with_options()`, `convert_with_options_handle()`, `convert_with_metadata()`, `convert_with_inline_images()`, `convert_with_visitor()`, and profiling. Includes `conversion_options()` helper, testthat test suite, CI workflow, lintr/styler pre-commit hooks, and task automation.\n- **R CRAN publishing infrastructure**: Added `publish-cran` job to publish workflow, `cran-comments.md`, and `NEWS.md` for CRAN submission compliance.\n\n### Changed\n\n- **Workspace restructuring**: Moved Ruby native crate out of the root Cargo workspace into a standalone workspace (matching Elixir/R pattern), resolving `clang-sys` link conflict with `ext-php-rs` 0.15.6.\n- **Rust update task**: Now updates dependencies in all separate workspaces (Ruby, Elixir, R) via `--manifest-path` entries.\n- **Upgraded `wasmtime`** from 41 to 42.\n- **Upgraded `ext-php-rs`** from 0.15.4 to 0.15.6.\n- **Upgraded `pyo3`** from 0.28.1 to 0.28.2.\n- **Upgraded `wasm-bindgen`** from 0.2.112 to 0.2.113.\n- **Upgraded `rustls`** from 0.23.36 to 0.23.37.\n\n## [2.25.1] - 2026-02-17\n\n### Fixed\n\n- **hOCR heading detection**: Improved hierarchy logic to use font size (`x_fsize`) and bbox height as a proxy when detecting headings. Large-font paragraphs now support longer text (up to 80 chars) and single-word headings. Added comprehensive test coverage for heading detection edge cases.\n\n## [2.25.0] - 2026-02-15\n\n### Added\n\n- **Bun runtime support**: Official support for Bun 1.2+ via Node-API compatibility. The existing NAPI-RS bindings work in Bun without changes. Added Bun to CI test matrix and updated documentation to reflect runtime compatibility.\n\n### Changed\n\n- **Vendored `markup5ever_rcdom`**: Brought the `markup5ever_rcdom` code (MIT/Apache-2.0) into the core crate as an internal `rcdom` module. This removes the external dependency on the \"+unofficial\" crate, eliminates the unused `xml5ever` transitive dependency, and removes the pinned `html5ever`/`markup5ever_rcdom` version constraints. See `ATTRIBUTIONS.md` for license details.\n- **Upgraded `html5ever`** from 0.36.1 to 0.38.0 (now unpinned).\n- **Upgraded `pyo3`** from 0.28.0 to 0.28.1.\n\n## [2.24.6] - 2026-02-14\n\n### Fixed\n\n- **Dependency update stability**: Pinned compatible `html5ever`/`markup5ever_rcdom` versions to prevent trait-mismatch breakages during workspace dependency updates.\n- **Python bindings build**: Added explicit `#[pyclass(from_py_object)]` on Python config wrapper classes to avoid PyO3 deprecation failures under `-D warnings`.\n- **Rust lint consistency**: Aligned crate-level clippy configuration so `multiple_crate_versions` does not fail Node/WASM/FFI crate lint runs.\n- **WASM dependency behavior**: Updated hashing dependency configuration to avoid wasm randomness backend breakage after dependency updates.\n- **PHP PIE publish verification (macOS)**: Hardened PIE verification/build scripts for Darwin linker behavior and shell-safe package spec handling.\n- **CI reliability**: Updated validation and Python CI tasks to reduce flakiness (PHP 8.4 setup in validate; avoid redundant Rust CLI release builds in Python test runs).\n\n## [2.24.5] - 2026-02-01\n\n### Fixed\n\n- **Subscript/superscript whitespace handling**: Subscript and superscript tags now trim inner whitespace and place it outside delimiters, matching the behavior of bold, italic, and strikethrough (issue #202).\n\n## [2.24.4] - 2026-01-31\n\n### Performance\n\n- **Reduced allocations in hot conversion paths**: Return `Cow<str>` from escape to avoid allocating on no-op paths, replace `.repeat()` with direct push loops in heading/list/table/div/paragraph formatters, eliminate `collect::<Vec>::join()` in text dedentation, and use `AHashMap` for hOCR property maps.\n\n### Fixed\n\n- **WASM builds**: Updated `getrandom` backend configuration from `\"js\"` to `\"wasm_js\"` for compatibility with getrandom 0.3.x.\n- **Elixir/Ruby vendor scripts**: Added missing `ahash` workspace dependency replacement for standalone builds.\n\n## [2.24.3] - 2026-01-31\n\n### Fixed\n\n- **Definition lists**: Ensure `<dl>/<dt>/<dd>` output is consistent regardless of HTML whitespace/minification, and properly indent multiline definition content (issue #200).\n- **Link labels**: Removed hard truncation of long link labels to avoid broken Markdown for large image links (issue #199).\n\n## [2.24.2] - 2026-01-29\n\n### Fixed\n\n- **Java packaging**: Bundle native FFI libraries in published Maven JAR for all platforms (linux-x86_64, linux-aarch64, osx-aarch64, windows-x86_64). The Java package now works out-of-the-box when installed from Maven Central without requiring local FFI builds or manual java.library.path configuration. Native libraries are automatically extracted to a temp directory on first use with platform detection and fallback support.\n\n## [2.24.1] - 2026-01-27\n\n### Fixed\n\n- **UTF-16 recovery**: Automatically recovers UTF-16 HTML (including data without BOM) that was read via lossy UTF-8 decoding, instead of rejecting it as binary data.\n- **URL sanitization**: Hardened markdown-like URL sanitization to extract the real URL from `...[text](url)` patterns in `href`/`src` attributes, preventing caller-side URL join/parsing errors.\n- **Issue #190 coverage**: Added regression fixtures and tests covering the reported real-world HTML inputs.\n\n## [2.24.0] - 2026-01-24\n\n### Changed\n\n- **Bindings API**: Removed `_json` conversion entrypoints across bindings; convert functions now pass full option payloads directly.\n\n### Fixed\n\n- **Visitor docs**: Corrected Python visitor documentation and examples (argument order + ctx access).\n- **Python visitor options**: `convert_with_visitor` now respects full conversion options payloads.\n- **skip_images**: Skip flag now suppresses SVG/graphic outputs in addition to `<img>`.\n- **Code block dedent**: Handles Unicode whitespace without panicking on UTF-8 boundaries.\n- **Input validation**: Tolerates small NUL byte artifacts and strips them before conversion.\n\n## [2.23.6] - 2026-01-21\n\n### Fixed\n\n- **pnpm lockfile synchronization**: Fixed pnpm lockfile to include Node.js platform-specific optional dependency version updates (2.19.0-rc.1 → 2.23.6) that were applied during v2.23.5 version sync. This resolves the `ERR_PNPM_OUTDATED_LOCKFILE` errors that caused the v2.23.5 publish workflow to fail.\n- **CI Java version**: Updated CI Java workflow from Java 24 to Java 25 to match maven.compiler.release=25 configuration, ensuring CI and local builds use the same compiler version.\n\n## [2.23.5] - 2026-01-21\n\n### Fixed\n\n- **Maven Central publishing**: Corrected group ID in Maven Central check script from legacy `io.github.goldziher` to `dev.kreuzberg`, enabling successful Java package publishing. This resolves the issue where Java v2.23.4 failed to publish to Maven Central.\n- **Go module publishing**: Added automated Go module tag creation (`packages/go/v{version}`) to publish workflow, ensuring Go packages are immediately available on Go proxy after release.\n- **Go FFI version synchronization**: Updated Go FFI default version constants from outdated versions (2.19.1/2.23.0) to 2.23.5 in both `ffi_loader.go` and `cmd/install/main.go`, ensuring automatic downloads use the correct library version.\n- **Node.js platform dependencies**: Synchronized all platform-specific optional dependencies in `@kreuzberg/html-to-markdown-node` package.json from 2.19.0-rc.1 to match main package version, preventing dependency resolution issues.\n- **Java benchmark packaging**: Updated benchmark-pom.xml to use correct group ID (`dev.kreuzberg`), version (2.23.5), and main class namespace (`dev.kreuzberg.benchmark.Benchmark`). Removed outdated generated `dependency-reduced-pom.xml`.\n- **PHP package references**: Updated all PHP package references from `goldziher/html-to-markdown` to `kreuzberg-dev/html-to-markdown` across composer.json, PIE verification scripts, and smoke test actions to reflect current package organization.\n- **Java smoke tests**: Updated smoke-java GitHub action to use correct group ID (`dev.kreuzberg`) and package namespace (`dev.kreuzberg.htmltomarkdown.SmokeTest`) for JAR installation and test execution.\n- **Build tooling**: Fixed Python script execution in task runner to use `uv run python3` instead of system python3, ensuring consistent dependency resolution. Added PyYAML and Jinja2 to workspace dev dependencies.\n- **Version sync automation**: Enhanced version sync script to automatically update Node.js platform-specific optional dependencies alongside main package version, preventing manual version drift.\n\n## [2.23.4] - 2026-01-20\n\n### Fixed\n\n- **TypeScript wrapper publishing**: Fixed TypeScript wrapper dependency resolution by installing dependencies directly from npm registry instead of from workspace after Node packages are published. This ensures `@kreuzberg/html-to-markdown-node` is available from npm when building the TypeScript wrapper, eliminating the workspace resolution issues that caused previous build failures.\n\n## [2.23.3] - 2026-01-20\n\n### Fixed\n\n- **Go FFI packaging**: Fixed missing `html_to_markdown.h` header file in Go FFI archive tarballs, which caused `go:generate` installation to fail with \"fatal error: 'html_to_markdown.h' file not found\". The header is now included in all platform archives (tar.gz and zip).\n- **TypeScript wrapper publishing**: Fixed pnpm lockfile frozen mode error during TypeScript wrapper dependency reinstallation by adding `--no-frozen-lockfile` flag. The reinstall step after publishing Node packages now correctly updates workspace dependencies despite lockfile version mismatches.\n\n## [2.23.2] - 2026-01-20\n\n### Fixed\n\n- **TypeScript wrapper publishing**: Fixed TypeScript wrapper build failures by moving the build and publish steps into the same `publish-node` job. This eliminates npm CDN propagation delays that caused `@kreuzberg/html-to-markdown` to fail building because `@kreuzberg/html-to-markdown-node` wasn't available yet. Added workspace dependency reinstallation step to ensure pnpm correctly resolves the local package after publishing.\n- **Go FFI library installation**: Fixed critical bugs in the `go:generate` install script that prevented automatic FFI library downloads:\n  - Corrected artifact naming from `go-ffi-{platform}.tar.gz` to `html-to-markdown-ffi-{version}-{platform}.tar.gz`\n  - Fixed platform mapping to match GitHub release artifacts (darwin-arm64, linux-x64, etc.)\n  - Added support for all library formats (.dylib for macOS, .so for Linux, .dll for Windows)\n- **Ruby native Cargo.toml**: Fixed workspace dependency configuration to use `workspace = true` instead of vendored path reference, preventing Cargo workspace resolution failures during builds.\n- **CI workflows**: Upgraded all CI workflows from Java 24 to Java 25 to match maven.compiler.release=25 configuration in pom.xml.\n- **Go linting**: Resolved golangci-lint warnings by adding constants for OS names and library names, and converting if-else chains to switch statements.\n\n### Changed\n\n- **Go README**: Updated installation documentation to explain the `go:generate` workflow for automatic FFI library installation, including details about caching in `~/.html-to-markdown/` and alternative manual configuration.\n\n## [2.23.1] - 2026-01-19\n\n### Fixed\n\n- **Go module versioning**: Created 14 missing Go module tags (packages/go/v2.16.1, v2.19.1-v2.19.8, v2.20.1, v2.21.1, v2.22.1-v2.22.5) to ensure all versions since v2.15.0 are available via Go proxy. Users can now `go get` any version from v2.15.0 onwards.\n- **TypeScript wrapper publishing**: Added missing `publish-typescript` job to publish workflow to properly publish `@kreuzberg/html-to-markdown` TypeScript wrapper package to npm alongside the native Node.js bindings (`@kreuzberg/html-to-markdown-node`).\n- **Ruby gem vendoring**: Fixed Ruby gem installation failures due to missing `.cargo-checksum.json` files. Updated gemspec to include hidden files with `File::FNM_DOTMATCH` flag, and improved vendoring script to generate checksums correctly with `--locked` flag and proper cleanup.\n- **Elixir package size**: Reduced Hex package size from 134 MB to under 128 MB limit by aggressively removing unnecessary files from vendored dependencies (tests, docs, examples, static libraries, Windows-only crates on Unix builds).\n\n### Added\n\n- **Go automatic FFI library installation**: Implemented `go:generate` pattern following Kreuzberg approach. Added `cmd/install` package that automatically downloads platform-specific FFI libraries from GitHub releases and generates CGO flags. Users can now run `go generate` after installation instead of manually setting `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables. FFI loader updated to check `~/.html-to-markdown/` for installed libraries.\n\n## [2.23.0] - 2026-01-18\n\n### Added\n\n- **Djot output format support**: New `output_format` option in `ConversionOptions` enables conversion to [Djot](https://djot.net/) lightweight markup language as an alternative to Markdown. Djot uses different syntax for emphasis (`_text_`), strong (`*text*`), strikethrough (`{-text-}`), inserted (`{+text+}`), highlighted (`{=text=}`), subscript (`~text~`), and superscript (`^text^`).\n- **CLI**: Added `--output-format` / `-f` flag to specify output format (`markdown` or `djot`)\n- **All language bindings**: OutputFormat enum/option added to Python, TypeScript/Node.js, Ruby, PHP, Elixir, Go, Java, and C# bindings\n- **Documentation**: Added Djot output format section to all package READMEs with syntax comparison table\n\n### Fixed\n\n- **Python**: Fixed async visitor bridge to properly await coroutines. `PyAsyncVisitorBridge::call_visitor_method_sync()` now detects async methods via `__await__` attribute and uses `PYTHON_TASK_LOCALS` event loop for proper async execution (issue #187)\n- **Ruby**: Fixed visitor parameter being ignored in `convert()` wrapper method. Now correctly passes visitor to native `convert_with_visitor` function when provided (issue #187)\n\n### Changed\n\n- **Rust**: Updated `async-visitor` feature to include required `tokio` \"sync\" feature for `Mutex` support\n- **Documentation**: Added comprehensive visitor pattern support matrix showing which bindings support visitors\n- **Documentation**: Documented WASM visitor pattern architectural limitation with four alternative approaches\n\n## [2.22.6] - 2026-01-16\n\n### Fixed\n\n- **Ruby gem dependency resolution**: Ruby native extension now uses workspace version inheritance with vendoring approach. During gem build, the entire `html-to-markdown` crate is vendored with exact dependency versions into `packages/ruby/vendor/`, making gems completely self-contained and eliminating crates.io dependency resolution during installation. Local development uses symlink to workspace crate for seamless workflow.\n- **URL parsing robustness**: Fixed IPv6 URL parsing error when processing malformed markdown-like URLs in HTML attributes (e.g., `//[domain.com/path](http://domain.com/path)`). New `sanitize_markdown_url()` function detects and extracts actual URLs from markdown syntax that wasn't properly converted in source HTML. Applied to both link `href` and image `src` attributes (fixes issue #186).\n\n### Changed\n\n- **Ruby gem build process**: Added `vendor-html-to-markdown.sh` script that creates standalone vendor workspace before gem packaging. Ruby native `Cargo.toml` now references vendored path for maximum reproducibility and build reliability.\n\n## [2.22.5] - 2026-01-16\n\n### Fixed\n\n- **Core**: Added `#[serde(default)]` attribute to `ConversionOptions` struct to enable partial JSON deserialization. This allows deserializing JSON with only a subset of fields specified, using default values for missing fields. Fixes compatibility with language bindings (C#, Go, Java) that serialize partial configuration objects.\n\n## [2.22.4] - 2026-01-15\n\n### Fixed\n\n- **Core**: Fixed `br_in_tables` option not being respected correctly. HTML `<br>` tags in table cells now properly convert to markdown line breaks (spaces or backslash style based on `newline_style` option), while block elements (divs, paragraphs) continue to generate literal `<br>` tags when needed for rowspan scenarios (issue #184)\n- **WASM**: Updated GitHub Pages demo to v2.22.4 with latest BR tag handling fixes\n\n## [2.22.3] - 2026-01-14\n\n### Fixed\n\n- **Python**: Exposed `skip_images` option in `ConversionOptions` API, including type stub files (.pyi) for proper type checking support (issue #183)\n- **Elixir**: Added `skip_images` option to `HtmlToMarkdown.Options` module (was completely missing from Elixir binding)\n- **Core**: Fixed `<br>` tags being output literally in table cells instead of converting to proper Markdown line breaks. Table cell paragraph and div separators now respect `newline_style` option (issue #184)\n\n## [2.22.2] - 2026-01-13\n\n### Fixed\n\n- **Ruby gem standalone build** - Fixed Ruby gem failing to build when installed from RubyGems. Removed `lints.workspace = true` (which requires workspace context) and added inline lint configuration. This resolves issue #181.\n- **Ruby gem version pinning** - Changed `html-to-markdown-rs` dependency from loose semver (`\"2.x.x\"`) to exact pin (`\"=2.22.2\"`) to prevent older gems from pulling incompatible newer crate versions.\n- **Version sync script** - Updated `sync_versions.py` to preserve exact version pin prefix (`=`) when syncing Ruby gem dependencies.\n\n## [2.22.1] - 2026-01-13\n\n### Fixed\n\n- **Java Maven Central publishing** - Fixed Maven Central deployment by adding proper `publish` profile with `central-publishing-maven-plugin` configuration. The plugin is now correctly activated with `-Ppublish` flag and uses `ossrh` server credentials.\n- **Java Spotless formatting** - Updated google-java-format to 1.28.0 for Java 25 compatibility.\n\n## [2.22.0] - 2026-01-13\n\n### Fixed\n\n- **C FFI visitor implementation** - Fixed `html_to_markdown_convert_with_visitor` to properly use the visitor handle during conversion instead of discarding it. Previously the visitor was created but the plain `convert()` function was called instead of `convert_with_visitor()`.\n- **C# visitor callbacks** - P/Invoke bindings now correctly invoke visitor callbacks during HTML-to-Markdown conversion (42/42 tests passing).\n- **Go visitor callbacks** - Removed regex-based post-processing workaround; Go bindings now use real FFI visitor callbacks with proper struct field ordering.\n- **PHP visitor callbacks** - Wired up `PhpVisitorBridge` to pass visitor to Rust core instead of ignoring the visitor parameter.\n- **Java visitor callbacks** - Added Panama FFI upcall stubs for all 38 visitor callbacks, enabling full visitor pattern support (95/95 tests passing).\n\n### Added\n\n- **Java `VisitorCallbackFactory`** - New class that creates Panama FFI upcall stubs for visitor callbacks, enabling Java code to receive callbacks from the Rust core during conversion.\n- **Java `HtmlToMarkdown.convertWithVisitor()`** - Public API method for converting HTML with a custom visitor implementation.\n\n## [2.21.1] - 2026-01-13\n\n### Added\n\n- **Serde serialization support for ConversionOptions** - Added `Serialize` and `Deserialize` traits to `ConversionOptions`, `PreprocessingOptions`, and all related structs. Enables JSON serialization/deserialization with camelCase field naming and lowercase string enum representations.\n\n### Changed\n\n- **Major refactor: Complete Phase 1 modular architecture** - Restructured core converter into modular handler components:\n  - Extracted block element handlers (block-level HTML elements)\n  - Extracted inline element handlers (2,363 lines of focused code)\n  - Extracted table, list, and media handlers (2,528 lines)\n  - Extracted semantic and form handlers (1,532 lines)\n  - Improved code organization and maintainability across all language bindings\n- **Unified FFI bindings architecture** - Consolidated common binding logic into shared crate, reducing duplication across Python, TypeScript, Ruby, PHP, Go, and Java bindings\n- **Added visitor callback code generation system** - FFI now supports dynamic visitor callbacks for all language bindings (Python, Ruby, PHP, Elixir, etc.)\n- **Enhanced preprocessing system** - Footer and nav element removal now integrated into preprocessing pipeline\n- **Improved custom element detection** - Enhanced `has_custom_element_tags` to accurately detect only tag names with hyphens\n\n### Internal\n\n- Updated dependencies across all language bindings (Python, Ruby, PHP, JavaScript, Go, etc.)\n- Refactored benchmark harness to modularize script adapters and reduce code duplication\n- Refactored performance examples to extract and reuse shared utilities\n- Improved sync_versions.py to handle all internal workspace dependency version pins\n- Refactored README generation script to modularize template handling\n- Improved clippy lint handling and CI coverage workflows\n- Added documentation to Node.js binding example files\n\n## [2.21.0] - 2026-01-10\n\n### Added\n\n- **`skip_images` configuration option** - New option to skip all `<img>` elements during conversion, enabling greater control over image handling in the output.\n- **Optional visitor parameter across all convert functions** - Unified API for applying visitor patterns to all conversion modes:\n  - `convert(html, options, visitor)` - Basic conversion with optional visitor\n  - `convert_with_inline_images(html, options, image_cfg, visitor)` - Inline image extraction with optional visitor\n  - `convert_with_metadata(html, options, metadata_cfg, visitor)` - Metadata extraction with optional visitor\n- **Visitor pattern integration with advanced features** - Support for using visitor pattern simultaneously with inline images and metadata extraction, providing complete control over the conversion process.\n- **Comprehensive test coverage** - Added tests validating `skip_images` functionality and visitor pattern integration across all conversion functions and language bindings.\n\n### Changed\n\n- **Visitor parameter unified across all APIs** - The visitor parameter is now optional on all conversion functions, enabling consistent API design across basic, inline-images, and metadata extraction paths.\n- **Improved feature-gated architecture** - Refined the feature gate handling for better flexibility when combining visitor patterns with other optional features.\n\n### Deprecated\n\n- **`convert_with_visitor()` function** - Deprecated in favor of passing visitor as an optional parameter to `convert()`. The dedicated function will be removed in a future major release. Use `convert(html, options, visitor)` instead.\n\n### Fixed\n\n- **Unused dependency warnings in npm packages** - Resolved unused dependency warnings reported during builds of JavaScript/TypeScript packages.\n- **Feature gate handling for visitor combinations** - Fixed issues with feature gate combinations when using visitor patterns alongside inline images and metadata extraction.\n\n## [2.20.1] - 2026-01-09\n\n### Code Quality\n\n- **Resolved all clippy warnings comprehensively**: Fixed 207+ clippy pedantic/nursery warnings across entire workspace\n  - Removed blanket `#![allow(clippy::pedantic)]` directives from all crate roots\n  - Fixed trivial copy pass-by-ref issues in converter functions\n  - Added missing documentation sections (# Errors) to public APIs\n  - Fixed doc markdown formatting (added backticks to technical terms)\n  - Applied selective allows only for architecturally justified cases\n  - FFI/binding layers use targeted allows due to interop constraints\n  - Core library maintains strict clippy compliance\n- **Updated workspace lint configuration**: Changed pedantic lints from deny to warn to allow module-level selective overrides\n- **Dependency modernization**: Migrated from `once_cell::sync::Lazy` to stdlib `std::sync::LazyLock` (stabilized in Rust 1.80+)\n\n## [2.20.0] - 2026-01-05\n\n### Dependencies\n\n- **Updated reqwest to 0.13.1**: Migrated to new rustls defaults\n  - rustls is now the default TLS backend (previously native-tls)\n  - aws-lc is the default crypto provider (previously ring)\n  - rustls-platform-verifier is used by default for root certificates\n  - All reqwest features updated to new naming conventions\n- **Updated development dependencies**: Updated pnpm packages, Ruby gems, and pre-commit hooks\n  - oxlint pre-commit hook updated from v1.36.0 to v1.37.0\n  - All language bindings dependencies refreshed\n\n### Infrastructure\n\n- **Fixed C# package update task**: Updated dotnet list command to specify project files explicitly\n  - Prevents \"project or solution file could not be found\" errors\n  - Now checks both HtmlToMarkdown.csproj and HtmlToMarkdown.Tests.csproj individually\n\n## [2.19.8] - 2026-01-05\n\n### Bug Fixes\n\n- **Blockquote newline preservation**: Fixed Issue #176 - Newlines were not preserved when block elements like `<strong>` were directly adjacent to `<blockquote>` elements\n  - Blockquotes now add proper spacing before and after themselves\n  - Fixed blockquote+paragraph spacing to match CommonMark spec\n  - Fixed blockquote+HR spacing to avoid extra newlines\n  - Added comprehensive regression tests to prevent future regressions\n  - Maintains CommonMark compliance (132/132 tests passing)\n\n### Improvements\n\n- **Debug logging cleanup**: Removed extensive debug logging from hOCR processing and core converter\n  - Removed ~30 debug eprintln! statements that were spamming output\n  - Removed unused debug parameters from hOCR functions (parse_properties, reconstruct_table, extract_hocr_document, etc.)\n  - Cleaner output and reduced noise during HTML to Markdown conversion\n\n## [2.19.7] - 2026-01-03\n\n### Improvements\n\n- **Homebrew bottle CI debugging**: Added verification steps to diagnose artifact upload/download issues\n  - Added verification after bottle creation to confirm file exists in workspace\n  - Added `if-no-files-found: error` to fail fast if bottle file not found during upload\n  - Added verification after artifact download to show what was actually retrieved\n  - These steps will help identify why Homebrew bottle artifacts aren't being found in release workflow\n\n## [2.19.6] - 2026-01-03\n\n### Bug Fixes\n\n- **WASM npm package publishing**: Fixed Issue #172 - WASM package was published with only 3 files (LICENSE, package.json, README.md) instead of 25 files\n  - Root cause: publish workflow downloaded WASM artifact tarballs but never extracted them before running `npm publish`\n  - Added extraction step in `.github/workflows/publish.yaml` to unpack dist/, dist-node/, and dist-web/ directories\n  - Added safeguard to remove .gitignore files from dist directories that could exclude content\n  - Complete package now includes all WASM binaries and JavaScript wrappers (7.8 MB unpacked)\n\n## [2.19.5] - 2025-01-02\n\n### Bug Fixes\n\n- **Homebrew bottle naming**: Fixed bottle filename format to match Homebrew convention\n  - Changed from double-dash (`html-to-markdown--2.19.x`) to single-dash (`html-to-markdown-2.19.x`)\n  - Homebrew constructs bottle URLs based on formula name and version, expecting single dash separator\n  - Fixes bottle download failures when installing via `brew install`\n\n## [2.19.4] - 2025-01-02\n\n### Bug Fixes\n\n- **Homebrew formula publishing**: Fixed publish workflow script that updates the Homebrew tap formula\n  - Corrected bottle block deletion regex (was looking for `# bottle do` instead of `bottle do`), preventing duplicate bottle blocks from accumulating on each release\n  - Added automatic source tarball SHA256 computation and formula update to ensure correct checksums\n  - Formula now properly replaces old bottle blocks with new ones rather than appending\n\n## [2.19.3] - 2025-01-02\n\n### Bug Fixes\n\n- **Table image processing**: Fixed Issue #175 - images inside Blogger-style HTML tables (e.g., `<table class=\"tr-caption-container\">`) were being stripped during conversion. Enhanced table scanner to recognize images as content and properly process non-table elements like `<a>` and `<img>` that are direct children of table elements.\n- **WASM npm package**: Fixed Issue #172 completely - package was published but missing all WASM binaries and JavaScript wrappers (only 23 KB with 3 files). Created `.npmignore` to include `dist/`, `dist-node/`, and `dist-web/` directories that were excluded by `.gitignore` during npm publish.\n- **PHP Packagist publishing**: Fixed version mismatch that caused Packagist to reject v2.19.2 tag. Updated `sync_versions.py` to synchronize both root `composer.json` and `packages/php/composer.json`.\n- **Test apps**: Fixed relative fixture paths in C#, Java, and Elixir test apps. Updated Elixir tests to handle tuple-returning API. Added Java native library path configuration.\n\n### Infrastructure\n\n- Enhanced `sync_versions.py` script to update root `composer.json` for Packagist validation\n- Recreated v2.19.2 git tag with correct composer.json version\n\n## [2.19.2] - 2025-12-30\n\n### Bug Fixes\n\n- **WASM npm package**: Fixed missing `.d.ts` files in published package by updating `files` field with glob patterns (fixes #172)\n- **Test apps**: Fixed API mismatches across all language test apps (Python, Node.js, WASM, Go, Java, C#)\n  - Python: Changed `convert_html_to_markdown()` to `convert()`\n  - Node.js: Updated to scoped package `@kreuzberg/html-to-markdown`\n  - WASM: Changed `convertHtmlToMarkdown()` to `convert()`\n  - Go: Updated FFI version from 2.16.0 to 2.19.1 with enhanced error handling\n  - Java: Added Maven wrapper files for portability\n  - C#: Updated to `KreuzbergDev.HtmlToMarkdown` package name\n- **Packagist publishing**: Added automated workflow job and moved `composer.json` to repository root\n- **Maven Central publishing**: Fixed GitHub secrets configuration (corrected `GPG_PASSPHRASE` typo)\n- **Go bindings**: Enhanced FFI download error messages with actionable troubleshooting guidance\n- **Pre-commit hooks**: Fixed Go linting errors (errcheck, staticcheck) and formatting violations\n\n### Infrastructure\n\n- Created new WASM test app with comprehensive smoke and integration tests\n- Updated all test apps to version 2.19.0 for consistent validation\n- Enhanced Java package formatting to comply with 120-character line limit\n\n## [2.19.1] - 2025-12-29\n\n### Bug Fixes\n\n- **Go formatting**: Applied `gofmt` to `packages/go/v2/htmltomarkdown/visitor.go` to align constant declarations\n- **Java tooling**: Upgraded google-java-format from 1.21.0 to 1.25.2 for Java 25 compatibility\n- **Homebrew distribution**: Added html-to-markdown formula to kreuzberg-dev homebrew tap for CLI installation\n\n## [2.19.0] - 2025-12-29\n\n### Breaking Changes\n\n- **npm package namespace**: All npm packages now use the `@kreuzberg` scope for better organization and discoverability\n  - `html-to-markdown-node` → `@kreuzberg/html-to-markdown-node`\n  - `html-to-markdown-wasm` → `@kreuzberg/html-to-markdown-wasm`\n- **Java package namespace**: Java binding now uses `dev.kreuzberg` package prefix instead of `com.goldziher`\n  - Updated all Maven artifact IDs and Java package names for semantic clarity\n  - Affects all public classes and imports in Java projects\n- **C# namespace**: C# bindings now use `KreuzbergDev` namespace instead of `Goldziher`\n  - Updated NuGet package ID to `KreuzbergDev.HtmlToMarkdown`\n  - All public types now under `KreuzbergDev.HtmlToMarkdown` namespace\n\n### Features\n\n- **XML table support (TEI/JATS formats)**: Added support for TEI (Text Encoding Initiative) and JATS (Journal Article Tag Suite) table elements\n  - `<row>` elements for table rows with proper cell grouping and nesting\n  - `<cell>` elements with full attribute support including `role=\"head\"` for header cells\n  - `<graphic>` elements for figure/image references within cells and content blocks\n  - Proper table structure preservation when converting scientific markup formats\n  - Aligns with CommonMark table output while respecting source document semantics\n\n### Bug Fixes\n\n- Fixed Clippy warnings across Rust core and all binding crates for cleaner compilation\n- Improved test suite with enhanced error messages and edge case coverage\n- Refined table element handling for robustness with malformed markup\n\n### Infrastructure\n\n- **CI/CD improvements**: Enhanced C# workflow for improved reliability and platform coverage\n- **Release distribution**: Added Homebrew bottle support for macOS CLI binary distribution\n- **Version synchronization**: All language bindings now synchronized to v2.19.0\n\n## [2.18.0] - 2025-12-28\n\n### Added\n\n- **Visitor Pattern**: Complete implementation of visitor pattern for custom HTML element processing across all 8 language bindings (Python, TypeScript, Ruby, PHP, Go, Java, C#, Elixir)\n  - Synchronous and asynchronous visitor support (where applicable per language)\n  - 40+ visitor methods with hooks for every HTML element type (text, links, images, headings, lists, tables, code blocks, and more)\n  - `NodeContext` provides element metadata: tag name, attributes, depth, parent tag, inline status, and sibling index\n  - Control flow options: Continue, Custom (provide custom markdown), Skip, PreserveHtml, or Error\n  - Element lifecycle callbacks: `visit_element_start` and `visit_element_end` for complete control\n  - **Python**: Full async visitor support with `convert_with_async_visitor()` function\n  - **TypeScript**: Async visitor with full type definitions\n  - **Ruby**: Sync visitor implementation with complete RBS type definitions\n  - **PHP**: Full visitor support with PHPStan level 9 compliance\n  - **Go**: Thread-safe visitor registry with markdown post-processing\n  - **Java**: Panama FFI visitor (JDK 21+)\n  - **C#**: P/Invoke visitor with cross-platform compatibility\n  - **Elixir**: Rustler NIF visitor implementation\n\n### Fixed\n\n- **HTML parsing for modern websites**: Fixed issue where JavaScript-heavy websites (like Reuters) would lose article body content during conversion (GitHub issue #167)\n  - The parser was incorrectly interpreting HTML-like strings inside `<script>` tags as actual HTML elements\n  - Script and style tags are now properly stripped during preprocessing while preserving JSON-LD metadata\n  - No performance impact on conversion speed\n- **Python API**: Fixed missing `ConversionOptionsHandle` export in public API (GitHub issue #166)\n  - Users can now import `ConversionOptionsHandle` directly from the `html_to_markdown` package\n  - Maintains backward compatibility with existing `OptionsHandle` import\n\n## [2.17.0] - 2025-12-22\n\n### Added\n\n- Go binding now auto-downloads the native FFI library from GitHub Releases with cache/override controls.\n- Release pipeline now publishes per-platform Go FFI artifacts for Go installs.\n\n## [2.16.1] - 2025-12-22\n\n### Fixed\n\n- Fast-path plain-text conversions now honor escape flags (asterisks/underscores/misc/ASCII).\n- Fast-path plain-text conversions now normalize whitespace and trim trailing spaces.\n- Fast-path plain-text conversions now respect `strip_newlines`.\n- Python CLI proxy now only applies v1 translation defaults when v1-only flags are present.\n\n## [2.16.0] - 2025-12-22\n\n### Added\n\n- Profiling harness and workflow for Rust core and bindings with consolidated flamegraph output.\n- Benchmark scenarios for inline images, metadata extraction, and raw metadata output across fixtures.\n- WASM profiling support with warmups and stable flamegraph parsing.\n- FFI byte-based conversion path plus metadata-raw benchmark coverage.\n\n### Changed\n\n- Bench harness now supports expanded fixture coverage and results consolidation.\n- Java benchmarks align on JDK 25 for consistent profiling runs.\n\n### Fixed\n\n- Node benchmark harness now runs from the package directory and uses native bindings.\n- Profiling stability fixes across Go, Elixir, Java, and WASM adapters.\n- Binary input detection now flags compressed/magic signatures and UTF-16 data with clearer errors.\n\n### Performance\n\n- Rust core conversion: metadata extraction, inline image handling, tag/whitespace caches, and text assembly hot paths.\n- Bindings interop: tighter metadata serialization/deserialization paths.\n- Rust bench harness (local, Apple M4): median ops/sec improved 18.8× on Wikipedia fixtures (53.7 → 1009.1).\n\n## [2.15.0] - 2025-12-19\n\n### Fixed\n\n- Rust core: clamp table `colspan`/`rowspan` to prevent pathological allocations on malformed HTML.\n- Rust core: reject binary-like inputs early to avoid OOMs when non-HTML data is passed to `convert`.\n\n## [2.14.11] - 2025-12-16\n\n### Fixed\n\n- C# (NuGet): fix `ConvertWithMetadata()` deserialization for metadata enums (`link_type`, `image_type`, `data_type`, `text_direction`) by honoring the JSON wire values.\n\n## [2.14.10] - 2025-12-16\n\n### Fixed\n\n- Python: release the GIL during native conversion so `ThreadPoolExecutor` parallelism doesn't regress performance, and always build the extension with metadata support (so `convert_with_metadata` is always available).\n\n## [2.14.9] - 2025-12-16\n\n### Fixed\n\n- Structured data: JSON-LD is now extracted from `<script type=\"application/ld+json\">` tags (including when placed in `<head>`), preserving the script contents for parsing.\n\n## [2.14.8] - 2025-12-15\n\n### Fixed\n\n- Rust crate (`html-to-markdown-rs`): enable the `metadata` feature by default so `convert_with_metadata` is available without extra Cargo features.\n\n## [2.14.7] - 2025-12-15\n\n### Fixed\n\n- Elixir (macOS): package now ships a `.cargo/config.toml` so Rustler can compile without requiring user-specific linker flags.\n\n## [2.14.6] - 2025-12-15\n\n### Fixed\n\n- RubyGems publish: skip duplicate `ruby`-platform gems when multiple CI jobs produce identical artifacts for the same version.\n- Hex publish: ensure the Rust core crate is staged into the Elixir package before publishing.\n\n## [2.14.5] - 2025-12-15\n\n### Fixed\n\n- RubyGems publish: prevent corrupted gem pushes by downloading `rubygems-*` artifacts into separate directories (no merge), and publishing gems recursively with an integrity check.\n\n## [2.14.4] - 2025-12-15\n\n### Fixed\n\n- Release pipeline: build the C# `osx-x64` native FFI library on `macos-15-intel` (macOS-13 runners are retired), unblocking NuGet publication.\n- Elixir (Hex): package now vendors the Rust core crate so `mix deps.get && mix test` works outside this monorepo.\n\n## [2.14.3] - 2025-12-15\n\n### Fixed\n\n- **Issue #150 / Discord report**: Python now always exports `convert_with_metadata` (no more `ImportError` on import).\n- **Issue #149**: Blockquote text now word-wraps when `wrap=true`.\n- **FFI JSON parity**: Metadata enums now serialize as snake_case (e.g. `external`, `relative`) to match cross-language expectations.\n- PHP test runner now always builds the extension with the `metadata` feature enabled (avoids missing `html_to_markdown_convert_with_metadata` when the workspace was built with `--no-default-features`).\n\n### Added\n\n- Elixir: `convert_with_metadata/3` + `MetadataConfig` backed by the Rust metadata extractor.\n\n### Changed\n\n- WASM: metadata bindings are enabled by default so the published npm package exports `convertWithMetadata`.\n- C# publish pipeline: stage native `html_to_markdown_ffi` libraries into the NuGet package under `runtimes/*/native`.\n- Go: module path now uses semantic import versioning (`.../packages/go/v2`), and docs/examples were updated accordingly.\n- Java: add `.sdkmanrc` for Java 25 + Maven 4; keep `maven-source-plugin` on `3.3.1` because `4.0.0-beta-1` is not compatible with Maven `4.0.0-rc-4`.\n\n## [2.14.2] - 2025-12-13\n\n### Changed\n\n- CI/release automation: extracted Maven installer logic into `scripts/common/install-maven-latest.sh` and applied repo-wide lint/format cleanups.\n\n## [2.14.1] - 2025-12-12\n\n### Fixed\n\n- **Issue #147**: Word wrap now works correctly in list items when using the `-w`/`--wrap` flag. List items with long text are properly wrapped while preserving list structure and indentation for both ordered and unordered lists.\n- **Issue #146**: `strip_tags` and `preserve_tags` options now correctly prevent `<meta>` and `<title>` tags from being extracted into YAML frontmatter when `extract_metadata` is enabled.\n- **Issue #145**: `strip_newlines=true` no longer causes excessive whitespace around block elements. Structural whitespace is now properly normalized while still removing newlines within paragraph content.\n\n## [2.14.0] - 2025-12-11\n\n### Added\n\n- **CLI Metadata Extraction**: New `--with-metadata` flag with JSON output support for extracting document metadata, headers, links, images, and structured data from HTML documents.\n  - Six extraction flags: `--extract-document`, `--extract-headers`, `--extract-links`, `--extract-images`, `--extract-structured-data`\n  - JSON output format with markdown and metadata fields: `{\"markdown\": \"...\", \"metadata\": {...}}`\n  - Feature enabled by default in CLI builds\n- **Go FFI Binding**: Complete `ConvertWithMetadata()` function with typed structs for metadata extraction.\n  - 12 Go struct types with JSON tags for type-safe metadata access\n  - JSON unmarshaling from FFI layer\n  - 18 comprehensive tests covering all metadata types\n- **Java FFI Binding**: Complete `convertWithMetadata()` method with Java records for metadata extraction.\n  - 11 Java record types using Panama FFM for FFI integration\n  - Proper enum types for link/image/text direction (no string-based parsing)\n  - Jackson JSON deserialization with error handling\n  - 33 comprehensive tests including negative test cases\n- **C# FFI Binding**: Complete `ConvertWithMetadata()` method with C# records for metadata extraction.\n  - 11 C# record types using P/Invoke for FFI integration\n  - System.Text.Json deserialization with proper error handling\n  - 23 comprehensive tests covering all metadata types\n- **FFI Core API**: New `html_to_markdown_convert_with_metadata()` C function for language-agnostic metadata extraction.\n  - JSON serialization for cross-language compatibility\n  - Proper memory management and error handling\n  - 17 comprehensive tests including memory safety tests\n\n### Changed\n\n- **Documentation Consolidation**: Migrated all standalone METADATA.md files into binding READMEs for improved maintainability.\n  - Deleted `packages/typescript/METADATA.md` (480 lines) and `packages/ruby/METADATA.md` (228 lines)\n  - Enhanced Python, PHP, TypeScript, Ruby, Go, Java, and C# READMEs with comprehensive metadata sections\n  - Root README now includes CLI metadata examples and links to all binding documentation\n  - Each binding README is now self-contained with full metadata documentation\n- **Type Definitions**: Enhanced metadata type definitions across all language bindings.\n  - Go: Complete struct types with JSON tags and godoc comments\n  - Java: Proper enum types (LinkType, ImageType, TextDirection) instead of strings\n  - C#: Complete record types with XML documentation\n  - Python: Fixed `max_structured_data_size` default (100KB → 1MB)\n  - TypeScript: Verified dimensions field type (Array<number> for compatibility)\n- **Docstrings**: Enhanced documentation strings across all language bindings.\n  - Rust core: Improved function and module documentation\n  - Python: Enhanced PyO3 docstrings with examples and type hints\n  - Ruby: Added YARD tags for better documentation generation\n  - PHP: Enhanced docblocks with detailed parameter descriptions\n\n### Fixed\n\n- **FFI Memory Safety**: Fixed critical memory safety bug where error paths could leave dangling metadata pointers.\n  - Both markdown and metadata pointers now set to null on any error\n  - Added comprehensive memory safety tests\n- **CLI Flag Implementation**: Fixed `--extract-document` flag not being mapped to MetadataConfig.\n  - Flag now correctly controls document metadata extraction\n  - Added 9 new CLI tests for metadata flags\n- **Java Type Safety**: Fixed metadata loss and silent failures from missing fields and string-based enums.\n  - Added dimensions field to ImageMetadata (was missing, causing 50% metadata loss)\n  - Changed linkType, imageType, textDirection from String to proper enum types\n  - Fixed exception swallowing in getLastError() - now logs errors and returns descriptive messages\n- **Python Default Values**: Fixed incorrect `max_structured_data_size` default (was 100KB, should be 1MB).\n  - Now uses `DEFAULT_MAX_STRUCTURED_DATA_SIZE` constant from Rust core\n- **Constants Extraction**: Eliminated DRY violations by extracting hardcoded magic numbers.\n  - Added `DEFAULT_MAX_STRUCTURED_DATA_SIZE: usize = 1_000_000` constant in Rust core\n  - Reused across FFI, CLI, and Python bindings\n\n### Technical Details\n\n- **Test Coverage**: Added 55 new tests across all bindings (71 → 126 total tests, 77% increase)\n  - FFI: 13 new tests (4 → 17 total)\n  - CLI: 9 new tests (67 → 76 total)\n  - Java: 33 new tests (0 → 33 total)\n  - Go: 18 tests total\n  - C#: 23 tests total\n- **Language Compliance**: Achieved 100% compliance across all bindings (up from 50%-100% range)\n  - All bindings now correctly implement metadata extraction with proper types\n  - Standardized error handling and JSON parsing patterns\n- **Documentation**: Added 3,500+ lines of comprehensive metadata documentation across all binding READMEs\n  - Migrated 708 lines from TypeScript and Ruby METADATA.md files\n  - Enhanced Python and PHP READMEs with extensive examples\n  - Added metadata sections to Go, Java, and C# READMEs\n\n## [2.13.0] - 2025-12-10\n\n### Added\n\n- Comprehensive metadata extraction API across all language bindings (Python, TypeScript, Ruby, PHP, WASM).\n- New `convert_with_metadata()` function returning both markdown and extracted metadata in a single pass.\n- Metadata extraction includes: document metadata (title, description, keywords, author, language, Open Graph, Twitter Card), header hierarchy (h1-h6 with IDs and nesting), link classification (internal/external/anchor/email/phone), image metadata with type detection (data URIs, inline SVGs, external, relative), and structured data (JSON-LD, Microdata, RDFa).\n- Python: 51 comprehensive integration tests with full TypedDict type stubs and mypy validation.\n- TypeScript: 14 vitest tests with auto-generated NAPI types, runtime feature detection via `hasMetadataSupport()`, and 600+ lines of documentation.\n- Ruby: 40+ RSpec tests with complete RBS type signatures and comprehensive API documentation.\n- PHP: 21 PHPUnit tests with PHPStan level max compliance and readonly Value Objects.\n- WASM: Complete metadata extraction with serde_wasm_bindgen serialization and getter/setter configuration structs.\n\n### Changed\n\n- Enabled metadata feature by default in TypeScript and Ruby bindings for production npm packages and gems.\n- Updated all language binding versions to 2.13.0 with synchronized version management.\n\n### Fixed\n\n- Ruby: Added missing wrapper method for `convert_with_metadata` and fixed redundant `?` symbols in RBS type annotations.\n- TypeScript: Enabled metadata feature in default Cargo features to ensure npm packages include metadata functionality.\n- WASM: Fixed 3 clippy style violations (Default trait implementation, unwrap_or_default usage, struct initialization pattern).\n\n## [2.12.1] - 2025-12-09\n\n### Fixed\n\n- Escape literal `|` characters inside table cells while leaving pipes inside `<code>` and `<pre>` untouched to avoid rendering backslashes in code spans/blocks (fixes #140).\n- Handle nested tables without double-escaping pipes and add regression coverage for table cells containing code spans/blocks and nested tables.\n- Preserve link-only list items when word wrapping is enabled so nested link lists are not merged or reflowed (fixes #143); added regression fixtures for the reported table-of-contents sample.\n\n### Changed\n\n- Updated dependency locks/manifests to align with the 2.12.1 release.\n- Downgraded Java Maven compiler/source plugins back to 3.x to keep CI builds compatible with Maven 3 runners.\n\n## [2.12.0] - 2025-12-08\n\n### Added\n\n- WebAssembly bundler target now supports Cloudflare Workers, Wrangler, and modern bundlers that provide `WebAssembly.Module` instead of `WebAssembly.Instance`.\n- Three new WASM usage examples demonstrating different deployment targets:\n  - `examples/wasm-node`: Node.js example using dist-node target\n  - `examples/wasm-rollup`: Browser example using dist-web target with Rollup\n  - `examples/wasm-cloudflare`: Cloudflare Workers example using bundler target with Wrangler\n\n### Changed\n\n- WASM bundler entry point now detects and handles `WebAssembly.Module` instances, building the proper import namespace for wasm-bindgen glue functions.\n\n## [2.11.4] - 2025-12-08\n\n### Fixed\n\n- Node/WASM bundles now post-process their generated JS files to import the shared `WasmConversionOptions` typedef and emit typed doc comments (including typed inline-image `attributes`), so no `any` annotations leak into the published `dist`, `dist-node`, `dist-web`, or docs bundles.\n\n## [2.11.3] - 2025-12-08\n\n### Fixed\n\n- Prevent link-label truncation from splitting multi-byte characters, which previously triggered a `PanicException` in the Python bindings when processing long anchors (resolves #139) and add a regression test to keep the truncation logic safe.\n\n## [2.11.2] - 2025-12-07\n\n### Added\n\n- Explicitly ship typing artefacts in every binding: npm packages export `.d.ts` files by default, Ruby gems now include `sig/**/*.rbs` even when building outside git, and the Python wheel bundles `_html_to_markdown.pyi` plus a `py.typed` marker for static type checkers.\n\n### Fixed\n\n- Cleaned up the Python API’s inline-image helper to avoid redundant casts flagged by `mypy --strict`.\n- Tightened PHP docblocks and psalm/phpstan annotations so option arrays use strongly typed shapes instead of `array<string, mixed>`.\n- Hardened the WASM, Node, and Python bindings so their `options` argument is fully typed end-to-end (no `any` escapes in `.d.ts` files or placeholder `Any` annotations).\n\n## [2.11.1] - 2025-12-05\n\n### Fixed\n\n- Preserve indentation in `<pre><code>` blocks while safely dedenting whitespace across multibyte characters to avoid panics when leading spaces are non-ASCII; regression fixture added for issue #134. Thanks @bbeardsley for the contribution.\n\n## [2.11.0] - 2025-12-04\n\n### Added\n\n- CLI `--url` flag with optional `--user-agent` override to fetch remote HTML directly, plus charset-aware decoding.\n- New GitHub Pages deploy workflow to publish the `docs/` demo from `main`.\n- Additional CLI integration tests covering URL fetching (including custom UA, legacy markup, frameset/noframes, cp1252 decoding).\n\n### Changed\n\n- Demo layout now keeps input/output panes equal height and responsive.\n- Rust core handles body-like content accidentally nested in `<head>` more gracefully.\n\n## [2.10.1] - 2025-12-02\n\n### Fixed\n\n- Normalize whitespace inside link labels (collapse newlines and extra spaces) so anchors with messy HTML do not emit multi-line `[]` text.\n- Flatten block children inside `<a>` (e.g., headings/paragraphs nested in anchors) into a single Markdown link instead of duplicating content; regression tests added for the reported Arabic product card case.\n\n### Changed\n\n- Synced all workspace/package versions to 2.10.1 via `task sync-versions`.\n\n## [2.10.0] - 2025-12-02\n\n### Added\n\n- Centralized panic guarding for all bindings (Python, Node, PHP, WASM, C FFI) using a shared Rust helper so panics surface as language-native errors instead of unwinding across FFI boundaries.\n- C FFI now stores the last error per thread and exposes it via `html_to_markdown_last_error`, with panic and UTF-8/null input diagnostics.\n- Ruby binding now uses the shared panic guard and emits consistent panic messages; specs cover panic interception across conversion entrypoints.\n\n### Changed\n\n- Wasmtime test harness initializes conversion options via struct literals to reduce clippy noise in CI.\n\n### Fixed\n\n- Rust coverage CI now forces `cargo-llvm-cov` reinstall to avoid cached binary conflicts on GitHub runners.\n- PHP smoke tests use the Packagist package name `goldziher/html-to-markdown`, matching README install instructions.\n\n## [2.9.3] - 2025-12-01\n\n### Changed\n\n- **Version sync** – Bumped the entire workspace (Rust, Python, npm, Ruby, Elixir, Java, C#, Go) to 2.9.3 via `task sync-versions` to prep the next patch release.\n- **Docs & install commands** – Pointed all Composer references to the published `goldziher/html-to-markdown` package and clarified npm usage to the shipped packages (`html-to-markdown-node` / `html-to-markdown-wasm`).\n\n### Fixed\n\n- **Go lint CI** – Replaced the invalid `go fmt -l` invocations with `gofmt -l` in the Taskfile so `task check`/CI lint runs complete successfully on Go 1.25.\n\n## [2.9.2] - 2025-11-28\n\n### Fixed\n\n- **UTF-8 safety (Fix #127)** – Guarded whitespace trimming against mid-codepoint truncation, eliminating byte-boundary panics on multilingual documents; added fixture and regression test for the reported Ruby-path crash.\n- **Image conversion (Fix #128)** – `<img>` elements with `width`/`height` now render as Markdown images instead of raw HTML; regression test covers inline-data URIs with dimensions.\n\n## [2.9.1] - 2025-11-22\n\n### Changed\n\n- **HTML repair fallback** – Minified or malformed pages now reparse via html5ever when inline/block nesting is broken, keeping content that previously vanished (e.g., SPA shells and Hacker News markup).\n- **Link label recovery** – Anchor text fallback prefers child formatting or hrefs only when appropriate, preventing empty labels while keeping CommonMark empty-link semantics intact.\n\n### Fixed\n\n- **Layout tables to lists** – Headless tables with mixed column counts/spans or nested tables render as list rows instead of broken Markdown tables, restoring Hacker News output.\n- **Issue 121 regressions** – Added fixtures/tests for the empty SPA and malformed Hacker News samples; both now produce full Markdown content without frontmatter noise.\n\n## [2.9.0] - 2025-11-20\n\n### Added\n\n- **Elixir bindings** – New `html_to_markdown` Hex package built with Rustler, exposing the Rust core converter to Elixir with configurable options plus `convert/2` and `convert!/2`.\n- **WASM runtime verification** – Added a Wasmtime-backed e2e suite (`e2e/wasm-wasmtime`) plus `task wasm:test:wasmtime` to compile the `html-to-markdown-wasm` artefact for `wasm32-unknown-unknown` and execute it inside Wasmtime. CI now runs these tests to ensure the WASM package works outside the browser runtime.\n\n### Changed\n\n- **Astral `tl` parser** – The HTML parser dependency now points to the actively maintained `astral-tl` fork (still imported as `tl`) so comment parsing stays up to date with upstream fixes.\n- **NuGet Package ID** – C# bindings now publish under `Goldziher.HtmlToMarkdown` to avoid clashing with an existing community package.\n- **Wasmtime CI Coverage** – The Wasmtime e2e job now runs on Linux x64, Linux arm64, macOS, and Windows runners so every GitHub-hosted architecture executes the WASM tests.\n\n### Fixed\n\n- **PHP PIE source bundle** – Release packaging strips the Wasmtime e2e workspace from the staged `Cargo.toml`, fixing the “failed to load manifest” error in the publish workflow.\n- **Horizontal rule rendering** – `<p>…</p><hr>` now emits a blank line before `---` while preserving blockquote spacing so the rule is never misinterpreted as a setext heading.\n- **Empty HTML comments** – Zero-width `<!---->` comment nodes are normalized before parsing, so comment placeholders no longer cause the following content to disappear.\n\n## [2.8.3] - 2025-11-15\n\n### Changed\n\n- **Deterministic uv installs** – Every `uv sync` invocation in CI and the Taskfile now runs with `--no-install-workspace`, ensuring Python dependencies are resolved without mutating editable installs before the subsequent build/test steps run.\n\n### Fixed\n\n- **NuGet Publishing** – Release automation now uses GitHub’s trusted publisher flow via `NuGet/login@v1` (OIDC → short-lived API key) before pushing artifacts, removing the dependency on long-lived secrets.\n- **Hex Publishing** – The release workflow invokes `mix hex.publish --yes` from `packages/elixir`, with `ex_doc` bundled as a dev dependency so documentation generation works during release.\n\n## [2.8.2] - 2025-11-15\n\n### Changed\n\n- **Unified Version Sync** – `scripts/sync_versions.py` now updates Elixir `@version` declarations, the C# `.csproj`, and the Java `pom.xml` (alongside every npm/pyproject/Gemfile manifest). `task sync-versions` bumps the entire multi-language stack to **2.8.2** in one shot.\n- **CI / Release Toolchains** – GitHub Actions now installs Elixir dependencies ahead of Credo and runs on **Elixir 1.19 + OTP 28.1**, matching the README prerequisites and preventing per-job regex recompilation warnings.\n- **Taskfile Coverage** – Added `elixir:update` plus full `java:{install,update,test,lint}` tasks so `task setup`, `task update`, `task test`, and `task lint` cover every published runtime (Go, C#, Elixir, Java) just like the CI workflows.\n\n## [2.8.1] - 2025-11-15\n\n### Fixed\n\n- **Release Pipeline** – Bumped all package manifests to v2.8.1 so the publish workflow can push fresh artifacts after the v2.8.0 smoke-test fixes (PyPI, npm, and RubyGems refuse re-uploads of the same version).\n\n## [2.8.0] - 2025-11-15\n\n### Added\n\n- **Java, C#, and Go Bindings (First Release)** – First public release of official Java (JNA), C# (.NET), and Go (CGO) language bindings. All three are integrated into the unified `task bench:bindings` harness and ship with comprehensive performance data in their READMEs. C# leads at ~1.4k ops/sec (≈171 MB/s), Go at ~1.3k ops/sec (≈165 MB/s), and Java at ~1.0k ops/sec (≈126 MB/s) on the 129 KB Wikipedia lists fixture.\n\n### Changed\n\n- **BREAKING: Preprocessing Disabled by Default** – HTML preprocessing is now disabled by default in the library API to prevent silent content loss. Previously, `<nav>`, `<form>`, and related elements (along with all their children) were dropped by default, causing important content inside these tags to be lost. Users who want preprocessing must now explicitly enable it via `PreprocessingOptions { enabled: true, ... }`. The CLI behavior is unchanged (preprocessing has always been opt-in with `--preprocess`).\n- **Rust Toolchain Settings** – All crates (including the Ruby binding) now inherit `edition = \"2024\"` and `rust-version = \"1.85\"` from the workspace to keep toolchain configuration centralized.\n- **GitHub Actions Workflow DRY** – Created 17 reusable composite actions (8 build actions + 9 smoke test actions) to eliminate ~267 lines of duplication between CI and publish workflows.\n- **Toolchain Management** – Migrated to official GitHub Actions parameters for Ruby Bundler 2.7.2 and PHP Composer 2.9.1, removing manual installation scripts.\n\n### Fixed\n\n- **Windows PHP Extension Build** – Replaced php-windows-builder orchestration with direct `cargo build` matching ext-php-rs's proven approach, resolving LLVM 19 MMX header incompatibilities and Zend symbol linking errors.\n- **Linux PHP Build** – Added php-config path capture and parameter passing to build-php-linux action, fixing \"php-config executable not found\" errors.\n- **Ruby Linux Build** – Set LD_LIBRARY_PATH on Linux builds to match magnus best practices, preventing potential \"strings.h not found\" errors.\n- **golangci-lint CI** – Split golangci-lint pre-commit hook into separate invocations for `packages/go` and `examples/go-smoke` modules, fixing \"directory prefix does not contain main module\" errors by running each check from within its Go module directory.\n- **Windows Go CGO Smoke Test** – Documented MSVC/MinGW toolchain incompatibility and skip Windows Go smoke test with informative message, as Go CGO uses MinGW which cannot link against MSVC-compiled Rust FFI libraries.\n- **Go Code Quality** – Removed redundant newline in `examples/go-smoke/main.go` fmt.Println call (detected by newly-working golangci-lint).\n\n## [2.7.2] - 2025-11-12\n\n### Fixed\n\n- **Node/WASM Binding Regression** – HTML preprocessing no longer drops `<html>`, `<head>`, or `<body>` wrappers when their classes resemble navigation chrome, so large Wikipedia fixtures once again emit full markdown (restoring the Vitest length/table expectations for Node bindings and keeping WASM conversions consistent).\n- **Cloudflare WASM Initialization** – Bundler builds of `html-to-markdown-wasm` now expose `initWasm()`/`wasmReady` so edge runtimes that instantiate WebAssembly modules asynchronously (Cloudflare Workers, Vite dev servers, etc.) can await initialization before calling `convert()`, eliminating the `__wbindgen_start` runtime error.\n- **Footer Retention (Fix #120)** – The Rust preprocessor keeps plain `<footer>` content unless the element carries explicit navigation hints (role/class/id). Python and Rust conversions once again preserve footer copy while still stripping true navigation footers such as `.site-footer` menus.\n- **Release Smoke Coverage** – The publish workflow now downloads the built artifacts (Node, WASM, Python wheels, Ruby gems, PHP zips) and reruns the README smoke installs across Linux/macOS/Windows before any packages are uploaded, ensuring we're testing the exact bits we ship.\n\n## [2.7.1] - 2025-11-12\n\n### Added\n\n- **Language-Specific Benchmarks** – Every binding README (Node, WASM, Python, Ruby, PHP, TypeScript) now publishes the latest `task bench:bindings` throughput numbers so runtime documentation stays aligned with the shared fixtures.\n- **Examples/Smoke Suite** – Added `examples/{node,wasm,python,ruby,php,rust}-smoke` plus an overview README to exercise both the published artifacts and local builds before a release.\n\n### Changed\n\n- **Docs Accuracy** – Node/WASM READMEs now clearly reference the real npm packages (`html-to-markdown-node`, `html-to-markdown-wasm`) and provide correct import samples.\n- **TypeScript README** – Highlights that the CLI wrapper inherits the native Node benchmarks.\n- **Repository Hygiene** – `.gitignore` now drops `.venv/`, vendor directories, and nested `node_modules/` so smoke tests and language-specific toolchains don’t dirty the tree.\n- **Ruby Build Metadata** – `extconf.rb` uses a relative path for the embedded Cargo crate and the crate’s `Cargo.toml` now declares explicit `edition`, `rust-version`, and dependency pins, allowing `gem install` outside the workspace.\n- **Version Sync Script** – `scripts/sync_versions.py` updates every `html-to-markdown-rs` dependency pin (workspace root plus downstream crates) to keep cross-language releases in lockstep.\n\n### Fixed\n\n- **Smoke Test Coverage** – Verified Node, WASM, Python, Ruby (local gem), PHP (Composer path repo), and Rust installs; documented gaps where external registries still need to publish `goldziher/html-to-markdown` or `html-to-markdown` 2.7.1 before release.\n\n## [2.7.0] - 2025-11-12\n\n### Added\n\n- **Zero-Copy Inline Images** – Node/N-API and WASM bindings now expose `convertInlineImagesBuffer` / `convertBytesWithInlineImages`, letting benchmark harnesses feed `Buffer`/`Uint8Array` data directly without creating intermediate JS strings.\n\n### Changed\n\n- **Rust Core Preprocessing** – HTML normalization (self-closing fixes, malformed `<` escaping, script/style stripping) now happens in a single streaming pass that hands owned buffers straight to `tl::parse_owned`, cutting multiple allocations from every conversion.\n- **Benchmark Harness + Docs** – Re-ran the cross-language runtime suite after the Rust core optimizations and refreshed the README tables, keeping the published throughput numbers (Node/Python/Rust/WASM/PHP) in sync with `tools/runtime-bench/results/latest.json`.\n- **Version Alignment** – Bumped every package (Rust crates, npm packages, PyPI distribution, Ruby gem, PHP extension, WASM bundle) to `2.7.0` via `task sync-versions`.\n\n### Fixed\n\n- **Ruby Benchmark Output** – The Ruby benchmark driver now emits JSON without relying on `json` native extensions, preventing `libruby` incompatibility errors during `task bench:bindings`.\n- **Nested `<strong>` Normalization (Fix #111)** – The Rust converter now tracks when bold markup is already active, so nested `<b>`/`<strong>` combinations (including `<mark>`, `<summary>`, `<legend>`) no longer generate `****` artifacts (`<b>bo<b>ld</b>er</b>` correctly becomes `**bolder**`). The CommonMark harness documents the four spec examples that expect stacked markers and skips them accordingly.\n- **Heading Whitespace (Fix #118)** – ATX/Setext headings swallow layout-only newlines and indentation inside `<h1>…<h6>` so pretty-printed HTML like `<h2>Heading\\n  Text</h2>` renders as a single Markdown heading line.\n- **Inline Whitespace Preservation** – Reworked the inline text pipeline so removing zero-width inline elements (e.g., `<input>`, `<script>`, empty `<b>`) no longer collapses surrounding spaces; fixtures like `test_chomp`, `test_form_with_inputs_inline_mode`, and checkbox/task-list rendering now match their expected double-space gaps.\n- **DOCTYPE Handling (Fix #119)** – `<!DOCTYPE …>` declarations are stripped during preprocessing so they never leak as stray `PUBLIC…` text in the output, even when metadata extraction is enabled.\n\n## [2.6.6] - 2025-11-10\n\n### Changed\n\n- **Ruby Gem Packaging** – Moved the `html-to-markdown-rb` crate under `packages/ruby/ext/html-to-markdown-rb/native` and pointed `extconf.rb` at that path so every published gem now contains the Cargo sources it needs to compile on install.\n- **Documentation Consistency** – Updated the root, crate, and package READMEs to drop references to the unrelated `html-to-markdown` npm package and to consistently list our supported targets (Node, WASM, Python, Ruby, PHP, CLI).\n- **Dependency Refresh** – Ran `task update` to upgrade Rust crates, npm packages, Bundler gems, Python requirements, and Composer dependencies across the monorepo.\n\n### Fixed\n\n- **Rust Clippy Lints** – Addressed `clippy::unnecessary-map-or` in the converter and hOCR table builder by using `.is_none_or`, keeping inline-image filtering and column pruning logic clear while allowing `cargo clippy -D warnings` to pass.\n- **PIE Source Packaging** – `scripts/package_php_pie_source.sh` now copies `packages/ruby/.../native` into the temporary workspace so the Ruby crate exists when PIE builds the PHP extension.\n\n## [2.6.3] - 2025-11-07\n\n### Fixed\n\n- **Release Pipeline** - Fixed missing `is_tag` output in publish workflow that caused all publishing jobs to be skipped\n- **Node.js Package Dependencies** - Added missing `optionalDependencies` to html-to-markdown-node package.json to properly link platform-specific binaries\n- **Version Management** - Created centralized version sync script (`scripts/sync_versions.py`) to maintain consistency across all package manifests (Rust, Node.js, Python, Ruby, WASM)\n- **Cargo Workspace** - Aligned html-to-markdown-rb crate version (was 2.5.7) with workspace version\n\n### Changed\n\n- Added `task sync-versions` command to Taskfile for easy version synchronization across the monorepo\n\n## [2.6.2] - 2025-11-07\n\n### Fixed\n\n- **Table Rowspan Support** - Fixed tables with rowspan cells to correctly duplicate cell content across spanned rows instead of showing empty cells (fixes #116)\n- **Node.js Platform Package Publishing** - Fixed workflow to correctly move packed .tgz files to npm directory for publishing\n- **Deprecation Warnings** - Updated CLI tests to use `CARGO_BIN_EXE` env var instead of deprecated `cargo_bin` method\n- **Deprecation Warnings** - Replaced deprecated `criterion::black_box` with `std::hint::black_box` in benchmarks\n- **Clippy Warnings** - Fixed field assignment warnings by using struct initialization with defaults\n\n## [2.6.1] - 2025-11-07\n\n### Fixed\n\n- **Node.js Platform Packages** - Fixed publishing of platform-specific npm packages. The workflow now correctly packs npm directories into .tgz files before publishing, ensuring all platform bindings (linux-x64-gnu, darwin-arm64, win32-x64-msvc, etc.) are published to npm.\n- **WASM Package Publishing** - Added proper WASM package publishing workflow to ensure html-to-markdown-wasm is published to npm registry.\n\n## [2.6.0] - 2025-11-07\n\n### Added\n\n- **PHP Extension Support** - Official PHP extension (`goldziher/html-to-markdown`) providing native HTML to Markdown conversion for PHP 8.2+\n  - Built with ext-php-rs for high-performance Rust-backed conversion\n  - Supports both Thread-Safe (TS) and Non-Thread-Safe (NTS) builds\n  - Available for Windows (x86, x64), Linux, and macOS\n  - Distributed via PIE (PHP Installer for Extensions) source bundles\n  - Prebuilt Windows binaries for PHP 8.2, 8.3, and 8.4\n  - Comprehensive test suite with PHPUnit\n\n### Changed\n\n- Refactored PHP build variable names from `HTM2MD_*` to `HTMLTOMARKDOWN_*` for improved clarity in Makefile.frag and config.m4\n- Bumped all package versions to 2.6.0 across Rust crates, npm packages, PyPI wheels, Ruby gem, and PHP extension\n\n## [2.5.7] - 2025-11-03\n\n### Added\n\n- Publish Windows PHP extension binaries alongside the PIE source bundle during the release pipeline, enabling one-click installs on every platform.\n- Build and archive the CLI binary for Linux (gnu & musl), macOS arm64, and Windows x86_64, plus ship prebuilt WASM bundles (dist/dist-node/dist-web) so every runtime gets first-class artifacts.\n\n### Changed\n\n- Renamed the PHP extension package to `goldziher/html-to-markdown`, moved the Composer metadata to the repository root, and refreshed the documentation/badges for every language target.\n- Bumped every package (Rust crates, npm packages, PyPI wheels, Ruby gem, PHP extension) to version 2.5.7.\n- Restored the Node.js N-API build matrix so macOS, Windows, and Linux binaries ship automatically with each npm release.\n\n### Fixed\n\n- Preserve ordered list numbering and indentation when list items render headings or HTML tables, so mixed block content stays under the correct bullet (fixes #107).\n\n## [2.5.6] - 2025-10-30\n\n### Changed\n\n- The Ruby gem now packages its own README at the gem root, so RubyGems renders the fully formatted documentation (benchmarks, configuration, CLI notes) without broken links.\n- Documentation links: the Ruby README now surfaces GitHub resources (issues, changelog, live demo) alongside feature highlights.\n- Bumped every package (Rust crates, npm, PyPI, Ruby gem) to version 2.5.6.\n\n## [2.5.5] - 2025-10-30\n\n### Changed\n\n- Synced documentation: the root README now links to every language guide, and the Ruby README highlights GitHub resources alongside feature docs.\n- Gem packaging now reads the README directly for the RubyGems long description while keeping Rubocop happy on all Ruby sources.\n- Bumped every package (Rust crates, npm, PyPI, Ruby gem) to version 2.5.5.\n\n## [2.5.4] - 2025-10-30\n\n### Changed\n\n- Polished the Ruby gem messaging and README with performance highlights, configuration examples, and CLI guidance to match other language docs.\n- Bumped every package (Rust crates, npm, PyPI, Ruby gem) to version 2.5.4.\n\n## [2.5.3] - 2025-10-30\n\n### Changed\n\n- Publish Ruby gems as precompiled artifacts for Linux (x86_64), macOS (arm64 & x86_64), and Windows (x64) via a matrix GitHub Action, ensuring the CLI executable matches the target platform.\n- Split the release workflow into prepare/build/publish stages so dry runs build artifacts without pushing, and trusted publishing now uploads every generated `.gem`.\n- Hardened the gem preparation script to clear stale CLI binaries before copying in the platform-specific build output.\n- Re-enabled the cross-language release workflow so crates.io, PyPI wheels/sdist, and both npm packages ship alongside the Ruby release.\n\n## [2.5.2] - 2025-10-29\n\n- Fix Ruby gem packaging to embed standalone Cargo manifest (no workspace inheritance) so installs compile out of tree successfully.\n- Bump versions across Rust, Node, Python, and Ruby bindings.\n\n## [2.5.1] - 2025-10-28\n\n### Added\n\n- Magnus-based Ruby gem (`html-to-markdown-rb`) with CLI proxy and comprehensive specs.\n\n### Changed\n\n- CI now includes Ruby coverage across macOS, Linux, and Windows, installing the appropriate toolchains (MSYS2 on Windows) for Magnus builds.\n- Release workflow prepares the Ruby gem via trusted publishing alongside existing crates/npm packages.\n\n### Fixed\n\n- Bundler version pinned to 2.5.12 to support Ruby 3.2 CI environments.\n\n## [2.5.0] - 2025-10-24\n\n### Added\n\n- **New `preserve_tags` option** - Preserve specific HTML tags in their original HTML form instead of converting them to Markdown. This is useful for complex elements like tables that may not convert well to Markdown. Fixes issue #95.\n  - Accepts a list of tag names (e.g., `[\"table\", \"form\"]`)\n  - Preserves all attributes and nested content as HTML\n  - Works independently of `strip_tags` - can use both options together\n  - Available in all bindings: Rust, Python, Node.js, and WASM\n  - Comprehensive test coverage in Rust, Python (pytest), and TypeScript (vitest)\n\n### Changed\n\n- **HTML preprocessing is now enabled by default** - The `PreprocessingOptions.enabled` default changed from `False` to `True` to ensure robust handling of malformed HTML. Users who want minimal preprocessing can explicitly set `enabled=False`.\n\n### Fixed\n\n- **Task list checkbox support** - Fixed sanitizer removing `<input type=\"checkbox\">` elements when `remove_forms` is enabled (default). Checkboxes are now preserved during preprocessing to enable proper task list conversion (`- [x]` / `- [ ]`).\n  - Added `input` tag to allowed tags in all sanitization presets (minimal, standard, aggressive)\n  - Preserved `type` and `checked` attributes on input elements\n  - Fixed pre-existing bug where task list checkboxes were silently removed\n- **Data URI support for inline images** - Fixed sanitizer stripping `data:` URLs from image src attributes. Base64-encoded inline images (data URIs) are now preserved during preprocessing.\n  - Added `data` to allowed URL schemes in all sanitization presets\n  - Fixes `convert_with_inline_images` functionality for base64-encoded images\n- **CDATA section handling** - Fixed test expectation for CDATA sections. CDATA sections are now correctly preserved as-is during HTML parsing instead of being partially stripped.\n- **hOCR word spacing** - Fixed missing whitespace between `<span class=\"ocrx_word\">` elements in hOCR documents. Words now have proper spaces between them.\n  - Modified `OcrxWord` converter to insert space before each word if output doesn't end with whitespace or markdown formatting characters\n  - Ensures proper word separation in OCR-generated documents without breaking markdown formatting (e.g., `*text*`, `[alt](url)`, `` `code` ``)\n- **hOCR detection with preprocessing** - Fixed hOCR documents not being detected when HTML preprocessing is enabled (new default). The sanitizer now preserves:\n  - `class` attributes on all elements (required for detecting hOCR element types)\n  - `<meta>` tags with `name` and `content` attributes (required for hOCR metadata detection)\n  - `<head>` tags (container for meta tags)\n- **hOCR metadata extraction after sanitization** - Fixed metadata extraction failing when preprocessing strips the `<head>` container element. The extractor now finds orphaned meta tags anywhere in the document, not just inside `<head>` elements.\n- **`preserve_tags` functionality with preprocessing** - Fixed `preserve_tags` not working when HTML preprocessing is enabled (the new default). The sanitizer now:\n  - Accepts the `preserve_tags` list and allows those tags through sanitization\n  - Preserves common HTML attributes (`id`, `class`, `style`, `title`, etc.) on preserved tags\n  - Prevents `remove_forms` from stripping form tags when they're in the preserve list\n  - Ensures tags and attributes survive preprocessing so they can be output as HTML\n- **SVG support for inline image extraction** - Fixed SVG elements being stripped by the sanitizer, breaking inline image capture. All sanitization presets now allow:\n  - SVG elements: `svg`, `circle`, `rect`, `path`, `line`, `polyline`, `polygon`, `ellipse`, `g`\n  - SVG attributes: `width`, `height`, `viewBox`, `cx`, `cy`, `r`, `x`, `y`, `d`, `fill`, `stroke`\n  - Enables `convert_with_inline_images` to capture inline SVG elements\n- **Robust handling of malformed angle brackets in HTML** - Fixed parser failures when bare `<` or `>` characters appear in HTML text content (e.g., `1<2`, mathematical comparisons). The converter now:\n  - Automatically escapes malformed angle brackets that aren't part of valid HTML tags\n  - Works correctly with preprocessing both enabled and disabled\n  - Handles edge cases like `1<2`, `1 < 2 < 3`, and angle brackets at tag boundaries\n  - Fixes issue #94 where content following malformed angle brackets was lost\n- Added comprehensive test coverage for malformed angle bracket handling in both Rust and Python test suites\n- Fixed WASM build configuration to use correct `getrandom` backend for wasm32-unknown-unknown targets\n\n## [2.4.1] - 2025-10-22\n\n### Fixed\n\n- Ensure npm publishes include the generated Node bindings and platform binaries by running the N-API build during CI.\n- Configure WebAssembly builds with the `wasm_js` backend and strip wasm-pack `.gitignore` files so published packages ship the compiled `.wasm` artifacts.\n\n## [2.4.0] - 2025-10-22\n\n### Changed\n\n- Updated Rust workspace dependencies (including `pyo3`) to their latest compatible releases and refreshed lockfiles.\n- Normalized hOCR conversion spacing by collapsing stray triple newlines, ensuring generated Markdown matches regression fixtures.\n\n### Fixed\n\n- Corrected the WASM crate to depend on `getrandom`'s `wasm_js` feature, restoring WebAssembly builds.\n- Expanded the Node package `files` list so published tarballs now include compiled `.node` artifacts, CommonJS shims, and typings.\n\n## [2.3.4] - 2025-10-12\n\n### Changed\n\n- Incremented all distribution metadata and CLI version checks to 2.3.4 following the previous release tag conflict.\n- Regenerated package metadata artifacts for the new patch release.\n\n## [2.3.3] - 2025-10-12\n\n### Added\n\n- Python API now exports inline image helpers (`InlineImage`, `InlineImageWarning`, and `InlineImageConfig`) alongside `convert_with_inline_images`, with dedicated regression tests.\n- Node and WASM bindings include inline image extraction examples and TypeScript definitions, validated by Vitest coverage.\n\n### Changed\n\n- Bumped all package metadata (Python, Rust, Node, WASM, CLI) to version 2.3.3 for a synchronized release.\n\n### Fixed\n\n- CLI `--version` test updated to assert the new release number.\n\n## [2.2.0] - 2025-10-11\n\n### Added\n\n- `hocr_spatial_tables` option on `ConversionOptions` (Rust, Python, CLI) with `--no-hocr-spatial-tables` flag to disable spatial table reconstruction when desired.\n- New hOCR regression fixtures for complex tables and code blocks to guard against formatting regressions.\n\n### Changed\n\n- Improved hOCR conversion heuristics to distinguish between dense paragraph layouts and true tables, yielding cleaner Markdown for scientific data.\n- hOCR code-block detection now preserves fenced formatting, restoring context headings when present.\n\n### Fixed\n\n- CLI `--version` output and package metadata now report version 2.2.0 consistently.\n\n## [2.1.1] - 2025-10-11\n\n### Fixed\n\n- Improve hOCR table reconstruction when tables are represented as paragraphs, ensuring Markdown tables are emitted for Tesseract outputs without explicit `ocr_table` markers.\n\n## [2.1.0] - 2025-10-11\n\n### Added\n\n- **Inline image extraction** - New `convert_with_inline_images()` function to extract embedded images during conversion\n  - Supports data URI images (`data:image/*`)\n  - Supports inline SVG elements\n  - Configurable via `InlineImageConfig` with options for:\n    - Maximum decoded size limits\n    - Custom filename prefixes\n    - SVG capture control\n    - Optional dimension inference for raster images\n  - Returns `HtmlExtraction` with markdown, extracted images, and warnings\n  - Available through both Rust and Python APIs\n\n### Changed\n\n- **Simplified API** - Removed `ParsingOptions` class in favor of direct `encoding` parameter on `ConversionOptions`\n- **Automatic hOCR table extraction** - hOCR tables are now extracted automatically without requiring configuration\n  - Removed `hocr_extract_tables` option (always enabled for hOCR content)\n  - Removed `hocr_table_column_threshold` option (uses built-in heuristics)\n  - Removed `hocr_table_row_threshold_ratio` option (uses built-in heuristics)\n- Updated pre-commit hook versions (commitlint v9.23.0, pyproject-fmt v2.10.0, ruff v0.14.0)\n\n### Fixed\n\n- **hOCR metadata now uses YAML frontmatter** instead of HTML comments for cleaner markdown output\n- **hOCR code organization** - Restructured spatial table reconstruction into dedicated `hocr/spatial.rs` module\n- **Conservative table detection** - hOCR spatial table reconstruction now only applies to explicit `ocr_table` elements, preventing false positives\n- Windows CLI binary detection - now correctly searches for `.exe` extension on Windows\n- CLI binary bundling in Python wheels - binary now included in package for all platforms\n- hOCR extractor Rust doctest - added missing import statement\n- 928 Python test expectations updated for CommonMark-compliant v2 defaults\n- Python 3.14-dev → Python 3.14 stable in CI workflows\n- Reorganized wheel preparation script to `scripts/` directory\n- Removed duplicate markdown documentation files (BENCHMARKS.md, PERFORMANCE.md, BENCHMARK_RESULTS.md, COMMONMARK_COMPLIANCE.md, REFACTORING_SUMMARY.md)\n\n## [2.0.0] - 2025-10-03\n\n### 🚀 Major Rewrite: Rust Backend\n\nVersion 2.0.0 represents a complete rewrite of html-to-markdown with a high-performance Rust backend, delivering **10-30x performance improvements** while maintaining full backward compatibility through a v1 compatibility layer.\n\n### ⚠️ Breaking Changes\n\n#### CommonMark-Compliant Defaults\n\nV2 adopts CommonMark-compliant defaults for better interoperability:\n\n| Option                  | V1 Default   | V2 Default | Reason                           |\n| ----------------------- | ------------ | ---------- | -------------------------------- |\n| `list_indent_width`     | 4            | 2          | CommonMark standard              |\n| `bullets`               | \"-\"          | \"\\*+-\"     | Cycling bullets for nested lists |\n| `escape_asterisks`      | true         | false      | Minimal escaping                 |\n| `escape_underscores`    | true         | false      | Minimal escaping                 |\n| `escape_misc`           | true         | false      | Minimal escaping                 |\n| `newline_style`         | \"backslash\"  | \"spaces\"   | CommonMark two-space line breaks |\n| `code_block_style`      | \"backticks\"  | \"indented\" | CommonMark 4-space indent        |\n| `heading_style`         | \"underlined\" | \"atx\"      | CommonMark `#` headings          |\n| `preprocessing.enabled` | false        | false      | No change (opt-in)               |\n\n**Migration**: If you relied on v1 defaults, explicitly set options to match v1 behavior.\n\n#### Removed CLI Flags\n\nThe following v1 CLI flags are **not supported** in v2. The Python CLI proxy will raise helpful error messages when these flags are used:\n\n| Removed Flag | Reason                | Migration                                 |\n| ------------ | --------------------- | ----------------------------------------- |\n| `--strip`    | Feature removed in v2 | Remove flag (feature no longer available) |\n| `--convert`  | Feature removed in v2 | Remove flag (feature no longer available) |\n\n**Note on Redundant Flags**: The following v1 flags are redundant in v2 (they match the defaults) but are **silently accepted** for backward compatibility:\n\n- `--no-escape-asterisks`, `--no-escape-underscores`, `--no-escape-misc` (v2 defaults to minimal escaping)\n- `--no-wrap` (v2 defaults to no wrapping)\n- `--no-autolinks` (Rust CLI defaults to no autolinks)\n- `--no-extract-metadata` (Rust CLI defaults to no metadata extraction)\n\nThese flags can be safely removed from your commands, or you can leave them for compatibility.\n\n**Note**: The Rust CLI only supports positive flags (e.g., `--escape-asterisks`, `--autolinks`, `--wrap`). Negative flags (`--no-*`) are only supported through the Python CLI proxy for v1 compatibility.\n\n#### CommonMark-Compliant List Formatting\n\n- **Tight lists no longer have blank lines before nested sublists** - This follows the [CommonMark specification](https://spec.commonmark.org/) for list formatting\n- Previous behavior (v1): `* Item 1\\n\\n    + Nested\\n`\n- New behavior (v2): `* Item 1\\n    + Nested\\n`\n- **Why**: CommonMark specifies that tight lists (lists without blank lines between items) should not have blank lines before nested sublists\n- **Impact**: Generated markdown will render identically in CommonMark-compliant renderers but may look different in source form\n- **Migration**: If you need the old behavior for specific platforms, you can post-process the output or use loose lists (with blank lines between items)\n\n### Added\n\n#### Core Rust Implementation\n\n- **Complete Rust rewrite** of HTML-to-Markdown conversion engine using `scraper` and `html5ever`\n- **Native Rust CLI** with improved argument parsing and validation\n- **PyO3 Python bindings** for seamless Rust/Python integration\n- **Automatic hOCR table extraction** with built-in heuristics for OCR documents\n\n#### New V2 API\n\n- Clean, modern API with dataclass-based configuration\n- `convert(html, options, preprocessing)` - primary API entry point\n- `ConversionOptions` - comprehensive conversion settings (now includes `encoding`)\n- `PreprocessingOptions` - HTML cleaning configuration\n- Legacy parsing options removed in favour of explicit encoding on `ConversionOptions`\n- Improved type safety with full type stubs (`.pyi` files)\n\n#### V1 Compatibility Layer\n\n- **100% backward compatible** v1 API through compatibility layer\n- `convert_to_markdown()` function with all v1 kwargs\n- Smart translation of v1 options to v2 dataclasses\n- CLI argument translation for v1 flags\n- Clear error messages for unsupported v1 features\n\n#### Testing & Quality\n\n- **77 new tests** for v1 compatibility (32 bindings + 26 CLI + 19 integration)\n- Comprehensive integration tests with actual CLI execution\n- Wheel testing workflow for cross-platform validation\n- Python 3.10, 3.12, 3.14-dev test matrix\n- Dual coverage reporting (Python + Rust)\n\n#### CI/CD Improvements\n\n- Shared build-wheels action for consistent wheel building\n- Test-wheels workflow with full test suite on built wheels\n- Rust coverage with `cargo-llvm-cov`\n- Python coverage in LCOV format\n- Automated wheel building for Python 3.10-3.13\n\n### Changed\n\n#### Performance\n\n- **60-80x faster** than v1 for most conversion operations (144-208 MB/s throughput)\n- Memory-efficient processing with Rust's zero-cost abstractions\n- Optimized table handling with rowspan/colspan tracking\n- Faster list processing with unified helpers\n\n#### Architecture\n\n- Removed Python implementation (`converters.py`, `processing.py`, `preprocessor.py`)\n- Migrated to Rust-based conversion engine\n- Simplified Python layer to thin wrapper around Rust bindings\n- CLI now proxies to native Rust binary with argument translation\n\n#### API Design\n\n- More explicit configuration with separate option classes\n- Better separation of concerns (conversion/preprocessing/parsing)\n- Clearer parameter naming and organization\n- Improved error messages and exception handling\n\n### Removed v1 Features\n\nThe following v1 features were **removed** in v2:\n\n- `code_language_callback` - Removed (use `code_language` option for default language)\n- `strip` option - Removed (use preprocessing options instead)\n- `convert` option - Removed (all supported tags are converted by default)\n- `convert_to_markdown_stream()` - Removed (html5ever does not support streaming parsing)\n\n### Not Yet Implemented\n\n- `custom_converters` - Planned for future release with Rust and Python callback support\n\n### Migration Guide\n\n#### For Most Users (No Changes Needed)\n\nIf you're using the v1 API, your code will continue to work:\n\n```python\nfrom html_to_markdown import convert_to_markdown\n\n# This still works in v2!\nmarkdown = convert_to_markdown(html, heading_style=\"atx\")\n```\n\n#### To Use New V2 API (Recommended)\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\noptions = ConversionOptions(heading_style=\"atx\")\nmarkdown = convert(html, options)\n```\n\n#### CLI Changes\n\nV1 CLI flags are automatically translated to v2:\n\n```bash\n# V1 style (still works)\nhtml-to-markdown --preprocess-html --escape-asterisks input.html\n\n# V2 style (recommended)\nhtml-to-markdown --preprocess input.html  # escaping is default\n```\n\n### Performance Benchmarks\n\nReal-world performance improvements over v1 (Apple M4):\n\n| Document Type       | Size  | V2 Latency | V2 Throughput | Speedup vs V1 (2.5 MB/s) |\n| ------------------- | ----- | ---------- | ------------- | ------------------------ |\n| Lists (Timeline)    | 129KB | 0.62ms     | 208 MB/s      | **83x**                  |\n| Tables (Countries)  | 360KB | 2.02ms     | 178 MB/s      | **71x**                  |\n| Mixed (Python wiki) | 656KB | 4.56ms     | 144 MB/s      | **58x**                  |\n\nV2's Rust engine delivers **60-80x higher throughput** than V1's Python/BeautifulSoup implementation across real-world documents.\n\n### Technical Details\n\n#### Rust Crates Structure\n\n```text\ncrates/\n├── html-to-markdown/       # Core conversion library\n├── html-to-markdown-py/    # Python bindings (PyO3)\n└── html-to-markdown-cli/   # Native CLI binary\n```\n\n#### Python Package Structure\n\n```text\nhtml_to_markdown/\n├── api.py                  # V2 API\n├── options.py              # V2 configuration dataclasses\n├── v1_compat.py           # V1 compatibility layer\n├── cli_proxy.py           # CLI argument translation\n├── _rust.pyi              # Rust binding type stubs\n└── __init__.py            # Public API exports\n```\n\n### Breaking Changes Summary\n\nNone if using v1 compatibility layer. If migrating to v2 API:\n\n1. **Import changes**: `convert_to_markdown` → `convert`\n1. **Configuration**: Kwargs → Dataclasses (`ConversionOptions`)\n1. **Defaults changed**: See CommonMark-compliant defaults table above\n1. **Removed features**: See [Removed v1 Features](#removed-v1-features) section above\n\n### Complete V1 vs V2 Comparison\n\n#### API Differences\n\n| Aspect                  | V1                              | V2                                               |\n| ----------------------- | ------------------------------- | ------------------------------------------------ |\n| **Primary API**         | `convert_to_markdown(**kwargs)` | `convert(html, options, preprocessing, parsing)` |\n| **Configuration**       | Keyword arguments               | Dataclasses (`ConversionOptions`, etc.)          |\n| **Type Safety**         | Basic type hints                | Full `.pyi` stubs + generics                     |\n| **Compatibility Layer** | N/A                             | `convert_to_markdown()` with v1 kwargs           |\n\n#### Performance Differences\n\n| Document Type       | V1 Throughput | V2 Throughput | Speedup |\n| ------------------- | ------------- | ------------- | ------- |\n| Lists (Timeline)    | 2.5 MB/s      | 208 MB/s      | **83x** |\n| Tables (Countries)  | 2.5 MB/s      | 178 MB/s      | **71x** |\n| Mixed (Python wiki) | 2.5 MB/s      | 144 MB/s      | **58x** |\n| Average             | 2.5 MB/s      | 177 MB/s      | **71x** |\n\n#### Implementation Differences\n\n| Component        | V1                         | V2                       |\n| ---------------- | -------------------------- | ------------------------ |\n| **HTML Parser**  | BeautifulSoup4 / lxml      | html5ever (Rust)         |\n| **Sanitizer**    | Custom Python              | html5ever DOM filtering  |\n| **Conversion**   | Pure Python (~3,850 lines) | Pure Rust (~4,800 lines) |\n| **Bindings**     | N/A                        | PyO3                     |\n| **CLI**          | Python wrapper             | Native Rust binary       |\n| **Dependencies** | bs4, lxml, soupsieve       | None (statically linked) |\n\n#### Output Differences (Default Settings)\n\n| HTML                     | V1 Output                | V2 Output           |\n| ------------------------ | ------------------------ | ------------------- |\n| `<ul><li>Item</li></ul>` | `*   Item` (4 spaces)    | `- Item` (2 spaces) |\n| `<h1>Title</h1>`         | `Title\\n=====`           | `# Title`           |\n| `Text*with*stars`        | `Text\\*with\\*stars`      | `Text*with*stars`   |\n| `<br>`                   | Two trailing spaces      | Backslash `\\`       |\n| `<pre>code</pre>`        | ```` ```\\ncode\\n``` ```` | Indented 4 spaces   |\n\nThese differences reflect v2's alignment with CommonMark specification.\n\n### Removed Python Implementation\n\n- Python implementation of HTML conversion\n- `html_to_markdown/converters.py` (1220 lines)\n- `html_to_markdown/processing.py` (1195 lines)\n- `html_to_markdown/preprocessor.py` (404 lines)\n- `html_to_markdown/whitespace.py` (293 lines)\n- `html_to_markdown/utils.py` (37 lines)\n- Several test files migrated to Rust or marked as `.skip`\n\nTotal: **~3,850 lines** of Python code removed, replaced by **~4,800 lines** of Rust\n\n### Notes\n\n- **Platform Support**: Wheels built for Linux, macOS, Windows on x86_64\n- **Python Version**: Requires Python 3.10+\n- **ABI Compatibility**: Uses `abi3` for Python 3.10+ wheel reuse\n- **Rust Version**: Built with stable Rust (tested on 1.75+)\n\n______________________________________________________________________\n\n## [1.x] - Previous Versions\n\nFor changes in v1.x releases, see git history before the v2 rewrite.\n\n[2.0.0]: https://github.com/kreuzberg-dev/html-to-markdown/compare/v1.x...v2.0.0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to html-to-markdown\n\n## Prerequisites\n\n### Core Development\n\n- **Python** 3.10+\n- **Rust** 1.80+ (stable)\n- **uv** - Python package manager ([install](https://docs.astral.sh/uv/))\n- **Task** - Task runner ([install](https://taskfile.dev/))\n- **prek** - Pre-commit hooks (`uv tool install prek`)\n\n### JavaScript/TypeScript Development (Optional)\n\n- **Node.js** 18+\n- **pnpm** 8+ - Fast package manager ([install](https://pnpm.io/installation))\n- **wasm-pack** - For WASM builds (`cargo install wasm-pack`)\n\n## Quick Setup\n\n```bash\n# Clone repository\ngit clone https://github.com/kreuzberg-dev/html-to-markdown.git\ncd html-to-markdown\n\n# Setup environment (installs deps, builds Rust, installs hooks)\ntask setup\n```\n\nThis will:\n\n1. Install Python dependencies with `uv sync`\n1. Build Rust extension with maturin\n1. Install prek hooks for commit linting and code quality\n\n## Development Workflow\n\n### Running Tests\n\n#### Python & Rust\n\n```bash\n# Python tests\ntask test:python\n\n# Rust tests\ntask test:rust\n\n# All Python + Rust tests\ntask test\n\n# With coverage\ntask cov:all\n```\n\n#### JavaScript/TypeScript\n\n```bash\n# Install dependencies first\npnpm install\n\n# All JavaScript tests\npnpm test\n\n# Specific packages\npnpm run test:node      # NAPI-RS bindings\npnpm run test:wasm      # WebAssembly bindings\npnpm run test:ts        # TypeScript package\n\n# Watch mode\ncd packages/typescript\npnpm test:watch\n\n# With coverage\ncd packages/typescript\npnpm test -- --coverage\n```\n\n### Code Quality\n\n#### Python & Rust\n\n```bash\n# Format code (Rust + Python)\ntask format\n\n# Run all linters\ntask lint\n\n# Build Rust components\ntask build\n```\n\n#### JavaScript/TypeScript\n\n```bash\n# Type checking\npnpm run typecheck\n\n# Build all packages\npnpm run build\n\n# Build specific targets\npnpm run build:node     # NAPI-RS native bindings\npnpm run build:wasm     # WebAssembly (all 3 targets)\npnpm run build:ts       # TypeScript wrapper\n\n# Clean build artifacts\npnpm run clean\n```\n\n### Benchmarking\n\n```bash\n# Quick benchmarks\ntask bench\n\n# All benchmarks\ntask bench:all\n```\n\n## Project Structure\n\nThis is a **monorepo** containing multiple language bindings and distributions:\n\n```text\nhtml-to-markdown/\n├── pnpm-workspace.yaml         # pnpm workspace configuration\n├── package.json                # Root workspace scripts\n│\n├── crates/                     # Rust crates\n│   ├── html-to-markdown/       # Core library (astral-tl parser)\n│   ├── html-to-markdown-cli/   # Rust CLI binary\n│   ├── html-to-markdown-node/  # NAPI-RS bindings for Node.js (~691k ops/sec)\n│   ├── html-to-markdown-wasm/  # wasm-bindgen for browsers (~229k ops/sec)\n│   └── html-to-markdown-py/    # PyO3 bindings powering the Python package\n│\n├── packages/                   # Releasable packages\n│   ├── python/                 # PyPI package (html_to_markdown)\n│   │   ├── html_to_markdown/   # Python sources\n│   │   └── tests/             # Python integration + unit tests\n│   ├── typescript/             # TypeScript package with CLI (npm)\n│   └── ruby/                   # Ruby gem sources/specs (RubyGems)\n│\n└── scripts/                    # Helper scripts (wheel prep, gem prep, demo)\n```\n\n### Package Distribution\n\n| Package                  | Registry  | Description                 |\n| ------------------------ | --------- | --------------------------- |\n| `html-to-markdown-rs`    | crates.io | Core Rust library           |\n| `html-to-markdown`       | PyPI      | Python package              |\n| `html-to-markdown`       | npm       | TypeScript package with CLI |\n| `html-to-markdown`       | RubyGems  | Ruby gem (Magnus bindings)  |\n| `html-to-markdown-node` | npm       | Native Node.js bindings     |\n| `html-to-markdown-wasm` | npm       | WebAssembly bindings        |\n\n## Making Changes\n\n### Rust Core Changes\n\n1. Edit code in `crates/html-to-markdown/src/`\n1. Run Rust tests: `task test:rust` or `cargo test`\n1. Rebuild bindings:\n   - Python: `task build`\n   - Node.js: `cd crates/html-to-markdown-node && pnpm run build`\n   - WASM: `cd crates/html-to-markdown-wasm && pnpm run build:all`\n1. Run integration tests: `task test:python` or `pnpm test`\n\n### Python API Changes\n\n1. Edit code in `packages/python/html_to_markdown/`\n1. Update type stubs in `_rust.pyi` if needed\n1. Run tests: `task test:python`\n\n### JavaScript/TypeScript Changes\n\n#### Node.js Bindings (`crates/html-to-markdown-node`)\n\n1. Edit Rust code in `src/lib.rs`\n1. Rebuild: `pnpm run build` (generates TypeScript types automatically)\n1. Test: `pnpm test` or `cargo test`\n\n#### WASM Bindings (`crates/html-to-markdown-wasm`)\n\n1. Edit Rust code in `src/lib.rs`\n1. Rebuild: `pnpm run build:all` (builds for bundler, nodejs, and web)\n1. Test: `pnpm test` or `cargo test`\n\n#### TypeScript Package with CLI (`packages/typescript`)\n\n1. Edit code in `src/` (library entrypoints + CLI)\n1. Build: `pnpm run build` (runs Node binding build + TypeScript emit)\n1. Lint: `pnpm run lint`\n1. Test: `pnpm test` or `pnpm test:watch`\n1. Test CLI locally: `node dist/cli.js input.html`\n\n#### Ruby Gem (`packages/ruby`)\n\n1. Edit Ruby sources in `lib/` and specs in `spec/`\n1. Build native extension: `bundle exec rake compile`\n1. Run specs: `bundle exec rake spec`\n\n### Adding Tests\n\n- **Rust tests**: Add to `crates/*/src/lib.rs` or `crates/*/tests/`\n- **Python tests**: Add to `packages/python/tests/` following pytest patterns\n- **TypeScript tests**: Add to `packages/typescript/tests/` using vitest\n- **Ruby specs**: Add to `packages/ruby/spec/`\n- **Integration tests**: Add to appropriate test directory\n\n## Testing\n\n### Test Without Releasing\n\nTo test wheels and binaries without creating a release:\n\n```bash\n# Test wheel building manually\ngh workflow run \"Test Wheel Building\"\n\n# Or manually build locally\npip install cibuildwheel\ncibuildwheel --output-dir wheelhouse\n\n# Test CLI binary locally\ncargo build --release --package html-to-markdown-cli\n./target/release/html-to-markdown --version\n```\n\n### CI Workflows\n\n- **ci-*.yaml**: Kreuzberg-style, path-filtered workflows (rust, python, node, wasm, ruby, php, go, java, elixir, validate)\n- **test-wheels.yaml**: Builds and tests wheels (manual or on Rust/config changes)\n- All workflows must pass before merging\n\n## Commit Guidelines\n\nCommits must follow [Conventional Commits](https://www.conventionalcommits.org/):\n\n```text\nfeat: add new feature\nfix: fix bug\ndocs: update documentation\nrefactor: refactor code\ntest: add tests\n```\n\nPrek enforces this automatically via commitlint hook.\n\n## Code Quality Standards\n\n### Python\n\n- **Formatting**: ruff (120 char line length)\n- **Linting**: ruff with ALL rules enabled (see pyproject.toml for ignores)\n- **Type checking**: mypy in strict mode\n\n### Rust\n\n- **Formatting**: `cargo fmt`\n- **Linting**: `cargo clippy` with `-D warnings`\n- **Style**: Follow standard Rust conventions\n- **Tests**: Required for all public APIs\n\n### TypeScript\n\n- **Formatting**: Prettier via tsup\n- **Type checking**: TypeScript 5.6+ in strict mode\n- **Linting**: ESLint (when configured)\n- **Tests**: vitest with coverage reporting\n- **Style**: 2-space indentation, trailing commas\n\nAll Python/Rust checks run automatically via prek on commit.\n\n## Pull Requests\n\n1. Fork the repository\n1. Create a feature branch (`git checkout -b feat/amazing-feature`)\n1. Make your changes\n1. Run `task test` and `task lint`\n1. Commit with conventional commit format\n1. Push and create a pull request\n\n## Release Process (Maintainers Only)\n\n### Pre-release Checklist\n\n1. **Update versions** in:\n\n   - `Cargo.toml` (workspace.package.version)\n   - `packages/*/package.json`\n   - `crates/html-to-markdown-node/package.json`\n   - `crates/html-to-markdown-wasm/package.json`\n\n     ```toml\n\n## Cargo.toml\n\n    [workspace.package]\n    version = \"2.4.2\"\n     ```\n\n     ```json\n    // package.json files\n    \"version\": \"2.4.2\"\n     ```\n\n1. Update `CHANGELOG.md` with changes\n\n1. Run full test suite:\n\n    ```bash\n    task test           # Python + Rust\n    pnpm test          # JavaScript/TypeScript\n    ```\n\n1. Build and verify all targets:\n\n    ```bash\n    task build:cli && ./target/release/html-to-markdown --version\n    pnpm run build     # All JS/TS packages\n    ```\n\n1. Commit changes: `git commit -m \"chore: bump version to 2.4.2\"`\n\n### Creating a Release\n\n1. **Create and push tag**:\n\n    ```bash\n    git tag -a v2.4.2 -m \"Release v2.4.2\"\n    git push origin v2.4.2\n    ```\n\n1. **Automated workflows trigger**:\n\n   - `release.yml` - GitHub release with CLI binaries\n   - `release-homebrew.yml` - Updates Homebrew formula\n   - `publish-cargo.yml` - Publishes to crates.io\n   - `release.yaml` - Publishes Python to PyPI\n   - Manual npm publish required (see below)\n\n1. **Publish npm packages** (manual):\n\n    ```bash\n    # Login to npm (once)\n    npm login\n\n    # Publish main TypeScript package (includes CLI)\n    cd packages/typescript\n    pnpm publish\n\n    # Publish native bindings (with pre-built binaries)\n    cd ../../crates/html-to-markdown-node\n    pnpm run build\n    pnpm publish\n\n    # Publish WASM\n    cd ../html-to-markdown-wasm\n    pnpm run build:all\n    pnpm publish\n    ```\n\n1. **Required secrets** (already configured):\n\n   - `CARGO_TOKEN` - From <https://crates.io/settings/tokens>\n   - `HOMEBREW_TOKEN` - GitHub token with `repo` scope\n   - PyPI uses trusted publishing (OIDC); no `PYPI_TOKEN` secret is required\n   - npm uses trusted publishing (OIDC); no `NPM_TOKEN` secret is required\n   - NuGet uses trusted publishing (OIDC); no `NUGET_API_KEY` secret is required\n\n#### Post-release Verification\n\nVerify all distributions are published:\n\n- **Rust**: <https://crates.io/crates/html-to-markdown-rs>\n- **Python**: <https://pypi.org/project/html-to-markdown/>\n- **npm (main)**: <https://www.npmjs.com/package/@kreuzberg/html-to-markdown>\n- **npm (wasm)**: <https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm>\n- **Homebrew**: <https://github.com/kreuzberg-dev/homebrew-tap>\n- **GitHub**: <https://github.com/kreuzberg-dev/html-to-markdown/releases>\n\n### Getting Help\n\n- **Issues**: [GitHub Issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord**: [Kreuzberg Community](https://discord.gg/pXxagNK2zN)\n\n### License\n\nBy contributing, you agree that your contributions will be licensed under the MIT License.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\n    \"crates/html-to-markdown\",\n    \"crates/html-to-markdown-cli\",\n    \"crates/html-to-markdown-ffi\",\n    \"crates/html-to-markdown-node\",\n    \"crates/html-to-markdown-php\",\n    \"crates/html-to-markdown-py\",\n    \"crates/html-to-markdown-wasm\",\n]\nexclude = [\n    \"e2e/rust\",\n    \"packages/elixir/native/html_to_markdown_nif\",\n    \"packages/r/src/rust\",\n    \"packages/ruby/ext/html_to_markdown_rb\",\n    \"packages/ruby/vendor\",\n    \"test_apps/rust\",\n    \"tests/test_apps/elixir/deps\",\n]\n\n[workspace.package]\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nauthors = [\"Na'aman Hirschfeld <naaman@kreuzberg.dev>\"]\ndescription = \"High-performance HTML to Markdown converter\"\nlicense = \"MIT\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nhomepage = \"https://kreuzberg.dev\"\ndocumentation = \"https://docs.rs/html-to-markdown-rs\"\nreadme = \"README.md\"\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\", \"web-programming\"]\nrust-version = \"1.85\"\n\n[workspace.dependencies]\nahash = { version = \"0.8\", default-features = false, features = [\n    \"std\",\n    \"compile-time-rng\",\n] }\nbase64 = \"0.22\"\nclap = { version = \"4.6\", features = [\"derive\"] }\nclap_complete = \"4.6\"\nclap_mangen = \"0.3\"\nencoding_rs = \"0.8\"\next-php-rs = \"0.15.12\"\nhtml-to-markdown-rs = { version = \"3.4.0-rc.1\", path = \"crates/html-to-markdown\" }\nhtml5ever = \"0.39.0\"\nonce_cell = \"1.21\"\npyo3 = { version = \"0.28.3\", features = [\"abi3-py310\"] }\nrayon = \"1.12\"\nregex = \"1.12\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\ntempfile = \"3.27\"\nthiserror = \"2.0\"\ntl = { package = \"astral-tl\", version = \"0.7.11\" }\ntoml = \"1.1\"\nwhich = \"8\"\n\n[workspace.lints.rust]\nunsafe_code = \"forbid\"\nmissing_docs = \"warn\"\nunused_must_use = \"deny\"\n\n[workspace.lints.clippy]\nall = { level = \"deny\", priority = -1 }\ncargo = { level = \"warn\", priority = -1 }\npedantic = { level = \"warn\", priority = -1 }\nnursery = { level = \"warn\", priority = -1 }\nmultiple_crate_versions = { level = \"allow\", priority = 1 } # Allow multiple dependency versions (common in large workspaces)\ncargo_common_metadata = { level = \"allow\", priority = 1 } # Binding crates are internal, don't need full crates.io metadata\n# Allow common patterns in alef-generated binding code\nunnecessary_cast = { level = \"allow\", priority = 1 }\nunused_unit = { level = \"allow\", priority = 1 }\nunwrap_or_default = { level = \"allow\", priority = 1 }\nderivable_impls = { level = \"allow\", priority = 1 }\nunnecessary_wraps = { level = \"allow\", priority = 1 }\nneedless_borrows_for_generic_args = { level = \"allow\", priority = 1 }\nuseless_conversion = { level = \"allow\", priority = 1 }\nunnecessary_fallible_conversions = { level = \"allow\", priority = 1 }\n\n[profile.release]\nlto = \"thin\"\ncodegen-units = 1\nopt-level = 3\nstrip = true\ndebug = 1\n\n[patch.crates-io]\nhtml-to-markdown-rs = { path = \"crates/html-to-markdown\" }\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2024-2025 Na'aman Hirschfeld\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-node\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-node?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.0.0\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/badge/R-htmltomarkdown-007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-007ec6\" alt=\"License\">\n  </a>\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev/demo/\">\n    <img src=\"https://img.shields.io/badge/%E2%96%B6%EF%B8%8F_Live_Demo-007ec6\" alt=\"Live Demo\">\n  </a>\n</div>\n\n<img width=\"3384\" height=\"573\" alt=\"Banner\" src=\"https://github.com/user-attachments/assets/478a83da-237b-446b-b3a8-e564c13e00a8\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown conversion powered by Rust. Ships as native bindings for **Rust, Python, TypeScript/Node.js, Ruby, PHP, Go, Java, C#, Elixir, R, C (FFI), and WebAssembly** with identical rendering across all runtimes.\n\n**[Documentation](https://docs.html-to-markdown.kreuzberg.dev)** | **[Live Demo](https://docs.html-to-markdown.kreuzberg.dev/demo/)** | **[API Reference](https://docs.rs/html-to-markdown-rs/)**\n\n## Highlights\n\n- **150-280 MB/s** throughput (10-80x faster than pure Python alternatives)\n- **12 language bindings** with consistent output across all runtimes\n- **Structured result** — `convert()` returns `ConversionResult` with `content`, `metadata`, `tables`, `images`, and `warnings`\n- **Metadata extraction** — title, headers, links, images, structured data (JSON-LD, Microdata, RDFa)\n- **Visitor pattern** — custom callbacks for content filtering, URL rewriting, domain-specific dialects\n- **Table extraction** — extract structured table data (cells, headers, rendered markdown) during conversion\n- **Secure by default** — built-in HTML sanitization via ammonia\n\n## Quick Start\n\n```bash\n# Rust\ncargo add html-to-markdown-rs\n\n# Python\npip install html-to-markdown\n\n# TypeScript / Node.js\nnpm install @kreuzberg/html-to-markdown-node\n\n# Ruby\ngem install html-to-markdown\n\n# CLI\ncargo install html-to-markdown-cli\n# or\nbrew install kreuzberg-dev/tap/html-to-markdown\n```\n\nSee the **[Installation Guide](https://docs.html-to-markdown.kreuzberg.dev/getting-started/installation/)** for all languages including PHP, Go, Java, C#, Elixir, R, and WASM.\n\n### Usage\n\n`convert()` is the single entry point. It returns a structured `ConversionResult`:\n\n```python\n# Python\nfrom html_to_markdown import convert\n\nresult = convert(\"<h1>Hello</h1><p>World</p>\")\nprint(result.content)        # # Hello\\n\\nWorld\nprint(result.metadata)       # title, links, headings, …\n```\n\n```typescript\n// TypeScript / Node.js\nimport { convert } from \"@kreuzberg/html-to-markdown-node\";\n\nconst result = convert(\"<h1>Hello</h1><p>World</p>\");\nconsole.log(result.content);    // # Hello\\n\\nWorld\nconsole.log(result.metadata);   // title, links, headings, …\n```\n\n```rust\n// Rust\nuse html_to_markdown_rs::convert;\n\nlet result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n```\n\n## Language Bindings\n\n| Language | Package | Install |\n|----------|---------|---------|\n| Rust | [html-to-markdown-rs](https://crates.io/crates/html-to-markdown-rs) | `cargo add html-to-markdown-rs` |\n| Python | [html-to-markdown](https://pypi.org/project/html-to-markdown/) | `pip install html-to-markdown` |\n| TypeScript / Node.js | [@kreuzberg/html-to-markdown-node](https://www.npmjs.com/package/@kreuzberg/html-to-markdown-node) | `npm install @kreuzberg/html-to-markdown-node` |\n| WebAssembly | [@kreuzberg/html-to-markdown-wasm](https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm) | `npm install @kreuzberg/html-to-markdown-wasm` |\n| Ruby | [html-to-markdown](https://rubygems.org/gems/html-to-markdown) | `gem install html-to-markdown` |\n| PHP | [kreuzberg-dev/html-to-markdown](https://packagist.org/packages/kreuzberg-dev/html-to-markdown) | `composer require kreuzberg-dev/html-to-markdown` |\n| Go | [htmltomarkdown](https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown) | `go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3` |\n| Java | [dev.kreuzberg:html-to-markdown](https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown) | Maven / Gradle |\n| C# | [KreuzbergDev.HtmlToMarkdown](https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/) | `dotnet add package KreuzbergDev.HtmlToMarkdown` |\n| Elixir | [html_to_markdown](https://hex.pm/packages/html_to_markdown) | `mix deps.get html_to_markdown` |\n| R | [htmltomarkdown](https://kreuzberg-dev.r-universe.dev/htmltomarkdown) | `install.packages(\"htmltomarkdown\")` |\n| C (FFI) | [releases](https://github.com/kreuzberg-dev/html-to-markdown/releases) | Pre-built `.so` / `.dll` / `.dylib` |\n\n## Part of the Kreuzberg Ecosystem\n\nhtml-to-markdown is developed by [kreuzberg.dev](https://kreuzberg.dev) and powers the HTML conversion pipeline in [Kreuzberg](https://docs.kreuzberg.dev), a document intelligence library for extracting text from PDFs, images, and office documents.\n\n## Contributing\n\nContributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.\n\n## License\n\nMIT License — see [LICENSE](LICENSE) for details.\n"
  },
  {
    "path": "Taskfile.yaml",
    "content": "version: \"3\"\n\n# ============================================================================\n# html-to-markdown — Root Taskfile\n# ============================================================================\n# Primary development interface. Rust core tasks delegate to .task/languages/rust.yml.\n# All binding language operations (setup, build, test, lint, format, clean, update)\n# are handled by alef v0.7.0.\n#\n# Command patterns:\n#   task setup         -> install all dependencies\n#   task build         -> build core + bindings\n#   task test          -> run all tests\n#   task lint          -> run all linters via prek\n#   task fmt           -> format all code\n#   task rust:build    -> Rust-specific tasks\n# ============================================================================\n\n# Preconditions for task setup:\n# - Rust toolchain (rustup) — install via https://rustup.rs\n# - alef CLI — install via cargo install --git https://github.com/kreuzberg-dev/alef\n# - prek — install via cargo install prek\n# - Per-language tools auto-skipped via alef preconditions; install as needed:\n#   - Python: uv, Node: pnpm (corepack enable), Ruby: bundle, PHP: composer\n#   - Go: go, Java: mvn, C#: dotnet, Elixir: mix, R: Rscript\n\nincludes:\n  rust:\n    taskfile: ./.task/languages/rust.yml\n  python:\n    taskfile: ./.task/languages/python.yml\n  version:\n    taskfile: ./.task/tools/version-sync.yml\n  docs:\n    taskfile: ./.task/tools/docs.yml\n  _tools:\n    taskfile: ./.task/tools/general.yml\n    flatten: true\n\ntasks:\n  # === Setup ===\n  setup:\n    desc: \"Install all dependencies and dev tools\"\n    cmds:\n      - task: rust:install\n      - alef setup\n      - prek install\n      - prek install --hook-type commit-msg\n\n  # === Build ===\n  build:\n    desc: \"Build core Rust library and all language bindings\"\n    cmds:\n      - task: rust:build\n      - alef build\n\n  build:dev:\n    desc: \"Build all in debug mode\"\n    cmds:\n      - task: rust:build:dev\n\n  build:release:\n    desc: \"Build all in release mode\"\n    cmds:\n      - task: rust:build:release\n\n  build:ci:\n    desc: \"Build all in CI mode\"\n    cmds:\n      - task: rust:build:ci\n\n  build:core:\n    desc: \"Build Rust core library only\"\n    cmds:\n      - task: rust:build\n\n  build:bindings:\n    desc: \"Build all language bindings (skip Rust core)\"\n    cmds:\n      - alef build\n\n  build:cli:\n    desc: \"Build the html-to-markdown CLI binary\"\n    cmds:\n      - task: rust:build:release\n\n  # === Test ===\n  test:\n    desc: \"Run all test suites across all languages\"\n    cmds:\n      - task: rust:test\n      - alef test\n\n  test:bindings:\n    desc: \"Run binding test suites only (skip Rust core)\"\n    cmds:\n      - alef test\n\n  test:ci:\n    desc: \"Run all tests with coverage (CI mode)\"\n    cmds:\n      - task: rust:test:ci\n      - alef test --coverage\n\n  # === Lint & Format ===\n  lint:\n    desc: \"Lint all code via prek (pre-commit framework)\"\n    cmds:\n      - prek run --all-files\n\n  lint:check:\n    desc: \"Lint all code WITHOUT auto-fix (check-only)\"\n    cmds:\n      - task: rust:lint:check\n\n  go:lint:\n    desc: \"Lint Go bindings with golangci-lint\"\n    cmds:\n      - golangci-lint run ./... --config .golangci.yml\n    dir: packages/go\n\n  ruby:format:\n    desc: \"Format Ruby bindings with rubocop\"\n    cmds:\n      - alef fmt --lang ruby\n\n  ruby:lint:\n    desc: \"Lint Ruby bindings with rubocop and steep\"\n    cmds:\n      - alef lint --lang ruby\n\n  php:format:\n    desc: \"Format PHP bindings with php-cs-fixer\"\n    cmds:\n      - alef fmt --lang php\n\n  php:lint:\n    desc: \"Lint PHP bindings with phpstan and php-cs-fixer\"\n    cmds:\n      - alef lint --lang php\n\n  csharp:format:\n    desc: \"Format C# bindings with dotnet format\"\n    cmds:\n      - alef fmt --lang csharp\n\n  csharp:lint:\n    desc: \"Lint C# bindings with dotnet format\"\n    cmds:\n      - alef lint --lang csharp\n\n  fmt:\n    desc: \"Format all code\"\n    cmds:\n      - alef fmt\n\n  format:\n    desc: \"Format all code (alias for fmt)\"\n    cmds:\n      - task: fmt\n\n  format:check:\n    desc: \"Check code formatting without modifications\"\n    cmds:\n      - task: rust:format:check\n\n  check:\n    desc: \"Run all checks (format + lint, no modifications)\"\n    cmds:\n      - task: format:check\n      - task: lint:check\n\n  # === Update / Upgrade ===\n  update:\n    desc: \"Update all dependencies (within major versions)\"\n    cmds:\n      - alef update\n      - prek autoupdate\n\n  upgrade:\n    desc: \"Upgrade all dependencies to latest (including breaking changes)\"\n    cmds:\n      - alef update --latest\n      - prek autoupdate\n\n  # === Coverage ===\n  cov:rust:\n    desc: \"Generate Rust coverage report\"\n    cmds:\n      - task: rust:coverage\n\n  cov:all:\n    desc: \"Generate coverage for all languages\"\n    cmds:\n      - task: rust:test:ci\n      - alef test --coverage\n\n  # === Version ===\n  versions:sync:\n    desc: \"Synchronize version from Cargo.toml to all package manifests\"\n    cmds:\n      - task: version:sync\n\n  # === Publish ===\n  publish:validate:\n    desc: \"Validate publish manifests are consistent\"\n    cmds:\n      - alef publish validate\n\n  publish:prepare:\n    desc: \"Prepare packages for publishing (vendor deps, stage FFI)\"\n    cmds:\n      - alef publish prepare\n\n  publish:build:\n    desc: \"Build release artifacts for all languages\"\n    cmds:\n      - alef publish build\n\n  publish:package:\n    desc: \"Package built artifacts into distributable archives\"\n    cmds:\n      - alef publish package -o dist/\n\n  # === Alef lifecycle ===\n  alef:generate:\n    desc: \"Run full alef pipeline (generate + stubs + scaffold + readme + sync + e2e) and `alef docs` (docs/reference for Zensical)\"\n    cmds:\n      - alef generate\n      - alef stubs\n      - alef scaffold\n      - alef readme\n      - alef sync-versions\n      - alef e2e generate\n      - alef e2e generate --registry\n      - alef docs\n\n  alef:verify:\n    desc: \"Check bindings are up-to-date (CI mode)\"\n    cmds:\n      - alef verify --exit-code\n\n  alef:sync:\n    desc: \"Sync version from Cargo.toml to all manifests\"\n    cmds:\n      - alef sync-versions\n\n  # === E2E tests ===\n  e2e:generate:\n    desc: \"Generate E2E tests for all languages from fixtures\"\n    cmds:\n      - alef e2e generate\n\n  e2e:test:\n    desc: \"Run E2E tests for all languages\"\n    cmds:\n      - alef test --e2e\n\n  e2e:smoke:all:\n    desc: \"Run smoke tests for all published packages (fast validation)\"\n    cmds:\n      - task: e2e:smoke:python\n      - task: e2e:smoke:node\n      - task: e2e:smoke:ruby\n      - task: e2e:smoke:php\n      - task: e2e:smoke:go\n      - task: e2e:smoke:java\n      - task: e2e:smoke:csharp\n      - task: e2e:smoke:elixir\n      - task: e2e:smoke:r\n\n  e2e:smoke:python:\n    desc: \"Smoke test Python package from PyPI\"\n    dir: test_apps/python\n    cmds:\n      - uv sync\n      - uv run pytest smoke_test.py -v\n\n  e2e:smoke:node:\n    desc: \"Smoke test Node package from npm\"\n    dir: test_apps/node\n    cmds:\n      - pnpm install\n      - pnpm test:smoke\n\n  e2e:smoke:ruby:\n    desc: \"Smoke test Ruby gem from RubyGems\"\n    dir: test_apps/ruby\n    cmds:\n      - bundle install\n      - bundle exec rspec smoke_test.rb\n\n  e2e:smoke:php:\n    desc: \"Smoke test PHP package from Packagist\"\n    dir: test_apps/php\n    cmds:\n      - composer install\n      - vendor/bin/phpunit smoke_test.php\n\n  e2e:smoke:go:\n    desc: \"Smoke test Go package from pkg.go.dev\"\n    dir: test_apps/go\n    cmds:\n      - go mod download\n      - go test -v -run Smoke\n\n  e2e:smoke:java:\n    desc: \"Smoke test Java package from Maven Central\"\n    dir: test_apps/java\n    cmds:\n      - ./mvnw{{if eq .OS \"windows\"}}.cmd{{end}} clean install -DskipTests\n      - ./mvnw{{if eq .OS \"windows\"}}.cmd{{end}} test -Dtest=SmokeTest\n\n  e2e:smoke:csharp:\n    desc: \"Smoke test C# package from NuGet\"\n    dir: test_apps/csharp\n    cmds:\n      - dotnet restore\n      - dotnet test --filter FullyQualifiedName~SmokeTest\n\n  e2e:smoke:elixir:\n    desc: \"Smoke test Elixir package from Hex.pm\"\n    dir: test_apps/elixir\n    cmds:\n      - mix deps.get\n      - mix test test/smoke_test.exs\n\n  e2e:smoke:r:\n    desc: \"Smoke test R package\"\n    dir: test_apps/r\n    cmds:\n      - Rscript smoke_test.R\n\n  e2e:test:all:\n    desc: \"Run comprehensive tests for all published packages\"\n    cmds:\n      - task: e2e:test:python\n      - task: e2e:test:node\n      - task: e2e:test:ruby\n      - task: e2e:test:php\n      - task: e2e:test:go\n      - task: e2e:test:java\n      - task: e2e:test:csharp\n      - task: e2e:test:elixir\n      - task: e2e:test:r\n\n  e2e:test:python:\n    desc: \"Comprehensive tests for Python package\"\n    dir: test_apps/python\n    cmds:\n      - uv sync\n      - uv run pytest comprehensive_test.py -v\n\n  e2e:test:node:\n    desc: \"Comprehensive tests for Node package\"\n    dir: test_apps/node\n    cmds:\n      - pnpm install\n      - pnpm test:comprehensive\n\n  e2e:test:ruby:\n    desc: \"Comprehensive tests for Ruby gem\"\n    dir: test_apps/ruby\n    cmds:\n      - bundle install\n      - bundle exec rspec comprehensive_test.rb\n\n  e2e:test:php:\n    desc: \"Comprehensive tests for PHP package\"\n    dir: test_apps/php\n    cmds:\n      - composer install\n      - vendor/bin/phpunit comprehensive_test.php\n\n  e2e:test:go:\n    desc: \"Comprehensive tests for Go package\"\n    dir: test_apps/go\n    cmds:\n      - go mod download\n      - go test -v -run Comprehensive\n\n  e2e:test:java:\n    desc: \"Comprehensive tests for Java package\"\n    dir: test_apps/java\n    cmds:\n      - ./mvnw{{if eq .OS \"windows\"}}.cmd{{end}} test -Dtest=ComprehensiveTest\n\n  e2e:test:csharp:\n    desc: \"Comprehensive tests for C# package\"\n    dir: test_apps/csharp\n    cmds:\n      - dotnet test --filter FullyQualifiedName~ComprehensiveTest\n\n  e2e:test:elixir:\n    desc: \"Comprehensive tests for Elixir package\"\n    dir: test_apps/elixir\n    cmds:\n      - mix test test/comprehensive_test.exs\n\n  e2e:test:r:\n    desc: \"Comprehensive tests for R package\"\n    dir: packages/r\n    cmds:\n      - Rscript -e 'devtools::test()'\n\n  # === test_apps aggregators (mirrors lllm naming convention) ===\n  test-apps:smoke:\n    desc: \"Run all test_app smoke tests (alias for e2e:smoke:all)\"\n    cmds:\n      - task: e2e:smoke:all\n\n  test-apps:comprehensive:\n    desc: \"Run all test_app comprehensive tests (alias for e2e:test:all)\"\n    cmds:\n      - task: e2e:test:all\n\n  # === Demo ===\n  build:demo:\n    desc: \"Build the GitHub Pages demo\"\n    cmds:\n      - ./scripts/build-demo.sh\n\n  serve:demo:\n    desc: \"Serve the GitHub Pages demo locally on port 8000\"\n    dir: docs\n    cmds:\n      - python3 -m http.server 8000\n\n  # === Documentation ===\n  docs:build:\n    desc: \"Build documentation site with zensical\"\n    cmds:\n      - task: python:install\n      - uv sync --group doc\n      - uv run zensical build --clean --strict\n\n  docs:serve:\n    desc: \"Serve documentation site locally with zensical (hot-reload)\"\n    cmds:\n      - task: python:install\n      - uv sync --group doc\n      - uv run zensical serve\n\n  # === Clean ===\n  clean:\n    desc: \"Remove build artifacts across all languages\"\n    cmds:\n      - task: rust:clean\n      - alef clean\n\n  # === Default ===\n  default:\n    desc: \"List available tasks\"\n    cmds:\n      - task --list\n"
  },
  {
    "path": "_typos.toml",
    "content": "[files]\nextend-exclude = [\n    # Test fixtures and real-world HTML samples contain intentional\n    # misspellings, non-English text, and obfuscated class names\n    \"test_documents/\",\n    \"fixtures/\",\n    \"test_apps/\",\n    # Test spec data (CommonMark spec, R inst/)\n    \"packages/python/tests/\",\n    \"packages/r/inst/\",\n    # Generated/bundled demo assets\n    \"docs/demo/\",\n    # Vendored and generated files\n    \"*.lock\",\n    \"vendor/\",\n    \"node_modules/\",\n    \"target/\",\n    \"dist/\",\n    \".alef/\",\n    \"*.svg\",\n]\n"
  },
  {
    "path": "alef.toml",
    "content": "version = \"0.13.9\"\nlanguages = [\"python\", \"node\", \"ruby\", \"php\", \"ffi\", \"go\", \"java\", \"csharp\", \"elixir\", \"wasm\", \"r\"]\n\n[crate]\nname = \"html-to-markdown-rs\"\ncore_import = \"html_to_markdown_rs\"\nsources = [\n  \"crates/html-to-markdown/src/lib.rs\",\n  \"crates/html-to-markdown/src/options/conversion.rs\",\n  \"crates/html-to-markdown/src/options/preprocessing.rs\",\n  \"crates/html-to-markdown/src/options/validation.rs\",\n  \"crates/html-to-markdown/src/types/result.rs\",\n  \"crates/html-to-markdown/src/types/document.rs\",\n  \"crates/html-to-markdown/src/types/tables.rs\",\n  \"crates/html-to-markdown/src/types/warnings.rs\",\n  \"crates/html-to-markdown/src/error.rs\",\n  \"crates/html-to-markdown/src/convert_api.rs\",\n  \"crates/html-to-markdown/src/metadata/types.rs\",\n  \"crates/html-to-markdown/src/visitor/traits.rs\",\n  \"crates/html-to-markdown/src/visitor/types.rs\",\n]\nversion_from = \"Cargo.toml\"\nworkspace_root = \".\"\nfeatures = [\"full\", \"metadata\", \"visitor\", \"serde\", \"inline-images\"]\n\n[include]\ntypes = [\"ConversionOptions\", \"ConversionResult\", \"HtmlVisitor\"]\nfunctions = [\"convert\"]\n\n[exclude]\ntypes = []\nfunctions = []\nmethods = []\n\n[output]\npython = \"crates/html-to-markdown-py/src/\"\nnode = \"crates/html-to-markdown-node/src/\"\nruby = \"packages/ruby/ext/html_to_markdown_rb/src/\"\nphp = \"crates/html-to-markdown-php/src/\"\nffi = \"crates/html-to-markdown-ffi/src/\"\ngo = \"packages/go/\"\nelixir = \"packages/elixir/native/html_to_markdown_nif/src/\"\nwasm = \"crates/html-to-markdown-wasm/src/\"\nr = \"packages/r/src/rust/src/\"\n\n[python]\nmodule_name = \"_html_to_markdown\"\npip_name = \"html-to-markdown\"\n\n[python.stubs]\noutput = \"packages/python/html_to_markdown/\"\n\n[node]\npackage_name = \"@kreuzberg/html-to-markdown-node\"\n\n[ffi]\nprefix = \"htm\"\nheader_name = \"html_to_markdown.h\"\nlib_name = \"html_to_markdown_ffi\"\nvisitor_callbacks = true\n\n[go]\nmodule = \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\npackage_name = \"htmltomarkdown\"\n\n[java]\npackage = \"dev.kreuzberg.htmltomarkdown\"\n\n[csharp]\nnamespace = \"HtmlToMarkdown\"\n\n[php]\nextension_name = \"html_to_markdown\"\nnamespace = \"HtmlToMarkdown\"\n\n[ruby]\ngem_name = \"html-to-markdown\"\n\n[ruby.stubs]\noutput = \"packages/ruby/sig/\"\n\n[elixir]\napp_name = \"html_to_markdown\"\n\n[r]\npackage_name = \"htmltomarkdown\"\n\n[readme]\ntemplate_dir = \"readme_templates\"\nsnippets_dir = \"docs/snippets\"\ndiscord_url = \"https://discord.gg/pXxagNK2zN\"\nbanner_url = \"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\"\n\n[readme.languages.python]\nname = \"Python\"\ntemplate = \"language_package.md\"\npackage_manager = [\"pip\"]\npackage_name = \"html-to-markdown\"\ninstall_command = \"pip install html-to-markdown\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with a clean Python API (powered by a Rust core).\nThe same engine also drives the Node.js, Ruby, PHP, and WebAssembly bindings, so rendered Markdown\nstays identical across runtimes. Wheels are published for Linux, macOS, and Windows.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.python.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = false\n\n[readme.languages.python.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.python.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nlatency = \"0.62ms\"\nthroughput = \"208 MB/s\"\n\n[[readme.languages.python.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nlatency = \"2.02ms\"\nthroughput = \"178 MB/s\"\n\n[[readme.languages.python.performance.benchmarks]]\nname = \"Mixed (Python wiki)\"\nsize = \"656KB\"\nlatency = \"4.56ms\"\nthroughput = \"144 MB/s\"\n\n[readme.languages.python.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.typescript]\nname = \"TypeScript (Node.js)\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/typescript/README.md\"\npackage_manager = [\"npm\", \"pnpm\", \"yarn\", \"bun\"]\npackage_name = \"@kreuzberg/html-to-markdown\"\ninstall_command = \"npm install @kreuzberg/html-to-markdown\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter for Node.js and Bun with full TypeScript support.\nThis package wraps native `@kreuzberg/html-to-markdown` bindings and provides a type-safe API.\n\"\"\"\nmigration_version = \"3.1.0\"\n\n[readme.languages.typescript.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = false\n\n[readme.languages.typescript.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.typescript.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nlatency = \"0.58ms\"\nthroughput = \"222 MB/s\"\n\n[[readme.languages.typescript.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nlatency = \"1.89ms\"\nthroughput = \"190 MB/s\"\n\n[[readme.languages.typescript.performance.benchmarks]]\nname = \"Mixed (Python wiki)\"\nsize = \"656KB\"\nlatency = \"4.21ms\"\nthroughput = \"156 MB/s\"\n\n[readme.languages.typescript.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.ruby]\nname = \"Ruby\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/ruby/README.md\"\npackage_manager = [\"gem\", \"bundler\"]\npackage_name = \"html-to-markdown\"\ninstall_command = \"gem install html-to-markdown\"\ndescription = \"\"\"\nBlazing-fast HTML to Markdown conversion for Ruby, powered by the same Rust engine used by our Python, Node.js, WebAssembly, and PHP packages.\nShip identical Markdown across every runtime while enjoying native extension performance with Magnus bindings.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.ruby.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = true\n\n[readme.languages.ruby.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.ruby.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nlatency = \"0.71ms\"\nthroughput = \"182 MB/s\"\n\n[[readme.languages.ruby.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nlatency = \"2.15ms\"\nthroughput = \"167 MB/s\"\n\n[[readme.languages.ruby.performance.benchmarks]]\nname = \"Mixed (Python wiki)\"\nsize = \"656KB\"\nlatency = \"4.89ms\"\nthroughput = \"134 MB/s\"\n\n[readme.languages.ruby.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.php]\nname = \"PHP\"\ntemplate = \"language_package.md\"\npackage_manager = [\"composer\", \"pie\"]\npackage_name = \"kreuzberg-dev/html-to-markdown\"\ninstall_command = \"composer require kreuzberg-dev/html-to-markdown\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with typed PHP bindings powered by a Rust core.\nProvides a type-safe API with full PHPStan level 9 support, modern PHP 8.2+ features, and comprehensive metadata extraction.\n\nNote: The package was previously published as `goldziher/html-to-markdown`, which still works for backward compatibility.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.php.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = false\n\n[readme.languages.php.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.php.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nops_sec = 3346\n\n[[readme.languages.php.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nops_sec = 973\n\n[[readme.languages.php.performance.benchmarks]]\nname = \"Medium (Python)\"\nsize = \"657KB\"\nops_sec = 485\n\n[readme.languages.php.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.go]\nname = \"Go\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/go/v3/README.md\"\npackage_manager = [\"go\"]\npackage_name = \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\ninstall_command = \"go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with Go bindings to the Rust core library.\nSupports automatic downloading of prebuilt FFI libraries for Linux, macOS, and Windows with customizable caching.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.go.features]\nvisitor_pattern = true\nmetadata_extraction = false\ncli_proxy = false\n\n[readme.languages.go.performance]\nplatform = \"Apple M4\"\nfunction = \"Convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.go.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nlatency = \"0.46ms\"\nthroughput = \"277.5 MB/s\"\n\n[[readme.languages.go.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nlatency = \"1.37ms\"\nthroughput = \"262.1 MB/s\"\n\n[[readme.languages.go.performance.benchmarks]]\nname = \"Mixed (Python wiki)\"\nsize = \"656KB\"\nlatency = \"2.75ms\"\nthroughput = \"237.9 MB/s\"\n\n[readme.languages.go.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.java]\nname = \"Java\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/java/README.md\"\npackage_manager = [\"maven\", \"gradle\"]\npackage_name = \"dev.kreuzberg:html-to-markdown\"\ninstall_command = \"\"\"\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.1.0</version>\n    <classifier>linux</classifier> <!-- or macos, windows -->\n</dependency>\"\"\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with Java Panama FFI bindings to the Rust core.\nUses Foreign Function & Memory API for zero-dependency, thread-safe conversion with full metadata extraction support.\n\"\"\"\nmigration_version = \"3.1.0\"\n\n[readme.languages.java.features]\nvisitor_pattern = true\nmetadata_extraction = false\ncli_proxy = false\n\n[readme.languages.java.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.java.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nops_sec = 2308\nthroughput = \"291.5 MB/s\"\n\n[[readme.languages.java.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nops_sec = 773\nthroughput = \"272.0 MB/s\"\n\n[[readme.languages.java.performance.benchmarks]]\nname = \"Mixed (Python)\"\nsize = \"656KB\"\nops_sec = 403\nthroughput = \"258.5 MB/s\"\n\n[readme.languages.java.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.csharp]\nname = \"C# / .NET\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/csharp/README.md\"\npackage_manager = [\"nuget\"]\npackage_name = \"KreuzbergDev.HtmlToMarkdown\"\ninstall_command = \"dotnet add package KreuzbergDev.HtmlToMarkdown\"\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with C#/.NET bindings using P/Invoke to the Rust core.\nProvides type-safe record-based APIs for metadata extraction, visitor patterns, and thread-safe concurrent conversion.\n\"\"\"\nmigration_version = \"3.1.0\"\n\n[readme.languages.csharp.features]\nvisitor_pattern = true\nmetadata_extraction = false\ncli_proxy = false\n\n[readme.languages.csharp.performance]\nplatform = \"Apple M4\"\nfunction = \"Convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.csharp.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nops_sec = 3111\nthroughput = \"392.9 MB/s\"\n\n[[readme.languages.csharp.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nops_sec = 853\nthroughput = \"300.1 MB/s\"\n\n[[readme.languages.csharp.performance.benchmarks]]\nname = \"Mixed (Python)\"\nsize = \"656KB\"\nops_sec = 456\nthroughput = \"292.3 MB/s\"\n\n[readme.languages.csharp.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.elixir]\nname = \"Elixir\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/elixir/README.md\"\npackage_manager = [\"mix\"]\npackage_name = \"html_to_markdown\"\ninstall_command = 'Add {:html_to_markdown, \"~> 3.0\"} to mix.exs deps'\ndescription = \"\"\"\nElixir bindings for the Rust html-to-markdown engine. The package exposes a fast HTML to Markdown converter implemented with Rustler.\nShip identical Markdown across every runtime while enjoying native performance with Rustler NIF bindings.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.elixir.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = false\n\n[readme.languages.elixir.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.elixir.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nops_sec = 2547\nthroughput = \"321.7 MB/s\"\n\n[[readme.languages.elixir.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nops_sec = 835\nthroughput = \"293.8 MB/s\"\n\n[[readme.languages.elixir.performance.benchmarks]]\nname = \"Medium (Python)\"\nsize = \"656KB\"\nops_sec = 439\nthroughput = \"281.5 MB/s\"\n\n[[readme.languages.elixir.performance.benchmarks]]\nname = \"Large (Rust)\"\nsize = \"567KB\"\nops_sec = 485\nthroughput = \"268.7 MB/s\"\n\n[[readme.languages.elixir.performance.benchmarks]]\nname = \"Small (Intro)\"\nsize = \"463KB\"\nops_sec = 581\nthroughput = \"262.9 MB/s\"\n\n[readme.languages.elixir.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[readme.languages.r]\nname = \"R\"\ntemplate = \"language_package.md\"\noutput_path = \"packages/r/README.md\"\npackage_manager = [\"install.packages\"]\npackage_name = \"htmltomarkdown\"\ninstall_command = 'install.packages(\"htmltomarkdown\")'\ndescription = \"\"\"\nHigh-performance HTML to Markdown converter with R bindings powered by a Rust core via extendr.\nShip identical Markdown across every runtime while enjoying native performance with extendr bindings.\n\"\"\"\nmigration_version = \"\"\n\n[readme.languages.r.features]\nvisitor_pattern = true\nmetadata_extraction = true\ncli_proxy = false\n\n[readme.languages.r.performance]\nplatform = \"Apple M4\"\nfunction = \"convert()\"\nnote = \"Real Wikipedia documents\"\n\n[[readme.languages.r.performance.benchmarks]]\nname = \"Lists (Timeline)\"\nsize = \"129KB\"\nlatency = \"0.68ms\"\nthroughput = \"190 MB/s\"\n\n[[readme.languages.r.performance.benchmarks]]\nname = \"Tables (Countries)\"\nsize = \"360KB\"\nlatency = \"2.10ms\"\nthroughput = \"171 MB/s\"\n\n[[readme.languages.r.performance.benchmarks]]\nname = \"Mixed (Python wiki)\"\nsize = \"656KB\"\nlatency = \"4.75ms\"\nthroughput = \"138 MB/s\"\n\n[readme.languages.r.snippets]\nbasic_usage = \"getting-started/basic_usage.md\"\nwith_options = \"getting-started/with_options.md\"\nmetadata_extraction = \"metadata/basic_extraction.md\"\nvisitor_basic = \"visitor/basic_visitor.md\"\ntable_extraction = \"table-extraction/basic_extraction.md\"\n\n[sync]\nextra_paths = [\n  \"packages/ruby/lib/html_to_markdown/version.rb\",\n  \"packages/python/html_to_markdown/__init__.py\",\n  \"crates/html-to-markdown-node/package.json\",\n  \"crates/html-to-markdown-node/npm/*/package.json\",\n]\n\n# test_apps version sync — use text_replacements to avoid clobbering unrelated semver strings\n[[sync.text_replacements]]\npath = \"test_apps/node/package.json\"\nsearch = '\"@kreuzberg/html-to-markdown\": \"[^\"]*\"'\nreplace = '\"@kreuzberg/html-to-markdown\": \"{version}\"'\n\n[[sync.text_replacements]]\npath = \"test_apps/bun/package.json\"\nsearch = '\"@kreuzberg/html-to-markdown\": \"[^\"]*\"'\nreplace = '\"@kreuzberg/html-to-markdown\": \"{version}\"'\n\n[[sync.text_replacements]]\npath = \"test_apps/wasm/package.json\"\nsearch = '\"@kreuzberg/html-to-markdown-wasm\": \"[^\"]*\"'\nreplace = '\"@kreuzberg/html-to-markdown-wasm\": \"{version}\"'\n\n[[sync.text_replacements]]\npath = \"test_apps/java/pom.xml\"\nsearch = \"<version>{version}</version>\"\nreplace = \"<version>{version}</version>\"\n\n[[sync.text_replacements]]\npath = \"test_apps/csharp/TestApp.csproj\"\nsearch = 'Include=\"KreuzbergDev.HtmlToMarkdown\" Version=\"[^\"]*\"'\nreplace = 'Include=\"KreuzbergDev.HtmlToMarkdown\" Version=\"{version}\"'\n\n[[sync.text_replacements]]\npath = \"test_apps/php/composer.json\"\nsearch = '\"kreuzberg-dev/html-to-markdown\": \"[^\"]*\"'\nreplace = '\"kreuzberg-dev/html-to-markdown\": \">={version}\"'\n\n[[trait_bridges]]\ntrait_name = \"HtmlVisitor\"\ntype_alias = \"VisitorHandle\"\nparam_name = \"visitor\"\nbind_via = \"options_field\"\noptions_type = \"ConversionOptions\"\n\n[scaffold]\ndescription = \"High-performance HTML to Markdown converter\"\nlicense = \"MIT\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nauthors = [\"Kreuzberg Team\"]\nkeywords = [\"html\", \"markdown\", \"converter\"]\n\n[publish]\ncore_crate = \"crates/html-to-markdown\"\n\n[publish.languages.ruby]\nvendor_mode = \"core-only\"\n\n[publish.languages.elixir]\nvendor_mode = \"core-only\"\nnif_versions = [\"2.16\", \"2.17\"]\n\n[publish.languages.r]\nvendor_mode = \"full\"\n\n[publish.languages.ffi]\npkg_config = true\ncmake_config = true\n\n[custom_modules]\nnode = [\"helpers\"]\n\n[build_commands.ruby]\nprecondition = \"command -v cargo >/dev/null 2>&1\"\nbuild = \"cd packages/ruby/ext/html_to_markdown_rb && cargo build\"\nbuild_release = \"cd packages/ruby/ext/html_to_markdown_rb && cargo build --release\"\n\n[lint.python]\nprecondition = \"command -v ruff >/dev/null 2>&1\"\nbefore = \"cd packages/python && uv sync\"\ntypecheck = \"cd packages/python && uv run mypy .\"\n\n[lint.node]\nprecondition = \"command -v npx >/dev/null 2>&1\"\nformat = \"npx oxfmt packages/typescript/src/\"\ncheck = \"npx oxlint packages/typescript/\"\n\n[lint.ruby]\nprecondition = \"command -v bundle >/dev/null 2>&1\"\nbefore = \"cd packages/ruby && bundle install\"\nformat = \"cd packages/ruby && bundle exec rubocop -a .\"\n\n[lint.php]\nprecondition = \"command -v php >/dev/null 2>&1\"\nbefore = \"cd packages/php && composer install\"\nformat = \"cd packages/php && vendor/bin/php-cs-fixer fix .\"\ncheck = \"cd packages/php && vendor/bin/phpstan --configuration=phpstan.neon --memory-limit=512M\"\n\n[lint.go]\nbefore = \"cargo build --release -p html-to-markdown-ffi\"\n\n[lint.elixir]\nprecondition = \"command -v mix >/dev/null 2>&1\"\nbefore = \"cd packages/elixir && mix deps.get\"\ncheck = \"cd packages/elixir && mix credo\"\n\n[lint.java]\nprecondition = \"command -v mvn >/dev/null 2>&1\"\ncheck = \"mvn -f packages/java/pom.xml spotless:check -q\"\n\n\n[lint.wasm]\nprecondition = \"command -v npx >/dev/null 2>&1\"\nformat = \"npx oxfmt packages/wasm/\"\ncheck = \"npx oxlint packages/wasm/\"\n\n[lint.r]\nprecondition = \"command -v Rscript >/dev/null 2>&1\"\nformat = \"cd packages/r && Rscript -e 'styler::style_dir(\\\"R\\\")'\"\ncheck = \"cd packages/r && Rscript -e 'lintr::lint_dir(\\\"R\\\")'\"\n\n[lint.ffi]\nprecondition = \"test -d e2e/c\"\nformat = \"find e2e/c -name '*.c' -o -name '*.h' | xargs clang-format -i\"\ncheck = \"cppcheck --std=c11 --enable=warning,style,performance --suppress=missingIncludeSystem --suppress=unusedStructMember e2e/c/\"\n\n[test.python]\nprecondition = \"command -v uv >/dev/null 2>&1\"\nbefore = \"cd e2e/python && uv sync && uv pip install --refresh ../../packages/python\"\ne2e = \"cd e2e/python && uv run pytest tests/ -q\"\n\n[test.node]\nprecondition = \"command -v npx >/dev/null 2>&1\"\nbefore = \"cd crates/html-to-markdown-node && napi build --output-dir . --release\"\ne2e = \"cd e2e/node && npx vitest run\"\n\n[test.go]\nprecondition = \"command -v go >/dev/null 2>&1\"\nbefore = \"cargo build --release -p html-to-markdown-ffi\"\ne2e = \"cd e2e/go && go test ./... -count=1\"\n\n[test.ruby]\nprecondition = \"command -v bundle >/dev/null 2>&1\"\nbefore = \"cd e2e/ruby && bundle install\"\ne2e = \"cd e2e/ruby && bundle exec rspec\"\n\n[test.php]\nprecondition = \"command -v php >/dev/null 2>&1\"\nbefore = \"cd e2e/php && composer install\"\ne2e = \"cd e2e/php && composer test\"\n\n[test.java]\nprecondition = \"command -v mvn >/dev/null 2>&1\"\nbefore = \"cargo build --release -p html-to-markdown-ffi\"\ne2e = \"cd e2e/java && mvn test -q\"\n\n[test.csharp]\nprecondition = \"command -v dotnet >/dev/null 2>&1\"\nbefore = \"cargo build --release -p html-to-markdown-ffi\"\ne2e = \"dotnet test e2e/csharp\"\n\n[test.elixir]\nprecondition = \"command -v mix >/dev/null 2>&1\"\nbefore = \"cd packages/elixir && mix deps.get && mix compile\"\ne2e = \"cd e2e/elixir && mix test\"\n\n[test.r]\nprecondition = \"which Rscript 2>/dev/null\"\ne2e = \"cd e2e/r && Rscript -e \\\"devtools::load_all('../../packages/r'); testthat::set_max_fails(Inf); testthat::test_dir('tests')\\\"\"\n\n[test.wasm]\nprecondition = \"command -v npx >/dev/null 2>&1\"\nbefore = \"wasm-pack build crates/html-to-markdown-wasm --release --target bundler && cd e2e/wasm && npm install\"\ne2e = \"cd e2e/wasm && npm test\"\n\n[test.rust]\nprecondition = \"command -v cargo >/dev/null 2>&1\"\ne2e = \"cd e2e/rust && cargo test\"\n\n[test.c]\nprecondition = \"command -v make >/dev/null 2>&1\"\nbefore = \"cargo build --release -p html-to-markdown-ffi\"\ne2e = \"cd e2e/c && make clean && make && DYLD_LIBRARY_PATH=../../target/release ./test_runner\"\n\n# E2E test generation configuration\n[e2e]\nfixtures = \"fixtures\"\noutput = \"e2e\"\nlanguages = [\n  \"python\",\n  \"node\",\n  \"ruby\",\n  \"php\",\n  \"c\",\n  \"go\",\n  \"java\",\n  \"csharp\",\n  \"elixir\",\n  \"wasm\",\n  \"r\",\n  \"rust\",\n]\nfields_optional = [\n  \"content\",\n  \"document\",\n  \"metadata.document.title\",\n  \"metadata.document.description\",\n  \"metadata.document.author\",\n  \"metadata.document.canonical_url\",\n  \"metadata.document.open_graph[title]\",\n  \"metadata.document.open_graph[description]\",\n  \"metadata.document.open_graph[image]\",\n  \"metadata.document.open_graph[type]\",\n  \"metadata.document.open_graph[url]\",\n  \"metadata.document.open_graph[site_name]\",\n  \"metadata.document.twitter_card[card]\",\n  \"metadata.document.twitter_card[title]\",\n  \"metadata.document.twitter_card[description]\",\n]\n\n[e2e.call]\nfunction = \"convert\"\nmodule = \"html_to_markdown_rs\"\nresult_var = \"result\"\nargs = [\n  { name = \"html\", field = \"html\", type = \"string\" },\n  { name = \"options\", field = \"options\", type = \"json_object\", optional = true },\n]\n\n[e2e.call.overrides.rust]\ncrate_name = \"html_to_markdown_rs\"\nmodule = \"html_to_markdown_rs\"\nfunction = \"convert\"\noptions_type = \"ConversionOptions\"\nwrap_options_in_some = true\nextra_args = [\"None\"]\nreturns_result = true\n\n[e2e.call.overrides.python]\nmodule = \"html_to_markdown\"\nfunction = \"convert\"\noptions_type = \"ConversionOptions\"\nenum_module = \"html_to_markdown.options\"\n\n[e2e.call.overrides.python.enum_fields]\nheadingStyle = \"HeadingStyle\"\ncodeBlockStyle = \"CodeBlockStyle\"\nlistIndentType = \"ListIndentType\"\nnewlineStyle = \"NewlineStyle\"\nwhitespaceMode = \"WhitespaceMode\"\nhighlightStyle = \"HighlightStyle\"\nlinkStyle = \"LinkStyle\"\noutputFormat = \"OutputFormat\"\n\n[e2e.call.overrides.node]\nmodule = \"html-to-markdown\"\nfunction = \"convert\"\noptions_type = \"ConversionOptions\"\nenum_module = \"html_to_markdown._html_to_markdown\"\n\n[e2e.call.overrides.go]\nmodule = \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\nfunction = \"Convert\"\nalias = \"htmd\"\noptions_type = \"ConversionOptions\"\nreturns_result = true\noptions_ptr = true\n\n[e2e.call.overrides.java]\nclass = \"dev.kreuzberg.htmltomarkdown.HtmlToMarkdown\"\nfunction = \"convert\"\noptions_type = \"ConversionOptions\"\nenum_module = \"html_to_markdown._html_to_markdown\"\n\n[e2e.call.overrides.csharp]\nclass = \"HtmlToMarkdownRs\"\nfunction = \"Convert\"\noptions_type = \"ConversionOptions\"\n\n[e2e.call.overrides.csharp.enum_fields]\nheadingStyle = \"HeadingStyle\"\ncodeBlockStyle = \"CodeBlockStyle\"\nlistIndentType = \"ListIndentType\"\nnewlineStyle = \"NewlineStyle\"\nwhitespaceMode = \"WhitespaceMode\"\nhighlightStyle = \"HighlightStyle\"\nlinkStyle = \"LinkStyle\"\noutputFormat = \"OutputFormat\"\n\n[e2e.call.overrides.php]\nmodule = \"HtmlToMarkdown\"\nclass = \"HtmlToMarkdown\"\nfunction = \"convert\"\nresult_is_simple = true\n\n[e2e.call.overrides.php.enum_fields]\nheadingStyle = \"HeadingStyle\"\ncodeBlockStyle = \"CodeBlockStyle\"\nlistIndentType = \"ListIndentType\"\nnewlineStyle = \"NewlineStyle\"\nwhitespaceMode = \"WhitespaceMode\"\nhighlightStyle = \"HighlightStyle\"\nlinkStyle = \"LinkStyle\"\noutputFormat = \"OutputFormat\"\n\n[e2e.call.overrides.ruby]\nmodule = \"html_to_markdown\"\noptions_type = \"Kreuzberg::ConversionOptions\"\nresult_is_simple = true\n\n[e2e.call.overrides.ruby.enum_fields]\nheadingStyle = \"HeadingStyle\"\ncodeBlockStyle = \"CodeBlockStyle\"\nlistIndentType = \"ListIndentType\"\nnewlineStyle = \"NewlineStyle\"\nwhitespaceMode = \"WhitespaceMode\"\nhighlightStyle = \"HighlightStyle\"\nlinkStyle = \"LinkStyle\"\noutputFormat = \"OutputFormat\"\n\n[e2e.call.overrides.elixir]\noptions_type = \"ConversionOptions\"\noptions_via = \"conversionoptions_default\"\n\n[e2e.call.overrides.elixir.enum_fields]\nheadingStyle = \"HeadingStyle\"\ncodeBlockStyle = \"CodeBlockStyle\"\nlistIndentType = \"ListIndentType\"\nnewlineStyle = \"NewlineStyle\"\nwhitespaceMode = \"WhitespaceMode\"\nhighlightStyle = \"HighlightStyle\"\nlinkStyle = \"LinkStyle\"\noutputFormat = \"OutputFormat\"\n\n[e2e.call.overrides.wasm]\noptions_type = \"JsConversionOptions\"\n\n[e2e.call.overrides.wasm.enum_fields]\nheadingStyle = \"JsHeadingStyle\"\ncodeBlockStyle = \"JsCodeBlockStyle\"\nlistIndentType = \"JsListIndentType\"\nnewlineStyle = \"JsNewlineStyle\"\nwhitespaceMode = \"JsWhitespaceMode\"\nhighlightStyle = \"JsHighlightStyle\"\nlinkStyle = \"JsLinkStyle\"\noutputFormat = \"JsOutputFormat\"\n\n[e2e.call.overrides.c]\nheader = \"html_to_markdown.h\"\nfunction = \"htm_convert\"\nprefix = \"htm\"\nresult_type = \"ConversionResult\"\n\n[e2e.registry]\noutput = \"test_apps\"\ncategories = [\"smoke\", \"conversion\"]\n\n[e2e.registry.packages.python]\nname = \"html-to-markdown\"\nversion = \">=3.2.0\"\n\n[e2e.registry.packages.node]\nname = \"@kreuzberg/html-to-markdown\"\nversion = \"^3.2.0\"\n\n[e2e.registry.packages.rust]\nname = \"html-to-markdown-rs\"\nversion = \"3.2.0\"\n\n[e2e.registry.packages.go]\nmodule = \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\nversion = \"v3.2.0\"\n\n[e2e.registry.packages.java]\nname = \"dev.kreuzberg:html-to-markdown\"\nversion = \"3.2.0\"\n\n[e2e.registry.packages.csharp]\nname = \"KreuzbergDev.HtmlToMarkdown\"\nversion = \"3.2.0\"\n\n[e2e.registry.packages.php]\nname = \"kreuzberg-dev/html-to-markdown-rs\"\nversion = \">=3.2.0\"\n\n[e2e.registry.packages.ruby]\nname = \"html-to-markdown\"\nversion = \"~> 3.2\"\n\n[e2e.registry.packages.elixir]\nname = \"html_to_markdown\"\nversion = \"~> 3.2\"\n\n[e2e.registry.packages.wasm]\nname = \"@kreuzberg/html-to-markdown-wasm\"\nversion = \"^3.2.0\"\n\n[e2e.registry.packages.r]\nname = \"htmltomarkdown\"\nversion = \"3.2.0\"\n\n[e2e.registry.packages.c]\nname = \"html-to-markdown-ffi\"\nversion = \"3.2.0\"\n\n[e2e.packages.rust]\npath = \"../../crates/html-to-markdown\"\n\n[e2e.packages.python]\nname = \"html-to-markdown\"\npath = \"../../packages/python\"\n\n[e2e.packages.node]\nname = \"html-to-markdown\"\npath = \"../../crates/html-to-markdown-node\"\n\n[e2e.packages.go]\nmodule = \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\npath = \"../../packages/go\"\nversion = \"v0.0.0\"\n\n[e2e.packages.php]\nname = \"kreuzberg-dev/html-to-markdown\"\npath = \"../../packages/php\"\n\n[e2e.packages.java]\nname = \"dev.kreuzberg:html-to-markdown\"\npath = \"../../packages/java\"\n\n[e2e.packages.csharp]\nname = \"HtmlToMarkdown\"\npath = \"../../packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj\"\n\n[e2e.packages.ruby]\nname = \"html-to-markdown\"\npath = \"../../packages/ruby\"\n\n[e2e.packages.elixir]\nname = \"html_to_markdown\"\npath = \"../../packages/elixir\"\n\n[e2e.packages.wasm]\nname = \"@kreuzberg/html-to-markdown-wasm\"\npath = \"../../crates/html-to-markdown-wasm\"\n\n[e2e.packages.r]\nname = \"htmltomarkdown\"\npath = \"../../packages/r\"\n\n[e2e.packages.c]\nname = \"html_to_markdown_ffi\"\npath = \"../../crates/html-to-markdown-ffi\"\n\n[e2e.fields]\n# Scalar metadata fields: fixture \"metadata.X\" -> API \"metadata.document.X\"\n\"metadata.title\" = \"metadata.document.title\"\n\"metadata.description\" = \"metadata.document.description\"\n\"metadata.author\" = \"metadata.document.author\"\n\"metadata.canonical_url\" = \"metadata.document.canonical_url\"\n# Collection metadata fields (returned as JSON strings in C FFI)\n\"metadata.headings\" = \"metadata.headers\"\n# Open Graph map fields\n\"metadata.open_graph.title\" = \"metadata.document.open_graph[title]\"\n\"metadata.open_graph.description\" = \"metadata.document.open_graph[description]\"\n\"metadata.open_graph.image\" = \"metadata.document.open_graph[image]\"\n\"metadata.open_graph.type\" = \"metadata.document.open_graph[type]\"\n\"metadata.open_graph.url\" = \"metadata.document.open_graph[url]\"\n\"metadata.open_graph.site_name\" = \"metadata.document.open_graph[site_name]\"\n# Twitter Card map fields\n\"metadata.twitter.card\" = \"metadata.document.twitter_card[card]\"\n\"metadata.twitter.title\" = \"metadata.document.twitter_card[title]\"\n\"metadata.twitter.description\" = \"metadata.document.twitter_card[description]\"\n\n[e2e.format]\npython = \"ruff check --fix {dir} && ruff format {dir}\"\ngo = \"gofmt -w {dir}\"\nrust = \"(cd {dir} && cargo fmt --all)\"\nc = \"find {dir} \\\\( -name '*.c' -o -name '*.h' \\\\) | xargs clang-format -i\"\n\n[e2e.fields_c_types]\n\"conversion_result.metadata\" = \"HtmlMetadata\"\n\"conversion_result.document\" = \"DocumentStructure\"\n\"html_metadata.document\" = \"DocumentMetadata\"\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"kreuzberg-dev/html-to-markdown\",\n  \"description\": \"Modern PHP API for the html_to_markdown native extension powered by the Rust html-to-markdown engine.\",\n  \"type\": \"php-ext\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"support\": {\n    \"issues\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n    \"source\": \"https://github.com/kreuzberg-dev/html-to-markdown\"\n  },\n  \"authors\": [\n    {\n      \"name\": \"Na'aman Hirschfeld\",\n      \"email\": \"naaman@kreuzberg.dev\",\n      \"role\": \"Lead Developer\"\n    }\n  ],\n  \"require\": {\n    \"php\": \"^8.4\"\n  },\n  \"require-dev\": {\n    \"friendsofphp/php-cs-fixer\": \"3.95.*\",\n    \"phpstan/phpstan\": \"^2.1\",\n    \"phpunit/phpunit\": \"^13.1\",\n    \"squizlabs/php_codesniffer\": \"^4.0\"\n  },\n  \"suggest\": {\n    \"ext-html_to_markdown\": \"Provides native conversion. Install via PIE (kreuzberg-dev/html-to-markdown) or download the release binary.\"\n  },\n  \"provide\": {\n    \"ext-html_to_markdown\": \"*\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"HtmlToMarkdown\\\\\": \"packages/php/src/\"\n    },\n    \"files\": [\n      \"packages/php/src/functions.php\"\n    ]\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"HtmlToMarkdown\\\\Tests\\\\\": \"packages/php/tests/\"\n    }\n  },\n  \"scripts\": {\n    \"phpstan\": \"php -d detect_unicode=0 vendor/bin/phpstan --configuration=packages/php/phpstan.neon\",\n    \"csfixer\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config packages/php/php-cs-fixer.php packages/php/src packages/php/tests\",\n    \"format\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config packages/php/php-cs-fixer.php packages/php/src packages/php/tests\",\n    \"format:check\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config packages/php/php-cs-fixer.php --dry-run packages/php/src packages/php/tests\",\n    \"test\": \"php packages/php/bin/run-tests.php\",\n    \"lint\": \"@phpstan\",\n    \"lint:fix\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config packages/php/php-cs-fixer.php packages/php/src packages/php/tests && php -d detect_unicode=0 vendor/bin/phpstan --configuration=packages/php/phpstan.neon\"\n  },\n  \"config\": {\n    \"sort-packages\": true,\n    \"platform\": {\n      \"php\": \"8.4.1\"\n    },\n    \"allow-plugins\": {\n      \"phpstan/extension-installer\": true\n    }\n  },\n  \"extra\": {\n    \"html-to-markdown\": {\n      \"binary-base-url\": \"https://github.com/kreuzberg-dev/html-to-markdown/releases/download\"\n    }\n  },\n  \"php-ext\": {\n    \"extension-name\": \"html_to_markdown\",\n    \"support-zts\": true,\n    \"support-nts\": true,\n    \"build-path\": \"crates/html-to-markdown-php\",\n    \"download-url-method\": [\"pre-packaged-binary\", \"composer-default\"],\n    \"configure-options\": [\n      {\n        \"name\": \"with-cargo-bin\",\n        \"description\": \"Path to the cargo executable used to build the extension.\",\n        \"needs-value\": true\n      }\n    ]\n  },\n  \"minimum-stability\": \"stable\",\n  \"prefer-stable\": true,\n  \"version\": \"3.4.0-rc.25\"\n}\n"
  },
  {
    "path": "crates/html-to-markdown/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-rs\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\ndocumentation.workspace = true\nreadme = \"README.md\"\nrust-version.workspace = true\ndescription = \"High-performance HTML to Markdown converter using the astral-tl parser. Part of the Kreuzberg ecosystem.\"\nkeywords = [\"html\", \"markdown\", \"converter\", \"astral-tl\", \"doc-processing\"]\ncategories = [\"parsing\", \"text-processing\", \"web-programming\"]\n\n[package.metadata.cargo-machete]\nignored = [\"once_cell\", \"ahash\"]\n\n[lib]\ncrate-type = [\"rlib\"]\n\n[features]\ndefault = [\"metadata\"]\nfull = [\"inline-images\", \"metadata\", \"visitor\", \"serde\"]\ninline-images = [\"dep:image\"]\nmetadata = [\"dep:serde\", \"dep:serde_json\"]\nvisitor = []\nserde = [\"dep:serde\", \"dep:serde_json\"]\n\n[dependencies]\nahash.workspace = true\nbase64.workspace = true\nhtml-escape = \"0.2.13\"\nhtml5ever.workspace = true\nimage = { version = \"0.25\", default-features = false, features = [\n    \"gif\",\n    \"jpeg\",\n    \"png\",\n    \"bmp\",\n    \"webp\",\n], optional = true }\nlru = \"0.17\"\nmemchr = \"2\"\nonce_cell.workspace = true\nregex.workspace = true\nserde = { version = \"1.0\", features = [\"derive\"], optional = true }\nserde_json = { version = \"1.0\", optional = true }\nthiserror.workspace = true\ntl.workspace = true\n\n[dev-dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/html-to-markdown/README.md",
    "content": "# html-to-markdown-rs\n\nHigh-performance HTML to Markdown converter built with Rust.\n\nThis crate is the core engine compiled into the Python wheels, Ruby gem, Node.js NAPI bindings, WebAssembly package, and CLI, ensuring identical Markdown output across every language.\n\n[![Crates.io](https://img.shields.io/crates/v/html-to-markdown-rs.svg)](https://crates.io/crates/html-to-markdown-rs)\n[![npm version](https://img.shields.io/npm/v/@kreuzberg/html-to-markdown.svg?logo=npm)](https://www.npmjs.com/package/@kreuzberg/html-to-markdown)\n[![PyPI version](https://img.shields.io/pypi/v/html-to-markdown.svg?logo=pypi)](https://pypi.org/project/html-to-markdown/)\n[![Gem Version](https://badge.fury.io/rb/html-to-markdown.svg)](https://rubygems.org/gems/html-to-markdown)\n[![Packagist](https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown.svg)](https://packagist.org/packages/kreuzberg-dev/html-to-markdown)\n[![docs.rs](https://docs.rs/html-to-markdown-rs/badge.svg)](https://docs.rs/html-to-markdown-rs)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE)\n\nFast, reliable HTML to Markdown conversion with full CommonMark compliance. Built with `html5ever` for correctness and a DOM-based filter for safe preprocessing.\n\n## Installation\n\n```toml\n[dependencies]\nhtml-to-markdown-rs = \"3.0\"\n```\n\n## Basic Usage\n\n`convert()` returns a structured `ConversionResult` with the converted text, metadata, tables, and more:\n\n```rust\nuse html_to_markdown_rs::convert;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let html = r#\"\n        <html lang=\"en\">\n          <head><title>Welcome</title></head>\n          <body>\n            <h1>Welcome</h1>\n            <p>This is <strong>fast</strong> conversion!</p>\n            <ul>\n                <li>Built with Rust</li>\n                <li>CommonMark compliant</li>\n            </ul>\n          </body>\n        </html>\n    \"#;\n\n    let result = convert(html, None)?;\n    println!(\"{}\", result.content.unwrap_or_default());\n\n    if let Some(metadata) = &result.metadata {\n        println!(\"Title: {:?}\", metadata.document.title);\n        println!(\"Headers: {:?}\", metadata.headers);\n    }\n\n    for table in &result.tables {\n        println!(\"Table with {} rows\", table.cells.len());\n    }\n\n    Ok(())\n}\n```\n\n## Error Handling\n\nConversion returns a `Result<ConversionResult, ConversionError>`. Inputs that look like binary data are rejected with\n`ConversionError::InvalidInput` to prevent runaway allocations. Table `colspan`/`rowspan` values are also clamped\ninternally to keep output sizes bounded.\n\n## Configuration\n\n### Builder Pattern\n\n```rust\nuse html_to_markdown_rs::{\n    convert, ConversionOptions, HeadingStyle, CodeBlockStyle,\n};\n\nlet options = ConversionOptions::builder()\n    .heading_style(HeadingStyle::Atx)\n    .list_indent_width(2)\n    .bullets(\"-\")\n    .autolinks(true)\n    .wrap(true)\n    .wrap_width(80)\n    .build();\n\nlet result = convert(html, Some(options))?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n```\n\n### Struct Literal\n\n```rust\nuse html_to_markdown_rs::{\n    convert, ConversionOptions, HeadingStyle, ListIndentType,\n};\n\nlet options = ConversionOptions {\n    heading_style: HeadingStyle::Atx,\n    list_indent_width: 2,\n    list_indent_type: ListIndentType::Spaces,\n    bullets: \"-\".to_string(),\n    strong_em_symbol: '*',\n    escape_asterisks: false,\n    escape_underscores: false,\n    newline_style: html_to_markdown_rs::NewlineStyle::Backslash,\n    code_block_style: html_to_markdown_rs::CodeBlockStyle::Backticks,\n    ..Default::default()\n};\n\nlet result = convert(html, Some(options))?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n```\n\n### Preserving HTML Tags\n\nThe `preserve_tags` option allows you to keep specific HTML tags in their original form instead of converting them to Markdown:\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nlet html = r#\"\n<p>Before table</p>\n<table class=\"data\">\n    <tr><th>Name</th><th>Value</th></tr>\n    <tr><td>Item 1</td><td>100</td></tr>\n</table>\n<p>After table</p>\n\"#;\n\nlet options = ConversionOptions {\n    preserve_tags: vec![\"table\".to_string()],\n    ..Default::default()\n};\n\nlet result = convert(html, Some(options))?;\n// result.content => \"Before table\\n\\n<table class=\\\"data\\\">...</table>\\n\\nAfter table\\n\"\n```\n\n## Web Scraping with Preprocessing\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions, PreprocessingOptions};\n\nlet mut options = ConversionOptions::default();\noptions.preprocessing.enabled = true;\noptions.preprocessing.preset = html_to_markdown_rs::PreprocessingPreset::Aggressive;\noptions.preprocessing.remove_navigation = true;\noptions.preprocessing.remove_forms = true;\n\nlet result = convert(scraped_html, Some(options))?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n```\n\n## Metadata Extraction\n\nMetadata is automatically included in the result. Configure which fields to extract via `MetadataConfig`:\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions, MetadataConfig};\n\nlet options = ConversionOptions::builder()\n    .metadata_config(MetadataConfig {\n        extract_headers: true,\n        extract_links: true,\n        extract_images: false,\n        ..Default::default()\n    })\n    .build();\n\nlet result = convert(html, Some(options))?;\nif let Some(metadata) = &result.metadata {\n    println!(\"Title: {:?}\", metadata.document.title);\n    for header in &metadata.headers {\n        println!(\"H{}: {}\", header.level, header.text);\n    }\n    for link in &metadata.links {\n        println!(\"Link: {} -> {}\", link.text, link.href);\n    }\n}\n```\n\n## Image Extraction\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nlet options = ConversionOptions::builder()\n    .extract_images(true)\n    .max_image_size(5 * 1024 * 1024) // 5 MB max\n    .infer_dimensions(true)\n    .build();\n\nlet result = convert(html, Some(options))?;\nprintln!(\"{}\", result.content.unwrap_or_default());\nfor img in &result.images {\n    println!(\"Image: {} ({} bytes)\", img.src, img.data.as_ref().map_or(0, |d| d.len()));\n}\n```\n\n## Table Extraction\n\nStructured table data is always included in `ConversionResult.tables`:\n\n```rust\nuse html_to_markdown_rs::convert;\n\nlet html = r#\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"#;\n\nlet result = convert(html, None)?;\n\nprintln!(\"{}\", result.content.unwrap_or_default());\nfor table in &result.tables {\n    println!(\"Table with {} rows:\", table.cells.len());\n    for (i, row) in table.cells.iter().enumerate() {\n        let prefix = if table.is_header_row[i] { \"Header\" } else { \"Row\" };\n        println!(\"  {}: {:?}\", prefix, row);\n    }\n}\n```\n\n## Custom Visitors\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\nuse html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};\n\nstruct NoImagesVisitor;\n\nimpl HtmlVisitor for NoImagesVisitor {\n    fn visit_image(\n        &mut self,\n        _ctx: &NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> VisitResult {\n        VisitResult::Skip\n    }\n}\n\nlet options = ConversionOptions::builder()\n    .visitor(Box::new(NoImagesVisitor))\n    .build();\n\nlet result = convert(html, Some(options))?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n```\n\n## Other Language Bindings\n\nThis is the core Rust library. For other languages:\n\n- **JavaScript/TypeScript**: [html-to-markdown-node](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/crates/html-to-markdown-node) (NAPI-RS) or [html-to-markdown-wasm](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/crates/html-to-markdown-wasm) (WebAssembly)\n- **Python**: [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/crates/html-to-markdown-py) (PyO3)\n- **PHP**: [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/packages/php) (PIE + Composer helpers)\n- **Ruby**: [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/packages/ruby) (Magnus + rb-sys)\n- **CLI**: [html-to-markdown-cli](https://github.com/kreuzberg-dev/html-to-markdown/tree/main/crates/html-to-markdown-cli)\n\n## Documentation\n\n- [Full Documentation](https://docs.html-to-markdown.kreuzberg.dev)\n- [API Reference](https://docs.rs/html-to-markdown-rs)\n- [Migration Guide (v2 -> v3)](https://docs.html-to-markdown.kreuzberg.dev/migration/v3/)\n- [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md)\n\n## Performance\n\n10-30x faster than pure Python/JavaScript implementations, delivering 150-280 MB/s throughput.\n\n## License\n\nMIT\n"
  },
  {
    "path": "crates/html-to-markdown/examples/basic.rs",
    "content": "//! Example: Basic HTML to Markdown conversion\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = \"<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>\";\n\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"HTML:\");\n            println!(\"{html}\");\n            println!(\"\\nMarkdown:\");\n            println!(\"{markdown}\");\n        }\n        Err(e) => {\n            eprintln!(\"Error: {e}\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/table.rs",
    "content": "//! Example: Converting HTML tables to Markdown\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = r\"<table>\n        <tr><th>Name</th><th>Age</th></tr>\n        <tr><td>Alice</td><td>30</td></tr>\n        <tr><td>Bob</td><td>25</td></tr>\n    </table>\";\n\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Markdown:\\n{markdown}\");\n        }\n        Err(e) => {\n            eprintln!(\"Error: {e}\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_deser.rs",
    "content": "#![allow(missing_docs)]\nuse html_to_markdown_rs::ConversionOptions;\n\nfn main() {\n    let json = r#\"{\"headingStyle\":\"\",\"listIndentType\":\"\",\"listIndentWidth\":2,\"bullets\":\"-*+\",\"strongEmSymbol\":\"*\",\"escapeAsterisks\":false,\"escapeUnderscores\":false,\"escapeMisc\":false,\"escapeAscii\":false,\"codeLanguage\":\"\",\"autolinks\":true,\"defaultTitle\":false,\"brInTables\":false,\"highlightStyle\":\"\",\"extractMetadata\":true,\"whitespaceMode\":\"\",\"stripNewlines\":false,\"wrap\":false,\"wrapWidth\":80,\"convertAsInline\":false,\"subSymbol\":\"\",\"supSymbol\":\"\",\"newlineStyle\":\"spaces\",\"codeBlockStyle\":\"tildes\",\"keepInlineImagesIn\":null,\"preprocessing\":{\"enabled\":false,\"preset\":\"\",\"removeNavigation\":false,\"removeForms\":false},\"encoding\":\"utf-8\",\"debug\":false,\"stripTags\":null,\"preserveTags\":null,\"skipImages\":false,\"linkStyle\":\"\",\"outputFormat\":\"\",\"includeDocumentStructure\":false,\"extractImages\":false,\"maxImageSize\":5242880,\"captureSvg\":false,\"inferDimensions\":true}\"#;\n\n    let opts: ConversionOptions = serde_json::from_str(json).unwrap();\n    println!(\"code_block_style: {:?}\", opts.code_block_style);\n\n    let result = html_to_markdown_rs::convert(\"<pre><code>some code</code></pre>\", Some(opts)).unwrap();\n    println!(\"result: {:?}\", result.content);\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_escape.rs",
    "content": "//! Example: Testing HTML escape sequences and special characters\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = \"<p>Use *wildcards* for search</p>\";\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test 1 - Asterisks:\");\n            println!(\"HTML: {html}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: Use \\\\*wildcards\\\\* for search\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = \"<p>variable_name in code</p>\";\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test 2 - Underscores:\");\n            println!(\"HTML: {html2}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: variable\\\\_name in code\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html3 = \"<code>use *wildcards* for search</code>\";\n    match convert(html3, None) {\n        Ok(markdown) => {\n            println!(\"Test 3 - Code (no escaping):\");\n            println!(\"HTML: {html3}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: `use *wildcards* for search`\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html4 = \"<p>List: 1. First item</p>\";\n    match convert(html4, None) {\n        Ok(markdown) => {\n            println!(\"Test 4 - Numbered list escape:\");\n            println!(\"HTML: {html4}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: List: 1\\\\. First item\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_inline_formatting.rs",
    "content": "#![allow(missing_docs)]\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn main() {\n    let html = \"<p>This is <mark>highlighted</mark> text</p>\";\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Mark (default):\");\n            println!(\"HTML: {html}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: This is ==highlighted== text\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = \"<p>This is <del>deleted</del> and <s>strikethrough</s> text</p>\";\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Del/Strike:\");\n            println!(\"HTML: {html2}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: This is ~~deleted~~ and ~~strikethrough~~ text\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html3 = \"<p>This is <ins>inserted</ins> text</p>\";\n    match convert(html3, None) {\n        Ok(markdown) => {\n            println!(\"Test - Ins:\");\n            println!(\"HTML: {html3}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: This is ==inserted== text\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html4 = \"<p>Press <kbd>Ctrl+C</kbd> and see <samp>output</samp></p>\";\n    match convert(html4, None) {\n        Ok(markdown) => {\n            println!(\"Test - Kbd/Samp:\");\n            println!(\"HTML: {html4}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: Press `Ctrl+C` and see `output`\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html5 = \"<p>The variable <var>x</var> is defined</p>\";\n    match convert(html5, None) {\n        Ok(markdown) => {\n            println!(\"Test - Var:\");\n            println!(\"HTML: {html5}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: The variable *x* is defined\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let opts = ConversionOptions {\n        sub_symbol: \"~\".to_string(),\n        sup_symbol: \"^\".to_string(),\n        ..Default::default()\n    };\n\n    let html6 = \"<p>H<sub>2</sub>O and x<sup>2</sup></p>\";\n    match convert(html6, Some(opts)) {\n        Ok(markdown) => {\n            println!(\"Test - Sub/Sup:\");\n            println!(\"HTML: {html6}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: H~2~O and x^2^\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html7 = r#\"<p>The <abbr title=\"World Health Organization\">WHO</abbr> announced</p>\"#;\n    match convert(html7, None) {\n        Ok(markdown) => {\n            println!(\"Test - Abbr:\");\n            println!(\"HTML: {html7}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: The WHO (World Health Organization) announced\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html8 = \"<p>This is <u>underlined</u> and <small>small</small> text</p>\";\n    match convert(html8, None) {\n        Ok(markdown) => {\n            println!(\"Test - U/Small:\");\n            println!(\"HTML: {html8}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: This is underlined and small text\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_lists.rs",
    "content": "//! Example: Testing HTML list conversion (ordered and unordered lists)\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = \"<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>\";\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Ordered list:\");\n            println!(\"HTML: {html}\");\n            println!(\"Markdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"1. First item\");\n            println!(\"2. Second item\");\n            println!(\"3. Third item\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = \"<ul><li>First item</li><li>Second item</li></ul>\";\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Unordered list:\");\n            println!(\"HTML: {html2}\");\n            println!(\"Markdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"* First item\");\n            println!(\"* Second item\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_semantic_tags.rs",
    "content": "//! Example: Testing HTML5 semantic tags (article, section, nav, etc.)\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = r\"<article>\n        <header><h1>Title</h1></header>\n        <section><p>Content here</p></section>\n        <footer><p>Footer</p></footer>\n    </article>\";\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Semantic blocks:\");\n            println!(\"HTML: {html}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = r#\"<figure>\n        <img src=\"image.jpg\" alt=\"Diagram\">\n        <figcaption>Figure 1: Example diagram</figcaption>\n    </figure>\"#;\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Figure/Figcaption:\");\n            println!(\"HTML: {html2}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected: ![Diagram](image.jpg)\");\n            println!(\"          *Figure 1: Example diagram*\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html3 = r\"<p>As <cite>Shakespeare</cite> said, <q>To be or not to be</q></p>\";\n    match convert(html3, None) {\n        Ok(markdown) => {\n            println!(\"Test - Cite/Quote:\");\n            println!(\"HTML: {html3}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: As *Shakespeare* said, \\\"To be or not to be\\\"\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html4 = r\"<dl>\n        <dt>Term 1</dt>\n        <dd>Definition 1</dd>\n        <dt>Term 2</dt>\n        <dd>Definition 2</dd>\n    </dl>\";\n    match convert(html4, None) {\n        Ok(markdown) => {\n            println!(\"Test - Definition list:\");\n            println!(\"HTML: {html4}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"**Term 1**\");\n            println!(\"Definition 1\");\n            println!();\n            println!(\"**Term 2**\");\n            println!(\"Definition 2\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html5 = r\"<hgroup>\n        <h1>Main Title</h1>\n        <h2>Subtitle</h2>\n    </hgroup>\";\n    match convert(html5, None) {\n        Ok(markdown) => {\n            println!(\"Test - Hgroup:\");\n            println!(\"HTML: {html5}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_tables.rs",
    "content": "//! Example: Converting HTML tables to Markdown\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = r\"<table>\n        <tr>\n            <th>Name</th>\n            <th>Age</th>\n        </tr>\n        <tr>\n            <td>Alice</td>\n            <td>30</td>\n        </tr>\n        <tr>\n            <td>Bob</td>\n            <td>25</td>\n        </tr>\n    </table>\";\n\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Simple table with header:\");\n            println!(\"HTML: {html}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"| Name | Age |\");\n            println!(\"| --- | --- |\");\n            println!(\"| Alice | 30 |\");\n            println!(\"| Bob | 25 |\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = r#\"<table>\n        <tr>\n            <th colspan=\"2\">Full Name</th>\n            <th>Age</th>\n        </tr>\n        <tr>\n            <td>Alice</td>\n            <td>Smith</td>\n            <td>30</td>\n        </tr>\n    </table>\"#;\n\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Table with colspan:\");\n            println!(\"HTML: {html2}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"| Full Name | | Age |\");\n            println!(\"| --- | --- | --- |\");\n            println!(\"| Alice | Smith | 30 |\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html3 = r\"<table>\n        <thead>\n            <tr>\n                <th>Product</th>\n                <th>Price</th>\n            </tr>\n        </thead>\n        <tbody>\n            <tr>\n                <td>Widget</td>\n                <td>$10</td>\n            </tr>\n            <tr>\n                <td>Gadget</td>\n                <td>$15</td>\n            </tr>\n        </tbody>\n    </table>\";\n\n    match convert(html3, None) {\n        Ok(markdown) => {\n            println!(\"Test - Table with thead/tbody:\");\n            println!(\"HTML: {html3}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"| Product | Price |\");\n            println!(\"| --- | --- |\");\n            println!(\"| Widget | $10 |\");\n            println!(\"| Gadget | $15 |\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_task_lists.rs",
    "content": "//! Example: Testing task list conversion (checkboxes)\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = r#\"<ul>\n        <li><input type=\"checkbox\"> Unchecked task</li>\n        <li><input type=\"checkbox\" checked> Checked task</li>\n        <li>Regular list item</li>\n    </ul>\"#;\n\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Task list:\");\n            println!(\"HTML: {html}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!(\"Expected:\");\n            println!(\"- [ ] Unchecked task\");\n            println!(\"- [x] Checked task\");\n            println!(\"- Regular list item\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = r#\"<ul>\n        <li><input type=\"checkbox\"> Parent task\n            <ul>\n                <li><input type=\"checkbox\" checked> Child task</li>\n            </ul>\n        </li>\n    </ul>\"#;\n\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Nested task list:\");\n            println!(\"HTML: {html2}\");\n            println!(\"\\nMarkdown:\\n{markdown}\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html3 = r\"<ruby>東京<rt>とうきょう</rt></ruby>\";\n\n    match convert(html3, None) {\n        Ok(markdown) => {\n            println!(\"Test - Ruby annotation:\");\n            println!(\"HTML: {html3}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: 東京 (とうきょう)\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/examples/test_whitespace.rs",
    "content": "//! Example: Testing whitespace handling and normalization\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nfn main() {\n    let html = \"<p>text    with    multiple    spaces</p>\";\n    match convert(html, None) {\n        Ok(markdown) => {\n            println!(\"Test - Multiple spaces:\");\n            println!(\"HTML: {html}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: text with multiple spaces\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n\n    let html2 = \"<p>text\\nwith\\nnewlines</p>\";\n    match convert(html2, None) {\n        Ok(markdown) => {\n            println!(\"Test - Newlines:\");\n            println!(\"HTML: {html2}\");\n            println!(\"Markdown: {markdown}\");\n            println!(\"Expected: text with newlines\");\n            println!();\n        }\n        Err(e) => eprintln!(\"Error: {e}\"),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/convert_api.rs",
    "content": "//! Main HTML to Markdown conversion API.\n//!\n//! This module provides the primary `convert()` function for converting HTML to Markdown.\n\nuse std::borrow::Cow;\n\n#[cfg(any(feature = \"metadata\", feature = \"inline-images\"))]\nuse crate::ConversionError;\nuse crate::error::Result;\nuse crate::options::{ConversionOptions, WhitespaceMode};\nuse crate::text;\nuse crate::types::ConversionResult;\nuse crate::validation::{Utf16Encoding, detect_utf16_encoding, validate_input};\n\n#[cfg(feature = \"metadata\")]\nuse crate::{HtmlMetadata, MetadataConfig};\n\n/// Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n/// and warnings.\n///\n/// # Arguments\n///\n/// * `html` — the HTML string to convert.\n/// * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n///   When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n///   attached via the `visitor` field on `ConversionOptions`.\n///\n/// # Example\n///\n/// ```\n/// use html_to_markdown_rs::convert;\n///\n/// let html = \"<h1>Hello World</h1>\";\n/// let result = convert(html, None).unwrap();\n/// assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n/// ```\n///\n/// # Errors\n///\n/// Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\npub fn convert(html: &str, options: Option<ConversionOptions>) -> Result<ConversionResult> {\n    #[cfg(any(feature = \"metadata\", feature = \"inline-images\"))]\n    use std::cell::RefCell;\n    #[cfg(any(feature = \"metadata\", feature = \"inline-images\"))]\n    use std::rc::Rc;\n\n    let options = options.unwrap_or_default();\n\n    #[cfg(feature = \"visitor\")]\n    let visitor = options.visitor.clone();\n\n    let normalized_html = normalize_input(html)?;\n\n    // Fast path: plain text with no HTML tags — skip full parsing pipeline.\n    if !options.wrap {\n        if let Some(markdown) = fast_text_only(normalized_html.as_ref(), &options) {\n            return Ok(ConversionResult {\n                content: Some(markdown),\n                ..ConversionResult::default()\n            });\n        }\n    }\n\n    // Determine whether metadata / inline-image extraction is requested.\n    #[cfg(feature = \"metadata\")]\n    let wants_metadata = options.extract_metadata;\n    #[cfg(not(feature = \"metadata\"))]\n    let wants_metadata = false;\n\n    #[cfg(feature = \"inline-images\")]\n    let wants_images = options.extract_images;\n    #[cfg(not(feature = \"inline-images\"))]\n    let wants_images = false;\n\n    // Build optional collectors based on requested features.\n    #[cfg(feature = \"metadata\")]\n    let metadata_collector = if wants_metadata {\n        Some(Rc::new(RefCell::new(crate::metadata::MetadataCollector::new(\n            MetadataConfig::default(),\n        ))))\n    } else {\n        None\n    };\n\n    #[cfg(feature = \"inline-images\")]\n    let image_collector = if wants_images {\n        use crate::inline_images::{DEFAULT_INLINE_IMAGE_LIMIT, InlineImageConfig as IIC};\n        Some(Rc::new(RefCell::new(crate::inline_images::InlineImageCollector::new(\n            IIC::new(DEFAULT_INLINE_IMAGE_LIMIT),\n        )?)))\n    } else {\n        None\n    };\n\n    // Build optional structure collector when requested.\n    let structure_collector: Option<std::rc::Rc<std::cell::RefCell<crate::types::StructureCollector>>> =\n        if options.include_document_structure {\n            Some(std::rc::Rc::new(std::cell::RefCell::new(\n                crate::types::StructureCollector::new(),\n            )))\n        } else {\n            None\n        };\n\n    #[cfg(not(feature = \"visitor\"))]\n    let visitor: Option<()> = None;\n\n    // Run the conversion pipeline.\n    // Pass structure_collector by value — convert_html_impl will consume it via Rc::try_unwrap\n    // to return the finished DocumentStructure. We must not hold a second Rc reference.\n    let (markdown, document, tables) = {\n        #[cfg(all(feature = \"metadata\", feature = \"inline-images\"))]\n        {\n            crate::converter::convert_html_impl(\n                normalized_html.as_ref(),\n                &options,\n                image_collector.as_ref().map(Rc::clone),\n                metadata_collector.as_ref().map(Rc::clone),\n                visitor,\n                structure_collector,\n            )?\n        }\n        #[cfg(all(feature = \"metadata\", not(feature = \"inline-images\")))]\n        {\n            crate::converter::convert_html_impl(\n                normalized_html.as_ref(),\n                &options,\n                None,\n                metadata_collector.as_ref().map(Rc::clone),\n                visitor,\n                structure_collector,\n            )?\n        }\n        #[cfg(all(not(feature = \"metadata\"), feature = \"inline-images\"))]\n        {\n            crate::converter::convert_html_impl(\n                normalized_html.as_ref(),\n                &options,\n                image_collector.as_ref().map(Rc::clone),\n                None,\n                visitor,\n                structure_collector,\n            )?\n        }\n        #[cfg(all(not(feature = \"metadata\"), not(feature = \"inline-images\")))]\n        {\n            crate::converter::convert_html_impl(\n                normalized_html.as_ref(),\n                &options,\n                None,\n                None,\n                visitor,\n                structure_collector,\n            )?\n        }\n    };\n\n    let markdown = if options.wrap {\n        crate::wrapper::wrap_markdown(&markdown, &options)\n    } else {\n        markdown\n    };\n\n    // Collect metadata if extracted.\n    #[cfg(feature = \"metadata\")]\n    let metadata = if let Some(collector) = metadata_collector {\n        Rc::try_unwrap(collector)\n            .map_err(|_| ConversionError::Other(\"failed to recover metadata state\".to_string()))?\n            .into_inner()\n            .finish()\n    } else {\n        HtmlMetadata::default()\n    };\n\n    // Collect inline images if extracted.\n    #[cfg(feature = \"inline-images\")]\n    let (images, image_warnings) = if let Some(collector) = image_collector {\n        let c = Rc::try_unwrap(collector)\n            .map_err(|_| ConversionError::Other(\"failed to recover inline image state\".to_string()))?\n            .into_inner();\n        c.finish()\n    } else {\n        (Vec::new(), Vec::new())\n    };\n\n    // Map InlineImageWarnings → ProcessingWarnings.\n    #[cfg(feature = \"inline-images\")]\n    let warnings: Vec<crate::types::ProcessingWarning> = image_warnings\n        .into_iter()\n        .map(|w| crate::types::ProcessingWarning {\n            kind: crate::types::WarningKind::ImageExtractionFailed,\n            message: w.message,\n        })\n        .collect();\n    #[cfg(not(feature = \"inline-images\"))]\n    let warnings: Vec<crate::types::ProcessingWarning> = Vec::new();\n\n    let _ = wants_metadata;\n    let _ = wants_images;\n\n    Ok(ConversionResult {\n        content: Some(markdown),\n        document,\n        #[cfg(feature = \"metadata\")]\n        metadata,\n        tables,\n        #[cfg(feature = \"inline-images\")]\n        images,\n        warnings,\n    })\n}\n\n/// Validate and normalize HTML input for conversion.\nfn normalize_input(html: &str) -> Result<Cow<'_, str>> {\n    let decoded = decode_utf16_if_needed(html);\n    match decoded {\n        Cow::Borrowed(borrowed) => {\n            validate_input(borrowed)?;\n            let sanitized = strip_nul_bytes(borrowed);\n            match sanitized {\n                Cow::Borrowed(b) => Ok(normalize_line_endings(b)),\n                Cow::Owned(o) => Ok(Cow::Owned(normalize_line_endings(&o).into_owned())),\n            }\n        }\n        Cow::Owned(mut owned) => {\n            validate_input(&owned)?;\n            if owned.contains('\\0') {\n                owned = owned.replace('\\0', \"\");\n            }\n            if owned.contains('\\r') {\n                owned = owned.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n            }\n            Ok(Cow::Owned(owned))\n        }\n    }\n}\n\n/// Attempt to decode UTF-16 HTML that was provided as a lossy UTF-8 string.\n///\n/// Some callers read raw bytes and convert with `from_utf8_lossy`, which preserves\n/// the NUL-byte pattern of UTF-16 input. When we detect that pattern, we can\n/// recover the original HTML instead of rejecting it as binary data.\nfn decode_utf16_if_needed(html: &str) -> Cow<'_, str> {\n    let bytes = html.as_bytes();\n    if !bytes.contains(&0) {\n        return Cow::Borrowed(html);\n    }\n\n    let Some(encoding) = detect_utf16_encoding(bytes) else {\n        return Cow::Borrowed(html);\n    };\n\n    let decoded = decode_utf16_bytes(bytes, encoding);\n    if decoded.is_empty() {\n        Cow::Borrowed(html)\n    } else {\n        Cow::Owned(decoded)\n    }\n}\n\nfn decode_utf16_bytes(bytes: &[u8], encoding: Utf16Encoding) -> String {\n    let (is_little_endian, skip_bom) = match encoding {\n        Utf16Encoding::BomLe => (true, true),\n        Utf16Encoding::BomBe => (false, true),\n        Utf16Encoding::NoBomLe => (true, false),\n        Utf16Encoding::NoBomBe => (false, false),\n    };\n\n    let mut units = Vec::with_capacity(bytes.len() / 2);\n    for chunk in bytes.chunks_exact(2) {\n        let unit = if is_little_endian {\n            u16::from_le_bytes([chunk[0], chunk[1]])\n        } else {\n            u16::from_be_bytes([chunk[0], chunk[1]])\n        };\n        units.push(unit);\n    }\n\n    let mut decoded = String::from_utf16_lossy(&units);\n    if skip_bom {\n        decoded = decoded.trim_start_matches('\\u{FEFF}').to_string();\n    }\n    decoded\n}\n\n/// Strip NUL bytes that can appear in malformed HTML inputs.\nfn strip_nul_bytes(html: &str) -> Cow<'_, str> {\n    if html.contains('\\0') {\n        Cow::Owned(html.replace('\\0', \"\"))\n    } else {\n        Cow::Borrowed(html)\n    }\n}\n\n/// Normalize line endings in HTML input.\n///\n/// Converts CRLF and CR line endings to LF for consistent processing.\nfn normalize_line_endings(html: &str) -> Cow<'_, str> {\n    if html.contains('\\r') {\n        Cow::Owned(html.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\"))\n    } else {\n        Cow::Borrowed(html)\n    }\n}\n\n/// Fast path for plain text (no HTML) conversion.\n///\n/// Skips HTML parsing if no angle brackets are present.\nfn fast_text_only(html: &str, options: &ConversionOptions) -> Option<String> {\n    if html.contains('<') {\n        return None;\n    }\n\n    let mut decoded = text::decode_html_entities_cow(html);\n    if options.strip_newlines && (decoded.contains('\\n') || decoded.contains('\\r')) {\n        decoded = Cow::Owned(decoded.replace(&['\\r', '\\n'][..], \" \"));\n    }\n    let trimmed = decoded.trim_end_matches('\\n');\n    if trimmed.is_empty() {\n        return Some(String::new());\n    }\n\n    let normalized = if options.whitespace_mode == WhitespaceMode::Normalized {\n        text::normalize_whitespace_cow(trimmed)\n    } else {\n        Cow::Borrowed(trimmed)\n    };\n\n    let escaped = if options.output_format == crate::options::OutputFormat::Plain {\n        normalized.into_owned()\n    } else if options.escape_misc || options.escape_asterisks || options.escape_underscores || options.escape_ascii {\n        text::escape(\n            normalized.as_ref(),\n            options.escape_misc,\n            options.escape_asterisks,\n            options.escape_underscores,\n            options.escape_ascii,\n        )\n        .into_owned()\n    } else {\n        normalized.into_owned()\n    };\n\n    let mut output = String::with_capacity(escaped.len() + 1);\n    output.push_str(&escaped);\n    while output.ends_with(' ') || output.ends_with('\\t') {\n        output.pop();\n    }\n    output.push('\\n');\n    Some(output)\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/blockquote.rs",
    "content": "//! Handler for blockquote elements.\n//!\n//! Converts HTML blockquote tags to Markdown blockquotes with support for:\n//! - Nested blockquotes\n//! - Optional `cite` attribute attribution\n//! - Proper indentation and prefixing with `> `\n//! - Spacing management for various contexts\n//! - Visitor callbacks for custom blockquote processing\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::ConversionOptions;\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle blockquote elements.\n///\n/// Processes blockquote content, applies `> ` prefix to each line,\n/// handles optional `cite` attribution, and manages spacing.\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    if ctx.convert_as_inline {\n        if let Some(node) = node_handle.get(parser) {\n            if let tl::Node::Tag(tag) = node {\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n        }\n        return;\n    }\n\n    let cite = if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            tag.attributes()\n                .get(\"cite\")\n                .flatten()\n                .map(|v| v.as_utf8_str().to_string())\n        } else {\n            None\n        }\n    } else {\n        None\n    };\n\n    let blockquote_ctx = Context {\n        blockquote_depth: ctx.blockquote_depth + 1,\n        ..ctx.clone()\n    };\n\n    let mut content = String::with_capacity(256);\n    if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &blockquote_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n    }\n\n    let trimmed_content = content.trim();\n\n    #[cfg(feature = \"visitor\")]\n    {\n        if let Some(ref visitor) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            if let Some(node) = node_handle.get(parser) {\n                if let tl::Node::Tag(tag) = node {\n                    let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n                    let node_id = node_handle.get_inner();\n                    let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n                    let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n                    let node_ctx = NodeContext {\n                        node_type: NodeType::Blockquote,\n                        tag_name: \"blockquote\".to_string(),\n                        attributes,\n                        depth,\n                        index_in_parent,\n                        parent_tag,\n                        is_inline: false,\n                    };\n\n                    let mut visitor_ref = visitor.borrow_mut();\n                    match visitor_ref.visit_blockquote(&node_ctx, trimmed_content, ctx.blockquote_depth) {\n                        VisitResult::Continue => {}\n                        VisitResult::Custom(custom) => {\n                            output.push_str(&custom);\n                            return;\n                        }\n                        VisitResult::Skip => return,\n                        VisitResult::PreserveHtml => {\n                            serialize_node_to_html(node_handle, parser, output);\n                            return;\n                        }\n                        VisitResult::Error(err) => {\n                            if ctx.visitor_error.borrow().is_none() {\n                                *ctx.visitor_error.borrow_mut() = Some(err);\n                            }\n                            return;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    if !trimmed_content.is_empty() {\n        if ctx.blockquote_depth > 0 {\n            output.push_str(\"\\n\\n\\n\");\n        } else if !output.is_empty() {\n            if output.ends_with(\"\\n\\n\") {\n                // Paragraph already added \\n\\n; blockquote needs just \\n\n                output.truncate(output.len() - 1);\n            } else if !output.ends_with('\\n') {\n                output.push_str(\"\\n\\n\");\n            } else if !output.ends_with(\"\\n\\n\") {\n                output.push('\\n');\n            }\n        }\n\n        let prefix = \"> \";\n\n        for line in trimmed_content.lines() {\n            output.push_str(prefix);\n            output.push_str(line.trim());\n            output.push('\\n');\n        }\n\n        if let Some(url) = cite {\n            output.push('\\n');\n            output.push_str(\"— <\");\n            output.push_str(&url);\n            output.push_str(\">\\n\\n\");\n        }\n\n        // Add trailing newlines only when appropriate for proper spacing\n        // (matching paragraph conditional logic for CommonMark compliance)\n        if !ctx.convert_as_inline && !ctx.in_table_cell && !ctx.in_list_item {\n            while output.ends_with('\\n') {\n                output.truncate(output.len() - 1);\n            }\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Serialize a node to HTML (used for PreserveHtml visitor result).\n#[cfg(feature = \"visitor\")]\nfn serialize_node_to_html(node_handle: &NodeHandle, parser: &Parser, output: &mut String) {\n    use crate::converter::serialize_node_to_html as core_serialize;\n    core_serialize(node_handle, parser, output);\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/container.rs",
    "content": "//! Handler for structural container elements.\n//!\n//! This module provides handlers for structural containers that process their\n//! children without special formatting or whitespace truncation:\n//! - body, html: Structural document containers\n//! - time, data: Inline semantic containers\n//! - thead, tbody, tfoot, tr, th, td: Table structure (handled elsewhere)\n//! - source: Media source element\n//! - wbr: Word break opportunity (no-op)\n\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle structural container elements that recursively process children.\n///\n/// This is used for elements like `body` and `html` that should process their\n/// children directly without any whitespace truncation or special formatting.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the HTML node\n/// * `parser` - The HTML parser\n/// * `output` - Accumulation buffer for Markdown output\n/// * `options` - Conversion options\n/// * `ctx` - Current conversion context\n/// * `depth` - Current recursion depth\n/// * `dom_ctx` - DOM context for tracking relationships\npub fn handle_structural_container(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let Some(node) = node_handle.get(parser) else {\n        return;\n    };\n\n    let tl::Node::Tag(tag) = node else {\n        return;\n    };\n\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        crate::converter::main::walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n    }\n}\n\n/// Handle pass-through container elements that process children inline.\n///\n/// This is used for semantic elements like `time` and `data` that wrap content\n/// but should not add any additional formatting or block breaks.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the HTML node\n/// * `parser` - The HTML parser\n/// * `output` - Accumulation buffer for Markdown output\n/// * `options` - Conversion options\n/// * `ctx` - Current conversion context\n/// * `depth` - Current recursion depth\n/// * `dom_ctx` - DOM context for tracking relationships\npub fn handle_passthrough(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let Some(node) = node_handle.get(parser) else {\n        return;\n    };\n\n    let tl::Node::Tag(tag) = node else {\n        return;\n    };\n\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        crate::converter::main::walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n    }\n}\n\n/// Handle no-op container elements that should be ignored.\n///\n/// This is used for elements like `wbr` (word break opportunity) and `source`\n/// (media source specification) that should not produce any output.\n///\n/// # Arguments\n/// * `_node_handle` - Handle to the HTML node (unused)\n/// * `_parser` - The HTML parser (unused)\n/// * `_output` - Accumulation buffer for Markdown output (unused)\n/// * `_options` - Conversion options (unused)\n/// * `_ctx` - Current conversion context (unused)\n/// * `_depth` - Current recursion depth (unused)\n/// * `_dom_ctx` - DOM context (unused)\n#[inline]\npub fn handle_noop(\n    _node_handle: &NodeHandle,\n    _parser: &Parser,\n    _output: &mut String,\n    _options: &ConversionOptions,\n    _ctx: &Context,\n    _depth: usize,\n    _dom_ctx: &DomContext,\n) {\n    // Intentionally empty: these elements produce no Markdown output\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/div.rs",
    "content": "//! Handler for div element.\n//!\n//! Converts HTML div elements to Markdown by processing children while maintaining\n//! appropriate spacing and context awareness for:\n//! - Table continuations: Uses table-specific line breaks\n//! - List continuations: Uses list indentation\n//! - Block context: Adds surrounding newlines for proper block separation\n\nuse crate::converter::main_helpers::trim_trailing_whitespace;\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handles div elements.\n///\n/// Divs are generic container elements that need special handling based on context:\n/// - When inline context: passes through children without separators\n/// - When in table cell: uses table-specific line breaks (<br> or backslash)\n/// - When in list item: uses list continuation indentation\n/// - When in block context: adds appropriate newlines before/after content\n///\n/// # Note\n/// This function references `walk_node` and helper functions from converter.rs\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    // If inline conversion mode, just pass children through\n    if ctx.convert_as_inline {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n        return;\n    }\n\n    let content_start_pos = output.len();\n\n    let is_table_continuation =\n        ctx.in_table_cell && !output.is_empty() && !output.ends_with('|') && !output.ends_with(\"<br>\");\n\n    let is_list_continuation = ctx.in_list_item\n        && !output.is_empty()\n        && !output.ends_with(\"* \")\n        && !output.ends_with(\"- \")\n        && !output.ends_with(\". \");\n\n    let needs_leading_sep = !ctx.in_table_cell\n        && !ctx.in_list_item\n        && !ctx.convert_as_inline\n        && !output.is_empty()\n        && !output.ends_with(\"\\n\\n\");\n\n    // Handle leading separators based on context\n    if is_table_continuation {\n        trim_trailing_whitespace(output);\n        if options.br_in_tables {\n            output.push_str(\"<br>\");\n        } else {\n            use crate::options::NewlineStyle;\n            match options.newline_style {\n                NewlineStyle::Spaces => output.push_str(\"  \\n\"),\n                NewlineStyle::Backslash => output.push_str(\"\\\\\\n\"),\n            }\n        }\n    } else if is_list_continuation {\n        add_list_continuation_indent(output, ctx.list_depth, false, options);\n    } else if needs_leading_sep {\n        trim_trailing_whitespace(output);\n        output.push_str(\"\\n\\n\");\n    }\n\n    // Process children\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n\n    let has_content = output.len() > content_start_pos;\n\n    if has_content {\n        if content_start_pos == 0 && output.starts_with('\\n') && !output.starts_with(\"\\n\\n\") {\n            output.remove(0);\n        }\n        trim_trailing_whitespace(output);\n\n        if ctx.in_table_cell {\n            // No trailing separator in table cells\n        } else if ctx.in_list_item {\n            if is_list_continuation {\n                if !output.ends_with('\\n') {\n                    output.push('\\n');\n                }\n            } else if !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n        } else if !ctx.in_list_item && !ctx.convert_as_inline {\n            if output.ends_with(\"\\n\\n\") {\n                // Already has proper spacing\n            } else if output.ends_with('\\n') {\n                output.push('\\n');\n            } else {\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    }\n}\n\n/// Helper function to add list continuation indentation\nfn add_list_continuation_indent(\n    output: &mut String,\n    list_depth: usize,\n    _block_level: bool,\n    _options: &ConversionOptions,\n) {\n    if !output.ends_with('\\n') {\n        output.push('\\n');\n    }\n\n    for _ in 0..list_depth {\n        output.push_str(\"  \");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/heading.rs",
    "content": "//! Handler for heading elements (h1-h6).\n//!\n//! Converts HTML heading tags to Markdown heading syntax with support for:\n//! - Multiple heading styles (ATX, underlined, closed ATX)\n//! - Inline content processing with proper text normalization\n//! - Metadata collection (headers, IDs)\n//! - Visitor callbacks for custom heading processing\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::{ConversionOptions, HeadingStyle};\nuse std::borrow::Cow;\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\n// These are imported from converter.rs and should be made accessible\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle heading elements (h1, h2, h3, h4, h5, h6).\n///\n/// Extracts the heading level from the tag name, processes inline content,\n/// normalizes text, and outputs formatted heading with proper spacing.\n///\n/// # Note\n/// This function references `walk_node` from converter.rs which must be\n/// accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Import walk_node from parent converter module\n    use crate::converter::walk_node;\n\n    let level = tag_name.chars().last().and_then(|c| c.to_digit(10)).unwrap_or(1) as usize;\n\n    // Add spacing before heading if needed (similar to paragraph handling)\n    let needs_leading_sep = !ctx.in_table_cell\n        && !ctx.in_list_item\n        && !ctx.convert_as_inline\n        && ctx.blockquote_depth == 0\n        && !output.is_empty()\n        && !output.ends_with(\"\\n\\n\");\n\n    if needs_leading_sep {\n        crate::converter::trim_trailing_whitespace(output);\n        output.push_str(\"\\n\\n\");\n    }\n\n    let mut text = String::new();\n    let heading_ctx = Context {\n        in_heading: true,\n        convert_as_inline: true,\n        heading_allow_inline_images: heading_allows_inline_images(tag_name, &ctx.keep_inline_images_in),\n        ..ctx.clone()\n    };\n\n    if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut text,\n                    options,\n                    &heading_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n    }\n\n    let trimmed = text.trim();\n    if !trimmed.is_empty() {\n        let normalized = normalize_heading_text(trimmed);\n\n        #[cfg(feature = \"visitor\")]\n        let heading_output = visitor_heading_output(\n            node_handle,\n            parser,\n            tag_name,\n            level,\n            &normalized,\n            output,\n            options,\n            ctx,\n            depth,\n            dom_ctx,\n        );\n\n        #[cfg(not(feature = \"visitor\"))]\n        let heading_output = {\n            let mut buf = String::new();\n            push_heading(&mut buf, ctx, options, level, normalized.as_ref());\n            Some(buf)\n        };\n\n        if let Some(heading_text) = heading_output {\n            output.push_str(&heading_text);\n        }\n\n        #[cfg(feature = \"metadata\")]\n        if ctx.metadata_wants_headers {\n            if let Some(ref collector) = ctx.metadata_collector {\n                if let Some(node) = node_handle.get(parser) {\n                    if let tl::Node::Tag(tag) = node {\n                        let id = tag\n                            .attributes()\n                            .get(\"id\")\n                            .flatten()\n                            .map(|v| v.as_utf8_str().to_string());\n                        collector\n                            .borrow_mut()\n                            .add_header(level as u8, normalized.to_string(), id, depth, 0);\n                    }\n                }\n            }\n        }\n\n        // Notify the structure collector if present.\n        // Skip headings inside table cells — they are part of the table content,\n        // not standalone structural headings.\n        if !ctx.in_table_cell {\n            if let Some(ref sc) = ctx.structure_collector {\n                if let Some(node) = node_handle.get(parser) {\n                    if let tl::Node::Tag(tag) = node {\n                        let id = tag\n                            .attributes()\n                            .get(\"id\")\n                            .flatten()\n                            .map(|v| v.as_utf8_str().to_string());\n                        sc.borrow_mut()\n                            .push_heading(level as u8, normalized.as_ref(), id.as_deref());\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Determine if a heading element should allow inline images.\npub fn heading_allows_inline_images(\n    tag_name: &str,\n    keep_inline_images_in: &std::rc::Rc<std::collections::HashSet<String>>,\n) -> bool {\n    keep_inline_images_in.contains(tag_name)\n}\n\n/// Normalize heading text by replacing newlines with spaces.\nfn normalize_heading_text(text: &str) -> Cow<'_, str> {\n    if !text.contains('\\n') && !text.contains('\\r') {\n        return Cow::Borrowed(text);\n    }\n\n    let mut normalized = String::with_capacity(text.len());\n    let mut pending_space = false;\n\n    for ch in text.chars() {\n        match ch {\n            '\\n' | '\\r' => {\n                if !normalized.is_empty() {\n                    pending_space = true;\n                }\n            }\n            ' ' | '\\t' if pending_space => {}\n            _ => {\n                if pending_space {\n                    if !normalized.ends_with(' ') {\n                        normalized.push(' ');\n                    }\n                    pending_space = false;\n                }\n                normalized.push(ch);\n            }\n        }\n    }\n\n    Cow::Owned(normalized)\n}\n\n/// Format heading output with appropriate markdown syntax.\npub fn push_heading(output: &mut String, ctx: &Context, options: &ConversionOptions, level: usize, text: &str) {\n    if text.is_empty() {\n        return;\n    }\n\n    if ctx.convert_as_inline {\n        output.push_str(text);\n        return;\n    }\n\n    if ctx.in_table_cell {\n        let is_table_continuation =\n            !output.is_empty() && !output.ends_with('|') && !output.ends_with(' ') && !output.ends_with(\"<br>\");\n        if is_table_continuation {\n            output.push_str(\"<br>\");\n        }\n        output.push_str(text);\n        return;\n    }\n\n    if ctx.in_list_item {\n        if output.ends_with('\\n') {\n            if let Some(indent) = continuation_indent_string(ctx.list_depth, options) {\n                output.push_str(&indent);\n            }\n        } else if !output.ends_with(' ') && !output.is_empty() {\n            output.push(' ');\n        }\n    } else if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n        if output.ends_with('\\n') {\n            output.push('\\n');\n        } else {\n            crate::converter::trim_trailing_whitespace(output);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n\n    let heading_suffix = if ctx.in_list_item || ctx.blockquote_depth > 0 {\n        \"\\n\"\n    } else {\n        \"\\n\\n\"\n    };\n\n    match options.heading_style {\n        HeadingStyle::Underlined => {\n            if level == 1 {\n                output.push_str(text);\n                output.push('\\n');\n                for _ in 0..text.len() {\n                    output.push('=');\n                }\n            } else if level == 2 {\n                output.push_str(text);\n                output.push('\\n');\n                for _ in 0..text.len() {\n                    output.push('-');\n                }\n            } else {\n                for _ in 0..level {\n                    output.push('#');\n                }\n                output.push(' ');\n                output.push_str(text);\n            }\n        }\n        HeadingStyle::Atx => {\n            for _ in 0..level {\n                output.push('#');\n            }\n            output.push(' ');\n            output.push_str(text);\n        }\n        HeadingStyle::AtxClosed => {\n            for _ in 0..level {\n                output.push('#');\n            }\n            output.push(' ');\n            output.push_str(text);\n            output.push(' ');\n            for _ in 0..level {\n                output.push('#');\n            }\n        }\n    }\n    output.push_str(heading_suffix);\n}\n\n/// Get continuation indent string for list items.\nfn continuation_indent_string(list_depth: usize, _options: &ConversionOptions) -> Option<String> {\n    if list_depth == 0 {\n        return None;\n    }\n    let mut indent = String::new();\n    for _ in 0..(4 * list_depth) {\n        indent.push(' ');\n    }\n    Some(indent)\n}\n\n/// Process heading with visitor callback if available.\n#[cfg(feature = \"visitor\")]\nfn visitor_heading_output(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    tag_name: &str,\n    level: usize,\n    normalized: &str,\n    _output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) -> Option<String> {\n    use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n    if let Some(ref visitor_handle) = ctx.visitor {\n        if let Some(node) = node_handle.get(parser) {\n            if let tl::Node::Tag(tag) = node {\n                let id_attr = tag\n                    .attributes()\n                    .get(\"id\")\n                    .flatten()\n                    .map(|v| v.as_utf8_str().to_string());\n\n                let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n                let node_id = node_handle.get_inner();\n                let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n                let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n                let node_ctx = NodeContext {\n                    node_type: NodeType::Heading,\n                    tag_name: tag_name.to_string(),\n                    attributes,\n                    depth,\n                    index_in_parent,\n                    parent_tag,\n                    is_inline: false,\n                };\n\n                let visit_result = {\n                    let mut visitor = visitor_handle.borrow_mut();\n                    visitor.visit_heading(&node_ctx, level as u32, normalized, id_attr.as_deref())\n                };\n                match visit_result {\n                    VisitResult::Continue => {\n                        let mut buf = String::new();\n                        push_heading(&mut buf, ctx, options, level, normalized);\n                        Some(buf)\n                    }\n                    VisitResult::Custom(custom) => Some(custom),\n                    VisitResult::Skip => None,\n                    VisitResult::Error(err) => {\n                        if ctx.visitor_error.borrow().is_none() {\n                            *ctx.visitor_error.borrow_mut() = Some(err);\n                        }\n                        None\n                    }\n                    VisitResult::PreserveHtml => {\n                        let mut buf = String::new();\n                        push_heading(&mut buf, ctx, options, level, normalized);\n                        Some(buf)\n                    }\n                }\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    } else {\n        let mut buf = String::new();\n        push_heading(&mut buf, ctx, options, level, normalized);\n        Some(buf)\n    }\n}\n\n/// Find a single heading element within a node, filtering out non-heading content.\n///\n/// Returns the heading level and node handle if the node contains exactly one\n/// heading with no other non-whitespace content. Returns None if:\n/// - The node is not a tag\n/// - Multiple headings are found\n/// - Non-whitespace non-heading content exists\n/// - Non-text comments exist\npub fn find_single_heading_child(node_handle: NodeHandle, parser: &Parser) -> Option<(usize, NodeHandle)> {\n    let node = node_handle.get(parser)?;\n\n    let tl::Node::Tag(tag) = node else {\n        return None;\n    };\n\n    let children = tag.children();\n    let mut heading_data: Option<(usize, NodeHandle)> = None;\n\n    for child_handle in children.top().iter() {\n        let Some(child_node) = child_handle.get(parser) else {\n            continue;\n        };\n\n        match child_node {\n            tl::Node::Raw(bytes) => {\n                if !bytes.as_utf8_str().trim().is_empty() {\n                    return None;\n                }\n            }\n            tl::Node::Tag(child_tag) => {\n                let name = crate::converter::utility::content::normalized_tag_name(child_tag.name().as_utf8_str());\n                {\n                    let level = heading_level_from_name(name.as_ref())?;\n                    if heading_data.is_some() {\n                        return None;\n                    }\n                    heading_data = Some((level, *child_handle));\n                }\n            }\n            tl::Node::Comment(_) => return None,\n        }\n    }\n\n    heading_data\n}\n\n/// Extract heading level from tag name (h1-h6).\n///\n/// Returns Some(level) for valid heading tags, None otherwise.\nfn heading_level_from_name(name: &str) -> Option<usize> {\n    match name {\n        \"h1\" => Some(1),\n        \"h2\" => Some(2),\n        \"h3\" => Some(3),\n        \"h4\" => Some(4),\n        \"h5\" => Some(5),\n        \"h6\" => Some(6),\n        _ => None,\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/horizontal_rule.rs",
    "content": "//! Handler for horizontal rule elements (hr).\n//!\n//! Converts HTML horizontal rule tags to Markdown horizontal rules (---)\n//! with appropriate spacing handling based on context.\n\nuse crate::converter::main_helpers::trim_trailing_whitespace;\nuse crate::converter::utility::siblings::get_previous_sibling_tag;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle horizontal rule elements (hr).\n///\n/// Converts to Markdown horizontal rule (---) with appropriate blank line\n/// spacing based on context and previous siblings.\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    _options: &crate::options::ConversionOptions,\n    ctx: &Context,\n    _depth: usize,\n    dom_ctx: &DomContext,\n) {\n    if !output.is_empty() {\n        let prev_tag = get_previous_sibling_tag(node_handle, parser, dom_ctx);\n        let last_line_is_blockquote = output\n            .rsplit('\\n')\n            .find(|line| !line.trim().is_empty())\n            .is_some_and(|line| line.trim_start().starts_with('>'));\n        let needs_blank_line = !ctx.in_paragraph && !matches!(prev_tag, Some(\"blockquote\")) && !last_line_is_blockquote;\n\n        // If previous element was a blockquote, it added \\n\\n; reduce to \\n\n        if matches!(prev_tag, Some(\"blockquote\")) && output.ends_with(\"\\n\\n\") {\n            output.truncate(output.len() - 1);\n        } else if ctx.in_paragraph || !needs_blank_line {\n            if !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n        } else {\n            trim_trailing_whitespace(output);\n            if output.ends_with('\\n') {\n                if !output.ends_with(\"\\n\\n\") {\n                    output.push('\\n');\n                }\n            } else {\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    }\n    output.push_str(\"---\\n\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/line_break.rs",
    "content": "//! Handler for line break elements (br).\n//!\n//! Converts HTML line break tags to Markdown line breaks using the configured\n//! newline style (spaces, backslash, or plain newline).\n\nuse crate::converter::main_helpers::trim_trailing_whitespace;\nuse crate::options::{ConversionOptions, NewlineStyle};\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle line break elements (br).\n///\n/// Converts to appropriate Markdown line break syntax based on the configured\n/// newline style and current context (e.g., in headings).\npub fn handle(\n    _node_handle: &NodeHandle,\n    _parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    _depth: usize,\n    _dom_ctx: &DomContext,\n) {\n    if ctx.in_heading {\n        trim_trailing_whitespace(output);\n        output.push_str(\"  \");\n    } else {\n        if output.is_empty() || output.ends_with('\\n') {\n            output.push('\\n');\n        } else {\n            match options.newline_style {\n                NewlineStyle::Spaces => output.push_str(\"  \\n\"),\n                NewlineStyle::Backslash => output.push_str(\"\\\\\\n\"),\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/mod.rs",
    "content": "pub mod blockquote;\npub mod container;\npub mod div;\npub mod heading;\npub mod horizontal_rule;\npub mod line_break;\npub mod paragraph;\npub mod preformatted;\npub mod table;\npub mod unknown;\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/paragraph.rs",
    "content": "//! Handler for paragraph elements (p, div).\n//!\n//! Converts HTML paragraph tags to Markdown paragraphs with proper spacing\n//! and support for:\n//! - Continuation handling in tables and lists\n//! - Proper blank line spacing\n//! - Empty element filtering\n//! - Visitor callbacks for custom paragraph processing\n\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle paragraph elements (p, div).\n///\n/// Processes children with proper context, manages spacing,\n/// and handles special cases for table cells and list items.\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let content_start_pos = output.len();\n\n    let is_table_continuation =\n        ctx.in_table_cell && !output.is_empty() && !output.ends_with('|') && !output.ends_with(\"<br>\");\n\n    let is_list_continuation = ctx.in_list_item\n        && !output.is_empty()\n        && !output.ends_with(\"* \")\n        && !output.ends_with(\"- \")\n        && !output.ends_with(\". \");\n\n    let after_code_block = output.ends_with(\"```\\n\");\n    let needs_leading_sep = !ctx.in_table_cell\n        && !ctx.in_list_item\n        && !ctx.convert_as_inline\n        && ctx.blockquote_depth == 0\n        && !output.is_empty()\n        && !output.ends_with(\"\\n\\n\")\n        && !after_code_block;\n\n    if is_table_continuation {\n        crate::converter::trim_trailing_whitespace(output);\n        output.push_str(\"<br>\");\n    } else if is_list_continuation {\n        add_list_continuation_indent(output, ctx.list_depth, true, options);\n    } else if needs_leading_sep {\n        crate::converter::trim_trailing_whitespace(output);\n        output.push_str(\"\\n\\n\");\n    }\n\n    let p_ctx = Context {\n        in_paragraph: true,\n        block_content_start: output.len(),\n        ..ctx.clone()\n    };\n\n    if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            let children = tag.children();\n            let child_handles: Vec<_> = children.top().iter().collect();\n\n            for (i, child_handle) in child_handles.iter().enumerate() {\n                if let Some(node) = child_handle.get(parser) {\n                    if let tl::Node::Raw(bytes) = node {\n                        let text = bytes.as_utf8_str();\n                        if text.trim().is_empty() && i > 0 && i < child_handles.len() - 1 {\n                            let prev = &child_handles[i - 1];\n                            let next = &child_handles[i + 1];\n                            if is_empty_inline_element(prev, parser, dom_ctx)\n                                && is_empty_inline_element(next, parser, dom_ctx)\n                            {\n                                continue;\n                            }\n                        }\n                    }\n                }\n\n                walk_node(child_handle, parser, output, options, &p_ctx, depth + 1, dom_ctx);\n            }\n        }\n    }\n\n    let has_content = output.len() > content_start_pos;\n\n    if has_content && !ctx.convert_as_inline && !ctx.in_table_cell {\n        output.push_str(\"\\n\\n\");\n    }\n\n    // Notify the structure collector if present and we produced non-empty top-level paragraph content.\n    if has_content && !ctx.in_table_cell && !ctx.in_list_item && !ctx.convert_as_inline {\n        if let Some(ref sc) = ctx.structure_collector {\n            let text = output[content_start_pos..].trim().to_string();\n            if !text.is_empty() {\n                sc.borrow_mut().push_paragraph(&text);\n            }\n        }\n    }\n}\n\n/// Add continuation indentation for list items.\nfn add_list_continuation_indent(\n    output: &mut String,\n    list_depth: usize,\n    needs_space: bool,\n    _options: &ConversionOptions,\n) {\n    if needs_space && !output.ends_with(' ') && !output.ends_with('\\n') {\n        output.push(' ');\n    }\n    for _ in 0..(4 * list_depth) {\n        output.push(' ');\n    }\n}\n\n/// Check if an element is empty (has no text content).\nfn is_empty_inline_element(node_handle: &NodeHandle, parser: &Parser, _dom_ctx: &DomContext) -> bool {\n    if let Some(node) = node_handle.get(parser) {\n        match node {\n            tl::Node::Tag(tag) => {\n                let tag_name = tag.name().as_utf8_str();\n                // Elements that are always empty or only contain attributes\n                matches!(tag_name.as_ref(), \"br\" | \"hr\" | \"img\" | \"input\" | \"meta\" | \"link\")\n            }\n            _ => false,\n        }\n    } else {\n        false\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/preformatted.rs",
    "content": "//! Handler for preformatted code elements (pre, code).\n//!\n//! Converts HTML preformatted and code tags to Markdown code blocks with support for:\n//! - Language detection from `class` attributes (language-*, lang-*)\n//! - Multiple code block styles (indented, backticks, tildes)\n//! - Code dedentation and whitespace normalization\n//! - Inline code formatting with backtick management\n//! - Visitor callbacks for custom code processing\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::{CodeBlockStyle, ConversionOptions, WhitespaceMode};\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle preformatted code blocks (pre element).\npub fn handle_pre(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let code_ctx = Context {\n        in_code: true,\n        ..ctx.clone()\n    };\n\n    let language = extract_language_from_pre(node_handle, parser);\n\n    let mut content = String::with_capacity(256);\n    if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &code_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n    }\n\n    if !content.is_empty() {\n        let leading_newlines = content.chars().take_while(|&c| c == '\\n').count();\n        let trailing_newlines = content.chars().rev().take_while(|&c| c == '\\n').count();\n        let core = content.trim_matches('\\n');\n        let is_whitespace_only = core.trim().is_empty();\n\n        let processed_content = if options.whitespace_mode == WhitespaceMode::Strict {\n            content\n        } else {\n            let mut core_text = if leading_newlines > 0 {\n                dedent_code_block(core)\n            } else {\n                core.to_string()\n            };\n\n            if is_whitespace_only {\n                let mut rebuilt = String::new();\n                for _ in 0..leading_newlines {\n                    rebuilt.push('\\n');\n                }\n                rebuilt.push_str(&core_text);\n                for _ in 0..trailing_newlines {\n                    rebuilt.push('\\n');\n                }\n                rebuilt\n            } else {\n                for _ in 0..trailing_newlines {\n                    core_text.push('\\n');\n                }\n                core_text\n            }\n        };\n\n        #[cfg(feature = \"visitor\")]\n        {\n            if let Some(ref visitor_handle) = ctx.visitor {\n                use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n                if let Some(node) = node_handle.get(parser) {\n                    if let tl::Node::Tag(tag) = node {\n                        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n                        let node_id = node_handle.get_inner();\n                        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n                        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n                        let node_ctx = NodeContext {\n                            node_type: NodeType::Pre,\n                            tag_name: \"pre\".to_string(),\n                            attributes,\n                            depth,\n                            index_in_parent,\n                            parent_tag,\n                            is_inline: false,\n                        };\n\n                        let visit_result = {\n                            let mut visitor = visitor_handle.borrow_mut();\n                            visitor.visit_code_block(&node_ctx, language.as_deref(), &processed_content)\n                        };\n                        match visit_result {\n                            VisitResult::Continue => {\n                                format_code_block(output, options, ctx, &processed_content, language.as_deref());\n                            }\n                            VisitResult::Custom(custom) => {\n                                output.push_str(&custom);\n                            }\n                            VisitResult::Skip => {\n                                // Skip code block\n                            }\n                            VisitResult::PreserveHtml => {\n                                format_code_block(output, options, ctx, &processed_content, language.as_deref());\n                            }\n                            VisitResult::Error(err) => {\n                                if ctx.visitor_error.borrow().is_none() {\n                                    *ctx.visitor_error.borrow_mut() = Some(err);\n                                }\n                            }\n                        }\n                    }\n                }\n            } else {\n                format_code_block(output, options, ctx, &processed_content, language.as_deref());\n            }\n        }\n\n        #[cfg(not(feature = \"visitor\"))]\n        {\n            format_code_block(output, options, ctx, &processed_content, language.as_deref());\n        }\n    }\n}\n\n/// Extract programming language from pre or nested code element's class attribute.\nfn extract_language_from_pre(node_handle: &NodeHandle, parser: &Parser) -> Option<String> {\n    if let Some(node) = node_handle.get(parser) {\n        if let tl::Node::Tag(tag) = node {\n            // First, try to extract language from <pre> tag's class attribute\n            if let Some(class_attr) = tag.attributes().get(\"class\") {\n                if let Some(class_bytes) = class_attr {\n                    let class_str = class_bytes.as_utf8_str();\n                    for cls in class_str.split_whitespace() {\n                        if let Some(stripped) = cls.strip_prefix(\"language-\") {\n                            return Some(String::from(stripped));\n                        } else if let Some(stripped) = cls.strip_prefix(\"lang-\") {\n                            return Some(String::from(stripped));\n                        }\n                    }\n                }\n            }\n\n            // If not found on <pre>, try to extract from nested <code> tag's class attribute\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    if child_tag.name() == \"code\" {\n                        if let Some(class_attr) = child_tag.attributes().get(\"class\") {\n                            if let Some(class_bytes) = class_attr {\n                                let class_str = class_bytes.as_utf8_str();\n                                for cls in class_str.split_whitespace() {\n                                    if let Some(stripped) = cls.strip_prefix(\"language-\") {\n                                        return Some(String::from(stripped));\n                                    } else if let Some(stripped) = cls.strip_prefix(\"lang-\") {\n                                        return Some(String::from(stripped));\n                                    }\n                                }\n                            }\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n    }\n    None\n}\n\n/// Format code block with appropriate markdown syntax.\nfn format_code_block(\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    content: &str,\n    language: Option<&str>,\n) {\n    match options.code_block_style {\n        CodeBlockStyle::Indented => {\n            if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n\n            let indented = content\n                .lines()\n                .map(|line| {\n                    if line.is_empty() {\n                        String::new()\n                    } else {\n                        format!(\"    {line}\")\n                    }\n                })\n                .collect::<Vec<_>>()\n                .join(\"\\n\");\n            output.push_str(&indented);\n\n            output.push_str(\"\\n\\n\");\n        }\n        CodeBlockStyle::Backticks | CodeBlockStyle::Tildes => {\n            if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n\n            let fence = if options.code_block_style == CodeBlockStyle::Backticks {\n                \"```\"\n            } else {\n                \"~~~\"\n            };\n\n            output.push_str(fence);\n            if let Some(lang) = language {\n                output.push_str(lang);\n            } else if !options.code_language.is_empty() {\n                output.push_str(&options.code_language);\n            }\n            output.push('\\n');\n            output.push_str(content);\n            output.push('\\n');\n            output.push_str(fence);\n            output.push('\\n');\n        }\n    }\n}\n\n/// Dedent code block by finding the minimum indentation and removing it.\nfn dedent_code_block(content: &str) -> String {\n    let lines: Vec<&str> = content.lines().collect();\n    if lines.is_empty() {\n        return String::new();\n    }\n\n    // Find minimum indentation of non-empty lines\n    let min_indent = lines\n        .iter()\n        .filter(|line| !line.trim().is_empty())\n        .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())\n        .min()\n        .unwrap_or(0);\n\n    let dedented: Vec<String> = lines\n        .iter()\n        .map(|line| {\n            if line.trim().is_empty() {\n                String::new()\n            } else {\n                let mut remaining = min_indent;\n                let mut cut = 0;\n                for (idx, ch) in line.char_indices() {\n                    if remaining == 0 {\n                        break;\n                    }\n                    if ch.is_whitespace() {\n                        remaining -= 1;\n                        cut = idx + ch.len_utf8();\n                    } else {\n                        break;\n                    }\n                }\n                line[cut..].to_string()\n            }\n        })\n        .collect();\n\n    dedented.join(\"\\n\")\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/builder.rs",
    "content": "//! Core table building and structure calculation.\n//!\n//! Handles main table element conversion, column calculation, and visitor\n//! pattern integration for custom table handling.\n\nuse std::borrow::Cow;\n\nuse super::cell::{collect_table_cells, get_colspan};\nuse super::cells::{append_layout_row, convert_table_row};\nuse super::scanner::scan_table;\nuse super::utils::{is_tag_name, normalized_tag_name};\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\n\n/// Maximum allowed table columns to prevent unbounded memory usage.\nconst MAX_TABLE_COLS: usize = 1000;\n\n/// Calculate total columns in a table.\n///\n/// Scans all rows and cells to determine the maximum column count,\n/// accounting for colspan values.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the table element\n/// * `parser` - HTML parser instance\n/// * `dom_ctx` - DOM context for tag name resolution\n///\n/// # Returns\n/// Maximum column count (minimum 1, maximum MAX_TABLE_COLS)\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn table_total_columns(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &super::super::super::DomContext,\n) -> usize {\n    let mut max_cols = 0usize;\n    let mut cells = Vec::new();\n\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                let tag_name = dom_ctx\n                    .tag_name_for(*child_handle, parser)\n                    .unwrap_or_else(|| normalized_tag_name(child_tag.name().as_utf8_str()));\n                match tag_name.as_ref() {\n                    \"thead\" | \"tbody\" | \"tfoot\" => {\n                        for row_handle in child_tag.children().top().iter() {\n                            if is_tag_name(row_handle, parser, dom_ctx, \"tr\") {\n                                collect_table_cells(row_handle, parser, dom_ctx, &mut cells);\n                                let col_count = cells\n                                    .iter()\n                                    .fold(0usize, |acc, h| acc.saturating_add(get_colspan(h, parser)));\n                                max_cols = max_cols.max(col_count);\n                            }\n                        }\n                    }\n                    \"tr\" | \"row\" => {\n                        collect_table_cells(child_handle, parser, dom_ctx, &mut cells);\n                        let col_count = cells\n                            .iter()\n                            .fold(0usize, |acc, h| acc.saturating_add(get_colspan(h, parser)));\n                        max_cols = max_cols.max(col_count);\n                    }\n                    _ => {}\n                }\n            }\n        }\n    }\n\n    max_cols.clamp(1, MAX_TABLE_COLS)\n}\n\n/// Convert an entire table element to Markdown.\n///\n/// Main entry point for table conversion. Analyzes table structure to determine\n/// if it should be rendered as a Markdown table or converted to list format.\n/// Handles layout tables, blank tables, and tables with semantic meaning.\n/// Integrates with visitor pattern for custom table handling.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the table element\n/// * `parser` - HTML parser instance\n/// * `output` - Mutable string to append table content\n/// * `options` - Conversion options\n/// * `ctx` - Conversion context (visitor, etc)\n/// * `dom_ctx` - DOM context\n/// * `depth` - Nesting depth\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn handle_table(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::super::Context,\n    dom_ctx: &super::super::super::DomContext,\n    depth: usize,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        #[cfg(feature = \"visitor\")]\n        let table_output_start = output.len();\n\n        #[cfg(feature = \"visitor\")]\n        let mut table_start_custom: Option<String> = None;\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n            use std::collections::BTreeMap;\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Table,\n                tag_name: \"table\".to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: false,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_table_start(&node_ctx)\n            };\n            match visit_result {\n                VisitResult::Continue => {}\n                VisitResult::Skip => return,\n                VisitResult::Custom(custom) => {\n                    table_start_custom = Some(custom);\n                }\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    return;\n                }\n                VisitResult::PreserveHtml => {\n                    output.push_str(&super::super::super::serialize_node(node_handle, parser));\n                    return;\n                }\n            }\n        }\n\n        let table_scan = scan_table(node_handle, parser, dom_ctx);\n        let row_count = table_scan.row_counts.len();\n        let mut distinct_counts: Vec<_> = table_scan.row_counts.iter().copied().filter(|c| *c > 0).collect();\n        distinct_counts.sort_unstable();\n        distinct_counts.dedup();\n\n        let has_border_zero = tag\n            .attributes()\n            .get(\"border\")\n            .is_some_and(|v| v.as_ref().is_some_and(|b| b.as_utf8_str() == \"0\"));\n        let looks_like_layout =\n            table_scan.nested_table_count > 1 || distinct_counts.len() > 1 || (table_scan.has_span && has_border_zero);\n        let link_count = table_scan.link_count;\n        let is_blank_table = !table_scan.has_text;\n\n        if !table_scan.has_header\n            && !table_scan.has_caption\n            && (looks_like_layout || is_blank_table || (row_count <= 2 && link_count >= 3))\n        {\n            // Skip truly blank tables (no text, no links, no images)\n            if is_blank_table && link_count == 0 {\n                return;\n            }\n\n            let table_children = tag.children();\n            for child_handle in table_children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n                    match tag_name.as_ref() {\n                        \"thead\" | \"tbody\" | \"tfoot\" => {\n                            for row_handle in child_tag.children().top().iter() {\n                                if let Some(tl::Node::Tag(row_tag)) = row_handle.get(parser) {\n                                    let row_tag_name = normalized_tag_name(row_tag.name().as_utf8_str());\n                                    if matches!(row_tag_name.as_ref(), \"tr\" | \"row\") {\n                                        append_layout_row(row_handle, parser, output, options, ctx, dom_ctx);\n                                    }\n                                }\n                            }\n                        }\n                        \"tr\" | \"row\" => append_layout_row(child_handle, parser, output, options, ctx, dom_ctx),\n                        \"colgroup\" | \"col\" => {}\n                        _ => {\n                            // Handle non-table-structure elements (like <a>, <img>, etc.) that may be\n                            // direct children of layout tables (e.g., Blogger table wrappers)\n                            super::super::super::walk_node(\n                                child_handle,\n                                parser,\n                                output,\n                                options,\n                                ctx,\n                                depth + 1,\n                                dom_ctx,\n                            );\n                        }\n                    }\n                }\n            }\n            if !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n            return;\n        }\n\n        let mut row_index = 0;\n        let total_cols = table_total_columns(node_handle, parser, dom_ctx);\n        let mut first_row_cols: Option<usize> = None;\n        let mut rowspan_tracker = vec![None; total_cols];\n        let mut row_cells = Vec::new();\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    let tag_name: Cow<'_, str> = dom_ctx.tag_info(child_handle.get_inner(), parser).map_or_else(\n                        || normalized_tag_name(child_tag.name().as_utf8_str()).into_owned().into(),\n                        |info| Cow::Borrowed(info.name.as_str()),\n                    );\n\n                    match tag_name.as_ref() {\n                        \"caption\" => {\n                            let mut text = String::new();\n                            let grandchildren = child_tag.children();\n                            {\n                                for grandchild_handle in grandchildren.top().iter() {\n                                    super::super::super::walk_node(\n                                        grandchild_handle,\n                                        parser,\n                                        &mut text,\n                                        options,\n                                        ctx,\n                                        0,\n                                        dom_ctx,\n                                    );\n                                }\n                            }\n                            let text = text.trim();\n                            if !text.is_empty() {\n                                let escaped_text = text.replace('-', r\"\\-\");\n                                output.push('*');\n                                output.push_str(&escaped_text);\n                                output.push_str(\"*\\n\\n\");\n                            }\n                        }\n\n                        \"thead\" | \"tbody\" | \"tfoot\" => {\n                            let is_header_section = tag_name.as_ref() == \"thead\";\n                            let section_children = child_tag.children();\n                            {\n                                for row_handle in section_children.top().iter() {\n                                    if let Some(tl::Node::Tag(row_tag)) = row_handle.get(parser) {\n                                        let row_tag_name = dom_ctx\n                                            .tag_name_for(*row_handle, parser)\n                                            .unwrap_or_else(|| normalized_tag_name(row_tag.name().as_utf8_str()));\n                                        if matches!(row_tag_name.as_ref(), \"tr\" | \"row\") {\n                                            if first_row_cols.is_none() {\n                                                collect_table_cells(row_handle, parser, dom_ctx, &mut row_cells);\n                                                let cols = row_cells\n                                                    .iter()\n                                                    .fold(0usize, |acc, h| acc.saturating_add(get_colspan(h, parser)));\n                                                first_row_cols = Some(cols.clamp(1, MAX_TABLE_COLS));\n                                            }\n                                            convert_table_row(\n                                                row_handle,\n                                                parser,\n                                                output,\n                                                options,\n                                                ctx,\n                                                row_index,\n                                                table_scan.has_span,\n                                                &mut rowspan_tracker,\n                                                total_cols,\n                                                first_row_cols.unwrap_or(total_cols),\n                                                dom_ctx,\n                                                depth + 1,\n                                                is_header_section,\n                                            );\n                                            row_index += 1;\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        \"tr\" | \"row\" => {\n                            if first_row_cols.is_none() {\n                                collect_table_cells(child_handle, parser, dom_ctx, &mut row_cells);\n                                let cols = row_cells\n                                    .iter()\n                                    .fold(0usize, |acc, h| acc.saturating_add(get_colspan(h, parser)));\n                                first_row_cols = Some(cols.clamp(1, MAX_TABLE_COLS));\n                            }\n                            convert_table_row(\n                                child_handle,\n                                parser,\n                                output,\n                                options,\n                                ctx,\n                                row_index,\n                                table_scan.has_span,\n                                &mut rowspan_tracker,\n                                total_cols,\n                                first_row_cols.unwrap_or(total_cols),\n                                dom_ctx,\n                                depth + 1,\n                                row_index == 0,\n                            );\n                            row_index += 1;\n                        }\n\n                        \"colgroup\" | \"col\" => {}\n\n                        _ => {\n                            // Handle non-table-structure elements (like <a>, <img>, etc.) that may be\n                            // direct children of tables without proper structure (e.g., Blogger table wrappers)\n                            super::super::super::walk_node(\n                                child_handle,\n                                parser,\n                                output,\n                                options,\n                                ctx,\n                                depth + 1,\n                                dom_ctx,\n                            );\n                        }\n                    }\n                }\n            }\n        }\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n            use std::collections::BTreeMap;\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Table,\n                tag_name: \"table\".to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: false,\n            };\n\n            let table_content = &output[table_output_start..];\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_table_end(&node_ctx, table_content)\n            };\n            match visit_result {\n                VisitResult::Continue => {\n                    if let Some(custom_start) = table_start_custom {\n                        output.insert_str(table_output_start, &custom_start);\n                    }\n                }\n                VisitResult::Custom(custom) => {\n                    let rows_output = output[table_output_start..].to_string();\n                    output.truncate(table_output_start);\n                    if let Some(custom_start) = table_start_custom {\n                        output.push_str(&custom_start);\n                    }\n                    output.push_str(&rows_output);\n                    output.push_str(&custom);\n                }\n                VisitResult::Skip => {\n                    output.truncate(table_output_start);\n                }\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                }\n                VisitResult::PreserveHtml => {\n                    output.truncate(table_output_start);\n                    output.push_str(&super::super::super::serialize_node(node_handle, parser));\n                }\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn single_nested_table_stays_as_table() {\n        let html = r\"<table><tr><td>Label</td><td><table><tr><td>A</td><td>B</td></tr></table></td></tr></table>\";\n        let result = crate::convert(html, None).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(content.contains('|'), \"should produce pipe table, not list\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/caption.rs",
    "content": "//! Caption element handler for table captions.\n//!\n//! Handles HTML `<caption>` elements within tables, converting them to\n//! Markdown with escaped hyphens to prevent interpretation as table separators.\n\n/// Handles caption elements within tables.\n///\n/// Extracts text content from the caption and formats it as italicized text\n/// with escaped hyphens to prevent Markdown table separator interpretation.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the caption element\n/// * `parser` - HTML parser instance\n/// * `output` - Output string to append caption text to\n/// * `options` - Conversion options\n/// * `ctx` - Conversion context\n/// * `depth` - Current recursion depth\n/// * `dom_ctx` - DOM context for tag name resolution\npub fn handle_caption(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::super::Context,\n    depth: usize,\n    dom_ctx: &super::super::super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut text = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::super::super::walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n        let text = text.trim();\n        if !text.is_empty() {\n            let escaped_text = text.replace('-', r\"\\-\");\n            output.push('*');\n            output.push_str(&escaped_text);\n            output.push_str(\"*\\n\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/cell.rs",
    "content": "//! Table cell conversion utilities.\n//!\n//! Handles conversion of table cell (td/th) elements to Markdown format,\n//! including colspan support and content normalization.\n\nuse std::borrow::Cow;\n\n/// Maximum allowed table columns to prevent unbounded memory usage.\nconst MAX_TABLE_COLS: usize = 1000;\n\n/// Get colspan attribute value from an element.\n///\n/// Reads the colspan attribute from a table cell, with bounds checking\n/// to prevent memory exhaustion attacks.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the cell element\n/// * `parser` - HTML parser instance\n///\n/// # Returns\n/// The colspan value (minimum 1, maximum MAX_TABLE_COLS)\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn get_colspan(node_handle: &tl::NodeHandle, parser: &tl::Parser) -> usize {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        if let Some(Some(bytes)) = tag.attributes().get(\"colspan\") {\n            if let Ok(colspan) = bytes.as_utf8_str().parse::<usize>() {\n                return clamp_table_span(colspan);\n            }\n        }\n    }\n    1\n}\n\n/// Get both colspan and rowspan in a single lookup.\n///\n/// More efficient than calling get_colspan and a separate rowspan lookup.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the cell element\n/// * `parser` - HTML parser instance\n///\n/// # Returns\n/// A tuple of (colspan, rowspan), both minimum 1 and maximum MAX_TABLE_COLS\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn get_colspan_rowspan(node_handle: &tl::NodeHandle, parser: &tl::Parser) -> (usize, usize) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let attrs = tag.attributes();\n        let colspan = attrs\n            .get(\"colspan\")\n            .flatten()\n            .and_then(|v| v.as_utf8_str().parse::<usize>().ok())\n            .map_or(1, clamp_table_span);\n        let rowspan = attrs\n            .get(\"rowspan\")\n            .flatten()\n            .and_then(|v| v.as_utf8_str().parse::<usize>().ok())\n            .map_or(1, clamp_table_span);\n        (colspan, rowspan)\n    } else {\n        (1, 1)\n    }\n}\n\n/// Clamp a table span value to safe bounds.\n///\n/// Prevents memory exhaustion by clamping colspan/rowspan values.\nfn clamp_table_span(value: usize) -> usize {\n    if value == 0 { 1 } else { value.min(MAX_TABLE_COLS) }\n}\n\n/// Collect table cells (td/th) from a row element.\n///\n/// Extracts only the direct cell children of a row, filtering by tag name.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the row element\n/// * `parser` - HTML parser instance\n/// * `dom_ctx` - DOM context for tag name resolution\n/// * `cells` - Mutable vector to populate with cell handles\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn collect_table_cells(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &super::super::super::DomContext,\n    cells: &mut Vec<tl::NodeHandle>,\n) {\n    cells.clear();\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            if let Some(cell_name) = dom_ctx.tag_name_for(*child_handle, parser) {\n                if matches!(cell_name.as_ref(), \"th\" | \"td\" | \"cell\") {\n                    cells.push(*child_handle);\n                }\n            }\n        }\n    }\n}\n\n/// Convert a table cell (td or th) to Markdown format.\n///\n/// Processes cell content and renders it with pipe delimiters for Markdown tables.\n/// Handles colspan by adding extra pipes, and escapes pipes in cell content.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the cell element\n/// * `parser` - HTML parser instance\n/// * `output` - Mutable string to append cell content\n/// * `options` - Conversion options (escape settings, br_in_tables)\n/// * `ctx` - Conversion context (visitor, etc)\n/// * `_tag_name` - Tag name (for consistency, not used)\n/// * `dom_ctx` - DOM context for content extraction\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn convert_table_cell(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::super::Context,\n    _tag_name: &str,\n    dom_ctx: &super::super::super::DomContext,\n) {\n    let mut text = String::with_capacity(128);\n\n    let cell_ctx = super::super::super::Context {\n        in_table_cell: true,\n        ..ctx.clone()\n    };\n\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        let has_tag_child = children\n            .top()\n            .iter()\n            .any(|child_handle| matches!(child_handle.get(parser), Some(tl::Node::Tag(_))));\n\n        if has_tag_child {\n            for child_handle in children.top().iter() {\n                super::super::super::walk_node(child_handle, parser, &mut text, options, &cell_ctx, 0, dom_ctx);\n            }\n        } else {\n            let raw = dom_ctx.text_content(*node_handle, parser);\n            let normalized = if options.whitespace_mode == crate::options::WhitespaceMode::Normalized {\n                crate::text::normalize_whitespace_cow(raw.as_str())\n            } else {\n                Cow::Borrowed(raw.as_str())\n            };\n            let escaped = crate::text::escape(\n                normalized.as_ref(),\n                options.escape_misc,\n                options.escape_asterisks,\n                options.escape_underscores,\n                options.escape_ascii,\n            );\n            if options.escape_misc {\n                text = escaped.into_owned();\n            } else {\n                text = escaped.replace('|', r\"\\|\");\n            }\n        }\n    }\n\n    let text = text.trim();\n    let text = if options.br_in_tables {\n        // When br_in_tables is enabled, markdown line breaks from <br> HTML tags\n        // are already properly formatted, just pass them through unchanged\n        text.to_string()\n    } else if text.contains('\\n') {\n        text.replace('\\n', \" \")\n    } else {\n        text.to_string()\n    };\n\n    let colspan = get_colspan(node_handle, parser);\n\n    output.push(' ');\n    output.push_str(&text);\n    for _ in 0..colspan {\n        output.push_str(\" |\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn rich_formatting_preserved_in_cells() {\n        let html = \"<table><tr><th>H</th></tr><tr><td><strong>Bold</strong> and <em>italic</em></td></tr></table>\";\n        let result = crate::convert(html, None).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"**Bold**\") || content.contains(\"__Bold__\"),\n            \"bold should be preserved: {}\",\n            content\n        );\n        assert!(\n            content.contains(\"*italic*\") || content.contains(\"_italic_\"),\n            \"italic should be preserved: {}\",\n            content\n        );\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/cells.rs",
    "content": "//! Cell and row handling for Markdown conversion.\n//!\n//! Provides functionality for processing table cells and rows, including:\n//! - Row conversion to Markdown format\n//! - Cell layout handling with colspan/rowspan support\n//! - Layout table row conversion to list items\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::content::normalized_tag_name;\nuse std::borrow::Cow;\n\nuse super::cell::{collect_table_cells, convert_table_cell, get_colspan_rowspan};\n\n/// Maximum allowed table columns to prevent unbounded memory usage.\nconst MAX_TABLE_COLS: usize = 1000;\n\n/// Append a layout table row as a list item.\n///\n/// For tables used for visual layout, converts rows to list items\n/// instead of table format for better readability.\n///\n/// # Arguments\n/// * `row_handle` - Handle to the row element\n/// * `parser` - HTML parser instance\n/// * `output` - Mutable string to append content\n/// * `options` - Conversion options\n/// * `ctx` - Conversion context\n/// * `dom_ctx` - DOM context\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn append_layout_row(\n    row_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::super::Context,\n    dom_ctx: &super::super::super::DomContext,\n) {\n    if let Some(tl::Node::Tag(row_tag)) = row_handle.get(parser) {\n        let mut row_text = String::new();\n        let row_children = row_tag.children();\n        for cell_handle in row_children.top().iter() {\n            if let Some(tl::Node::Tag(cell_tag)) = cell_handle.get(parser) {\n                let cell_name: Cow<'_, str> = dom_ctx.tag_info(cell_handle.get_inner(), parser).map_or_else(\n                    || normalized_tag_name(cell_tag.name().as_utf8_str()).into_owned().into(),\n                    |info| Cow::Borrowed(info.name.as_str()),\n                );\n                if matches!(cell_name.as_ref(), \"td\" | \"th\" | \"cell\") {\n                    let mut cell_text = String::new();\n                    let cell_ctx = super::super::super::Context {\n                        convert_as_inline: true,\n                        ..ctx.clone()\n                    };\n                    let cell_children = cell_tag.children();\n                    for cell_child in cell_children.top().iter() {\n                        super::super::super::walk_node(\n                            cell_child,\n                            parser,\n                            &mut cell_text,\n                            options,\n                            &cell_ctx,\n                            0,\n                            dom_ctx,\n                        );\n                    }\n                    let cell_content = crate::text::normalize_whitespace_cow(&cell_text);\n                    if !cell_content.trim().is_empty() {\n                        if !row_text.is_empty() {\n                            row_text.push(' ');\n                        }\n                        row_text.push_str(cell_content.trim());\n                    }\n                }\n            }\n        }\n\n        let trimmed = row_text.trim();\n        if !trimmed.is_empty() {\n            if !output.is_empty() && !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n            let formatted = trimmed.strip_prefix(\"- \").unwrap_or(trimmed).trim_start();\n            output.push_str(\"- \");\n            output.push_str(formatted);\n            output.push('\\n');\n        }\n    }\n}\n\n/// Convert a table row (tr) to Markdown format.\n///\n/// Processes all cells in a row, handling colspan and rowspan for proper\n/// column alignment. Renders header separator row after the first row.\n/// Integrates with visitor pattern for custom row handling.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the row element\n/// * `parser` - HTML parser instance\n/// * `output` - Mutable string to append row content\n/// * `options` - Conversion options\n/// * `ctx` - Conversion context (visitor, etc)\n/// * `row_index` - Index of this row in the table\n/// * `has_span` - Whether table has colspan/rowspan\n/// * `rowspan_tracker` - Mutable array tracking rowspan remainder for each column\n/// * `total_cols` - Total columns in the table\n/// * `header_cols` - Columns to render in separator row\n/// * `dom_ctx` - DOM context\n/// * `depth` - Nesting depth\n/// * `is_header` - Whether this is a header row\n#[allow(clippy::too_many_arguments)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn convert_table_row(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::super::Context,\n    row_index: usize,\n    has_span: bool,\n    rowspan_tracker: &mut [Option<usize>],\n    total_cols: usize,\n    header_cols: usize,\n    dom_ctx: &super::super::super::DomContext,\n    depth: usize,\n    is_header: bool,\n) {\n    let mut row_text = String::with_capacity(256);\n    let mut cells = Vec::new();\n\n    collect_table_cells(node_handle, parser, dom_ctx, &mut cells);\n\n    #[cfg(feature = \"visitor\")]\n    let cell_contents: Vec<String> = if ctx.visitor.is_some() {\n        cells\n            .iter()\n            .map(|cell_handle| {\n                let mut text = String::new();\n                let cell_ctx = super::super::super::Context {\n                    in_table_cell: true,\n                    ..ctx.clone()\n                };\n                if let Some(tl::Node::Tag(tag)) = cell_handle.get(parser) {\n                    for child_handle in tag.children().top().iter() {\n                        super::super::super::walk_node(child_handle, parser, &mut text, options, &cell_ctx, 0, dom_ctx);\n                    }\n                }\n                crate::text::normalize_whitespace_cow(&text).trim().to_string()\n            })\n            .collect()\n    } else {\n        Vec::new()\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n        use std::collections::BTreeMap;\n\n        if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::TableRow,\n                tag_name: \"tr\".to_string(),\n                attributes,\n                depth,\n                index_in_parent: row_index,\n                parent_tag: Some(\"table\".to_string()),\n                is_inline: false,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_table_row(&node_ctx, &cell_contents, is_header)\n            };\n            match visit_result {\n                VisitResult::Continue => {}\n                VisitResult::Skip => return,\n                VisitResult::Custom(custom) => {\n                    output.push_str(&custom);\n                    return;\n                }\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    return;\n                }\n                VisitResult::PreserveHtml => {\n                    output.push_str(&super::super::super::serialize_node(node_handle, parser));\n                    return;\n                }\n            }\n        }\n    }\n\n    if has_span {\n        let mut col_index = 0;\n        let mut cell_iter = cells.iter();\n\n        loop {\n            if col_index < total_cols {\n                if let Some(Some(remaining_rows)) = rowspan_tracker.get_mut(col_index) {\n                    if *remaining_rows > 0 {\n                        row_text.push(' ');\n                        row_text.push_str(\" |\");\n                        *remaining_rows -= 1;\n                        if *remaining_rows == 0 {\n                            rowspan_tracker[col_index] = None;\n                        }\n                        col_index += 1;\n                        continue;\n                    }\n                }\n            }\n\n            if let Some(cell_handle) = cell_iter.next() {\n                convert_table_cell(cell_handle, parser, &mut row_text, options, ctx, \"\", dom_ctx);\n\n                let (colspan, rowspan) = get_colspan_rowspan(cell_handle, parser);\n\n                if rowspan > 1 && col_index < total_cols {\n                    rowspan_tracker[col_index] = Some(rowspan - 1);\n                }\n\n                col_index = col_index.saturating_add(colspan);\n            } else {\n                break;\n            }\n        }\n    } else {\n        for cell_handle in &cells {\n            convert_table_cell(cell_handle, parser, &mut row_text, options, ctx, \"\", dom_ctx);\n        }\n    }\n\n    output.push('|');\n    output.push_str(&row_text);\n    output.push('\\n');\n\n    let is_first_row = row_index == 0;\n    if is_first_row {\n        let total_cols = header_cols.clamp(1, MAX_TABLE_COLS);\n        output.push_str(\"| \");\n        for i in 0..total_cols {\n            if i > 0 {\n                output.push_str(\" | \");\n            }\n            output.push_str(\"---\");\n        }\n        output.push_str(\" |\\n\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/layout.rs",
    "content": "//! Table layout and indentation utilities.\n//!\n//! Handles table indentation for list context and table content\n//! formatting when tables are nested within lists.\n\nuse crate::options::ListIndentType;\n\n/// Indent table lines for list context.\n///\n/// When a table appears inside a list item, this function indents the table\n/// content so it maintains proper list nesting.\n///\n/// # Arguments\n/// * `table_content` - The Markdown table content to indent\n/// * `list_depth` - The nesting depth in the list hierarchy\n/// * `options` - Conversion options (for indent type)\n///\n/// # Returns\n/// Indented table content\npub fn indent_table_for_list(\n    table_content: &str,\n    list_depth: usize,\n    options: &crate::options::ConversionOptions,\n) -> String {\n    if list_depth == 0 {\n        return table_content.to_string();\n    }\n\n    let Some(mut indent) = continuation_indent_string(list_depth, options) else {\n        return table_content.to_string();\n    };\n\n    if matches!(options.list_indent_type, ListIndentType::Spaces) {\n        let space_count = indent.chars().filter(|c| *c == ' ').count();\n        if space_count < 4 {\n            for _ in 0..(4 - space_count) {\n                indent.push(' ');\n            }\n        }\n    }\n\n    let mut result = String::with_capacity(table_content.len() + indent.len() * 4);\n    for segment in table_content.split_inclusive('\\n') {\n        if segment.starts_with('|') {\n            result.push_str(&indent);\n            result.push_str(segment);\n        } else {\n            result.push_str(segment);\n        }\n    }\n    result\n}\n\n/// Get continuation indent string for list nesting.\nfn continuation_indent_string(list_depth: usize, options: &crate::options::ConversionOptions) -> Option<String> {\n    use crate::converter::list::utils::continuation_indent_string;\n    continuation_indent_string(list_depth, options)\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/mod.rs",
    "content": "//! Table element handler for HTML to Markdown conversion.\n//!\n//! This module provides specialized handling for table elements including:\n//! - Table structure detection and scanning (TableScan)\n//! - Row and cell conversion to Markdown table format\n//! - Cell content processing with colspan/rowspan support\n//! - Layout table detection (tables used for visual layout)\n//! - Integration with the visitor pattern for custom table handling\n//!\n//! Tables are converted to Markdown pipe-delimited format with header separators.\n//! Layout tables (tables without proper semantic headers) may be converted to lists\n//! instead of tables for better readability.\n\npub mod builder;\npub mod caption;\npub mod cell;\npub mod cells;\npub mod layout;\npub mod scanner;\npub(super) mod utils;\n\n// Re-export types from parent module for submodule access\n\n// Re-export for use in converter.rs\npub use caption::handle_caption;\n\n/// Dispatches table element handling to the main convert_table function.\n///\n/// # Usage in converter.rs\n/// ```text\n/// if \"table\" == tag_name {\n///     crate::converter::block::table::handle_table(\n///         node_handle,\n///         parser,\n///         output,\n///         options,\n///         ctx,\n///         dom_ctx,\n///         depth,\n///     );\n///     return;\n/// }\n/// ```\npub fn dispatch_table_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::Context,\n    depth: usize,\n    dom_ctx: &super::super::DomContext,\n) -> bool {\n    match tag_name {\n        \"table\" => {\n            builder::handle_table(node_handle, parser, output, options, ctx, dom_ctx, depth);\n            true\n        }\n        _ => false,\n    }\n}\n\n/// Handles table output with context-aware formatting.\n///\n/// This function wraps `handle_table` with list indentation and whitespace management logic.\n/// It handles two distinct contexts:\n///\n/// 1. **List Context** (`ctx.in_list_item = true`):\n///    - Indents table content using `indent_table_for_list` for proper nesting\n///    - Preserves special caption formatting (lines starting with `*`)\n///    - Adds newlines for proper list item separation\n///\n/// 2. **Normal Context**:\n///    - Ensures proper spacing with double newlines around tables\n///    - Handles various output state scenarios (empty, ends with newline, etc.)\n///\n/// # Arguments\n/// * `node_handle` - The table node handle\n/// * `parser` - The HTML parser instance\n/// * `output` - The output string being built\n/// * `options` - Conversion options (includes list indent type)\n/// * `ctx` - Conversion context (includes list state)\n/// * `dom_ctx` - DOM context for tree structure info\n/// * `depth` - Current nesting depth\npub fn handle_table_with_context(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::Context,\n    dom_ctx: &super::super::DomContext,\n    depth: usize,\n) {\n    let mut table_output = String::new();\n    builder::handle_table(node_handle, parser, &mut table_output, options, ctx, dom_ctx, depth);\n\n    // Feed the table into the structure collector when document structure extraction is enabled.\n    // Use push_table_data so that ConversionResult.tables is populated with both the grid and\n    // the markdown rendering that was just generated above.\n    if let Some(ref sc) = ctx.structure_collector {\n        if let Some(grid) = collect_table_grid(node_handle, parser, options, ctx, dom_ctx) {\n            sc.borrow_mut().push_table_data(grid, table_output.trim().to_string());\n        }\n    }\n\n    if ctx.in_list_item {\n        let has_caption = table_output.starts_with('*');\n\n        if !has_caption {\n            use crate::converter::main_helpers::trim_trailing_whitespace;\n            trim_trailing_whitespace(output);\n            if !output.is_empty() && !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n        }\n\n        let indented = layout::indent_table_for_list(&table_output, ctx.list_depth, options);\n        output.push_str(&indented);\n    } else {\n        if !output.ends_with(\"\\n\\n\") {\n            if output.is_empty() || !output.ends_with('\\n') {\n                output.push_str(\"\\n\\n\");\n            } else {\n                output.push('\\n');\n            }\n        }\n        output.push_str(&table_output);\n    }\n\n    if !output.ends_with('\\n') {\n        output.push('\\n');\n    }\n}\n\n/// Collect a [`crate::types::TableGrid`] from the DOM for the structure collector.\n///\n/// Walks the table's rows and cells, extracting text content and span attributes\n/// to build a structured grid representation.\nfn collect_table_grid(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::Context,\n    dom_ctx: &super::super::DomContext,\n) -> Option<crate::types::TableGrid> {\n    use utils::{is_tag_name, normalized_tag_name};\n\n    let tl::Node::Tag(tag) = node_handle.get(parser)? else {\n        return None;\n    };\n\n    let mut grid_cells = Vec::new();\n    let mut row_index: u32 = 0;\n    let mut max_cols: u32 = 0;\n    let mut cell_handles = Vec::new();\n\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n            match tag_name.as_ref() {\n                \"thead\" | \"tbody\" | \"tfoot\" => {\n                    let is_header_section = tag_name.as_ref() == \"thead\";\n                    for row_handle in child_tag.children().top().iter() {\n                        if is_tag_name(row_handle, parser, dom_ctx, \"tr\") {\n                            collect_grid_row(\n                                row_handle,\n                                parser,\n                                options,\n                                ctx,\n                                dom_ctx,\n                                &mut cell_handles,\n                                &mut grid_cells,\n                                &mut row_index,\n                                &mut max_cols,\n                                is_header_section,\n                            );\n                        }\n                    }\n                }\n                \"tr\" | \"row\" => {\n                    let is_first = row_index == 0;\n                    collect_grid_row(\n                        child_handle,\n                        parser,\n                        options,\n                        ctx,\n                        dom_ctx,\n                        &mut cell_handles,\n                        &mut grid_cells,\n                        &mut row_index,\n                        &mut max_cols,\n                        is_first,\n                    );\n                }\n                _ => {}\n            }\n        }\n    }\n\n    if row_index == 0 {\n        return None;\n    }\n\n    Some(crate::types::TableGrid {\n        rows: row_index,\n        cols: max_cols,\n        cells: grid_cells,\n    })\n}\n\n/// Process a single table row for grid collection.\n#[allow(clippy::too_many_arguments)]\nfn collect_grid_row(\n    row_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::super::Context,\n    dom_ctx: &super::super::DomContext,\n    cell_handles: &mut Vec<tl::NodeHandle>,\n    grid_cells: &mut Vec<crate::types::GridCell>,\n    row_index: &mut u32,\n    max_cols: &mut u32,\n    is_header_section: bool,\n) {\n    use cell::{collect_table_cells, get_colspan_rowspan};\n\n    collect_table_cells(row_handle, parser, dom_ctx, cell_handles);\n\n    let mut col_index: u32 = 0;\n    for cell_handle in cell_handles.iter() {\n        let is_header = is_header_section\n            || dom_ctx\n                .tag_name_for(*cell_handle, parser)\n                .is_some_and(|name| name.as_ref() == \"th\");\n\n        let mut text = String::new();\n        let cell_ctx = super::super::Context {\n            in_table_cell: true,\n            ..ctx.clone()\n        };\n        if let Some(tl::Node::Tag(cell_tag)) = cell_handle.get(parser) {\n            for child_handle in cell_tag.children().top().iter() {\n                super::super::walk_node(child_handle, parser, &mut text, options, &cell_ctx, 0, dom_ctx);\n            }\n        }\n        let content = crate::text::normalize_whitespace_cow(&text).trim().to_string();\n\n        let (colspan, rowspan) = get_colspan_rowspan(cell_handle, parser);\n\n        grid_cells.push(crate::types::GridCell {\n            content,\n            row: *row_index,\n            col: col_index,\n            row_span: rowspan as u32,\n            col_span: colspan as u32,\n            is_header,\n        });\n\n        col_index += colspan as u32;\n    }\n    if col_index > *max_cols {\n        *max_cols = col_index;\n    }\n    *row_index += 1;\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/scanner.rs",
    "content": "//! Table scanning and analysis utilities.\n//!\n//! Provides the TableScan struct and scanning functions for analyzing table structure\n//! to determine if it should be rendered as a Markdown table or converted to list format.\n\nuse crate::converter::utility::content::normalized_tag_name;\nuse std::borrow::Cow;\n\n/// Scan results for a table element.\n///\n/// Contains metadata about table structure to determine optimal rendering:\n/// - Row counts for consistency checking\n/// - Presence of headers, captions, and nested tables\n/// - Presence of colspan/rowspan (spanning cells)\n/// - Link and text content counts\n#[derive(Default)]\npub struct TableScan {\n    /// Number of cells in each row\n    pub row_counts: Vec<usize>,\n    /// Whether any cells have colspan or rowspan attributes\n    pub has_span: bool,\n    /// Whether the table has header cells (th elements or role=\"head\")\n    pub has_header: bool,\n    /// Whether the table has a caption element\n    pub has_caption: bool,\n    /// Number of nested tables found inside this table\n    pub nested_table_count: usize,\n    /// Count of anchor elements in the table\n    pub link_count: usize,\n    /// Whether the table contains text content (not empty)\n    pub has_text: bool,\n}\n\n/// Scan a table element for structural metadata.\n///\n/// Analyzes the table to determine characteristics that influence rendering:\n/// - Whether to render as a Markdown table or layout table\n/// - If spanning cells are present\n/// - If the table has semantic meaning (headers, captions)\n///\n/// # Arguments\n/// * `node_handle` - Handle to the table element\n/// * `parser` - HTML parser instance\n/// * `dom_ctx` - DOM context for tag name resolution\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn scan_table(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &super::super::super::DomContext,\n) -> TableScan {\n    let mut scan = TableScan::default();\n    scan_table_node(node_handle, parser, dom_ctx, true, &mut scan);\n    scan\n}\n\n/// Recursively scan table structure.\n///\n/// Internal recursive function that walks the table tree and collects metadata.\n///\n/// # Arguments\n/// * `node_handle` - Current node to scan\n/// * `parser` - HTML parser instance\n/// * `dom_ctx` - DOM context for tag name resolution\n/// * `is_root` - Whether this is the root table element\n/// * `scan` - Mutable scan results to accumulate\n#[allow(clippy::trivially_copy_pass_by_ref)]\nfn scan_table_node(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &super::super::super::DomContext,\n    is_root: bool,\n    scan: &mut TableScan,\n) {\n    if let Some(node) = node_handle.get(parser) {\n        match node {\n            tl::Node::Raw(bytes) => {\n                if !scan.has_text {\n                    let raw = bytes.as_utf8_str();\n                    let decoded = crate::text::decode_html_entities_cow(raw.as_ref());\n                    if !decoded.trim().is_empty() {\n                        scan.has_text = true;\n                    }\n                }\n            }\n            tl::Node::Tag(tag) => {\n                let tag_name: Cow<'_, str> = dom_ctx.tag_info(node_handle.get_inner(), parser).map_or_else(\n                    || normalized_tag_name(tag.name().as_utf8_str()).into_owned().into(),\n                    |info| Cow::Borrowed(info.name.as_str()),\n                );\n\n                match tag_name.as_ref() {\n                    \"a\" => scan.link_count += 1,\n                    \"caption\" => scan.has_caption = true,\n                    \"th\" => scan.has_header = true,\n                    \"img\" | \"graphic\" => {\n                        // Images with src or alt attributes count as content\n                        if tag.attributes().get(\"src\").is_some() || tag.attributes().get(\"alt\").is_some() {\n                            scan.has_text = true;\n                        }\n                    }\n                    \"cell\" => {\n                        // Check if cell has role=\"head\" attribute\n                        if let Some(role) = tag.attributes().get(\"role\") {\n                            if let Some(role_val) = role {\n                                let role_str = role_val.as_utf8_str();\n                                if role_str == \"head\" {\n                                    scan.has_header = true;\n                                }\n                            }\n                        }\n                    }\n                    \"table\" if !is_root => scan.nested_table_count += 1,\n                    \"tr\" | \"row\" => {\n                        let mut cell_count = 0;\n                        for child in tag.children().top().iter() {\n                            if let Some(tl::Node::Tag(cell_tag)) = child.get(parser) {\n                                let cell_name: Cow<'_, str> = dom_ctx\n                                    .tag_info(child.get_inner(), parser)\n                                    .map(|info| Cow::Borrowed(info.name.as_str()))\n                                    .unwrap_or_else(|| {\n                                        normalized_tag_name(cell_tag.name().as_utf8_str()).into_owned().into()\n                                    });\n                                if matches!(cell_name.as_ref(), \"td\" | \"th\" | \"cell\") {\n                                    cell_count += super::cell::get_colspan(child, parser);\n                                    let attrs = cell_tag.attributes();\n                                    if attrs.get(\"colspan\").is_some() || attrs.get(\"rowspan\").is_some() {\n                                        scan.has_span = true;\n                                    }\n                                }\n                            }\n                            scan_table_node(child, parser, dom_ctx, false, scan);\n                        }\n                        scan.row_counts.push(cell_count);\n                        return;\n                    }\n                    _ => {}\n                }\n\n                for child in tag.children().top().iter() {\n                    scan_table_node(child, parser, dom_ctx, false, scan);\n                }\n            }\n            _ => {}\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/table/utils.rs",
    "content": "//! Utility functions for table processing.\n//!\n//! Provides helper functions for tag name normalization and comparison.\n\npub(super) use crate::converter::main_helpers::tag_name_eq;\npub(super) use crate::converter::utility::content::normalized_tag_name;\n\n/// Check if a node has a specific tag name.\n///\n/// Handles both direct tag matching and DOM context-based tag resolution.\n///\n/// # Arguments\n/// * `node_handle` - Handle to the node\n/// * `parser` - HTML parser instance\n/// * `dom_ctx` - DOM context for tag name resolution\n/// * `name` - Expected tag name\n///\n/// # Returns\n/// True if node has the specified tag name\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub(super) fn is_tag_name(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &super::super::super::DomContext,\n    name: &str,\n) -> bool {\n    if let Some(info) = dom_ctx.tag_info(node_handle.get_inner(), parser) {\n        return info.name == name;\n    }\n    matches!(\n        node_handle.get(parser),\n        Some(tl::Node::Tag(tag)) if tag_name_eq(tag.name().as_utf8_str(), name)\n    )\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/block/unknown.rs",
    "content": "//! Handler for unknown/unspecified HTML elements.\n//!\n//! Processes HTML elements that don't have specific handlers by passing through\n//! their children while managing whitespace and content preservation:\n//! - Processes all child nodes recursively\n//! - Validates UTF-8 character boundaries when checking content\n//! - Preserves code blocks (indented or fenced) while removing empty content\n//! - Manages trailing whitespace intelligently\n\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handles unknown/unspecified HTML elements.\n///\n/// For elements without specific handlers, this function:\n/// 1. Records the output position before processing children\n/// 2. Processes all children recursively\n/// 3. Checks if any content was added\n/// 4. Validates UTF-8 character boundaries for safe string slicing\n/// 5. Detects if added content is a code block (indented or fenced)\n/// 6. Removes empty content while preserving code blocks and spacing\n///\n/// # UTF-8 Safety\n/// The function carefully handles UTF-8 boundaries to prevent panic when\n/// slicing the output string. It finds the nearest valid boundary if the\n/// position falls in the middle of a multi-byte character.\n///\n/// # Code Block Detection\n/// Code blocks (identified by markdown formatting) are always preserved,\n/// even if they appear \"empty\" according to trim().\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let len_before = output.len();\n    let had_trailing_space = output.ends_with(' ');\n\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n\n    let len_after = output.len();\n    if len_after > len_before {\n        let start_idx = if output.is_char_boundary(len_before) {\n            len_before\n        } else {\n            // Find the nearest valid UTF-8 character boundary\n            let capped = len_before.min(output.len());\n            output\n                .char_indices()\n                .map(|(idx, _)| idx)\n                .take_while(|idx| *idx <= capped)\n                .last()\n                .unwrap_or(capped)\n        };\n\n        let added_content = output[start_idx..].to_string();\n\n        // Detect code blocks by markdown formatting\n        let is_code_block =\n            added_content.starts_with(\"    \") || added_content.starts_with(\"```\") || added_content.starts_with(\"~~~\");\n\n        // Remove empty content while preserving code blocks\n        if added_content.trim().is_empty() && !is_code_block {\n            output.truncate(start_idx);\n            if !had_trailing_space && added_content.contains(' ') {\n                output.push(' ');\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/context.rs",
    "content": "//! Conversion context for HTML to Markdown conversion.\n//!\n//! The `Context` struct maintains state during the recursive tree walk that transforms\n//! HTML to Markdown. It tracks nesting levels, element types, and feature-specific collectors\n//! that are passed through the conversion pipeline.\n\n#[cfg(any(feature = \"inline-images\", feature = \"visitor\"))]\nuse std::cell::RefCell;\n#[cfg(feature = \"metadata\")]\nuse std::collections::BTreeMap;\nuse std::collections::HashSet;\nuse std::rc::Rc;\n\n#[cfg(feature = \"inline-images\")]\nuse crate::inline_images::InlineImageCollector;\n\nuse crate::converter::reference_collector::ReferenceCollectorHandle;\nuse crate::types::structure_collector::StructureCollectorHandle;\n\n/// Handle type for inline image collector when feature is enabled.\n#[cfg(feature = \"inline-images\")]\npub type InlineCollectorHandle = Rc<RefCell<InlineImageCollector>>;\n/// Placeholder type when inline-images feature is disabled.\n#[cfg(not(feature = \"inline-images\"))]\npub type InlineCollectorHandle = ();\n\n/// Payload type for image metadata extraction.\n#[cfg(feature = \"metadata\")]\npub type ImageMetadataPayload = (BTreeMap<String, String>, Option<u32>, Option<u32>);\n\n/// Conversion context that tracks state during HTML to Markdown conversion.\n///\n/// This context is passed through the recursive tree walker and maintains information\n/// about the current position in the document tree, nesting levels, and enabled features.\n#[derive(Clone)]\npub struct Context {\n    /// Are we inside a code-like element (pre, code, kbd, samp)?\n    pub(crate) in_code: bool,\n    /// Current list item counter for ordered lists\n    pub(crate) list_counter: usize,\n    /// Are we in an ordered list (vs unordered)?\n    pub(crate) in_ordered_list: bool,\n    /// Blockquote nesting depth\n    pub(crate) blockquote_depth: usize,\n    /// Are we inside a table cell (td/th)?\n    pub(crate) in_table_cell: bool,\n    /// Should we convert block elements as inline?\n    pub(crate) convert_as_inline: bool,\n    /// Depth of inline formatting elements (strong/emphasis/span/etc).\n    pub(crate) inline_depth: usize,\n    /// Are we inside a list item?\n    pub(crate) in_list_item: bool,\n    /// List nesting depth (for indentation)\n    pub(crate) list_depth: usize,\n    /// Unordered list nesting depth (for bullet cycling)\n    pub(crate) ul_depth: usize,\n    /// Are we inside any list (ul or ol)?\n    pub(crate) in_list: bool,\n    /// Is this a \"loose\" list where all items should have blank lines?\n    pub(crate) loose_list: bool,\n    /// Did a previous list item have block children?\n    pub(crate) prev_item_had_blocks: bool,\n    /// Are we inside a heading element (h1-h6)?\n    pub(crate) in_heading: bool,\n    /// Whether inline images should remain markdown inside the current heading.\n    pub(crate) heading_allow_inline_images: bool,\n    /// Are we inside a paragraph element?\n    pub(crate) in_paragraph: bool,\n    /// Output buffer position where the current block's content starts.\n    /// Used to distinguish paragraph-break newlines from a previous block\n    /// vs. newlines generated within the current block.\n    pub(crate) block_content_start: usize,\n    /// Are we inside a ruby element?\n    pub(crate) in_ruby: bool,\n    /// Are we inside a `<strong>` / `<b>` element?\n    pub(crate) in_strong: bool,\n    /// Are we inside a link element (collecting link label text)?\n    pub(crate) in_link: bool,\n    /// Tag names that should be stripped during conversion.\n    pub(crate) strip_tags: Rc<HashSet<String>>,\n    /// Tag names that should be preserved as raw HTML.\n    pub(crate) preserve_tags: Rc<HashSet<String>>,\n    /// Tag names that allow inline images inside headings.\n    pub(crate) keep_inline_images_in: Rc<HashSet<String>>,\n    /// Node IDs matching `exclude_selectors` — these nodes and all descendants are dropped.\n    pub(crate) excluded_node_ids: Rc<HashSet<u32>>,\n    #[cfg(feature = \"inline-images\")]\n    /// Shared collector for inline images when enabled.\n    pub(crate) inline_collector: Option<InlineCollectorHandle>,\n    #[cfg(feature = \"metadata\")]\n    /// Shared collector for metadata when enabled.\n    pub(crate) metadata_collector: Option<crate::metadata::MetadataCollectorHandle>,\n    #[cfg(feature = \"metadata\")]\n    pub(crate) metadata_wants_document: bool,\n    #[cfg(feature = \"metadata\")]\n    pub(crate) metadata_wants_headers: bool,\n    #[cfg(feature = \"metadata\")]\n    pub(crate) metadata_wants_links: bool,\n    #[cfg(feature = \"metadata\")]\n    pub(crate) metadata_wants_images: bool,\n    #[cfg(feature = \"metadata\")]\n    pub(crate) metadata_wants_structured_data: bool,\n    #[cfg(feature = \"visitor\")]\n    /// Optional visitor for custom HTML traversal callbacks.\n    pub(crate) visitor: Option<crate::visitor::VisitorHandle>,\n    #[cfg(feature = \"visitor\")]\n    /// Stores the first visitor error encountered during traversal.\n    pub(crate) visitor_error: Rc<RefCell<Option<String>>>,\n    /// Optional structure collector for building a [`crate::types::DocumentStructure`].\n    ///\n    /// Populated when `options.include_document_structure == true`.\n    pub(crate) structure_collector: Option<StructureCollectorHandle>,\n    /// Optional reference collector for reference-style links.\n    pub(crate) reference_collector: Option<ReferenceCollectorHandle>,\n}\n\nimpl Context {\n    /// Set the pre-computed set of node IDs that match `exclude_selectors`.\n    ///\n    /// Called in `convert_html_impl` after DOM parsing, before the walk starts.\n    pub(crate) fn set_excluded_node_ids(&mut self, ids: HashSet<u32>) {\n        self.excluded_node_ids = Rc::new(ids);\n    }\n\n    /// Create a new conversion context from options and optional collectors.\n    #[allow(clippy::too_many_arguments)]\n    #[cfg_attr(\n        any(not(feature = \"inline-images\"), not(feature = \"metadata\"), not(feature = \"visitor\")),\n        allow(unused_variables)\n    )]\n    pub fn new(\n        options: &crate::options::ConversionOptions,\n        inline_collector: Option<InlineCollectorHandle>,\n        #[cfg(feature = \"metadata\")] metadata_collector: Option<crate::metadata::MetadataCollectorHandle>,\n        #[cfg(not(feature = \"metadata\"))] _metadata_collector: Option<()>,\n        #[cfg(feature = \"visitor\")] visitor: Option<crate::visitor::VisitorHandle>,\n        #[cfg(not(feature = \"visitor\"))] _visitor: Option<()>,\n        structure_collector: Option<StructureCollectorHandle>,\n        reference_collector: Option<ReferenceCollectorHandle>,\n    ) -> Self {\n        #[cfg(feature = \"metadata\")]\n        let (\n            metadata_wants_document,\n            metadata_wants_headers,\n            metadata_wants_links,\n            metadata_wants_images,\n            metadata_wants_structured_data,\n        ) = if let Some(ref collector) = metadata_collector {\n            let guard = collector.borrow();\n            (\n                guard.wants_document(),\n                guard.wants_headers(),\n                guard.wants_links(),\n                guard.wants_images(),\n                guard.wants_structured_data(),\n            )\n        } else {\n            (false, false, false, false, false)\n        };\n\n        Self {\n            in_code: false,\n            list_counter: 0,\n            in_ordered_list: false,\n            blockquote_depth: 0,\n            in_table_cell: false,\n            convert_as_inline: options.convert_as_inline,\n            inline_depth: 0,\n            in_list_item: false,\n            list_depth: 0,\n            ul_depth: 0,\n            in_list: false,\n            loose_list: false,\n            prev_item_had_blocks: false,\n            in_heading: false,\n            heading_allow_inline_images: false,\n            in_paragraph: false,\n            block_content_start: 0,\n            in_ruby: false,\n            in_strong: false,\n            in_link: false,\n            strip_tags: Rc::new(options.strip_tags.iter().cloned().collect()),\n            preserve_tags: Rc::new(options.preserve_tags.iter().cloned().collect()),\n            keep_inline_images_in: Rc::new(options.keep_inline_images_in.iter().cloned().collect()),\n            excluded_node_ids: Rc::new(HashSet::new()),\n            #[cfg(feature = \"inline-images\")]\n            inline_collector,\n            #[cfg(feature = \"metadata\")]\n            metadata_collector,\n            #[cfg(feature = \"metadata\")]\n            metadata_wants_document,\n            #[cfg(feature = \"metadata\")]\n            metadata_wants_headers,\n            #[cfg(feature = \"metadata\")]\n            metadata_wants_links,\n            #[cfg(feature = \"metadata\")]\n            metadata_wants_images,\n            #[cfg(feature = \"metadata\")]\n            metadata_wants_structured_data,\n            #[cfg(feature = \"visitor\")]\n            visitor: visitor.clone(),\n            #[cfg(feature = \"visitor\")]\n            visitor_error: Rc::new(RefCell::new(None)),\n            structure_collector,\n            reference_collector,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/dom_context.rs",
    "content": "//! DOM context providing efficient access to parent/child relationships and text content.\n//!\n//! This module defines the `DomContext` structure which is built once during conversion\n//! and provides O(1) access to node relationships via precomputed maps. It also includes\n//! an LRU cache for text content extraction to avoid redundant string allocations.\n\nuse lru::LruCache;\nuse std::cell::{OnceCell, RefCell};\n\nuse crate::converter::main_helpers::is_inline_element;\nuse crate::converter::utility::content::{is_block_level_name, normalized_tag_name};\nuse crate::text;\n\n/// Cached information about an HTML tag element.\n///\n/// This struct stores pre-computed information about tag elements to avoid\n/// repeated parsing during tree traversal.\npub struct TagInfo {\n    /// The normalized (lowercase) tag name.\n    pub(crate) name: String,\n    /// Whether this element behaves like an inline element (including script/style).\n    pub(crate) is_inline_like: bool,\n    /// Whether this element is a block-level element.\n    pub(crate) is_block: bool,\n}\n\n/// DOM context that provides efficient access to parent/child relationships and text content.\n///\n/// This context is built once during conversion and provides O(1) access to node relationships\n/// via precomputed maps. It also includes an LRU cache for text content extraction.\npub struct DomContext {\n    pub(crate) parent_map: Vec<Option<u32>>,\n    pub(crate) children_map: Vec<Option<Vec<tl::NodeHandle>>>,\n    pub(crate) sibling_index_map: Vec<Option<usize>>,\n    pub(crate) root_children: Vec<tl::NodeHandle>,\n    pub(crate) node_map: Vec<Option<tl::NodeHandle>>,\n    pub(crate) tag_info_map: Vec<OnceCell<Option<TagInfo>>>,\n    pub(crate) prev_inline_like_map: Vec<OnceCell<bool>>,\n    pub(crate) next_inline_like_map: Vec<OnceCell<bool>>,\n    pub(crate) next_tag_map: Vec<OnceCell<Option<u32>>>,\n    pub(crate) next_whitespace_map: Vec<OnceCell<bool>>,\n    pub(crate) text_cache: RefCell<LruCache<u32, String>>,\n}\n\nimpl DomContext {\n    pub(crate) fn ensure_capacity(&mut self, id: u32) {\n        let idx = id as usize;\n        if self.parent_map.len() <= idx {\n            let new_len = idx + 1;\n            self.parent_map.resize(new_len, None);\n            self.children_map.resize_with(new_len, || None);\n            self.sibling_index_map.resize_with(new_len, || None);\n            self.node_map.resize(new_len, None);\n            self.tag_info_map.resize_with(new_len, OnceCell::new);\n            self.prev_inline_like_map.resize_with(new_len, OnceCell::new);\n            self.next_inline_like_map.resize_with(new_len, OnceCell::new);\n            self.next_tag_map.resize_with(new_len, OnceCell::new);\n            self.next_whitespace_map.resize_with(new_len, OnceCell::new);\n        }\n    }\n\n    pub(crate) fn parent_of(&self, id: u32) -> Option<u32> {\n        self.parent_map.get(id as usize).copied().flatten()\n    }\n\n    pub(crate) fn node_handle(&self, id: u32) -> Option<&tl::NodeHandle> {\n        self.node_map.get(id as usize).and_then(|node| node.as_ref())\n    }\n\n    pub(crate) fn children_of(&self, id: u32) -> Option<&Vec<tl::NodeHandle>> {\n        self.children_map\n            .get(id as usize)\n            .and_then(|children| children.as_ref())\n    }\n\n    pub(crate) fn sibling_index(&self, id: u32) -> Option<usize> {\n        self.sibling_index_map.get(id as usize).copied().flatten()\n    }\n\n    pub(crate) fn tag_info(&self, id: u32, parser: &tl::Parser) -> Option<&TagInfo> {\n        self.tag_info_map\n            .get(id as usize)\n            .and_then(|cell| cell.get_or_init(|| self.build_tag_info(id, parser)).as_ref())\n    }\n\n    pub(crate) fn tag_name_for<'a>(\n        &'a self,\n        node_handle: tl::NodeHandle,\n        parser: &'a tl::Parser,\n    ) -> Option<std::borrow::Cow<'a, str>> {\n        if let Some(info) = self.tag_info(node_handle.get_inner(), parser) {\n            return Some(std::borrow::Cow::Borrowed(info.name.as_str()));\n        }\n        if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n            return Some(normalized_tag_name(tag.name().as_utf8_str()));\n        }\n        None\n    }\n\n    pub(crate) fn next_tag_name<'a>(&'a self, node_handle: tl::NodeHandle, parser: &'a tl::Parser) -> Option<&'a str> {\n        let next_id = self.next_tag_id(node_handle.get_inner(), parser)?;\n        self.tag_info(next_id, parser).map(|info| info.name.as_str())\n    }\n\n    pub(crate) fn previous_inline_like(&self, node_handle: tl::NodeHandle, parser: &tl::Parser) -> bool {\n        let id = node_handle.get_inner();\n        self.prev_inline_like_map.get(id as usize).is_some_and(|cell| {\n            *cell.get_or_init(|| {\n                let parent = self.parent_of(id);\n                let siblings = if let Some(parent_id) = parent {\n                    if let Some(children) = self.children_of(parent_id) {\n                        children\n                    } else {\n                        return false;\n                    }\n                } else {\n                    &self.root_children\n                };\n\n                let Some(position) = self\n                    .sibling_index(id)\n                    .or_else(|| siblings.iter().position(|handle| handle.get_inner() == id))\n                else {\n                    return false;\n                };\n\n                for sibling in siblings.iter().take(position).rev() {\n                    if let Some(info) = self.tag_info(sibling.get_inner(), parser) {\n                        return info.is_inline_like;\n                    }\n                    if let Some(tl::Node::Raw(raw)) = sibling.get(parser) {\n                        if raw.as_utf8_str().trim().is_empty() {\n                            continue;\n                        }\n                        return false;\n                    }\n                }\n\n                false\n            })\n        })\n    }\n\n    pub(crate) fn next_inline_like(&self, node_handle: tl::NodeHandle, parser: &tl::Parser) -> bool {\n        let id = node_handle.get_inner();\n        self.next_inline_like_map.get(id as usize).is_some_and(|cell| {\n            *cell.get_or_init(|| {\n                let parent = self.parent_of(id);\n                let siblings = if let Some(parent_id) = parent {\n                    if let Some(children) = self.children_of(parent_id) {\n                        children\n                    } else {\n                        return false;\n                    }\n                } else {\n                    &self.root_children\n                };\n\n                let Some(position) = self\n                    .sibling_index(id)\n                    .or_else(|| siblings.iter().position(|handle| handle.get_inner() == id))\n                else {\n                    return false;\n                };\n\n                for sibling in siblings.iter().skip(position + 1) {\n                    if let Some(info) = self.tag_info(sibling.get_inner(), parser) {\n                        return info.is_inline_like;\n                    }\n                    if let Some(tl::Node::Raw(raw)) = sibling.get(parser) {\n                        if raw.as_utf8_str().trim().is_empty() {\n                            continue;\n                        }\n                        return false;\n                    }\n                }\n\n                false\n            })\n        })\n    }\n\n    pub(crate) fn next_whitespace_text(&self, node_handle: tl::NodeHandle, parser: &tl::Parser) -> bool {\n        let id = node_handle.get_inner();\n        self.next_whitespace_map.get(id as usize).is_some_and(|cell| {\n            *cell.get_or_init(|| {\n                let parent = self.parent_of(id);\n                let siblings = if let Some(parent_id) = parent {\n                    if let Some(children) = self.children_of(parent_id) {\n                        children\n                    } else {\n                        return false;\n                    }\n                } else {\n                    &self.root_children\n                };\n\n                let Some(position) = self\n                    .sibling_index(id)\n                    .or_else(|| siblings.iter().position(|handle| handle.get_inner() == id))\n                else {\n                    return false;\n                };\n\n                for sibling in siblings.iter().skip(position + 1) {\n                    if let Some(node) = sibling.get(parser) {\n                        match node {\n                            tl::Node::Raw(raw) => return raw.as_utf8_str().trim().is_empty(),\n                            tl::Node::Tag(_) => return false,\n                            tl::Node::Comment(_) => {}\n                        }\n                    }\n                }\n\n                false\n            })\n        })\n    }\n\n    pub(crate) fn next_tag_id(&self, id: u32, parser: &tl::Parser) -> Option<u32> {\n        self.next_tag_map\n            .get(id as usize)\n            .and_then(|cell| {\n                cell.get_or_init(|| {\n                    let parent = self.parent_of(id);\n                    let siblings = if let Some(parent_id) = parent {\n                        self.children_of(parent_id)?\n                    } else {\n                        &self.root_children\n                    };\n\n                    let position = self\n                        .sibling_index(id)\n                        .or_else(|| siblings.iter().position(|handle| handle.get_inner() == id))?;\n\n                    for sibling in siblings.iter().skip(position + 1) {\n                        if self.tag_info(sibling.get_inner(), parser).is_some() {\n                            let sibling_id = sibling.get_inner();\n                            return Some(sibling_id);\n                        }\n                        if let Some(tl::Node::Raw(raw)) = sibling.get(parser) {\n                            if !raw.as_utf8_str().trim().is_empty() {\n                                return None;\n                            }\n                        }\n                    }\n                    None\n                })\n                .as_ref()\n            })\n            .copied()\n    }\n\n    pub(crate) fn build_tag_info(&self, id: u32, parser: &tl::Parser) -> Option<TagInfo> {\n        let node_handle = self.node_handle(id)?;\n        match node_handle.get(parser) {\n            Some(tl::Node::Tag(tag)) => {\n                let name = normalized_tag_name(tag.name().as_utf8_str()).into_owned();\n                let is_inline = is_inline_element(&name);\n                let is_inline_like = is_inline || matches!(name.as_str(), \"script\" | \"style\");\n                let is_block = is_block_level_name(&name, is_inline);\n                Some(TagInfo {\n                    name,\n                    is_inline_like,\n                    is_block,\n                })\n            }\n            _ => None,\n        }\n    }\n\n    pub(crate) fn text_content(&self, node_handle: tl::NodeHandle, parser: &tl::Parser) -> String {\n        let id = node_handle.get_inner();\n        let cached = {\n            let mut cache = self.text_cache.borrow_mut();\n            cache.get(&id).cloned()\n        };\n        if let Some(value) = cached {\n            return value;\n        }\n\n        let value = self.text_content_uncached(node_handle, parser);\n        self.text_cache.borrow_mut().put(id, value.clone());\n        value\n    }\n\n    pub(crate) fn text_content_uncached(&self, node_handle: tl::NodeHandle, parser: &tl::Parser) -> String {\n        let mut text = String::with_capacity(64);\n        if let Some(node) = node_handle.get(parser) {\n            match node {\n                tl::Node::Raw(bytes) => {\n                    let raw = bytes.as_utf8_str();\n                    let decoded = text::decode_html_entities_cow(raw.as_ref());\n                    text.push_str(decoded.as_ref());\n                }\n                tl::Node::Tag(tag) => {\n                    let children = tag.children();\n                    for child_handle in children.top().iter() {\n                        text.push_str(&self.text_content(*child_handle, parser));\n                    }\n                }\n                tl::Node::Comment(_) => {}\n            }\n        }\n        text\n    }\n\n    /// Get the parent tag name for a given node ID.\n    ///\n    /// Returns the tag name of the parent element if it exists and is a tag,\n    /// otherwise returns None.\n    #[cfg_attr(not(feature = \"visitor\"), allow(dead_code))]\n    pub(crate) fn parent_tag_name(&self, node_id: u32, parser: &tl::Parser) -> Option<String> {\n        let parent_id = self.parent_of(node_id)?;\n        let parent_handle = self.node_handle(parent_id)?;\n\n        if let Some(info) = self.tag_info(parent_id, parser) {\n            return Some(info.name.clone());\n        }\n\n        if let Some(tl::Node::Tag(tag)) = parent_handle.get(parser) {\n            let name = normalized_tag_name(tag.name().as_utf8_str());\n            return Some(name.into_owned());\n        }\n\n        None\n    }\n\n    /// Get the index of a node among its siblings.\n    ///\n    /// Returns the 0-based index if the node has siblings,\n    /// otherwise returns None.\n    #[cfg_attr(not(feature = \"visitor\"), allow(dead_code))]\n    pub(crate) fn get_sibling_index(&self, node_id: u32) -> Option<usize> {\n        self.sibling_index(node_id)\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/form/elements.rs",
    "content": "//! Handlers for HTML form elements.\n//!\n//! This module provides comprehensive handling for all HTML form-related elements,\n//! including form containers, input controls, and measurement elements.\n//!\n//! Processed elements:\n//! - **Form containers**: `<form>`, `<fieldset>`, `<legend>`, `<label>`\n//! - **Text inputs**: `<input>`, `<textarea>`, `<button>`\n//! - **Select controls**: `<select>`, `<option>`, `<optgroup>`\n//! - **Measurement**: `<progress>`, `<meter>`, `<output>`, `<datalist>`\n//!\n//! In Markdown, forms are typically not fully representable, so the handlers\n//! extract and format the content in a readable manner.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\nuse super::walk_node;\nuse std::borrow::Cow;\n\n/// Handles the `<form>` element.\n///\n/// A form element is a container for form controls. In Markdown, it's rendered\n/// as a block container with its content visible.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is collected, trimmed, and wrapped with blank lines\n/// - **Empty content**: Skipped entirely\npub fn handle_form(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Collect content\n        let mut content = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            // Add spacing before if needed\n            if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                output.push_str(\"\\n\\n\");\n            }\n\n            // Output content\n            output.push_str(trimmed);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<fieldset>` element.\n///\n/// A fieldset element groups form controls with an optional legend.\n/// In Markdown, it's rendered as a block container.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is collected, trimmed, and wrapped with blank lines\n/// - **Empty content**: Skipped entirely\npub fn handle_fieldset(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Collect content\n        let mut content = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            // Add spacing before if needed\n            if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                output.push_str(\"\\n\\n\");\n            }\n\n            // Output content\n            output.push_str(trimmed);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<legend>` element.\n///\n/// A legend element provides a caption for a fieldset. It's rendered as\n/// strong (bold) text to distinguish it from regular content.\n///\n/// # Behavior\n///\n/// - **Block mode**: Content is wrapped in strong markers (e.g., `**text**`)\n/// - **Inline mode**: Content is rendered without emphasis\n/// - Uses the configured strong/emphasis symbol from ConversionOptions\npub fn handle_legend(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::new();\n\n        // Set strong context for nested content\n        let mut legend_ctx = ctx.clone();\n        if !ctx.convert_as_inline {\n            legend_ctx.in_strong = true;\n        }\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &legend_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            if ctx.convert_as_inline {\n                output.push_str(trimmed);\n            } else {\n                let mut symbol = String::with_capacity(2);\n                symbol.push(options.strong_em_symbol);\n                symbol.push(options.strong_em_symbol);\n                output.push_str(&symbol);\n                output.push_str(trimmed);\n                output.push_str(&symbol);\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    }\n}\n\n/// Handles the `<label>` element.\n///\n/// A label element associates text with a form control. It's rendered as\n/// a block or inline element depending on context.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Non-empty content is output followed by blank lines (in block mode)\n/// - Blank lines are suppressed in inline mode\npub fn handle_label(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            output.push_str(trimmed);\n            if !ctx.convert_as_inline {\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    }\n}\n\n/// Handles the `<input>` element.\n///\n/// An input element represents a form control for user input. Since input\n/// elements typically have no text content, this handler produces no output.\npub fn handle_input(\n    _tag_name: &str,\n    _node_handle: &tl::NodeHandle,\n    _parser: &tl::Parser,\n    _output: &mut String,\n    _options: &crate::options::ConversionOptions,\n    _ctx: &super::Context,\n    _depth: usize,\n    _dom_ctx: &super::DomContext,\n) {\n    // Input elements have no text content; render nothing\n}\n\n/// Handles the `<textarea>` element.\n///\n/// A textarea element represents a multi-line text input. Its content is\n/// rendered as plain text, with blank lines added after in block mode.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Blank lines are added after content in block mode only\npub fn handle_textarea(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<select>` element.\n///\n/// A select element represents a dropdown list of options. Its options are\n/// rendered as a list, with newlines between options.\n///\n/// # Behavior\n///\n/// - Content (options) is collected from children\n/// - A single newline is added after the select in block mode\npub fn handle_select(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push('\\n');\n        }\n    }\n}\n\n/// Handles the `<option>` element.\n///\n/// An option element represents a choice within a select element.\n/// Selected options are marked with a bullet point (`*`) in block mode.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - If the option has the `selected` attribute, it's prefixed with `* ` in block mode\n/// - A newline is added after each option in block mode\npub fn handle_option(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let selected = tag.attributes().iter().any(|(name, _)| name.as_ref() == \"selected\");\n\n        let mut text = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = text.trim();\n        if !trimmed.is_empty() {\n            if selected && !ctx.convert_as_inline {\n                output.push_str(\"* \");\n            }\n            output.push_str(trimmed);\n            if !ctx.convert_as_inline {\n                output.push('\\n');\n            }\n        }\n    }\n}\n\n/// Handles the `<optgroup>` element.\n///\n/// An optgroup element groups options within a select element with an optional label.\n/// The label is rendered as strong (bold) text, followed by the grouped options.\n///\n/// # Behavior\n///\n/// - The `label` attribute is output as strong text (if present)\n/// - Options within the group are rendered normally\npub fn handle_optgroup(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let label = tag\n            .attributes()\n            .get(\"label\")\n            .flatten()\n            .map_or(Cow::Borrowed(\"\"), |v| v.as_utf8_str());\n\n        if !label.is_empty() {\n            let mut symbol = String::with_capacity(2);\n            symbol.push(options.strong_em_symbol);\n            symbol.push(options.strong_em_symbol);\n            output.push_str(&symbol);\n            output.push_str(&label);\n            output.push_str(&symbol);\n            output.push('\\n');\n        }\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n    }\n}\n\n/// Handles the `<button>` element.\n///\n/// A button element represents a clickable button. Its text content is rendered\n/// as plain text, with blank lines added in block mode.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Blank lines are added after content in block mode only\npub fn handle_button(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<progress>` element.\n///\n/// A progress element represents a progress bar. It typically has no visible\n/// text content, but we render any child content present.\n///\n/// # Behavior\n///\n/// - Content is collected from children (usually empty)\n/// - Blank lines are added after content in block mode only\npub fn handle_progress(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<meter>` element.\n///\n/// A meter element represents a scalar measurement (e.g., disk usage, temperature).\n/// It typically has no visible text content, but we render any child content.\n///\n/// # Behavior\n///\n/// - Content is collected from children (usually empty)\n/// - Blank lines are added after content in block mode only\npub fn handle_meter(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<output>` element.\n///\n/// An output element represents the result of a calculation. It renders its\n/// text content as plain output, with blank lines in block mode.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Blank lines are added after content in block mode only\npub fn handle_output(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<datalist>` element.\n///\n/// A datalist element provides a list of predefined options for an input element.\n/// Options are rendered as a list with newlines between them.\n///\n/// # Behavior\n///\n/// - Content (options) is collected from children\n/// - A single newline is added after the datalist in block mode\npub fn handle_datalist(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let start_len = output.len();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        if !ctx.convert_as_inline && output.len() > start_len {\n            output.push('\\n');\n        }\n    }\n}\n\n/// Dispatcher for form elements.\n///\n/// Routes all form-related elements to their respective handlers.\npub fn handle(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    match tag_name {\n        \"form\" => handle_form(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"fieldset\" => handle_fieldset(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"legend\" => handle_legend(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"label\" => handle_label(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"input\" => handle_input(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"textarea\" => handle_textarea(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"select\" => handle_select(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"option\" => handle_option(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"optgroup\" => handle_optgroup(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"button\" => handle_button(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"progress\" => handle_progress(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"meter\" => handle_meter(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"output\" => handle_output(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"datalist\" => handle_datalist(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/form/mod.rs",
    "content": "//! Form element handlers for HTML to Markdown conversion.\n//!\n//! This module provides specialized handlers for HTML form elements:\n//! - Container elements (form, fieldset, legend, label)\n//! - Input elements (input, textarea, select, option, optgroup, button)\n//! - Measurement elements (progress, meter, output, datalist)\n//!\n//! These handlers are designed to be extracted from the main `converter.rs`\n//! file and integrated through the dispatcher function.\n//!\n//! **Integration Pattern:**\n//! Each handler function takes the same signature:\n//! - `tag_name: &str` - The HTML tag being processed\n//! - `node_handle: &NodeHandle` - The DOM node handle\n//! - `parser: &Parser` - The HTML parser reference\n//! - `output: &mut String` - The output buffer to write to\n//! - `options: &ConversionOptions` - Conversion configuration\n//! - `ctx: &Context` - Processing context (state tracking)\n//! - `depth: usize` - Current DOM tree depth\n//! - `dom_ctx: &DomContext` - DOM context for tree relationships\n//!\n//! The main dispatcher function `dispatch_form_handler` routes tags to\n//! their appropriate handlers and returns a boolean indicating success.\n\npub mod elements;\n\n// Re-export types from parent module for submodule access\npub use super::walk_node;\npub use super::{Context, DomContext};\n\n// Re-export handler function for direct use\npub use elements::handle as handle_form_elements;\n\n// Re-exports are done via the dispatch function parameter types\n\n/// Dispatches form element handling to the appropriate handler.\n///\n/// This function routes form-related HTML elements to their specialized handlers\n/// based on tag name. It is designed to be called from the main `walk_node`\n/// function in `converter.rs`.\n///\n/// # Routing Table\n///\n/// The following tag routes are supported:\n/// - **Containers**: form, fieldset, legend, label\n/// - **Inputs**: input, textarea, select, option, optgroup, button\n/// - **Measurements**: progress, meter, output, datalist\n///\n/// # Returns\n///\n/// Returns `true` if the tag was successfully handled by a form handler,\n/// `false` if the tag is not a form element and requires other handling.\n///\n/// # Example\n///\n/// ```text\n/// if dispatch_form_handler(tag_name, &node_handle, &parser, output, options, ctx, depth, dom_ctx) {\n///     // Tag was handled\n/// } else {\n///     // Continue with other handlers\n/// }\n/// ```\npub fn dispatch_form_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) -> bool {\n    match tag_name {\n        // Form containers and metadata\n        \"form\" | \"fieldset\" | \"legend\" | \"label\" | \"input\" | \"textarea\" | \"select\" | \"option\" | \"optgroup\"\n        | \"button\" | \"progress\" | \"meter\" | \"output\" | \"datalist\" => {\n            handle_form_elements(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/format/djot.rs",
    "content": "//! Djot format renderer.\n\nuse super::FormatRenderer;\n\n/// Renderer for Djot lightweight markup output.\n#[derive(Debug, Clone, Copy, Default)]\npub struct DjotRenderer;\n\nimpl FormatRenderer for DjotRenderer {\n    fn emphasis(&self, content: &str) -> String {\n        format!(\"_{content}_\")\n    }\n\n    fn strong(&self, content: &str, _symbol: char) -> String {\n        // Djot always uses single asterisk for strong\n        format!(\"*{content}*\")\n    }\n\n    fn strikethrough(&self, content: &str) -> String {\n        format!(\"{{-{content}-}}\")\n    }\n\n    fn highlight(&self, content: &str) -> String {\n        format!(\"{{={content}=}}\")\n    }\n\n    fn inserted(&self, content: &str) -> String {\n        format!(\"{{+{content}+}}\")\n    }\n\n    fn subscript(&self, content: &str, _custom_symbol: &str) -> String {\n        // Djot has native subscript support\n        format!(\"~{content}~\")\n    }\n\n    fn superscript(&self, content: &str, _custom_symbol: &str) -> String {\n        // Djot has native superscript support\n        format!(\"^{content}^\")\n    }\n\n    fn span_with_attributes(&self, content: &str, classes: &[&str], id: Option<&str>) -> String {\n        if classes.is_empty() && id.is_none() {\n            return content.to_string();\n        }\n\n        let mut attrs = classes.iter().map(|c| format!(\".{c}\")).collect::<Vec<_>>();\n        if let Some(id_val) = id {\n            attrs.push(format!(\"#{id_val}\"));\n        }\n        format!(\"[{content}]{{{}}}\", attrs.join(\" \"))\n    }\n\n    fn div_with_attributes(&self, content: &str, classes: &[&str]) -> String {\n        if classes.is_empty() {\n            return content.to_string();\n        }\n        let class_str = classes.join(\" \");\n        format!(\"::: {class_str}\\n{content}\\n:::\")\n    }\n\n    fn is_djot(&self) -> bool {\n        true\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/format/markdown.rs",
    "content": "//! Markdown format renderer.\n\nuse super::FormatRenderer;\n\n/// Renderer for standard Markdown output.\n#[derive(Debug, Clone, Copy, Default)]\npub struct MarkdownRenderer;\n\nimpl FormatRenderer for MarkdownRenderer {\n    fn emphasis(&self, content: &str) -> String {\n        format!(\"*{content}*\")\n    }\n\n    fn strong(&self, content: &str, symbol: char) -> String {\n        format!(\"{symbol}{symbol}{content}{symbol}{symbol}\")\n    }\n\n    fn strikethrough(&self, content: &str) -> String {\n        format!(\"~~{content}~~\")\n    }\n\n    fn highlight(&self, content: &str) -> String {\n        format!(\"=={content}==\")\n    }\n\n    fn inserted(&self, content: &str) -> String {\n        format!(\"++{content}++\")\n    }\n\n    fn subscript(&self, content: &str, custom_symbol: &str) -> String {\n        if custom_symbol.is_empty() {\n            format!(\"~{content}~\")\n        } else {\n            format!(\"{custom_symbol}{content}{custom_symbol}\")\n        }\n    }\n\n    fn superscript(&self, content: &str, custom_symbol: &str) -> String {\n        if custom_symbol.is_empty() {\n            format!(\"^{content}^\")\n        } else {\n            format!(\"{custom_symbol}{content}{custom_symbol}\")\n        }\n    }\n\n    fn span_with_attributes(&self, content: &str, _classes: &[&str], _id: Option<&str>) -> String {\n        // Markdown doesn't support span attributes, just return content\n        content.to_string()\n    }\n\n    fn div_with_attributes(&self, content: &str, _classes: &[&str]) -> String {\n        // Markdown doesn't support div attributes, just return content\n        content.to_string()\n    }\n\n    fn is_djot(&self) -> bool {\n        false\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/format/mod.rs",
    "content": "//! Output format renderers for HTML to Markdown/Djot conversion.\n//!\n//! This module provides format-specific rendering through the `FormatRenderer` trait,\n//! allowing clean separation between Markdown and Djot output syntax.\n\nmod djot;\nmod markdown;\n\n/// Trait for format-specific rendering of inline elements.\n///\n/// Implementations provide the syntax for emphasis, strong, strikethrough, etc.\n/// in their respective output formats.\npub trait FormatRenderer: Send + Sync {\n    /// Render emphasis (em, i elements)\n    fn emphasis(&self, content: &str) -> String;\n\n    /// Render strong emphasis (strong, b elements)\n    fn strong(&self, content: &str, symbol: char) -> String;\n\n    /// Render strikethrough (del, s elements)\n    fn strikethrough(&self, content: &str) -> String;\n\n    /// Render highlight (mark element)\n    fn highlight(&self, content: &str) -> String;\n\n    /// Render inserted text (ins element)\n    fn inserted(&self, content: &str) -> String;\n\n    /// Render subscript (sub element)\n    fn subscript(&self, content: &str, custom_symbol: &str) -> String;\n\n    /// Render superscript (sup element)\n    fn superscript(&self, content: &str, custom_symbol: &str) -> String;\n\n    /// Render span with attributes (for Djot: [text]{.class})\n    fn span_with_attributes(&self, content: &str, classes: &[&str], id: Option<&str>) -> String;\n\n    /// Render div with attributes (for Djot: ::: class)\n    fn div_with_attributes(&self, content: &str, classes: &[&str]) -> String;\n\n    /// Check if this is Djot format\n    fn is_djot(&self) -> bool;\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/blockquote.rs",
    "content": "//! Blockquote element handler for HTML to Markdown conversion.\n//!\n//! Handles `<blockquote>` elements including:\n//! - Basic blockquote markdown output with `> ` prefix\n//! - Nested blockquotes\n//! - Citation URLs via `cite` attribute\n//! - Visitor callback integration\n\nuse crate::converter::Context;\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main::walk_node;\nuse crate::options::ConversionOptions;\n\n#[cfg(feature = \"visitor\")]\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::serialization::serialize_node_to_html;\n\n/// Handle a `<blockquote>` element and convert to Markdown.\n///\n/// This handler processes blockquote elements including:\n/// - Converting inline blockquotes by processing children as inline\n/// - Handling nested blockquotes via blockquote_depth tracking\n/// - Processing citation URLs from cite attribute\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Adding proper spacing and blockquote prefix formatting\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_blockquote(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // If in inline conversion mode, just process children inline\n    if ctx.convert_as_inline {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n        return;\n    }\n\n    // Extract cite attribute if present\n    let cite = tag\n        .attributes()\n        .get(\"cite\")\n        .flatten()\n        .map(|v| v.as_utf8_str().to_string());\n\n    // Create context for nested blockquote processing\n    let blockquote_ctx = Context {\n        blockquote_depth: ctx.blockquote_depth + 1,\n        ..ctx.clone()\n    };\n\n    // Process blockquote content\n    let mut content = String::with_capacity(256);\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(\n                child_handle,\n                parser,\n                &mut content,\n                options,\n                &blockquote_ctx,\n                depth + 1,\n                dom_ctx,\n            );\n        }\n    }\n\n    let trimmed_content = content.trim();\n\n    // Handle visitor integration\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Blockquote,\n            tag_name: \"blockquote\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: false,\n        };\n\n        let mut visitor_ref = visitor.borrow_mut();\n        match visitor_ref.visit_blockquote(&node_ctx, trimmed_content, ctx.blockquote_depth) {\n            VisitResult::Continue => {}\n            VisitResult::Custom(custom) => {\n                output.push_str(&custom);\n                return;\n            }\n            VisitResult::Skip => return,\n            VisitResult::PreserveHtml => {\n                let mut html_output = String::new();\n                serialize_node_to_html(node_handle, parser, &mut html_output);\n                output.push_str(&html_output);\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    // Output blockquote if content is not empty\n    if !trimmed_content.is_empty() {\n        // Add proper spacing based on blockquote depth\n        if ctx.blockquote_depth > 0 {\n            output.push_str(\"\\n\\n\\n\");\n        } else if !output.is_empty() {\n            if output.ends_with(\"\\n\\n\") {\n                // Paragraph already added \\n\\n; blockquote needs just \\n\n                output.truncate(output.len() - 1);\n            } else if !output.ends_with('\\n') {\n                output.push_str(\"\\n\\n\");\n            } else if !output.ends_with(\"\\n\\n\") {\n                output.push('\\n');\n            }\n        }\n\n        // Add blockquote prefix to each line\n        let prefix = \"> \";\n\n        for line in trimmed_content.lines() {\n            output.push_str(prefix);\n            output.push_str(line.trim());\n            output.push('\\n');\n        }\n\n        // Add citation if present\n        if let Some(url) = cite {\n            output.push('\\n');\n            output.push_str(\"— <\");\n            output.push_str(&url);\n            output.push_str(\">\\n\\n\");\n        }\n\n        // Add trailing newlines only when appropriate for proper spacing\n        // (matching paragraph conditional logic for CommonMark compliance)\n        if !ctx.convert_as_inline && !ctx.in_table_cell && !ctx.in_list_item {\n            while output.ends_with('\\n') {\n                output.truncate(output.len() - 1);\n            }\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/code_block.rs",
    "content": "//! Code and pre element handlers for HTML to Markdown conversion.\n//!\n//! Handles `<code>` and `<pre>` elements including:\n//! - Inline code with backtick formatting\n//! - Code block formatting (indented or fenced)\n//! - Language detection from class attributes\n//! - Whitespace normalization and dedenting\n//! - Visitor callback integration\n\nuse crate::converter::Context;\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main::walk_node;\nuse crate::converter::text::dedent_code_block;\nuse crate::options::ConversionOptions;\n\n#[cfg(feature = \"visitor\")]\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::serialization::serialize_node;\n\n/// Handle an inline `<code>` element and convert to Markdown.\n///\n/// This handler processes inline code elements including:\n/// - Extracting code content and applying backtick delimiters\n/// - Handling backticks in content by using multiple delimiters\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Generating appropriate markdown output with proper escaping\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_code(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let code_ctx = Context {\n        in_code: true,\n        ..ctx.clone()\n    };\n\n    if ctx.in_code {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, &code_ctx, depth + 1, dom_ctx);\n            }\n        }\n    } else {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &code_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n\n        let trimmed = &content;\n\n        if !content.trim().is_empty() {\n            #[cfg(feature = \"visitor\")]\n            let code_output = if let Some(ref visitor_handle) = ctx.visitor {\n                use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n                let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n                let node_id = node_handle.get_inner();\n                let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n                let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n                let node_ctx = NodeContext {\n                    node_type: NodeType::Code,\n                    tag_name: \"code\".to_string(),\n                    attributes,\n                    depth,\n                    index_in_parent,\n                    parent_tag,\n                    is_inline: true,\n                };\n\n                let visit_result = {\n                    let mut visitor = visitor_handle.borrow_mut();\n                    visitor.visit_code_inline(&node_ctx, trimmed)\n                };\n                match visit_result {\n                    VisitResult::Continue => None,\n                    VisitResult::Custom(custom) => Some(custom),\n                    VisitResult::Skip => Some(String::new()),\n                    VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n                    VisitResult::Error(err) => {\n                        if ctx.visitor_error.borrow().is_none() {\n                            *ctx.visitor_error.borrow_mut() = Some(err);\n                        }\n                        None\n                    }\n                }\n            } else {\n                None\n            };\n\n            #[cfg(feature = \"visitor\")]\n            if let Some(custom_output) = code_output {\n                output.push_str(&custom_output);\n            } else {\n                format_inline_code(trimmed, output);\n            }\n\n            #[cfg(not(feature = \"visitor\"))]\n            {\n                format_inline_code(trimmed, output);\n            }\n        }\n    }\n}\n\n/// Handle a `<pre>` element and convert to Markdown.\n///\n/// This handler processes code block elements including:\n/// - Extracting language information from class attributes\n/// - Processing whitespace and dedenting code content\n/// - Supporting multiple code block styles (indented, backticks, tildes)\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Generating appropriate markdown output\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_pre(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let code_ctx = Context {\n        in_code: true,\n        ..ctx.clone()\n    };\n\n    #[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\n    let language: Option<String> = {\n        let mut lang: Option<String> = None;\n\n        // First, try to extract language from <pre> tag's class attribute\n        if let Some(class_attr) = tag.attributes().get(\"class\") {\n            if let Some(class_bytes) = class_attr {\n                let class_str = class_bytes.as_utf8_str();\n                for cls in class_str.split_whitespace() {\n                    if let Some(stripped) = cls.strip_prefix(\"language-\") {\n                        lang = Some(String::from(stripped));\n                        break;\n                    } else if let Some(stripped) = cls.strip_prefix(\"lang-\") {\n                        lang = Some(String::from(stripped));\n                        break;\n                    }\n                }\n            }\n        }\n\n        // If not found on <pre>, try to extract from nested <code> tag's class attribute\n        if lang.is_none() {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    if child_tag.name() == \"code\" {\n                        if let Some(class_attr) = child_tag.attributes().get(\"class\") {\n                            if let Some(class_bytes) = class_attr {\n                                let class_str = class_bytes.as_utf8_str();\n                                for cls in class_str.split_whitespace() {\n                                    if let Some(stripped) = cls.strip_prefix(\"language-\") {\n                                        lang = Some(String::from(stripped));\n                                        break;\n                                    } else if let Some(stripped) = cls.strip_prefix(\"lang-\") {\n                                        lang = Some(String::from(stripped));\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n\n        lang\n    };\n\n    let mut content = String::with_capacity(256);\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(\n                child_handle,\n                parser,\n                &mut content,\n                options,\n                &code_ctx,\n                depth + 1,\n                dom_ctx,\n            );\n        }\n    }\n\n    if !content.is_empty() {\n        let leading_newlines = content.chars().take_while(|&c| c == '\\n').count();\n        let trailing_newlines = content.chars().rev().take_while(|&c| c == '\\n').count();\n        let core = content.trim_matches('\\n');\n        let is_whitespace_only = core.trim().is_empty();\n\n        let processed_content = if options.whitespace_mode == crate::options::WhitespaceMode::Strict {\n            content\n        } else {\n            // Always dedent code blocks to remove common leading whitespace\n            let mut core_text = dedent_code_block(core);\n\n            if is_whitespace_only {\n                let mut rebuilt = String::new();\n                for _ in 0..leading_newlines {\n                    rebuilt.push('\\n');\n                }\n                rebuilt.push_str(&core_text);\n                for _ in 0..trailing_newlines {\n                    rebuilt.push('\\n');\n                }\n                rebuilt\n            } else {\n                for _ in 0..trailing_newlines {\n                    core_text.push('\\n');\n                }\n                core_text\n            }\n        };\n\n        #[cfg(feature = \"visitor\")]\n        let code_block_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Pre,\n                tag_name: \"pre\".to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: false,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_code_block(&node_ctx, language.as_deref(), &processed_content)\n            };\n            match visit_result {\n                VisitResult::Continue => None,\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => Some(String::new()),\n                VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n            }\n        } else {\n            None\n        };\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(custom_output) = code_block_output {\n            output.push_str(&custom_output);\n        } else {\n            format_code_block(&processed_content, language.as_deref(), output, options, ctx);\n        }\n\n        #[cfg(not(feature = \"visitor\"))]\n        {\n            format_code_block(&processed_content, language.as_deref(), output, options, ctx);\n        }\n\n        if let Some(ref sc) = ctx.structure_collector {\n            sc.borrow_mut().push_code(&processed_content, language.as_deref());\n        }\n    }\n}\n\n/// Format inline code with appropriate backtick delimiters.\n///\n/// Handles:\n/// - Single backticks for normal content\n/// - Double backticks when content contains backticks\n/// - Space padding when needed to avoid backtick adjacency\nfn format_inline_code(content: &str, output: &mut String) {\n    let contains_backtick = content.contains('`');\n\n    let needs_delimiter_spaces = {\n        let first_char = content.chars().next();\n        let last_char = content.chars().last();\n        let starts_with_space = first_char == Some(' ');\n        let ends_with_space = last_char == Some(' ');\n        let starts_with_backtick = first_char == Some('`');\n        let ends_with_backtick = last_char == Some('`');\n        let all_spaces = content.chars().all(|c| c == ' ');\n\n        all_spaces\n            || starts_with_backtick\n            || ends_with_backtick\n            || (starts_with_space && ends_with_space && contains_backtick)\n    };\n\n    let (num_backticks, needs_spaces) = if contains_backtick {\n        let max_consecutive = content\n            .chars()\n            .fold((0, 0), |(max, current), c| {\n                if c == '`' {\n                    let new_current = current + 1;\n                    (max.max(new_current), new_current)\n                } else {\n                    (max, 0)\n                }\n            })\n            .0;\n        let num = if max_consecutive == 1 { 2 } else { 1 };\n        (num, needs_delimiter_spaces)\n    } else {\n        (1, needs_delimiter_spaces)\n    };\n\n    for _ in 0..num_backticks {\n        output.push('`');\n    }\n    if needs_spaces {\n        output.push(' ');\n    }\n    output.push_str(content);\n    if needs_spaces {\n        output.push(' ');\n    }\n    for _ in 0..num_backticks {\n        output.push('`');\n    }\n}\n\n/// Format a code block with the specified style and language.\n///\n/// Supports:\n/// - Indented style (4-space indentation)\n/// - Fenced style with backticks (```language)\n/// - Fenced style with tildes (~~~language)\nfn format_code_block(\n    content: &str,\n    language: Option<&str>,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n) {\n    match options.code_block_style {\n        crate::options::CodeBlockStyle::Indented => {\n            if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n\n            let indented = content\n                .lines()\n                .map(|line| {\n                    if line.is_empty() {\n                        String::new()\n                    } else {\n                        format!(\"    {line}\")\n                    }\n                })\n                .collect::<Vec<_>>()\n                .join(\"\\n\");\n            output.push_str(&indented);\n\n            output.push_str(\"\\n\\n\");\n        }\n        crate::options::CodeBlockStyle::Backticks | crate::options::CodeBlockStyle::Tildes => {\n            if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n\n            let fence = if options.code_block_style == crate::options::CodeBlockStyle::Backticks {\n                \"```\"\n            } else {\n                \"~~~\"\n            };\n\n            output.push_str(fence);\n            if let Some(lang) = language {\n                output.push_str(lang);\n            } else if !options.code_language.is_empty() {\n                output.push_str(&options.code_language);\n            }\n            output.push('\\n');\n            output.push_str(content);\n            output.push('\\n');\n            output.push_str(fence);\n            output.push('\\n');\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/graphic.rs",
    "content": "//! Graphic element handler for HTML to Markdown conversion.\n//!\n//! Handles `<graphic>` elements including:\n//! - Alternative source attributes (url, href, xlink:href, src)\n//! - Fallback alt text from filename attribute\n//! - Metadata collection for graphic extraction\n//! - Visitor callback integration\n\nuse std::borrow::Cow;\n#[cfg(any(feature = \"metadata\", feature = \"visitor\"))]\nuse std::collections::BTreeMap;\n\nuse crate::converter::Context;\nuse crate::converter::dom_context::DomContext;\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::ConversionOptions;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::serialization::serialize_node;\n\n#[cfg(feature = \"metadata\")]\ntype GraphicMetadataPayload = (BTreeMap<String, String>, Option<u32>, Option<u32>);\n\n/// Handle a `<graphic>` element and convert to Markdown.\n///\n/// This handler processes graphic elements including:\n/// - Extracting source from url, href, xlink:href, or src attributes\n/// - Using alt attribute, with fallback to filename\n/// - Collecting metadata when the metadata feature is enabled\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Generating appropriate markdown output\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_graphic(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Check source attributes in order: url, href, xlink:href, src\n    let src = tag\n        .attributes()\n        .get(\"url\")\n        .flatten()\n        .or_else(|| tag.attributes().get(\"href\").flatten())\n        .or_else(|| tag.attributes().get(\"xlink:href\").flatten())\n        .or_else(|| tag.attributes().get(\"src\").flatten())\n        .map_or(Cow::Borrowed(\"\"), |v| v.as_utf8_str());\n\n    // Use \"alt\" attribute, fallback to \"filename\"\n    let alt = tag\n        .attributes()\n        .get(\"alt\")\n        .flatten()\n        .map(|v| v.as_utf8_str())\n        .or_else(|| tag.attributes().get(\"filename\").flatten().map(|v| v.as_utf8_str()))\n        .unwrap_or(Cow::Borrowed(\"\"));\n\n    let title = tag.attributes().get(\"title\").flatten().map(|v| v.as_utf8_str());\n\n    // Collect metadata payload if metadata feature is enabled\n    #[cfg(feature = \"metadata\")]\n    #[allow(clippy::useless_let_if_seq)]\n    let mut metadata_payload: Option<GraphicMetadataPayload> = None;\n    #[cfg(feature = \"metadata\")]\n    if ctx.metadata_wants_images {\n        let mut attributes_map = BTreeMap::new();\n        let mut width: Option<u32> = None;\n        let mut height: Option<u32> = None;\n        for (key, value_opt) in tag.attributes().iter() {\n            let key_str = key.to_string();\n            if key_str == \"url\" || key_str == \"href\" || key_str == \"xlink:href\" || key_str == \"src\" {\n                continue;\n            }\n            let value = value_opt.map(|v| v.to_string()).unwrap_or_default();\n            if key_str == \"width\" {\n                if let Ok(parsed) = value.parse::<u32>() {\n                    width = Some(parsed);\n                }\n            } else if key_str == \"height\" {\n                if let Ok(parsed) = value.parse::<u32>() {\n                    height = Some(parsed);\n                }\n            }\n            attributes_map.insert(key_str, value);\n        }\n        metadata_payload = Some((attributes_map, width, height));\n    }\n\n    let keep_as_markdown = ctx.in_heading && ctx.heading_allow_inline_images;\n\n    let should_use_alt_text =\n        !keep_as_markdown && (ctx.convert_as_inline || (ctx.in_heading && !ctx.heading_allow_inline_images));\n\n    // Generate graphic output with visitor integration\n    #[cfg(feature = \"visitor\")]\n    let graphic_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Image,\n            tag_name: \"graphic\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_image(&node_ctx, &src, &alt, title.as_deref())\n        };\n        match visit_result {\n            VisitResult::Continue => Some(format_graphic_markdown(\n                &src,\n                &alt,\n                title.as_deref(),\n                should_use_alt_text,\n                options.link_style,\n                ctx.reference_collector.as_ref(),\n            )),\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => None,\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n            VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n        }\n    } else {\n        Some(format_graphic_markdown(\n            &src,\n            &alt,\n            title.as_deref(),\n            should_use_alt_text,\n            options.link_style,\n            ctx.reference_collector.as_ref(),\n        ))\n    };\n\n    #[cfg(not(feature = \"visitor\"))]\n    let graphic_output = Some(format_graphic_markdown(\n        &src,\n        &alt,\n        title.as_deref(),\n        should_use_alt_text,\n        options.link_style,\n        ctx.reference_collector.as_ref(),\n    ));\n\n    if !options.skip_images {\n        if let Some(graphic_text) = graphic_output {\n            output.push_str(&graphic_text);\n        }\n    }\n\n    // Add graphic to metadata collector\n    #[cfg(feature = \"metadata\")]\n    if ctx.metadata_wants_images {\n        if let Some(ref collector) = ctx.metadata_collector {\n            if let Some((attributes_map, width, height)) = metadata_payload {\n                if !src.is_empty() {\n                    let dimensions = match (width, height) {\n                        (Some(w), Some(h)) => Some((w, h)),\n                        _ => None,\n                    };\n                    collector.borrow_mut().add_image(\n                        src.to_string(),\n                        if alt.is_empty() { None } else { Some(alt.to_string()) },\n                        title.as_deref().map(std::string::ToString::to_string),\n                        dimensions,\n                        attributes_map,\n                    );\n                }\n            }\n        }\n    }\n}\n\n/// Format a graphic as Markdown syntax.\n///\n/// If `use_alt_only` is true, returns just the alt text.\n/// Otherwise returns the full `![alt](src \"title\")` syntax.\nfn format_graphic_markdown(\n    src: &str,\n    alt: &str,\n    title: Option<&str>,\n    use_alt_only: bool,\n    link_style: crate::options::validation::LinkStyle,\n    reference_collector: Option<&crate::converter::reference_collector::ReferenceCollectorHandle>,\n) -> String {\n    if use_alt_only {\n        return alt.to_string();\n    }\n    if link_style == crate::options::validation::LinkStyle::Reference {\n        if let Some(collector) = reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(src, title);\n            let mut buf = String::with_capacity(alt.len() + 10);\n            buf.push_str(\"![\");\n            buf.push_str(alt);\n            buf.push_str(\"][\");\n            buf.push_str(&ref_num.to_string());\n            buf.push(']');\n            return buf;\n        }\n    }\n    let mut buf = String::with_capacity(src.len() + alt.len() + 10);\n    buf.push_str(\"![\");\n    buf.push_str(alt);\n    buf.push_str(\"](\");\n    buf.push_str(src);\n    if let Some(title_text) = title {\n        buf.push_str(\" \\\"\");\n        buf.push_str(title_text);\n        buf.push('\"');\n    }\n    buf.push(')');\n    buf\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/image.rs",
    "content": "//! Image element handler for HTML to Markdown conversion.\n//!\n//! Handles `<img>` elements including:\n//! - Basic image markdown output `![alt](src \"title\")`\n//! - Metadata collection for image extraction\n//! - Inline data URI image handling\n//! - Visitor callback integration\n\nuse std::borrow::Cow;\n#[cfg(any(feature = \"metadata\", feature = \"inline-images\", feature = \"visitor\"))]\nuse std::collections::BTreeMap;\n\nuse crate::converter::Context;\nuse crate::converter::dom_context::DomContext;\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::preprocessing::sanitize_markdown_url;\nuse crate::options::ConversionOptions;\n\n#[cfg(feature = \"inline-images\")]\nuse crate::converter::media::handle_inline_data_image;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::serialization::serialize_node;\n\n#[cfg(feature = \"metadata\")]\ntype ImageMetadataPayload = (BTreeMap<String, String>, Option<u32>, Option<u32>);\n\n/// Handle an `<img>` element and convert to Markdown.\n///\n/// This handler processes image elements including:\n/// - Extracting src, alt, and title attributes\n/// - Collecting metadata when the metadata feature is enabled\n/// - Handling inline data URIs when the inline-images feature is enabled\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Generating appropriate markdown output\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_img(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let src = tag.attributes().get(\"src\").flatten().map_or(Cow::Borrowed(\"\"), |v| {\n        let s = v.as_utf8_str();\n        Cow::Owned(sanitize_markdown_url(&s).into_owned())\n    });\n\n    let alt = tag\n        .attributes()\n        .get(\"alt\")\n        .flatten()\n        .map_or(Cow::Borrowed(\"\"), |v| v.as_utf8_str());\n\n    let title = tag.attributes().get(\"title\").flatten().map(|v| v.as_utf8_str());\n\n    // Collect metadata payload if metadata feature is enabled\n    #[cfg(feature = \"metadata\")]\n    #[allow(clippy::useless_let_if_seq)]\n    let mut metadata_payload: Option<ImageMetadataPayload> = None;\n    #[cfg(feature = \"metadata\")]\n    if ctx.metadata_wants_images {\n        let mut attributes_map = BTreeMap::new();\n        let mut width: Option<u32> = None;\n        let mut height: Option<u32> = None;\n        for (key, value_opt) in tag.attributes().iter() {\n            let key_str = key.to_string();\n            if key_str == \"src\" {\n                continue;\n            }\n            let value = value_opt.map(|v| v.to_string()).unwrap_or_default();\n            if key_str == \"width\" {\n                if let Ok(parsed) = value.parse::<u32>() {\n                    width = Some(parsed);\n                }\n            } else if key_str == \"height\" {\n                if let Ok(parsed) = value.parse::<u32>() {\n                    height = Some(parsed);\n                }\n            }\n            attributes_map.insert(key_str, value);\n        }\n        metadata_payload = Some((attributes_map, width, height));\n    }\n\n    // Handle inline data URI images\n    #[cfg(feature = \"inline-images\")]\n    if let Some(ref collector_ref) = ctx.inline_collector {\n        if src.trim_start().starts_with(\"data:\") {\n            let mut attributes_map = BTreeMap::new();\n            for (key, value_opt) in tag.attributes().iter() {\n                let key_str = key.to_string();\n                let keep = key_str == \"width\"\n                    || key_str == \"height\"\n                    || key_str == \"filename\"\n                    || key_str == \"aria-label\"\n                    || key_str.starts_with(\"data-\");\n                if keep {\n                    let value = value_opt.map(|value| value.to_string()).unwrap_or_default();\n                    attributes_map.insert(key_str, value);\n                }\n            }\n            handle_inline_data_image(\n                collector_ref,\n                src.as_ref(),\n                alt.as_ref(),\n                title.as_deref(),\n                attributes_map,\n            );\n        }\n    }\n\n    let keep_as_markdown = ctx.in_heading && ctx.heading_allow_inline_images;\n\n    let should_use_alt_text =\n        !keep_as_markdown && (ctx.convert_as_inline || (ctx.in_heading && !ctx.heading_allow_inline_images));\n\n    // Generate image output with visitor integration\n    #[cfg(feature = \"visitor\")]\n    let image_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Image,\n            tag_name: \"img\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_image(&node_ctx, &src, &alt, title.as_deref())\n        };\n        match visit_result {\n            VisitResult::Continue => Some(format_image_markdown(\n                &src,\n                &alt,\n                title.as_deref(),\n                should_use_alt_text,\n                options.link_style,\n                ctx.reference_collector.as_ref(),\n            )),\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => None,\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n            VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n        }\n    } else {\n        Some(format_image_markdown(\n            &src,\n            &alt,\n            title.as_deref(),\n            should_use_alt_text,\n            options.link_style,\n            ctx.reference_collector.as_ref(),\n        ))\n    };\n\n    #[cfg(not(feature = \"visitor\"))]\n    let image_output = Some(format_image_markdown(\n        &src,\n        &alt,\n        title.as_deref(),\n        should_use_alt_text,\n        options.link_style,\n        ctx.reference_collector.as_ref(),\n    ));\n\n    // Only output image if skip_images is not enabled\n    if !options.skip_images {\n        if let Some(img_text) = image_output {\n            output.push_str(&img_text);\n        }\n    }\n\n    // Add image to metadata collector\n    #[cfg(feature = \"metadata\")]\n    if ctx.metadata_wants_images {\n        if let Some(ref collector) = ctx.metadata_collector {\n            if let Some((attributes_map, width, height)) = metadata_payload {\n                if !src.is_empty() {\n                    let dimensions = match (width, height) {\n                        (Some(w), Some(h)) => Some((w, h)),\n                        _ => None,\n                    };\n                    collector.borrow_mut().add_image(\n                        src.to_string(),\n                        if alt.is_empty() { None } else { Some(alt.to_string()) },\n                        title.as_deref().map(std::string::ToString::to_string),\n                        dimensions,\n                        attributes_map,\n                    );\n                }\n            }\n        }\n    }\n\n    if let Some(ref sc) = ctx.structure_collector {\n        let src_opt = if src.is_empty() { None } else { Some(src.as_ref()) };\n        let alt_opt = if alt.is_empty() { None } else { Some(alt.as_ref()) };\n        sc.borrow_mut().push_image(src_opt, alt_opt);\n    }\n}\n\n/// Format an image as Markdown syntax.\n///\n/// If `use_alt_only` is true, returns just the alt text.\n/// Otherwise returns the full `![alt](src \"title\")` syntax.\nfn format_image_markdown(\n    src: &str,\n    alt: &str,\n    title: Option<&str>,\n    use_alt_only: bool,\n    link_style: crate::options::validation::LinkStyle,\n    reference_collector: Option<&crate::converter::reference_collector::ReferenceCollectorHandle>,\n) -> String {\n    if use_alt_only {\n        return alt.to_string();\n    }\n    if link_style == crate::options::validation::LinkStyle::Reference {\n        if let Some(collector) = reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(src, title);\n            let mut buf = String::with_capacity(alt.len() + 10);\n            buf.push_str(\"![\");\n            buf.push_str(alt);\n            buf.push_str(\"][\");\n            buf.push_str(&ref_num.to_string());\n            buf.push(']');\n            return buf;\n        }\n    }\n    let mut buf = String::with_capacity(src.len() + alt.len() + 10);\n    buf.push_str(\"![\");\n    buf.push_str(alt);\n    buf.push_str(\"](\");\n    buf.push_str(src);\n    if let Some(title_text) = title {\n        buf.push_str(\" \\\"\");\n        buf.push_str(title_text);\n        buf.push('\"');\n    }\n    buf.push(')');\n    buf\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/link.rs",
    "content": "//! Link element handler for HTML to Markdown conversion.\n//!\n//! Handles `<a>` elements including:\n//! - Basic link markdown output `[text](href \"title\")`\n//! - Autolinks when text matches href\n//! - Links containing heading elements\n//! - Complex link content with mixed block/inline elements\n//! - Visitor callback integration\n//! - Link metadata collection\n\n#[cfg(any(feature = \"metadata\", feature = \"visitor\"))]\nuse std::collections::BTreeMap;\n\nuse crate::converter::Context;\nuse crate::converter::block::heading::{find_single_heading_child, heading_allows_inline_images, push_heading};\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::inline::link::append_markdown_link;\nuse crate::converter::main::walk_node;\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::content::{\n    collect_link_label_text, escape_link_label, get_text_content, normalize_link_label, normalized_tag_name,\n};\nuse crate::options::ConversionOptions;\nuse crate::text;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::serialization::serialize_node;\n\n/// Handle an `<a>` (link) element and convert to Markdown.\n///\n/// This handler processes link elements including:\n/// - Extracting href and title attributes\n/// - Detecting autolinks (where text equals href)\n/// - Handling links that contain heading elements\n/// - Processing complex link content (mixed block/inline)\n/// - Invoking visitor callbacks when the visitor feature is enabled\n/// - Collecting link metadata when the metadata feature is enabled\n/// - Generating appropriate markdown link output\n#[allow(clippy::too_many_arguments)]\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_link(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let href_attr = tag\n        .attributes()\n        .get(\"href\")\n        .flatten()\n        .map(|v| text::decode_html_entities(&v.as_utf8_str()));\n    let title = tag\n        .attributes()\n        .get(\"title\")\n        .flatten()\n        .map(|v| v.as_utf8_str().to_string());\n\n    if let Some(href) = href_attr {\n        let raw_text = text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx))\n            .trim()\n            .to_string();\n\n        let is_autolink = options.autolinks\n            && !options.default_title\n            && !href.is_empty()\n            && (raw_text == href || (href.starts_with(\"mailto:\") && raw_text == href[7..]));\n\n        if is_autolink {\n            output.push('<');\n            if href.starts_with(\"mailto:\") && raw_text == href[7..] {\n                output.push_str(&raw_text);\n            } else {\n                output.push_str(&href);\n            }\n            output.push('>');\n            return;\n        }\n\n        if let Some((heading_level, heading_handle)) = find_single_heading_child(*node_handle, parser) {\n            if let Some(heading_node) = heading_handle.get(parser) {\n                if let tl::Node::Tag(heading_tag) = heading_node {\n                    let heading_name = normalized_tag_name(heading_tag.name().as_utf8_str()).into_owned();\n                    let mut heading_text = String::new();\n                    let heading_ctx = Context {\n                        in_heading: true,\n                        convert_as_inline: true,\n                        heading_allow_inline_images: heading_allows_inline_images(\n                            &heading_name,\n                            &ctx.keep_inline_images_in,\n                        ),\n                        ..ctx.clone()\n                    };\n                    walk_node(\n                        &heading_handle,\n                        parser,\n                        &mut heading_text,\n                        options,\n                        &heading_ctx,\n                        depth + 1,\n                        dom_ctx,\n                    );\n                    let trimmed_heading = heading_text.trim();\n                    if !trimmed_heading.is_empty() {\n                        let escaped_label = escape_link_label(trimmed_heading);\n                        let mut link_buffer = String::new();\n                        append_markdown_link(\n                            &mut link_buffer,\n                            &escaped_label,\n                            href.as_str(),\n                            title.as_deref(),\n                            raw_text.as_str(),\n                            options,\n                            ctx.reference_collector.as_ref(),\n                        );\n                        push_heading(output, ctx, options, heading_level, link_buffer.as_str());\n                        return;\n                    }\n                }\n            }\n        }\n\n        let children: Vec<_> = tag.children().top().iter().copied().collect();\n        let (inline_label, _block_nodes, saw_block) = collect_link_label_text(&children, parser, dom_ctx);\n        let mut label = if saw_block {\n            let mut content = String::new();\n            let link_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                convert_as_inline: true,\n                ..ctx.clone()\n            };\n            for child_handle in &children {\n                let mut child_buf = String::new();\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut child_buf,\n                    options,\n                    &link_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n                if !child_buf.trim().is_empty()\n                    && !content.is_empty()\n                    && !content.chars().last().is_none_or(char::is_whitespace)\n                    && !child_buf.chars().next().is_none_or(char::is_whitespace)\n                {\n                    content.push(' ');\n                }\n                content.push_str(&child_buf);\n            }\n            if content.trim().is_empty() {\n                normalize_link_label(&inline_label)\n            } else {\n                normalize_link_label(&content)\n            }\n        } else {\n            let mut content = String::new();\n            let link_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                ..ctx.clone()\n            };\n            for child_handle in &children {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &link_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n            normalize_link_label(&content)\n        };\n\n        if label.is_empty() && saw_block {\n            let fallback = text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx));\n            label = normalize_link_label(&fallback);\n        }\n\n        if label.is_empty() && !raw_text.is_empty() {\n            label = normalize_link_label(&raw_text);\n        }\n\n        if label.is_empty() && !href.is_empty() && !children.is_empty() {\n            label = href.clone();\n        }\n\n        // Normalize Wikipedia-style back-reference links: <a href=\"#cite_ref-N\">^</a>\n        // These produce `[^](#cite_ref-N)` which is confusing (looks like a footnote).\n        // Convert to `[↑](#cite_ref-N)` to avoid ambiguity with markdown footnote syntax.\n        if label == \"^\" && href.starts_with('#') {\n            label = \"↑\".to_string();\n        }\n\n        let escaped_label = escape_link_label(&label);\n\n        #[cfg(feature = \"visitor\")]\n        let link_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Link,\n                tag_name: \"a\".to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: true,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_link(&node_ctx, &href, &label, title.as_deref())\n            };\n            match visit_result {\n                VisitResult::Continue => {\n                    let mut buf = String::new();\n                    append_markdown_link(\n                        &mut buf,\n                        &escaped_label,\n                        href.as_str(),\n                        title.as_deref(),\n                        label.as_str(),\n                        options,\n                        ctx.reference_collector.as_ref(),\n                    );\n                    Some(buf)\n                }\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => None,\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n                VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n            }\n        } else {\n            let mut buf = String::new();\n            append_markdown_link(\n                &mut buf,\n                &escaped_label,\n                href.as_str(),\n                title.as_deref(),\n                label.as_str(),\n                options,\n                ctx.reference_collector.as_ref(),\n            );\n            Some(buf)\n        };\n\n        #[cfg(not(feature = \"visitor\"))]\n        let link_output = {\n            let mut buf = String::new();\n            append_markdown_link(\n                &mut buf,\n                &escaped_label,\n                href.as_str(),\n                title.as_deref(),\n                label.as_str(),\n                options,\n                ctx.reference_collector.as_ref(),\n            );\n            Some(buf)\n        };\n\n        if let Some(link_text) = link_output {\n            output.push_str(&link_text);\n        }\n\n        #[cfg(feature = \"metadata\")]\n        if ctx.metadata_wants_links {\n            if let Some(ref collector) = ctx.metadata_collector {\n                let rel_attr = tag\n                    .attributes()\n                    .get(\"rel\")\n                    .flatten()\n                    .map(|v| v.as_utf8_str().to_string());\n                let mut attributes_map = BTreeMap::new();\n                for (key, value_opt) in tag.attributes().iter() {\n                    let key_str = key.to_string();\n                    if key_str == \"href\" {\n                        continue;\n                    }\n\n                    let value = value_opt.map(|v| v.to_string()).unwrap_or_default();\n                    attributes_map.insert(key_str, value);\n                }\n                collector\n                    .borrow_mut()\n                    .add_link(href.clone(), label, title.clone(), rel_attr, attributes_map);\n            }\n        }\n    } else {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/handlers/mod.rs",
    "content": "//! Element handlers extracted from the main conversion pipeline.\n//!\n//! This module contains handler functions for specific HTML elements,\n//! allowing the main walk_node function to delegate to specialized handlers.\n//!\n//! Each handler takes the standard set of parameters:\n//! - `node_handle`: Reference to the DOM node\n//! - `tag`: The HTML tag being processed\n//! - `parser`: The DOM parser\n//! - `output`: The output string buffer\n//! - `options`: Conversion options\n//! - `ctx`: Conversion context\n//! - `depth`: Current tree depth\n//! - `dom_ctx`: DOM context cache\n\npub mod blockquote;\npub mod code_block;\npub mod graphic;\npub mod image;\npub mod link;\n\npub use blockquote::handle_blockquote;\npub use code_block::{handle_code, handle_pre};\npub use graphic::handle_graphic;\npub use image::handle_img;\npub use link::handle_link;\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/code.rs",
    "content": "//! Handler for code-related inline elements (code, kbd, samp).\n//!\n//! Converts HTML code elements to Markdown inline code formatting with support for:\n//! - Inline code blocks with backtick delimiters\n//! - Keyboard input (<kbd>) rendered as code\n//! - Sample output (<samp>) rendered as code\n//! - Smart backtick escaping for nested backticks\n//! - Delimiter spacing to prevent ambiguous parsing\n//! - Nested code context tracking (suppress formatting in <code> within <code>)\n//! - Visitor callbacks for custom code processing\n//! - Whitespace normalization for kbd/samp elements\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::ConversionOptions;\nuse crate::text;\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\n// These are imported from converter.rs and should be made accessible\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handler for code-related inline elements: code, kbd (keyboard), and samp (sample output).\n///\n/// Processes code content based on context:\n/// - For <code> within <code>: passes content through without wrapping backticks (nested code detection)\n/// - For <kbd> and <samp>: normalizes whitespace and wraps with backticks\n/// - For standalone <code>: applies smart backtick escaping and delimiter spacing\n/// - Handles visitor callbacks for custom behavior when feature is enabled\n/// - Properly escapes backticks in content that contains them\n///\n/// # Note\n/// This function references helper functions and `walk_node` from converter.rs\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Import helper functions from parent converter module\n\n    match tag_name {\n        \"code\" => {\n            handle_code(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"kbd\" | \"samp\" => {\n            handle_kbd_samp(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        _ => {}\n    }\n}\n\n/// Handle inline code element (<code> tag).\n///\n/// Smart handling of nested code:\n/// - If already in code context (ctx.in_code), just process children without wrapping\n/// - Otherwise, wraps content with backticks with smart escaping logic\n///\n/// Smart backtick escaping:\n/// - Detects consecutive backticks in content\n/// - Adds extra backticks if needed to avoid ambiguous parsing\n/// - Adds space delimiters if content starts/ends with backticks or spaces\nfn handle_code(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let code_ctx = Context {\n        in_code: true,\n        ..ctx.clone()\n    };\n\n    // Nested code detection: if already in code, just process children\n    if ctx.in_code {\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, &code_ctx, depth + 1, dom_ctx);\n        }\n    } else {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &code_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n\n        let trimmed = &content;\n\n        if !content.trim().is_empty() {\n            #[cfg(feature = \"visitor\")]\n            let code_output = if let Some(ref visitor_handle) = ctx.visitor {\n                use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n                let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n                let node_id = node_handle.get_inner();\n                let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n                let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n                let node_ctx = NodeContext {\n                    node_type: NodeType::Code,\n                    tag_name: tag.name().as_utf8_str().to_string(),\n                    attributes,\n                    depth,\n                    index_in_parent,\n                    parent_tag,\n                    is_inline: true,\n                };\n\n                let visit_result = {\n                    let mut visitor = visitor_handle.borrow_mut();\n                    visitor.visit_code_inline(&node_ctx, trimmed)\n                };\n                match visit_result {\n                    VisitResult::Continue => None,\n                    VisitResult::Custom(custom) => Some(custom),\n                    VisitResult::Skip => Some(String::new()),\n                    VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n                    VisitResult::Error(err) => {\n                        if ctx.visitor_error.borrow().is_none() {\n                            *ctx.visitor_error.borrow_mut() = Some(err);\n                        }\n                        None\n                    }\n                }\n            } else {\n                None\n            };\n\n            #[cfg(feature = \"visitor\")]\n            if let Some(custom_output) = code_output {\n                output.push_str(&custom_output);\n            } else {\n                render_code_with_escaping(trimmed, output);\n            }\n\n            #[cfg(not(feature = \"visitor\"))]\n            {\n                render_code_with_escaping(trimmed, output);\n            }\n        }\n    }\n}\n\n/// Handle keyboard and sample output elements (<kbd> and <samp> tags).\n///\n/// These elements are rendered as inline code with:\n/// - Whitespace normalization (via text::normalize_whitespace)\n/// - Chomp inline handling for prefix/suffix spacing\n/// - Simple single backtick wrapping (no smart escaping for keyboard/sample)\nfn handle_kbd_samp(\n    _tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::{append_inline_suffix, chomp_inline, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let _tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let code_ctx = Context {\n        in_code: true,\n        ..ctx.clone()\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = _tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(\n                child_handle,\n                parser,\n                &mut content,\n                options,\n                &code_ctx,\n                depth + 1,\n                dom_ctx,\n            );\n        }\n    }\n\n    // Normalize whitespace for kbd/samp (unlike code, which preserves it)\n    let normalized = text::normalize_whitespace(&content);\n    let (prefix, suffix, trimmed) = chomp_inline(&normalized);\n\n    if !content.trim().is_empty() {\n        output.push_str(prefix);\n        output.push('`');\n        output.push_str(trimmed);\n        output.push('`');\n        append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n    } else if !content.is_empty() {\n        output.push_str(prefix);\n        append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n    }\n}\n\n/// Render inline code with smart backtick escaping.\n///\n/// Handles the logic for:\n/// 1. Detecting backticks in content\n/// 2. Calculating the required number of backticks\n/// 3. Adding delimiter spaces when needed\n///\n/// # Backtick Escaping Logic\n///\n/// - If content contains no backticks: use single backtick delimiters\n/// - If content has single backticks: use double backtick delimiters\n/// - If content has consecutive backticks: use single backtick but rely on spacing\n///\n/// # Delimiter Space Rules\n///\n/// Add space delimiters if:\n/// - Content is all spaces\n/// - Content starts/ends with backtick\n/// - Content starts and ends with spaces AND contains backticks\nfn render_code_with_escaping(trimmed: &str, output: &mut String) {\n    let contains_backtick = trimmed.contains('`');\n\n    let needs_delimiter_spaces = {\n        let first_char = trimmed.chars().next();\n        let last_char = trimmed.chars().last();\n        let starts_with_space = first_char == Some(' ');\n        let ends_with_space = last_char == Some(' ');\n        let starts_with_backtick = first_char == Some('`');\n        let ends_with_backtick = last_char == Some('`');\n        let all_spaces = trimmed.chars().all(|c| c == ' ');\n\n        all_spaces\n            || starts_with_backtick\n            || ends_with_backtick\n            || (starts_with_space && ends_with_space && contains_backtick)\n    };\n\n    let (num_backticks, needs_spaces) = if contains_backtick {\n        let max_consecutive = trimmed\n            .chars()\n            .fold((0, 0), |(max, current), c| {\n                if c == '`' {\n                    let new_current = current + 1;\n                    (max.max(new_current), new_current)\n                } else {\n                    (max, 0)\n                }\n            })\n            .0;\n        let num = if max_consecutive == 1 { 2 } else { 1 };\n        (num, needs_delimiter_spaces)\n    } else {\n        (1, needs_delimiter_spaces)\n    };\n\n    for _ in 0..num_backticks {\n        output.push('`');\n    }\n    if needs_spaces {\n        output.push(' ');\n    }\n    output.push_str(trimmed);\n    if needs_spaces {\n        output.push(' ');\n    }\n    for _ in 0..num_backticks {\n        output.push('`');\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/emphasis.rs",
    "content": "//! Handler for emphasis elements (strong, b, em, i).\n//!\n//! Converts HTML emphasis tags to Markdown formatting with support for:\n//! - Bold/strong formatting using configurable symbols (** or __)\n//! - Italic/emphasis formatting using configurable symbols (* or _)\n//! - Nested emphasis context tracking\n//! - Code context handling (suppress formatting in <code>)\n//! - Visitor callbacks for custom emphasis processing\n//! - Bootstrap caret detection (.caret class)\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::{ConversionOptions, OutputFormat};\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\n// These are imported from converter.rs and should be made accessible\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handler for emphasis elements: strong, b (bold) and em, i (italic).\n///\n/// Processes emphasis content based on context:\n/// - Suppresses formatting when already in strong/code context\n/// - Applies configurable emphasis symbols (* or _)\n/// - Handles nested emphasis with proper context tracking\n/// - Supports visitor callbacks for custom behavior\n/// - Detects Bootstrap caret elements (.caret class)\n///\n/// # Note\n/// This function references helper functions and `walk_node` from converter.rs\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Import helper functions from parent converter module\n\n    match tag_name {\n        \"strong\" | \"b\" => {\n            handle_strong(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"em\" | \"i\" => {\n            handle_emphasis(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        _ => {}\n    }\n}\n\n/// Handle strong/bold emphasis (strong, b tags).\nfn handle_strong(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{append_inline_suffix, chomp_inline, get_text_content, serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    if ctx.in_code {\n        // Suppress formatting in code context, just process children\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    } else {\n        let mut content = String::with_capacity(64);\n        let children = tag.children();\n        {\n            let strong_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                in_strong: true,\n                ..ctx.clone()\n            };\n            for child_handle in children.top().iter() {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &strong_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n\n        #[cfg(feature = \"visitor\")]\n        let strong_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            let text_content = get_text_content(node_handle, parser, dom_ctx);\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Strong,\n                tag_name: tag.name().as_utf8_str().to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: true,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_strong(&node_ctx, &text_content)\n            };\n            match visit_result {\n                VisitResult::Continue => None,\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => Some(String::new()),\n                VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n            }\n        } else {\n            None\n        };\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(custom_output) = strong_output {\n            output.push_str(&custom_output);\n        } else {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if ctx.in_strong {\n                    output.push_str(trimmed);\n                } else if options.output_format == OutputFormat::Djot {\n                    // Djot uses single asterisk for strong\n                    output.push('*');\n                    output.push_str(trimmed);\n                    output.push('*');\n                } else {\n                    output.push(options.strong_em_symbol);\n                    output.push(options.strong_em_symbol);\n                    output.push_str(trimmed);\n                    output.push(options.strong_em_symbol);\n                    output.push(options.strong_em_symbol);\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            }\n        }\n\n        #[cfg(not(feature = \"visitor\"))]\n        {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if ctx.in_strong {\n                    output.push_str(trimmed);\n                } else if options.output_format == OutputFormat::Djot {\n                    // Djot uses single asterisk for strong\n                    output.push('*');\n                    output.push_str(trimmed);\n                    output.push('*');\n                } else {\n                    output.push(options.strong_em_symbol);\n                    output.push(options.strong_em_symbol);\n                    output.push_str(trimmed);\n                    output.push(options.strong_em_symbol);\n                    output.push(options.strong_em_symbol);\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            }\n        }\n    }\n}\n\n/// Handle emphasis/italic (em, i tags).\nfn handle_emphasis(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{append_inline_suffix, chomp_inline, get_text_content, serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    if ctx.in_code {\n        // Suppress formatting in code context, just process children\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    } else {\n        let mut content = String::with_capacity(64);\n        let children = tag.children();\n        {\n            let em_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                ..ctx.clone()\n            };\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, &em_ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        #[cfg(feature = \"visitor\")]\n        let em_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            let text_content = get_text_content(node_handle, parser, dom_ctx);\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Em,\n                tag_name: tag.name().as_utf8_str().to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: true,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_emphasis(&node_ctx, &text_content)\n            };\n            match visit_result {\n                VisitResult::Continue => None,\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => Some(String::new()),\n                VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n            }\n        } else {\n            None\n        };\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(custom_output) = em_output {\n            output.push_str(&custom_output);\n        } else {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if options.output_format == OutputFormat::Djot {\n                    // Djot uses underscore for emphasis\n                    output.push('_');\n                    output.push_str(trimmed);\n                    output.push('_');\n                } else {\n                    output.push(options.strong_em_symbol);\n                    output.push_str(trimmed);\n                    output.push(options.strong_em_symbol);\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            } else if let Some(class_value) = tag\n                .attributes()\n                .get(\"class\")\n                .and_then(|v| v.as_ref().map(|val| val.as_utf8_str().to_string()))\n            {\n                if class_value.contains(\"caret\") && !output.ends_with(' ') {\n                    output.push_str(\" > \");\n                }\n            }\n        }\n\n        #[cfg(not(feature = \"visitor\"))]\n        {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if options.output_format == OutputFormat::Djot {\n                    // Djot uses underscore for emphasis\n                    output.push('_');\n                    output.push_str(trimmed);\n                    output.push('_');\n                } else {\n                    output.push(options.strong_em_symbol);\n                    output.push_str(trimmed);\n                    output.push(options.strong_em_symbol);\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            } else if let Some(class_value) = tag\n                .attributes()\n                .get(\"class\")\n                .and_then(|v| v.as_ref().map(|val| val.as_utf8_str().to_string()))\n            {\n                if class_value.contains(\"caret\") && !output.ends_with(' ') {\n                    output.push_str(\" > \");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/link.rs",
    "content": "//! Handler for link elements (a, anchor).\n//!\n//! Converts HTML anchor tags to Markdown links with support for:\n//! - Standard Markdown link syntax `[label](href \"title\")`\n//! - Autolinks for simple URLs like `<https://example.com>`\n//! - Link label escaping for special Markdown characters\n//! - Heading-in-link special handling (wraps link around heading)\n//! - Visitor callbacks for custom link processing\n//! - Metadata collection for links (links, URLs, titles, rel attributes)\n//! - Block-level content within links (via inline context)\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::content::{collect_link_label_text, escape_link_label, normalize_link_label};\nuse crate::converter::utility::preprocessing::sanitize_markdown_url;\nuse crate::options::ConversionOptions;\n#[cfg(any(feature = \"metadata\", feature = \"visitor\"))]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\n// These are imported from converter.rs and should be made accessible\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handler for anchor/link elements: `<a>`.\n///\n/// Processes anchor tags to generate Markdown links:\n/// - Detects autolinks (link text matches href)\n/// - Extracts and normalizes link labels\n/// - Handles nested headings within links\n/// - Escapes special characters in labels\n/// - Collects metadata when feature is enabled\n/// - Supports visitor callbacks for custom processing\n///\n/// # Link Label Extraction\n/// For links with block-level content, extracts text separately.\n/// Collapses newlines and normalizes whitespace per Markdown spec.\n///\n/// # Autolinks\n/// When `autolinks` option is enabled, detects links where the text equals\n/// the href (e.g., `<a href=\"https://example.com\">https://example.com</a>`)\n/// and outputs as `<https://example.com>` instead.\n///\n/// # Note\n/// This function references helper functions from converter.rs\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Import helper functions from parent converter module\n    use crate::converter::block::heading::{heading_allows_inline_images, push_heading};\n    use crate::converter::utility::content::normalized_tag_name;\n    #[allow(unused_imports)]\n    use crate::converter::utility::serialization::serialize_node;\n    use crate::converter::{find_single_heading_child, get_text_content, walk_node};\n\n    let Some(node) = node_handle.get(parser) else {\n        return;\n    };\n\n    let tl::Node::Tag(tag) = node else {\n        return;\n    };\n\n    // Extract href and title attributes\n    let href_attr = tag.attributes().get(\"href\").flatten().map(|v| {\n        let decoded = crate::text::decode_html_entities(&v.as_utf8_str());\n        sanitize_markdown_url(&decoded).into_owned()\n    });\n    let title = tag\n        .attributes()\n        .get(\"title\")\n        .flatten()\n        .map(|v| v.as_utf8_str().to_string());\n\n    if let Some(href) = href_attr {\n        let raw_text = crate::text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx))\n            .trim()\n            .to_string();\n\n        // If we're already inside a link, just render the text content, don't create a nested link\n        if ctx.in_link {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n            return;\n        }\n\n        // Check if this should be rendered as an autolink\n        let is_autolink = options.autolinks\n            && !options.default_title\n            && !href.is_empty()\n            && (raw_text == href || (href.starts_with(\"mailto:\") && raw_text == href[7..]));\n\n        if is_autolink {\n            output.push('<');\n            if href.starts_with(\"mailto:\") && raw_text == href[7..] {\n                output.push_str(&raw_text);\n            } else {\n                output.push_str(&href);\n            }\n            output.push('>');\n            return;\n        }\n\n        // Check if link contains a single heading child element\n        if let Some((heading_level, heading_handle)) = find_single_heading_child(*node_handle, parser) {\n            if let Some(heading_node) = heading_handle.get(parser) {\n                if let tl::Node::Tag(heading_tag) = heading_node {\n                    let heading_name = normalized_tag_name(heading_tag.name().as_utf8_str()).into_owned();\n                    let mut heading_text = String::new();\n                    let heading_ctx = Context {\n                        in_heading: true,\n                        convert_as_inline: true,\n                        heading_allow_inline_images: heading_allows_inline_images(\n                            &heading_name,\n                            &ctx.keep_inline_images_in,\n                        ),\n                        ..ctx.clone()\n                    };\n                    walk_node(\n                        &heading_handle,\n                        parser,\n                        &mut heading_text,\n                        options,\n                        &heading_ctx,\n                        depth + 1,\n                        dom_ctx,\n                    );\n                    let trimmed_heading = heading_text.trim();\n                    if !trimmed_heading.is_empty() {\n                        let escaped_label = escape_link_label(trimmed_heading);\n                        let mut link_buffer = String::new();\n                        append_markdown_link(\n                            &mut link_buffer,\n                            &escaped_label,\n                            href.as_str(),\n                            title.as_deref(),\n                            raw_text.as_str(),\n                            options,\n                            ctx.reference_collector.as_ref(),\n                        );\n                        push_heading(output, ctx, options, heading_level, link_buffer.as_str());\n                        return;\n                    }\n                }\n            }\n        }\n\n        // Collect link label from children\n        let children: Vec<_> = tag.children().top().iter().copied().collect();\n        let (inline_label, _block_nodes, saw_block) = collect_link_label_text(&children, parser, dom_ctx);\n        let mut label = if saw_block {\n            let mut content = String::new();\n            let link_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                convert_as_inline: true,\n                in_link: true,\n                ..ctx.clone()\n            };\n            for child_handle in &children {\n                let mut child_buf = String::new();\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut child_buf,\n                    options,\n                    &link_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n                if !child_buf.trim().is_empty()\n                    && !content.is_empty()\n                    && !content.chars().last().is_none_or(char::is_whitespace)\n                    && !child_buf.chars().next().is_none_or(char::is_whitespace)\n                {\n                    content.push(' ');\n                }\n                content.push_str(&child_buf);\n            }\n            if content.trim().is_empty() {\n                normalize_link_label(&inline_label)\n            } else {\n                normalize_link_label(&content)\n            }\n        } else {\n            let mut content = String::new();\n            let link_ctx = Context {\n                inline_depth: ctx.inline_depth + 1,\n                in_link: true,\n                ..ctx.clone()\n            };\n            for child_handle in &children {\n                walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &link_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n            normalize_link_label(&content)\n        };\n\n        // Apply fallback label strategies\n        if label.is_empty() && saw_block {\n            let fallback = crate::text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx));\n            label = normalize_link_label(&fallback);\n        }\n\n        if label.is_empty() && !raw_text.is_empty() {\n            label = normalize_link_label(&raw_text);\n        }\n\n        if label.is_empty() && !href.is_empty() && !children.is_empty() {\n            label = href.clone();\n        }\n\n        // Truncate label if it exceeds maximum length\n        let escaped_label = escape_link_label(&label);\n\n        // Handle visitor callbacks if feature is enabled\n        #[cfg(feature = \"visitor\")]\n        let link_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Link,\n                tag_name: \"a\".to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: true,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_link(&node_ctx, &href, &label, title.as_deref())\n            };\n            match visit_result {\n                VisitResult::Continue => {\n                    let mut buf = String::new();\n                    append_markdown_link(\n                        &mut buf,\n                        &escaped_label,\n                        href.as_str(),\n                        title.as_deref(),\n                        label.as_str(),\n                        options,\n                        ctx.reference_collector.as_ref(),\n                    );\n                    Some(buf)\n                }\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => None,\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n                VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n            }\n        } else {\n            let mut buf = String::new();\n            append_markdown_link(\n                &mut buf,\n                &escaped_label,\n                href.as_str(),\n                title.as_deref(),\n                label.as_str(),\n                options,\n                ctx.reference_collector.as_ref(),\n            );\n            Some(buf)\n        };\n\n        #[cfg(not(feature = \"visitor\"))]\n        let link_output = {\n            let mut buf = String::new();\n            append_markdown_link(\n                &mut buf,\n                &escaped_label,\n                href.as_str(),\n                title.as_deref(),\n                label.as_str(),\n                options,\n                ctx.reference_collector.as_ref(),\n            );\n            Some(buf)\n        };\n\n        if let Some(link_text) = link_output {\n            output.push_str(&link_text);\n        }\n\n        // Collect metadata if feature is enabled\n        #[cfg(feature = \"metadata\")]\n        if ctx.metadata_wants_links {\n            if let Some(ref collector) = ctx.metadata_collector {\n                let rel_attr = tag\n                    .attributes()\n                    .get(\"rel\")\n                    .flatten()\n                    .map(|v| v.as_utf8_str().to_string());\n                let mut attributes_map = BTreeMap::new();\n                for (key, value_opt) in tag.attributes().iter() {\n                    let key_str = key.to_string();\n                    if key_str == \"href\" {\n                        continue;\n                    }\n\n                    let value = value_opt.map(|v| v.to_string()).unwrap_or_default();\n                    attributes_map.insert(key_str, value);\n                }\n                collector\n                    .borrow_mut()\n                    .add_link(href.clone(), label, title.clone(), rel_attr, attributes_map);\n            }\n        }\n    } else {\n        // No href: just process children as inline content\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n}\n\n/// Format and append a Markdown link to the output string.\n///\n/// Generates the link syntax: `[label](href \"title\")`\n/// Handles special cases:\n/// - Empty href renders as `[label]()`\n/// - Hrefs with spaces/newlines get wrapped in angle brackets: `[label](<URL with spaces>)`\n/// - Unbalanced parentheses in href get escaped: `[label](url\\(example\\))`\n/// - Titles are wrapped in quotes and quotes inside are escaped\n/// - When `default_title` option is true and raw_text equals href, adds href as title\n///\n/// # Arguments\n/// * `output` - Output buffer to append the link to\n/// * `label` - The link text (already escaped)\n/// * `href` - The URL/destination\n/// * `title` - Optional link title attribute\n/// * `raw_text` - Original unprocessed text (for default_title option)\n/// * `options` - Conversion options\npub fn append_markdown_link(\n    output: &mut String,\n    label: &str,\n    href: &str,\n    title: Option<&str>,\n    raw_text: &str,\n    options: &ConversionOptions,\n    reference_collector: Option<&crate::converter::reference_collector::ReferenceCollectorHandle>,\n) {\n    if options.link_style == crate::options::validation::LinkStyle::Reference && !href.is_empty() {\n        if let Some(collector) = reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(href, title);\n            output.push('[');\n            output.push_str(label);\n            output.push_str(\"][\");\n            output.push_str(&ref_num.to_string());\n            output.push(']');\n            return;\n        }\n    }\n\n    output.push('[');\n    output.push_str(label);\n    output.push_str(\"](\");\n\n    if href.is_empty() {\n        output.push_str(\"<>\");\n    } else if href.contains(' ') || href.contains('\\n') {\n        output.push('<');\n        output.push_str(href);\n        output.push('>');\n    } else {\n        let open_count = href.chars().filter(|&c| c == '(').count();\n        let close_count = href.chars().filter(|&c| c == ')').count();\n\n        if open_count == close_count {\n            output.push_str(href);\n        } else {\n            let escaped_href = href.replace('(', \"\\\\(\").replace(')', \"\\\\)\");\n            output.push_str(&escaped_href);\n        }\n    }\n\n    if let Some(title_text) = title {\n        output.push_str(\" \\\"\");\n        if title_text.contains('\"') {\n            let escaped_title = title_text.replace('\"', \"\\\\\\\"\");\n            output.push_str(&escaped_title);\n        } else {\n            output.push_str(title_text);\n        }\n        output.push('\"');\n    } else if options.default_title && raw_text == href {\n        output.push_str(\" \\\"\");\n        if href.contains('\"') {\n            let escaped_href = href.replace('\"', \"\\\\\\\"\");\n            output.push_str(&escaped_href);\n        } else {\n            output.push_str(href);\n        }\n        output.push('\"');\n    }\n\n    output.push(')');\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/mod.rs",
    "content": "//! Inline element handlers for HTML to Markdown conversion.\n//!\n//! This module provides specialized handlers for inline HTML elements:\n//! - Emphasis elements (strong, b, em, i)\n//! - Links (a)\n//! - Code elements (code, kbd, samp)\n//! - Semantic elements (mark, del, s, ins, u, small, sub, sup, var, dfn, abbr)\n//! - Ruby annotation elements (ruby, rb, rt, rp, rtc)\n//!\n//! These handlers are designed to be extracted from the main `converter.rs`\n//! file and integrated through the dispatcher function.\n//!\n//! **Integration Pattern:**\n//! Each handler function takes the same signature:\n//! - `tag_name: &str` - The HTML tag being processed\n//! - `node_handle: &NodeHandle` - The DOM node handle\n//! - `parser: &Parser` - The HTML parser reference\n//! - `output: &mut String` - The output buffer to write to\n//! - `options: &ConversionOptions` - Conversion configuration\n//! - `ctx: &Context` - Processing context (state tracking)\n//! - `depth: usize` - Current DOM tree depth\n//! - `dom_ctx: &DomContext` - DOM context for tree relationships\n//!\n//! The main dispatcher function `dispatch_inline_handler` routes tags to\n//! their appropriate handlers and returns a boolean indicating success.\n\npub mod code;\npub mod emphasis;\npub mod link;\npub mod ruby;\npub mod semantic;\n\n// Re-export types from parent module for submodule access\n\n// Re-export handler functions for internal use by dispatcher (crate-private)\n// pub(crate) use ruby::handle as handle_ruby;\n\n/// Dispatches inline element handling to the appropriate handler.\n///\n/// This function routes inline HTML elements to their specialized handlers\n/// based on tag name. It is designed to be called from the main `walk_node`\n/// function in `converter.rs`.\n///\n/// # Routing Table\n///\n/// The following tag routes are supported:\n///\n/// | Tag(s) | Handler | Description |\n/// |--------|---------|-------------|\n/// | `strong`, `b` | emphasis | Bold/strong text formatting |\n/// | `em`, `i` | emphasis | Italic/emphasis text formatting |\n/// | `a` | link | Hyperlinks and anchors |\n/// | `code`, `kbd`, `samp` | code | Inline code and keyboard input |\n/// | `mark`, `del`, `s`, `ins`, `u`, `small`, `sub`, `sup`, `var`, `dfn`, `abbr`, `span` | semantic | Semantic formatting |\n/// | `ruby`, `rb`, `rt`, `rp`, `rtc` | ruby | Ruby annotations (East Asian typography) |\n///\n/// # Return Value\n///\n/// Returns `true` if the tag was recognized and handled, `false` otherwise.\n/// This allows the caller to distinguish between:\n/// - Handled inline elements (return `true`)\n/// - Unhandled elements (return `false`) that should be processed as text or passed through\n///\n/// # Usage in converter.rs\n///\n/// ```text\n/// if crate::converter::inline::dispatch_inline_handler(\n///     &tag_name,\n///     &node_handle,\n///     parser,\n///     output,\n///     options,\n///     ctx,\n///     depth,\n///     dom_ctx,\n/// ) {\n///     return; // Element was handled, move to next sibling\n/// }\n/// // Element was not handled, process as default inline element\n/// ```\n///\n/// # Parameters\n///\n/// * `tag_name` - The normalized HTML tag name (lowercase)\n/// * `node_handle` - The DOM node handle from the parser\n/// * `parser` - Reference to the tl HTML parser\n/// * `output` - Output buffer to write converted content to\n/// * `options` - Conversion configuration options\n/// * `ctx` - Processing context with state tracking\n/// * `depth` - Current DOM tree depth for recursion tracking\n/// * `dom_ctx` - DOM context for accessing tree structure\n///\n/// # Example\n///\n/// For `<strong>Bold text</strong>`, the dispatcher:\n/// 1. Recognizes \"strong\" tag\n/// 2. Routes to emphasis handler\n/// 3. Returns `true`\n/// 4. Emphasis handler outputs `**Bold text**` to output buffer\n///\n/// For `<span>Normal text</span>`, the dispatcher:\n/// 1. Fails to recognize \"span\" tag\n/// 2. Returns `false`\n/// 3. Caller processes as default inline content\npub fn dispatch_inline_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &crate::converter::Context,\n    depth: usize,\n    dom_ctx: &crate::converter::DomContext,\n) -> bool {\n    match tag_name {\n        // Emphasis elements: strong, b (bold) and em, i (italic)\n        \"strong\" | \"b\" | \"em\" | \"i\" => {\n            emphasis::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Link elements: a (anchor)\n        \"a\" => {\n            link::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Code elements: code, kbd (keyboard input), samp (sample output)\n        \"code\" | \"kbd\" | \"samp\" => {\n            code::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Semantic elements: mark, del, s, ins, u, small, sub, sup, var, dfn, abbr, span\n        \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\" | \"span\" => {\n            semantic::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Ruby annotation elements: ruby, rb, rt, rp, rtc\n        \"ruby\" | \"rb\" | \"rt\" | \"rp\" | \"rtc\" => {\n            ruby::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Unknown element - not handled by inline dispatcher\n        _ => false,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    /// Test that all expected tags are properly dispatched\n    #[test]\n    fn test_dispatcher_routes_emphasis_tags() {\n        assert!(matches!(\n            (\"strong\", \"strong\"),\n            (tag, _) if matches!(tag, \"strong\" | \"b\" | \"em\" | \"i\")\n        ));\n        assert!(matches!(\n            (\"em\", \"em\"),\n            (tag, _) if matches!(tag, \"strong\" | \"b\" | \"em\" | \"i\")\n        ));\n    }\n\n    #[test]\n    fn test_dispatcher_routes_code_tags() {\n        assert!(matches!(\n            (\"code\", \"code\"),\n            (tag, _) if matches!(tag, \"code\" | \"kbd\" | \"samp\")\n        ));\n        assert!(matches!(\n            (\"kbd\", \"kbd\"),\n            (tag, _) if matches!(tag, \"code\" | \"kbd\" | \"samp\")\n        ));\n    }\n\n    #[test]\n    fn test_dispatcher_routes_semantic_tags() {\n        assert!(matches!(\n            (\"mark\", \"mark\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n        assert!(matches!(\n            (\"del\", \"del\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n        assert!(matches!(\n            (\"sub\", \"sub\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n        assert!(matches!(\n            (\"var\", \"var\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n        assert!(matches!(\n            (\"dfn\", \"dfn\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n        assert!(matches!(\n            (\"abbr\", \"abbr\"),\n            (tag, _) if matches!(tag, \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\")\n        ));\n    }\n\n    #[test]\n    fn test_dispatcher_recognizes_link_tag() {\n        assert!(matches!(\n            (\"a\", \"a\"),\n            (tag, _) if tag == \"a\"\n        ));\n    }\n\n    #[test]\n    fn test_dispatcher_routes_ruby_tags() {\n        assert!(matches!(\n            (\"ruby\", \"ruby\"),\n            (tag, _) if matches!(tag, \"ruby\" | \"rb\" | \"rt\" | \"rp\" | \"rtc\")\n        ));\n        assert!(matches!(\n            (\"rt\", \"rt\"),\n            (tag, _) if matches!(tag, \"ruby\" | \"rb\" | \"rt\" | \"rp\" | \"rtc\")\n        ));\n    }\n\n    #[test]\n    fn test_unknown_tags_not_routed() {\n        // These should fall through to default handling\n        let unknown_tags = vec![\"div\", \"p\", \"section\", \"article\", \"table\"];\n        for tag in unknown_tags {\n            assert!(matches!(\n                (tag, tag),\n                (tag, _) if !matches!(\n                    tag,\n                    \"strong\" | \"b\" | \"em\" | \"i\" | \"a\" | \"code\" | \"kbd\" | \"samp\"\n                    | \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"var\" | \"dfn\" | \"abbr\"\n                    | \"ruby\" | \"rb\" | \"rt\" | \"rp\" | \"rtc\" | \"span\"\n                )\n            ));\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/ruby.rs",
    "content": "//! Handler for ruby annotation inline elements (ruby, rb, rt, rp, rtc).\n//!\n//! Converts HTML ruby annotation elements to Markdown format with support for:\n//! - Ruby base text elements (<ruby>, <rb>)\n//! - Ruby text annotations (<rt>) for phonetic guidance (common in CJK)\n//! - Ruby parentheses (<rp>) for fallback presentation in browsers without ruby support\n//! - Ruby text container (<rtc>) for secondary annotations or separate ruby text grouping\n//! - Interleaved rendering mode: rb/rt pairs rendered inline (rb1(rt1)rb2(rt2))\n//! - Grouped rendering mode: all rb text followed by rt annotations in parentheses\n//! - Proper handling of CJK (Chinese/Japanese/Korean) text with multiple annotations\n//! - Visitor callbacks for custom ruby processing\n//! - Whitespace normalization and trimming\n\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handles ruby annotation elements: ruby, rb, rt, rp, rtc.\n///\n/// Ruby annotations are used in East Asian typography to show pronunciation guides\n/// or provide alternate text. The handler supports two rendering modes:\n///\n/// # Rendering Modes\n///\n/// **Interleaved mode** (when rb and rt elements are alternated without rtc):\n/// - Renders ruby text inline with base text: `base(annotation)base(annotation)`\n/// - Example: `<ruby><rb>漢</rb><rt>かん</rt></ruby>` → `漢(かん)`\n///\n/// **Grouped mode** (when rtc is present or rb/rt are not interleaved):\n/// - Renders all base text first, then all annotations in parentheses: `base(annotation1annotation2)`\n/// - Handles multiple rt elements and rtc (ruby text container) grouping\n/// - Example: `<ruby><rb>東</rb><rb>京</rb><rt>とう</rt><rt>きょう</rt></ruby>` → `東京(とうきょう)`\n///\n/// # Element Handling\n///\n/// - `<ruby>`: Main container, detects layout and delegates to appropriate rendering mode\n/// - `<rb>`: Base text; content is extracted and used in output\n/// - `<rt>`: Annotation text; wrapped in parentheses in standalone contexts\n/// - `<rp>`: Ruby parentheses (fallback for browsers without ruby support); skipped in most contexts\n/// - `<rtc>`: Ruby text container for grouped annotations; content extracted after rt annotations\n///\n/// # Note\n/// This function references `walk_node` and `normalized_tag_name` from converter.rs,\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    // Import helper functions from parent converter module\n    use crate::converter::{normalized_tag_name, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    match tag_name {\n        \"ruby\" => {\n            // Clone context for ruby children processing\n            let ruby_ctx = ctx.clone();\n\n            // Scan child elements to determine rendering mode\n            let tag_sequence: Vec<String> = tag\n                .children()\n                .top()\n                .iter()\n                .filter_map(|child_handle| {\n                    if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                        let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n                        // Only track rb, rt, rtc tags to determine structure\n                        if matches!(tag_name.as_ref(), \"rb\" | \"rt\" | \"rtc\") {\n                            Some(tag_name.into_owned())\n                        } else {\n                            None\n                        }\n                    } else {\n                        None\n                    }\n                })\n                .collect();\n\n            // Detect presence of ruby text container\n            let has_rtc = tag_sequence.iter().any(|tag| tag == \"rtc\");\n\n            // Detect interleaved mode: rb followed immediately by rt\n            let is_interleaved = tag_sequence.windows(2).any(|w| w[0] == \"rb\" && w[1] == \"rt\");\n\n            if is_interleaved && !has_rtc {\n                // Interleaved rendering: process rb/rt pairs inline\n                let mut current_base = String::new();\n                let children = tag.children();\n                {\n                    for child_handle in children.top().iter() {\n                        if let Some(node) = child_handle.get(parser) {\n                            match node {\n                                tl::Node::Tag(child_tag) => {\n                                    let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n                                    if tag_name == \"rt\" {\n                                        // Process rt (ruby text/annotation)\n                                        let mut annotation = String::new();\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut annotation,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                        // Output any pending base text\n                                        if !current_base.is_empty() {\n                                            output.push_str(current_base.trim());\n                                            current_base.clear();\n                                        }\n                                        // Output annotation text\n                                        output.push_str(annotation.trim());\n                                    } else if tag_name == \"rb\" {\n                                        // Process rb (ruby base)\n                                        if !current_base.is_empty() {\n                                            output.push_str(current_base.trim());\n                                            current_base.clear();\n                                        }\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut current_base,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                    } else if tag_name != \"rp\" {\n                                        // Skip rp, process other elements into current_base\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut current_base,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                    }\n                                }\n                                tl::Node::Raw(_) => {\n                                    // Process raw text nodes\n                                    walk_node(\n                                        child_handle,\n                                        parser,\n                                        &mut current_base,\n                                        options,\n                                        &ruby_ctx,\n                                        depth,\n                                        dom_ctx,\n                                    );\n                                }\n                                _ => {}\n                            }\n                        }\n                    }\n                }\n                // Flush remaining base text\n                if !current_base.is_empty() {\n                    output.push_str(current_base.trim());\n                }\n            } else {\n                // Grouped rendering: collect all bases, then annotations\n                let mut base_text = String::new();\n                let mut rt_annotations = Vec::new();\n                let mut rtc_content = String::new();\n\n                let children = tag.children();\n                {\n                    for child_handle in children.top().iter() {\n                        if let Some(node) = child_handle.get(parser) {\n                            match node {\n                                tl::Node::Tag(child_tag) => {\n                                    let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n                                    if tag_name == \"rt\" {\n                                        // Collect rt annotations\n                                        let mut annotation = String::new();\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut annotation,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                        rt_annotations.push(annotation);\n                                    } else if tag_name == \"rtc\" {\n                                        // Collect rtc (ruby text container) content\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut rtc_content,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                    } else if tag_name != \"rp\" {\n                                        // Collect base text (skip rp elements)\n                                        walk_node(\n                                            child_handle,\n                                            parser,\n                                            &mut base_text,\n                                            options,\n                                            &ruby_ctx,\n                                            depth,\n                                            dom_ctx,\n                                        );\n                                    }\n                                }\n                                tl::Node::Raw(_) => {\n                                    // Collect raw text into base\n                                    walk_node(child_handle, parser, &mut base_text, options, &ruby_ctx, depth, dom_ctx);\n                                }\n                                _ => {}\n                            }\n                        }\n                    }\n                }\n\n                // Output base text\n                let trimmed_base = base_text.trim();\n                output.push_str(trimmed_base);\n\n                // Output rt annotations in parentheses if present\n                if !rt_annotations.is_empty() {\n                    let rt_text = rt_annotations.iter().map(|s| s.trim()).collect::<Vec<_>>().join(\"\");\n                    if !rt_text.is_empty() {\n                        // Wrap in parentheses only if we have rtc content and multiple annotations\n                        if has_rtc && !rtc_content.trim().is_empty() && rt_annotations.len() > 1 {\n                            output.push('(');\n                            output.push_str(&rt_text);\n                            output.push(')');\n                        } else {\n                            output.push_str(&rt_text);\n                        }\n                    }\n                }\n\n                // Output rtc content after rt annotations\n                if !rtc_content.trim().is_empty() {\n                    output.push_str(rtc_content.trim());\n                }\n            }\n        }\n\n        \"rb\" => {\n            // Ruby base text element (typically used within ruby)\n            // When standalone, just extract and output the text\n            let mut text = String::new();\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n            output.push_str(text.trim());\n        }\n\n        \"rt\" => {\n            // Ruby text/annotation element\n            // When standalone (outside ruby context), wrap annotation in parentheses\n            let mut text = String::new();\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n            let trimmed = text.trim();\n\n            // Check if output already ends with opening paren (interleaved mode)\n            if output.ends_with('(') {\n                output.push_str(trimmed);\n            } else {\n                // Otherwise wrap annotation in parentheses\n                output.push('(');\n                output.push_str(trimmed);\n                output.push(')');\n            }\n        }\n\n        \"rp\" => {\n            // Ruby parenthesis element (fallback for non-ruby-supporting browsers)\n            // In Markdown output, generally skip these as annotations are in parentheses\n            let mut content = String::new();\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n            let trimmed = content.trim();\n            // Only output non-empty rp content\n            if !trimmed.is_empty() {\n                output.push_str(trimmed);\n            }\n        }\n\n        \"rtc\" => {\n            // Ruby text container element\n            // When standalone, just process children normally\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n        }\n\n        _ => {\n            // Fallback for unknown ruby-related tags: process children normally\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/semantic/marks.rs",
    "content": "//! Handlers for mark/highlight and strikethrough/underline elements.\n//!\n//! Contains:\n//! - Mark (highlight) element with configurable styles\n//! - Strikethrough (del, s tags) with ~~ syntax\n//! - Inserted/underlined text (ins, u tags) with == syntax\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::{ConversionOptions, OutputFormat};\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle mark (highlight) element with configurable styles.\n///\n/// Supports multiple highlight styles:\n/// - DoubleEqual: `==highlighted==`\n/// - Html: `<mark>highlighted</mark>`\n/// - Bold: `**highlighted**`\n/// - None: just pass through content\npub fn handle_mark(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{get_text_content, serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    #[cfg(feature = \"visitor\")]\n    let mark_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let text_content = get_text_content(node_handle, parser, dom_ctx);\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Mark,\n            tag_name: tag.name().as_utf8_str().to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_mark(&node_ctx, &text_content)\n        };\n        match visit_result {\n            VisitResult::Continue => None,\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => Some(String::new()),\n            VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n        }\n    } else {\n        None\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(custom_output) = mark_output {\n        output.push_str(&custom_output);\n        return;\n    }\n\n    if ctx.convert_as_inline {\n        // In inline conversion context, just pass through children\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    } else {\n        use crate::options::HighlightStyle;\n        match options.highlight_style {\n            HighlightStyle::DoubleEqual => {\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"{=\");\n                } else {\n                    output.push_str(\"==\");\n                }\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"=}\");\n                } else {\n                    output.push_str(\"==\");\n                }\n            }\n            HighlightStyle::Html => {\n                output.push_str(\"<mark>\");\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n                output.push_str(\"</mark>\");\n            }\n            HighlightStyle::Bold => {\n                let mut symbol = String::with_capacity(2);\n                symbol.push(options.strong_em_symbol);\n                symbol.push(options.strong_em_symbol);\n                output.push_str(&symbol);\n                let bold_ctx = Context {\n                    in_strong: true,\n                    ..ctx.clone()\n                };\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, &bold_ctx, depth + 1, dom_ctx);\n                }\n                output.push_str(&symbol);\n            }\n            HighlightStyle::None => {\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n        }\n    }\n}\n\n/// Handle strikethrough element (del, s tags).\n///\n/// Converts to `~~content~~` syntax. Suppresses formatting in code context.\n/// Supports visitor callbacks when the visitor feature is enabled.\n#[allow(unused_variables)]\npub fn handle_strikethrough(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::{append_inline_suffix, chomp_inline, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    if ctx.in_code {\n        // Suppress strikethrough in code context, just process children\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    } else {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n        }\n\n        #[cfg(feature = \"visitor\")]\n        let strikethrough_output = if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::converter::get_text_content;\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n            let text_content = get_text_content(node_handle, parser, dom_ctx);\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let node_id = node_handle.get_inner();\n            let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n            let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::Strikethrough,\n                tag_name: tag_name.to_string(),\n                attributes,\n                depth,\n                index_in_parent,\n                parent_tag,\n                is_inline: true,\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_strikethrough(&node_ctx, &text_content)\n            };\n            match visit_result {\n                VisitResult::Continue => None,\n                VisitResult::Custom(custom) => Some(custom),\n                VisitResult::Skip => Some(String::new()),\n                VisitResult::PreserveHtml => {\n                    use crate::converter::serialize_node;\n                    Some(serialize_node(node_handle, parser))\n                }\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    None\n                }\n            }\n        } else {\n            None\n        };\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(custom_output) = strikethrough_output {\n            output.push_str(&custom_output);\n        } else {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"{-\");\n                } else {\n                    output.push_str(\"~~\");\n                }\n                output.push_str(trimmed);\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"-}\");\n                } else {\n                    output.push_str(\"~~\");\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            }\n        }\n\n        #[cfg(not(feature = \"visitor\"))]\n        {\n            let (prefix, suffix, trimmed) = chomp_inline(&content);\n            if !content.trim().is_empty() {\n                output.push_str(prefix);\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"{-\");\n                } else {\n                    output.push_str(\"~~\");\n                }\n                output.push_str(trimmed);\n                if options.output_format == OutputFormat::Djot {\n                    output.push_str(\"-}\");\n                } else {\n                    output.push_str(\"~~\");\n                }\n                append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n            } else if !content.is_empty() {\n                output.push_str(prefix);\n                append_inline_suffix(output, suffix, false, node_handle, parser, dom_ctx);\n            }\n        }\n    }\n}\n\n/// Handle inserted/underlined text (ins tag).\n///\n/// Converts to `==content==` syntax. Supports visitor callbacks when enabled.\npub fn handle_inserted(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::{append_inline_suffix, chomp_inline, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    #[cfg(feature = \"visitor\")]\n    let underline_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::converter::get_text_content;\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let text_content = get_text_content(node_handle, parser, dom_ctx);\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Underline,\n            tag_name: \"ins\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_underline(&node_ctx, &text_content)\n        };\n        match visit_result {\n            VisitResult::Continue => None,\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => Some(String::new()),\n            VisitResult::PreserveHtml => {\n                use crate::converter::serialize_node;\n                Some(serialize_node(node_handle, parser))\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n        }\n    } else {\n        None\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(custom_output) = underline_output {\n        output.push_str(&custom_output);\n    } else {\n        let (prefix, suffix, trimmed) = chomp_inline(&content);\n        if !trimmed.is_empty() {\n            output.push_str(prefix);\n            if options.output_format == OutputFormat::Djot {\n                output.push_str(\"{+\");\n            } else {\n                output.push_str(\"==\");\n            }\n            output.push_str(trimmed);\n            if options.output_format == OutputFormat::Djot {\n                output.push_str(\"+}\");\n            } else {\n                output.push_str(\"==\");\n            }\n            append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n        }\n    }\n\n    #[cfg(not(feature = \"visitor\"))]\n    {\n        let (prefix, suffix, trimmed) = chomp_inline(&content);\n        if !trimmed.is_empty() {\n            output.push_str(prefix);\n            if options.output_format == OutputFormat::Djot {\n                output.push_str(\"{+\");\n            } else {\n                output.push_str(\"==\");\n            }\n            output.push_str(trimmed);\n            if options.output_format == OutputFormat::Djot {\n                output.push_str(\"+}\");\n            } else {\n                output.push_str(\"==\");\n            }\n            append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n        }\n    }\n}\n\n/// Handle underline element (u tag).\n///\n/// Just passes through content (HTML doesn't have native underline in Markdown).\n/// Supports visitor callbacks when enabled, which can provide custom formatting.\npub fn handle_underline(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::converter::get_text_content;\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let text_content = get_text_content(node_handle, parser, dom_ctx);\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Underline,\n            tag_name: \"u\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_underline(&node_ctx, &text_content)\n        };\n        match visit_result {\n            VisitResult::Continue => {\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n            VisitResult::Custom(custom) => {\n                output.push_str(&custom);\n            }\n            VisitResult::Skip => {}\n            VisitResult::PreserveHtml => {\n                use crate::converter::serialize_node;\n                output.push_str(&serialize_node(node_handle, parser));\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                let children = tag.children();\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n        }\n    } else {\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n\n    #[cfg(not(feature = \"visitor\"))]\n    {\n        let children = tag.children();\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n}\n\n#[cfg(all(test, feature = \"visitor\"))]\nmod tests {\n    use crate::convert;\n    use crate::options::ConversionOptions;\n    use crate::visitor::{HtmlVisitor, NodeContext, VisitResult};\n    use std::cell::RefCell;\n    use std::rc::Rc;\n\n    #[derive(Debug)]\n    struct MarkSkipVisitor;\n\n    impl HtmlVisitor for MarkSkipVisitor {\n        fn visit_mark(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n\n    #[derive(Debug)]\n    struct MarkCustomVisitor;\n\n    impl HtmlVisitor for MarkCustomVisitor {\n        fn visit_mark(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Custom(\"REPLACED\".to_string())\n        }\n    }\n\n    #[derive(Debug)]\n    struct MarkPreserveVisitor;\n\n    impl HtmlVisitor for MarkPreserveVisitor {\n        fn visit_mark(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::PreserveHtml\n        }\n    }\n\n    fn make_visitor<V: HtmlVisitor + 'static>(v: V) -> ConversionOptions {\n        ConversionOptions {\n            visitor: Some(Rc::new(RefCell::new(v))),\n            ..ConversionOptions::default()\n        }\n    }\n\n    #[test]\n    fn test_visitor_mark_skip() {\n        let html = \"<p>before <mark>highlighted</mark> after</p>\";\n        let result = convert(html, Some(make_visitor(MarkSkipVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            !content.contains(\"highlighted\"),\n            \"mark content should be absent: {}\",\n            content\n        );\n        assert!(\n            content.contains(\"before\"),\n            \"surrounding text should be present: {}\",\n            content\n        );\n    }\n\n    #[test]\n    fn test_visitor_mark_custom() {\n        let html = \"<p>before <mark>highlighted</mark> after</p>\";\n        let result = convert(html, Some(make_visitor(MarkCustomVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"REPLACED\"),\n            \"custom output should be present: {}\",\n            content\n        );\n    }\n\n    #[test]\n    fn test_visitor_mark_preserve_html() {\n        let html = \"<p>before <mark>highlighted</mark> after</p>\";\n        let result = convert(html, Some(make_visitor(MarkPreserveVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"<mark>highlighted</mark>\"),\n            \"original html should be preserved: {}\",\n            content\n        );\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/semantic/mod.rs",
    "content": "//! Handler for semantic inline elements (mark, del, s, ins, u, small, sub, sup, var, dfn, abbr, span).\n//!\n//! Converts HTML semantic tags to Markdown formatting with support for:\n//! - Highlight/mark element with configurable styles (==, ::, ^^, <mark>)\n//! - Strikethrough (del, s tags) with ~~ syntax\n//! - Underline/inserted text (ins, u tags) with == syntax\n//! - Small text (passes through without formatting)\n//! - Subscript and superscript with configurable symbols\n//! - Variable (var) and definition (dfn) text with italic formatting\n//! - Abbreviation (abbr) text with optional title attribute\n//! - Span element with special handling for OCR words and whitespace\n//! - Visitor callbacks for custom processing (feature-gated)\n\nmod marks;\nmod typography;\n\nuse crate::options::ConversionOptions;\nuse tl::{NodeHandle, Parser};\n\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handler for semantic inline elements: mark, del, s, ins, u, small, sub, sup, var, dfn, abbr, span.\n///\n/// Processes semantic content based on tag and options:\n/// - Mark: configurable highlight style (==, ::, ^^, <mark>, **bold, none)\n/// - Del/S: strikethrough with ~~ and visitor callback support\n/// - Ins: underline with == and visitor callback support\n/// - U: underline with visitor callback support\n/// - Small: pass through without formatting\n/// - Sub/Sup: wrap with configurable symbols\n/// - Var: wrap with italic symbol (strong_em_symbol)\n/// - Dfn: wrap with italic symbol (strong_em_symbol)\n/// - Abbr: pass through content with optional title in parentheses\n/// - Span: pass through content with special handling for OCR words and whitespace\n///\n/// # Note\n/// This function references helper functions and `walk_node` from converter.rs\n/// which must be accessible (pub(crate)) for this module to work correctly.\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    match tag_name {\n        \"mark\" => {\n            marks::handle_mark(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"del\" | \"s\" => {\n            marks::handle_strikethrough(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"ins\" => {\n            marks::handle_inserted(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"u\" => {\n            marks::handle_underline(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"small\" => {\n            typography::handle_small(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"sub\" => {\n            typography::handle_subscript(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"sup\" => {\n            typography::handle_superscript(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"var\" => {\n            typography::handle_variable(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"dfn\" => {\n            typography::handle_definition(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"abbr\" => {\n            typography::handle_abbreviation(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"span\" => {\n            typography::handle_span(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/inline/semantic/typography.rs",
    "content": "//! Handlers for typography and text semantic elements.\n//!\n//! Contains:\n//! - Small text (pass through)\n//! - Subscript and superscript with configurable symbols\n//! - Variable (var) and definition (dfn) text with italic formatting\n//! - Abbreviation (abbr) with optional title\n//! - Span element with special OCR handling\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::{ConversionOptions, OutputFormat};\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle small element.\n///\n/// Small text has no direct Markdown equivalent, so just pass through content.\npub fn handle_small(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n    }\n}\n\n/// Handle subscript element (sub tag).\n///\n/// Wraps content with configurable subscript symbol from options.\npub fn handle_subscript(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{append_inline_suffix, chomp_inline, get_text_content, serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    if ctx.in_code {\n        output.push_str(&content);\n        return;\n    }\n\n    #[cfg(feature = \"visitor\")]\n    let sub_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let text_content = get_text_content(node_handle, parser, dom_ctx);\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Subscript,\n            tag_name: tag.name().as_utf8_str().to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_subscript(&node_ctx, &text_content)\n        };\n        match visit_result {\n            VisitResult::Continue => None,\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => Some(String::new()),\n            VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n        }\n    } else {\n        None\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(custom_output) = sub_output {\n        output.push_str(&custom_output);\n        return;\n    }\n\n    let (prefix, suffix, trimmed) = chomp_inline(&content);\n    if !trimmed.is_empty() {\n        output.push_str(prefix);\n        if options.output_format == OutputFormat::Djot {\n            output.push('~');\n            output.push_str(trimmed);\n            output.push('~');\n        } else if !options.sub_symbol.is_empty() {\n            output.push_str(&options.sub_symbol);\n            output.push_str(trimmed);\n            if options.sub_symbol.starts_with('<') && !options.sub_symbol.starts_with(\"</\") {\n                output.push_str(&options.sub_symbol.replace('<', \"</\"));\n            } else {\n                output.push_str(&options.sub_symbol);\n            }\n        } else {\n            output.push_str(trimmed);\n        }\n        append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n    }\n}\n\n/// Handle superscript element (sup tag).\n///\n/// Wraps content with configurable superscript symbol from options.\npub fn handle_superscript(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    #[allow(unused_imports)]\n    use crate::converter::{append_inline_suffix, chomp_inline, get_text_content, serialize_node, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    if ctx.in_code {\n        output.push_str(&content);\n        return;\n    }\n\n    #[cfg(feature = \"visitor\")]\n    let sup_output = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let text_content = get_text_content(node_handle, parser, dom_ctx);\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Superscript,\n            tag_name: tag.name().as_utf8_str().to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_superscript(&node_ctx, &text_content)\n        };\n        match visit_result {\n            VisitResult::Continue => None,\n            VisitResult::Custom(custom) => Some(custom),\n            VisitResult::Skip => Some(String::new()),\n            VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                None\n            }\n        }\n    } else {\n        None\n    };\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(custom_output) = sup_output {\n        output.push_str(&custom_output);\n        return;\n    }\n\n    let (prefix, suffix, trimmed) = chomp_inline(&content);\n    if !trimmed.is_empty() {\n        output.push_str(prefix);\n        if options.output_format == OutputFormat::Djot {\n            output.push('^');\n            output.push_str(trimmed);\n            output.push('^');\n        } else if !options.sup_symbol.is_empty() {\n            output.push_str(&options.sup_symbol);\n            output.push_str(trimmed);\n            if options.sup_symbol.starts_with('<') && !options.sup_symbol.starts_with(\"</\") {\n                output.push_str(&options.sup_symbol.replace('<', \"</\"));\n            } else {\n                output.push_str(&options.sup_symbol);\n            }\n        } else {\n            output.push_str(trimmed);\n        }\n        append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n    }\n}\n\n/// Handle variable element (var tag).\n///\n/// Wraps content with italic symbol (strong_em_symbol from options).\npub fn handle_variable(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::{append_inline_suffix, chomp_inline, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    let (prefix, suffix, trimmed) = chomp_inline(&content);\n    if !trimmed.is_empty() {\n        output.push_str(prefix);\n        output.push(options.strong_em_symbol);\n        output.push_str(trimmed);\n        output.push(options.strong_em_symbol);\n        append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n    }\n}\n\n/// Handle definition element (dfn tag).\n///\n/// Wraps content with italic symbol (strong_em_symbol from options).\npub fn handle_definition(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::{append_inline_suffix, chomp_inline, walk_node};\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    let (prefix, suffix, trimmed) = chomp_inline(&content);\n    if !trimmed.is_empty() {\n        output.push_str(prefix);\n        output.push(options.strong_em_symbol);\n        output.push_str(trimmed);\n        output.push(options.strong_em_symbol);\n        append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n    }\n}\n\n/// Handle abbreviation element (abbr tag).\n///\n/// Passes through content and optionally appends title attribute in parentheses.\npub fn handle_abbreviation(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(32);\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n    }\n\n    let trimmed = content.trim();\n\n    if !trimmed.is_empty() {\n        output.push_str(trimmed);\n\n        if let Some(title) = tag.attributes().get(\"title\").flatten().map(|v| v.as_utf8_str()) {\n            let trimmed_title = title.trim();\n            if !trimmed_title.is_empty() {\n                output.push_str(\" (\");\n                output.push_str(trimmed_title);\n                output.push(')');\n            }\n        }\n    }\n}\n\n/// Handle span element.\n///\n/// Processes span elements with special handling for:\n/// - OCR words (elements with class \"ocrx_word\"): adds space before if needed\n/// - Whitespace normalization in normalized mode: removes single newlines\n/// - Otherwise passes through content normally\npub fn handle_span(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    // Check if this is an OCR word span (class=\"ocrx_word\")\n    let is_hocr_word = tag.attributes().iter().any(|(name, value)| {\n        name.as_ref() == \"class\" && value.as_ref().is_some_and(|v| v.as_ref().contains(\"ocrx_word\"))\n    });\n\n    // Add space before OCR words if needed\n    if is_hocr_word\n        && !output.is_empty()\n        && !output.ends_with(' ')\n        && !output.ends_with('\\t')\n        && !output.ends_with('\\n')\n    {\n        output.push(' ');\n    }\n\n    // Handle whitespace normalization\n    if !ctx.in_code\n        && options.whitespace_mode == crate::options::WhitespaceMode::Normalized\n        && output.ends_with('\\n')\n        && !output.ends_with(\"\\n\\n\")\n    {\n        output.pop();\n    }\n\n    // Process children normally\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n    }\n}\n\n#[cfg(all(test, feature = \"visitor\"))]\nmod tests {\n    use crate::convert;\n    use crate::options::ConversionOptions;\n    use crate::visitor::{HtmlVisitor, NodeContext, VisitResult};\n    use std::cell::RefCell;\n    use std::rc::Rc;\n\n    #[derive(Debug)]\n    struct SubSkipVisitor;\n\n    impl HtmlVisitor for SubSkipVisitor {\n        fn visit_subscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n\n    #[derive(Debug)]\n    struct SubCustomVisitor;\n\n    impl HtmlVisitor for SubCustomVisitor {\n        fn visit_subscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Custom(\"REPLACED\".to_string())\n        }\n    }\n\n    #[derive(Debug)]\n    struct SubPreserveVisitor;\n\n    impl HtmlVisitor for SubPreserveVisitor {\n        fn visit_subscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::PreserveHtml\n        }\n    }\n\n    #[derive(Debug)]\n    struct SupSkipVisitor;\n\n    impl HtmlVisitor for SupSkipVisitor {\n        fn visit_superscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n\n    #[derive(Debug)]\n    struct SupCustomVisitor;\n\n    impl HtmlVisitor for SupCustomVisitor {\n        fn visit_superscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Custom(\"REPLACED\".to_string())\n        }\n    }\n\n    #[derive(Debug)]\n    struct SupPreserveVisitor;\n\n    impl HtmlVisitor for SupPreserveVisitor {\n        fn visit_superscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::PreserveHtml\n        }\n    }\n\n    fn make_visitor<V: HtmlVisitor + 'static>(v: V) -> ConversionOptions {\n        ConversionOptions {\n            visitor: Some(Rc::new(RefCell::new(v))),\n            ..ConversionOptions::default()\n        }\n    }\n\n    #[test]\n    fn test_visitor_subscript_skip() {\n        let html = \"<p>H<sub>2</sub>O</p>\";\n        let result = convert(html, Some(make_visitor(SubSkipVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(!content.contains('2'), \"sub content should be absent: {}\", content);\n        assert!(content.contains('H'), \"surrounding text should be present: {}\", content);\n    }\n\n    #[test]\n    fn test_visitor_subscript_custom() {\n        let html = \"<p>H<sub>2</sub>O</p>\";\n        let result = convert(html, Some(make_visitor(SubCustomVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"REPLACED\"),\n            \"custom output should be present: {}\",\n            content\n        );\n    }\n\n    #[test]\n    fn test_visitor_subscript_preserve_html() {\n        let html = \"<p>H<sub>2</sub>O</p>\";\n        let result = convert(html, Some(make_visitor(SubPreserveVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"<sub>2</sub>\"),\n            \"original html should be preserved: {}\",\n            content\n        );\n    }\n\n    #[test]\n    fn test_visitor_superscript_skip() {\n        let html = \"<p>E=mc<sup>2</sup></p>\";\n        let result = convert(html, Some(make_visitor(SupSkipVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(!content.contains('2'), \"sup content should be absent: {}\", content);\n    }\n\n    #[test]\n    fn test_visitor_superscript_custom() {\n        let html = \"<p>E=mc<sup>2</sup></p>\";\n        let result = convert(html, Some(make_visitor(SupCustomVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"REPLACED\"),\n            \"custom output should be present: {}\",\n            content\n        );\n    }\n\n    #[test]\n    fn test_visitor_superscript_preserve_html() {\n        let html = \"<p>E=mc<sup>2</sup></p>\";\n        let result = convert(html, Some(make_visitor(SupPreserveVisitor))).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"<sup>2</sup>\"),\n            \"original html should be preserved: {}\",\n            content\n        );\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/definition.rs",
    "content": "//! Definition list handling (dl, dt, dd elements).\n//!\n//! Processes definition lists with:\n//! - Definition terms (dt)\n//! - Definition descriptions (dd)\n//! - Plain block formatting (no Pandoc colon syntax)\n\nuse crate::options::ConversionOptions;\nuse tl;\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle definition list element (<dl>).\n///\n/// Groups dt/dd pairs and formats them with proper Markdown separation.\npub fn handle_dl(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let tag = match node_handle.get(parser) {\n        Some(tl::Node::Tag(t)) => t,\n        _ => return,\n    };\n\n    if ctx.convert_as_inline {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                use crate::converter::walk_node;\n                walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n            }\n        }\n        return;\n    }\n\n    let mut content = String::new();\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            crate::converter::walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);\n        }\n    }\n\n    let trimmed = content.trim();\n    if !trimmed.is_empty() {\n        if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n            output.push_str(\"\\n\\n\");\n        }\n        output.push_str(trimmed);\n        output.push_str(\"\\n\\n\");\n    }\n}\n\n/// Handle definition term element (<dt>).\n///\n/// Outputs the term text followed by a newline.\npub fn handle_dt(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let tag = match node_handle.get(parser) {\n        Some(tl::Node::Tag(t)) => t,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(64);\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            crate::converter::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n    let trimmed = content.trim();\n    if !trimmed.is_empty() {\n        if ctx.convert_as_inline {\n            output.push_str(trimmed);\n        } else {\n            output.push_str(trimmed);\n            output.push('\\n');\n        }\n    }\n}\n\n/// Handle definition description element (<dd>).\n///\n/// Outputs the description as a plain block.\npub fn handle_dd(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let tag = match node_handle.get(parser) {\n        Some(tl::Node::Tag(t)) => t,\n        _ => return,\n    };\n\n    let mut content = String::with_capacity(128);\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            crate::converter::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n\n    let trimmed = content.trim();\n\n    if ctx.convert_as_inline {\n        if !trimmed.is_empty() {\n            output.push_str(trimmed);\n        }\n    } else if !trimmed.is_empty() {\n        output.push_str(trimmed);\n        output.push_str(\"\\n\\n\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/item.rs",
    "content": "//! List item handling (li element).\n//!\n//! Processes list items with support for:\n//! - Task list detection and rendering (checkboxes)\n//! - Block-level children detection\n//! - Proper bullet/number formatting\n//! - Indentation and spacing\n\nuse crate::converter::main_helpers::tag_name_eq;\nuse crate::converter::main_helpers::trim_trailing_whitespace;\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::content::normalized_tag_name;\nuse crate::converter::walk_node;\nuse crate::options::ConversionOptions;\nuse tl;\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle list item element (<li>).\n///\n/// Processes list item content with support for task lists (checkboxes),\n/// proper indentation, and block-level element detection.\n#[allow(clippy::too_many_arguments)]\npub fn handle_li(\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    if ctx.list_depth > 0 {\n        let indent = match options.list_indent_type {\n            crate::options::ListIndentType::Tabs => \"\\t\".repeat(ctx.list_depth),\n            crate::options::ListIndentType::Spaces => \" \".repeat(ctx.list_depth * options.list_indent_width),\n        };\n        output.push_str(&indent);\n    }\n\n    let mut has_block_children = false;\n    let children = tag.children();\n    {\n        for child_handle in children.top().iter() {\n            if let Some(info) = dom_ctx.tag_info(child_handle.get_inner(), parser) {\n                if matches!(\n                    info.name.as_str(),\n                    \"p\" | \"div\" | \"blockquote\" | \"pre\" | \"table\" | \"hr\" | \"dl\"\n                ) {\n                    has_block_children = true;\n                    break;\n                }\n            } else if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                let tag_name = normalized_tag_name(child_tag.name().as_utf8_str());\n                if matches!(\n                    tag_name.as_ref(),\n                    \"p\" | \"div\" | \"blockquote\" | \"pre\" | \"table\" | \"hr\" | \"dl\"\n                ) {\n                    has_block_children = true;\n                    break;\n                }\n            }\n        }\n    }\n\n    #[allow(clippy::trivially_copy_pass_by_ref)]\n    fn find_checkbox<'a>(node_handle: &tl::NodeHandle, parser: &'a tl::Parser<'a>) -> Option<(bool, tl::NodeHandle)> {\n        if let Some(tl::Node::Tag(node_tag)) = node_handle.get(parser) {\n            if tag_name_eq(node_tag.name().as_utf8_str(), \"input\") {\n                let input_type = node_tag.attributes().get(\"type\").flatten().map(|v| v.as_utf8_str());\n\n                if input_type.as_deref() == Some(\"checkbox\") {\n                    let checked = node_tag.attributes().get(\"checked\").is_some();\n                    return Some((checked, *node_handle));\n                }\n            }\n\n            let children = node_tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    if let Some(result) = find_checkbox(child_handle, parser) {\n                        return Some(result);\n                    }\n                }\n            }\n        }\n        None\n    }\n\n    let (is_task_list, task_checked, checkbox_node) = if let Some((checked, node)) = find_checkbox(node_handle, parser)\n    {\n        (true, checked, Some(node))\n    } else {\n        (false, false, None)\n    };\n\n    let li_ctx = Context {\n        in_list_item: true,\n        list_depth: ctx.list_depth + 1,\n        ..ctx.clone()\n    };\n\n    if is_task_list {\n        output.push('-');\n        output.push(' ');\n        output.push_str(if task_checked { \"[x]\" } else { \"[ ]\" });\n\n        #[allow(clippy::ref_option)]\n        fn is_checkbox_node(node_handle: &tl::NodeHandle, checkbox: &Option<tl::NodeHandle>) -> bool {\n            if let Some(cb) = checkbox {\n                node_handle == cb\n            } else {\n                false\n            }\n        }\n\n        #[allow(clippy::ref_option)]\n        fn contains_checkbox<'a>(\n            node_handle: &tl::NodeHandle,\n            parser: &'a tl::Parser<'a>,\n            checkbox: &Option<tl::NodeHandle>,\n        ) -> bool {\n            if is_checkbox_node(node_handle, checkbox) {\n                return true;\n            }\n            if let Some(tl::Node::Tag(node_tag)) = node_handle.get(parser) {\n                let children = node_tag.children();\n                {\n                    for child_handle in children.top().iter() {\n                        if contains_checkbox(child_handle, parser, checkbox) {\n                            return true;\n                        }\n                    }\n                }\n            }\n            false\n        }\n\n        #[allow(clippy::too_many_arguments, clippy::ref_option)]\n        fn render_li_content<'a>(\n            node_handle: &tl::NodeHandle,\n            parser: &'a tl::Parser<'a>,\n            output: &mut String,\n            options: &ConversionOptions,\n            ctx: &Context,\n            depth: usize,\n            checkbox: &Option<tl::NodeHandle>,\n            dom_ctx: &DomContext,\n        ) {\n            if is_checkbox_node(node_handle, checkbox) {\n                return;\n            }\n\n            if contains_checkbox(node_handle, parser, checkbox) {\n                if let Some(tl::Node::Tag(node_tag)) = node_handle.get(parser) {\n                    let children = node_tag.children();\n                    {\n                        for child_handle in children.top().iter() {\n                            render_li_content(child_handle, parser, output, options, ctx, depth, checkbox, dom_ctx);\n                        }\n                    }\n                }\n            } else {\n                walk_node(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        let mut task_text = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                render_li_content(\n                    child_handle,\n                    parser,\n                    &mut task_text,\n                    options,\n                    &li_ctx,\n                    depth + 1,\n                    &checkbox_node,\n                    dom_ctx,\n                );\n            }\n        }\n        output.push(' ');\n        let trimmed_task = task_text.trim();\n        if !trimmed_task.is_empty() {\n            output.push_str(trimmed_task);\n        }\n    } else {\n        if !ctx.in_table_cell {\n            if ctx.in_ordered_list {\n                use std::fmt::Write;\n                let _ = write!(output, \"{}. \", ctx.list_counter);\n            } else {\n                let bullets: Vec<char> = options.bullets.chars().collect();\n                let bullet_index = if ctx.ul_depth > 0 { ctx.ul_depth - 1 } else { 0 };\n                let bullet = if bullets.is_empty() {\n                    '*'\n                } else {\n                    bullets[bullet_index % bullets.len()]\n                };\n                output.push(bullet);\n                output.push(' ');\n            }\n        }\n\n        let item_start_pos = output.len();\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, &li_ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        trim_trailing_whitespace(output);\n\n        if !ctx.in_table_cell {\n            if let Some(ref sc) = ctx.structure_collector {\n                if item_start_pos <= output.len() && output.is_char_boundary(item_start_pos) {\n                    let rendered = &output[item_start_pos..];\n                    let content = rendered.trim();\n                    if !content.is_empty() {\n                        sc.borrow_mut().push_list_item(content);\n                    }\n                }\n            }\n        }\n\n        #[cfg(feature = \"visitor\")]\n        if let Some(ref visitor_handle) = ctx.visitor {\n            use crate::visitor::{NodeContext, NodeType, VisitResult};\n            use std::collections::BTreeMap;\n\n            let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n            let parent_tag = dom_ctx\n                .parent_of(node_handle.get_inner())\n                .and_then(|pid| dom_ctx.tag_name_for(dom_ctx.node_handle(pid).copied()?, parser))\n                .map(|s| s.to_string());\n\n            let index = dom_ctx.sibling_index(node_handle.get_inner()).unwrap_or(0);\n\n            let node_ctx = NodeContext {\n                node_type: NodeType::ListItem,\n                tag_name: \"li\".to_string(),\n                attributes,\n                depth,\n                index_in_parent: index,\n                parent_tag,\n                is_inline: false,\n            };\n\n            let last_line_start = output.rfind('\\n').map_or(0, |pos| pos + 1);\n            let last_line = &output[last_line_start..];\n\n            let (marker, text_content) = if is_task_list {\n                let task_marker = if task_checked { \"- [x]\" } else { \"- [ ]\" };\n                let text_start = last_line.find(task_marker).map_or(0, |pos| pos + task_marker.len());\n                (task_marker.to_string(), last_line[text_start..].trim().to_string())\n            } else if ctx.in_ordered_list {\n                let marker_text = format!(\"{}.\", ctx.list_counter);\n                let text_start = last_line.find(&marker_text).map_or(0, |pos| pos + marker_text.len());\n                (marker_text, last_line[text_start..].trim().to_string())\n            } else {\n                let bullets: Vec<char> = options.bullets.chars().collect();\n                let bullet_index = if ctx.ul_depth > 0 { ctx.ul_depth - 1 } else { 0 };\n                let bullet = if bullets.is_empty() {\n                    '*'\n                } else {\n                    bullets[bullet_index % bullets.len()]\n                };\n                let bullet_str = bullet.to_string();\n                let text_start = last_line.find(bullet).map_or(0, |pos| pos + 1);\n                (bullet_str, last_line[text_start..].trim().to_string())\n            };\n\n            let visit_result = {\n                let mut visitor = visitor_handle.borrow_mut();\n                visitor.visit_list_item(&node_ctx, ctx.in_ordered_list, &marker, &text_content)\n            };\n            match visit_result {\n                VisitResult::Continue => {}\n                VisitResult::Custom(custom) => {\n                    output.truncate(last_line_start);\n                    output.push_str(&custom);\n                    if !ctx.in_table_cell && !output.ends_with('\\n') {\n                        output.push('\\n');\n                    }\n                    return;\n                }\n                VisitResult::Skip => {\n                    output.truncate(last_line_start);\n                    return;\n                }\n                VisitResult::PreserveHtml => {\n                    output.truncate(last_line_start);\n                    use crate::converter::serialize_node_to_html;\n                    serialize_node_to_html(node_handle, parser, output);\n                    if !ctx.in_table_cell && !output.ends_with('\\n') {\n                        output.push('\\n');\n                    }\n                    return;\n                }\n                VisitResult::Error(err) => {\n                    if ctx.visitor_error.borrow().is_none() {\n                        *ctx.visitor_error.borrow_mut() = Some(err);\n                    }\n                    return;\n                }\n            }\n        }\n    }\n\n    if !ctx.in_table_cell {\n        if has_block_children || ctx.loose_list || ctx.prev_item_had_blocks {\n            if !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n        } else if !output.ends_with('\\n') {\n            output.push('\\n');\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/mod.rs",
    "content": "//! List element handlers for HTML to Markdown conversion.\n//!\n//! This module provides specialized handling for various list types:\n//! - **Ordered lists**: `<ol>` with counter management and formatting options\n//! - **Unordered lists**: `<ul>` with bullet cycling based on nesting depth\n//! - **List items**: `<li>` with task list and block-level detection\n//! - **Definition lists**: `<dl>`, `<dt>`, `<dd>` elements\n//! - **List utilities**: Indentation, loose/tight list detection, nesting depth calculation\n\npub mod definition;\npub mod item;\npub mod ordered;\npub mod unordered;\npub mod utils;\n\n// Re-export types from parent module for submodule access\n\n// Re-export utility function needed by table builder\n\n/// Dispatches list element handling to the appropriate handler.\n///\n/// Returns `true` if the element was handled, `false` otherwise.\n///\n/// # Supported Elements\n///\n/// - `ol`: Ordered list - routed to `ordered::handle`\n/// - `ul`: Unordered list - routed to `unordered::handle`\n/// - `li`: List item - routed to `item::handle_li`\n/// - `dl`: Definition list - routed to `definition::handle_dl`\n/// - `dt`: Definition term - routed to `definition::handle_dt`\n/// - `dd`: Definition description - routed to `definition::handle_dd`\npub fn dispatch_list_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) -> bool {\n    match tag_name {\n        \"ol\" => {\n            ordered::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"ul\" => {\n            unordered::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"li\" => {\n            item::handle_li(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"dl\" => {\n            definition::handle_dl(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"dt\" => {\n            definition::handle_dt(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"dd\" => {\n            definition::handle_dd(node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/ordered.rs",
    "content": "//! Ordered list handling (ol, li elements).\n//!\n//! Processes ordered lists with support for:\n//! - Custom start counters\n//! - Nested list handling\n//! - Loose/tight list detection\n//! - Proper indentation and numbering\n\nuse super::utils::{\n    add_list_leading_separator, add_nested_list_trailing_separator, calculate_list_nesting_depth, is_loose_list,\n    process_list_children,\n};\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::ConversionOptions;\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl;\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle ordered list element (<ol>).\n///\n/// Extracts the `start` attribute to set initial counter value,\n/// detects loose/tight list format, and processes list items.\n#[allow(clippy::too_many_arguments)]\npub fn handle_ol(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    add_list_leading_separator(output, ctx);\n\n    let nested_depth = calculate_list_nesting_depth(ctx);\n    let is_loose = is_loose_list(*node_handle, parser, dom_ctx);\n\n    let tag = match node_handle.get(parser) {\n        Some(tl::Node::Tag(t)) => t,\n        _ => return,\n    };\n\n    let start = tag\n        .attributes()\n        .get(\"start\")\n        .flatten()\n        .and_then(|v| v.as_utf8_str().parse::<usize>().ok())\n        .unwrap_or(1);\n\n    #[cfg(feature = \"visitor\")]\n    let list_output_start = output.len();\n\n    #[cfg(feature = \"visitor\")]\n    let mut list_start_custom: Option<String> = None;\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let parent_tag = dom_ctx\n            .parent_of(node_handle.get_inner())\n            .and_then(|pid| dom_ctx.tag_name_for(dom_ctx.node_handle(pid).copied()?, parser))\n            .map(|s| s.to_string());\n\n        let index = dom_ctx.sibling_index(node_handle.get_inner()).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::List,\n            tag_name: \"ol\".to_string(),\n            attributes,\n            depth,\n            index_in_parent: index,\n            parent_tag,\n            is_inline: false,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_list_start(&node_ctx, true)\n        };\n        match visit_result {\n            VisitResult::Continue => {}\n            VisitResult::Custom(custom) => {\n                list_start_custom = Some(custom);\n            }\n            VisitResult::Skip => {\n                return;\n            }\n            VisitResult::PreserveHtml => {\n                use crate::converter::serialize_node_to_html;\n                serialize_node_to_html(node_handle, parser, output);\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    if !ctx.in_table_cell {\n        if let Some(ref sc) = ctx.structure_collector {\n            sc.borrow_mut().push_list_start(true);\n        }\n    }\n\n    process_list_children(\n        *node_handle,\n        parser,\n        output,\n        options,\n        ctx,\n        depth,\n        true,\n        is_loose,\n        nested_depth,\n        start,\n        dom_ctx,\n    );\n\n    if !ctx.in_table_cell {\n        if let Some(ref sc) = ctx.structure_collector {\n            sc.borrow_mut().push_list_end();\n        }\n    }\n\n    add_nested_list_trailing_separator(output, ctx);\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let parent_tag = dom_ctx\n            .parent_of(node_handle.get_inner())\n            .and_then(|pid| dom_ctx.tag_name_for(dom_ctx.node_handle(pid).copied()?, parser))\n            .map(|s| s.to_string());\n\n        let index = dom_ctx.sibling_index(node_handle.get_inner()).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::List,\n            tag_name: \"ol\".to_string(),\n            attributes,\n            depth,\n            index_in_parent: index,\n            parent_tag,\n            is_inline: false,\n        };\n\n        let list_content = &output[list_output_start..];\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_list_end(&node_ctx, true, list_content)\n        };\n        match visit_result {\n            VisitResult::Continue => {\n                if let Some(custom_start) = list_start_custom {\n                    output.insert_str(list_output_start, &custom_start);\n                }\n            }\n            VisitResult::Custom(custom) => {\n                let children_output = output[list_output_start..].to_string();\n                output.truncate(list_output_start);\n                if let Some(custom_start) = list_start_custom {\n                    output.push_str(&custom_start);\n                }\n                output.push_str(&children_output);\n                output.push_str(&custom);\n            }\n            VisitResult::Skip => {\n                output.truncate(list_output_start);\n            }\n            VisitResult::PreserveHtml => {\n                output.truncate(list_output_start);\n                use crate::converter::serialize_node_to_html;\n                serialize_node_to_html(node_handle, parser, output);\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                output.truncate(list_output_start);\n            }\n        }\n    }\n}\n\n/// Public alias for `handle_ol` to match the expected module interface.\npub use handle_ol as handle;\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/unordered.rs",
    "content": "//! Unordered list handling (ul, li elements).\n//!\n//! Processes unordered lists with support for:\n//! - Bullet cycling based on nesting depth\n//! - Nested list handling\n//! - Loose/tight list detection\n//! - Proper indentation\n\nuse super::utils::{\n    add_list_leading_separator, add_nested_list_trailing_separator, calculate_list_nesting_depth, is_loose_list,\n    process_list_children,\n};\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::options::ConversionOptions;\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\nuse tl;\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handle unordered list element (<ul>).\n///\n/// Detects loose/tight list format, handles nested bullets,\n/// and processes list items with proper indentation.\n#[allow(clippy::too_many_arguments)]\npub fn handle_ul(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    add_list_leading_separator(output, ctx);\n\n    let nested_depth = calculate_list_nesting_depth(ctx);\n    let is_loose = is_loose_list(*node_handle, parser, dom_ctx);\n\n    #[allow(unused_variables)]\n    let tag = match node_handle.get(parser) {\n        Some(tl::Node::Tag(t)) => t,\n        _ => return,\n    };\n\n    #[cfg(feature = \"visitor\")]\n    let list_output_start = output.len();\n\n    #[cfg(feature = \"visitor\")]\n    let mut list_start_custom: Option<String> = None;\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let parent_tag = dom_ctx\n            .parent_of(node_handle.get_inner())\n            .and_then(|pid| dom_ctx.tag_name_for(dom_ctx.node_handle(pid).copied()?, parser))\n            .map(|s| s.to_string());\n\n        let index = dom_ctx.sibling_index(node_handle.get_inner()).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::List,\n            tag_name: \"ul\".to_string(),\n            attributes,\n            depth,\n            index_in_parent: index,\n            parent_tag,\n            is_inline: false,\n        };\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_list_start(&node_ctx, false)\n        };\n        match visit_result {\n            VisitResult::Continue => {}\n            VisitResult::Custom(custom) => {\n                list_start_custom = Some(custom);\n            }\n            VisitResult::Skip => {\n                return;\n            }\n            VisitResult::PreserveHtml => {\n                use crate::converter::serialize_node_to_html;\n                serialize_node_to_html(node_handle, parser, output);\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    if !ctx.in_table_cell {\n        if let Some(ref sc) = ctx.structure_collector {\n            sc.borrow_mut().push_list_start(false);\n        }\n    }\n\n    process_list_children(\n        *node_handle,\n        parser,\n        output,\n        options,\n        ctx,\n        depth,\n        false,\n        is_loose,\n        nested_depth,\n        1,\n        dom_ctx,\n    );\n\n    if !ctx.in_table_cell {\n        if let Some(ref sc) = ctx.structure_collector {\n            sc.borrow_mut().push_list_end();\n        }\n    }\n\n    add_nested_list_trailing_separator(output, ctx);\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n        let parent_tag = dom_ctx\n            .parent_of(node_handle.get_inner())\n            .and_then(|pid| dom_ctx.tag_name_for(dom_ctx.node_handle(pid).copied()?, parser))\n            .map(|s| s.to_string());\n\n        let index = dom_ctx.sibling_index(node_handle.get_inner()).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::List,\n            tag_name: \"ul\".to_string(),\n            attributes,\n            depth,\n            index_in_parent: index,\n            parent_tag,\n            is_inline: false,\n        };\n\n        let list_content = &output[list_output_start..];\n\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_list_end(&node_ctx, false, list_content)\n        };\n        match visit_result {\n            VisitResult::Continue => {\n                if let Some(custom_start) = list_start_custom {\n                    output.insert_str(list_output_start, &custom_start);\n                }\n            }\n            VisitResult::Custom(custom) => {\n                let children_output = output[list_output_start..].to_string();\n                output.truncate(list_output_start);\n                if let Some(custom_start) = list_start_custom {\n                    output.push_str(&custom_start);\n                }\n                output.push_str(&children_output);\n                output.push_str(&custom);\n            }\n            VisitResult::Skip => {\n                output.truncate(list_output_start);\n            }\n            VisitResult::PreserveHtml => {\n                output.truncate(list_output_start);\n                use crate::converter::serialize_node_to_html;\n                serialize_node_to_html(node_handle, parser, output);\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                output.truncate(list_output_start);\n            }\n        }\n    }\n}\n\n/// Public alias for `handle_ul` to match the expected module interface.\npub use handle_ul as handle;\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/list/utils.rs",
    "content": "//! Utility functions for list processing.\n//!\n//! Contains helper functions for loose list detection, indentation calculation,\n//! list spacing, and list child processing.\n\nuse crate::converter::main_helpers::{tag_name_eq, trim_trailing_whitespace};\nuse crate::options::{ConversionOptions, ListIndentType};\nuse tl;\n\n// Type aliases for Context and DomContext to avoid circular imports\n// These are imported from converter.rs and should be made accessible\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Calculate indentation level for list item continuations.\n///\n/// Returns the number of 4-space indent groups needed for list continuations.\n///\n/// List continuations (block elements inside list items) need special indentation:\n/// - Base indentation: (depth - 1) groups (for the nesting level)\n/// - Content indentation: depth groups (for the list item content)\n/// - Combined formula: (2 * depth - 1) groups of 4 spaces each\n///\n/// # Examples\n///\n/// ```text\n/// * Item 1           (depth=0, no continuation)\n/// * Item 2           (depth=0)\n///     Continuation   (depth=0: 0 groups = 0 spaces)\n///\n/// * Level 1          (depth=0)\n///     + Level 2      (depth=1)\n///             Cont   (depth=1: (2*1-1) = 1 group = 4 spaces, total 12 with bullet indent)\n/// ```\npub const fn calculate_list_continuation_indent(depth: usize) -> usize {\n    if depth > 0 { 2 * depth - 1 } else { 0 }\n}\n\n/// Check if a list (ul or ol) is \"loose\".\n///\n/// A loose list is one where any list item contains block-level elements\n/// like paragraphs (<p>). In loose lists, all items should have blank line\n/// separation (ending with \\n\\n) regardless of their own content.\n///\n/// # Examples\n///\n/// ```html\n/// <!-- Loose list (has <p> in an item) -->\n/// <ul>\n///   <li><p>Item 1</p></li>\n///   <li>Item 2</li>  <!-- Also gets \\n\\n ending -->\n/// </ul>\n///\n/// <!-- Tight list (no block elements) -->\n/// <ul>\n///   <li>Item 1</li>\n///   <li>Item 2</li>\n/// </ul>\n/// ```\npub fn is_loose_list(node_handle: tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                let is_li = dom_ctx.tag_info(child_handle.get_inner(), parser).map_or_else(\n                    || {\n                        matches!(\n                            child_handle.get(parser),\n                            Some(tl::Node::Tag(child_tag))\n                                if tag_name_eq(child_tag.name().as_utf8_str(), \"li\")\n                        )\n                    },\n                    |info| info.name == \"li\",\n                );\n                if !is_li {\n                    continue;\n                }\n\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    let li_children = child_tag.children();\n                    for li_child_handle in li_children.top().iter() {\n                        let is_p = dom_ctx.tag_info(li_child_handle.get_inner(), parser).map_or_else(\n                            || {\n                                matches!(\n                                    li_child_handle.get(parser),\n                                    Some(tl::Node::Tag(li_child_tag))\n                                        if tag_name_eq(li_child_tag.name().as_utf8_str(), \"p\")\n                                )\n                            },\n                            |info| info.name == \"p\",\n                        );\n                        if is_p {\n                            return true;\n                        }\n                    }\n                }\n            }\n        }\n    }\n    false\n}\n\n/// Add list continuation indentation to output.\n///\n/// Used when block elements (like <p> or <div>) appear inside list items.\n/// Adds appropriate line separation and indentation to continue the list item.\n///\n/// # Arguments\n///\n/// * `output` - The output string to append to\n/// * `list_depth` - Current list nesting depth\n/// * `blank_line` - If true, adds blank line separation (\\n\\n); if false, single newline (\\n)\n///\n/// # Examples\n///\n/// ```text\n/// Paragraph continuation (blank_line = true):\n///   * First para\n///\n///       Second para  (blank line + indentation)\n///\n/// Div continuation (blank_line = false):\n///   * First div\n///       Second div   (single newline + indentation)\n/// ```\npub fn add_list_continuation_indent(\n    output: &mut String,\n    list_depth: usize,\n    blank_line: bool,\n    options: &ConversionOptions,\n) {\n    trim_trailing_whitespace(output);\n\n    if blank_line {\n        if !output.ends_with(\"\\n\\n\") {\n            if output.ends_with('\\n') {\n                output.push('\\n');\n            } else {\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    } else if !output.ends_with('\\n') {\n        output.push('\\n');\n    }\n\n    let indent_level = calculate_list_continuation_indent(list_depth);\n    let indent_char = match options.list_indent_type {\n        ListIndentType::Tabs => \"\\t\",\n        ListIndentType::Spaces => {\n            for _ in 0..options.list_indent_width {\n                output.push(' ');\n            }\n            return;\n        }\n    };\n    for _ in 0..indent_level {\n        output.push_str(indent_char);\n    }\n}\n\n/// Calculate the indentation string for list continuations based on depth and options.\npub fn continuation_indent_string(list_depth: usize, options: &ConversionOptions) -> Option<String> {\n    let indent_level = calculate_list_continuation_indent(list_depth);\n    if indent_level == 0 {\n        return None;\n    }\n\n    let mut indent = String::new();\n    match options.list_indent_type {\n        ListIndentType::Tabs => {\n            for _ in 0..indent_level {\n                indent.push('\\t');\n            }\n        }\n        ListIndentType::Spaces => {\n            for _ in 0..(options.list_indent_width * indent_level) {\n                indent.push(' ');\n            }\n        }\n    }\n    Some(indent)\n}\n\n/// Add appropriate leading separator before a list.\n///\n/// Lists need different separators depending on context:\n/// - In table cells: <br> tag if there's already content\n/// - Outside lists: blank line (\\n\\n) if needed\n/// - Inside list items: blank line before nested list\npub fn add_list_leading_separator(output: &mut String, ctx: &Context) {\n    if ctx.in_table_cell {\n        let is_table_continuation =\n            !output.is_empty() && !output.ends_with('|') && !output.ends_with(' ') && !output.ends_with(\"<br>\");\n        if is_table_continuation {\n            output.push_str(\"<br>\");\n        }\n        return;\n    }\n\n    if !output.is_empty() && !ctx.in_list {\n        let needs_newline =\n            !output.ends_with(\"\\n\\n\") && !output.ends_with(\"* \") && !output.ends_with(\"- \") && !output.ends_with(\". \");\n        if needs_newline {\n            output.push_str(\"\\n\\n\");\n        }\n        return;\n    }\n\n    if ctx.in_list_item && !output.is_empty() {\n        let needs_newline =\n            !output.ends_with('\\n') && !output.ends_with(\"* \") && !output.ends_with(\"- \") && !output.ends_with(\". \");\n        if needs_newline {\n            trim_trailing_whitespace(output);\n            output.push('\\n');\n        }\n    }\n}\n\n/// Add appropriate trailing separator after a nested list.\n///\n/// Nested lists inside list items need trailing newlines to separate\n/// from following content. In loose lists, use blank line (\\n\\n). In tight lists, single newline (\\n).\npub fn add_nested_list_trailing_separator(output: &mut String, ctx: &Context) {\n    if !ctx.in_list_item {\n        return;\n    }\n\n    if ctx.loose_list {\n        if !output.ends_with(\"\\n\\n\") {\n            if !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n            output.push('\\n');\n        }\n    } else if !output.ends_with('\\n') {\n        output.push('\\n');\n    }\n}\n\n/// Calculate the nesting depth for a list.\n///\n/// If we're in a list but NOT in a list item, this is incorrectly nested HTML\n/// and we need to increment the depth. If in a list item, the depth was already\n/// incremented by the <li> element.\npub const fn calculate_list_nesting_depth(ctx: &Context) -> usize {\n    if ctx.in_list && !ctx.in_list_item {\n        ctx.list_depth + 1\n    } else {\n        ctx.list_depth\n    }\n}\n\n/// Check if a node is a list item element.\npub fn is_list_item(node_handle: tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {\n    if let Some(info) = dom_ctx.tag_info(node_handle.get_inner(), parser) {\n        return info.name == \"li\";\n    }\n    matches!(\n        node_handle.get(parser),\n        Some(tl::Node::Tag(tag)) if tag_name_eq(tag.name().as_utf8_str(), \"li\")\n    )\n}\n\n/// Process a list's children, tracking which items had block elements.\n///\n/// This is used to determine proper spacing between list items.\n/// Returns true if the last processed item had block children.\n#[allow(clippy::too_many_arguments)]\npub fn process_list_children(\n    node_handle: tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    is_ordered: bool,\n    is_loose: bool,\n    nested_depth: usize,\n    start_counter: usize,\n    dom_ctx: &DomContext,\n) {\n    let mut counter = start_counter;\n\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Raw(bytes)) = child_handle.get(parser) {\n                    if bytes.as_utf8_str().trim().is_empty() {\n                        continue;\n                    }\n                }\n\n                let list_ctx = Context {\n                    in_ordered_list: is_ordered,\n                    list_counter: if is_ordered { counter } else { 0 },\n                    in_list: true,\n                    list_depth: nested_depth,\n                    ul_depth: if is_ordered { ctx.ul_depth } else { ctx.ul_depth + 1 },\n                    loose_list: is_loose,\n                    prev_item_had_blocks: false,\n                    ..ctx.clone()\n                };\n\n                use crate::converter::walk_node;\n                walk_node(child_handle, parser, output, options, &list_ctx, depth, dom_ctx);\n\n                if is_ordered && is_list_item(*child_handle, parser, dom_ctx) {\n                    counter += 1;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/main.rs",
    "content": "//! Main conversion pipeline for HTML to Markdown.\n//!\n//! This module implements the core conversion functions and the recursive tree walker\n//! that transforms HTML DOM nodes into Markdown output.\n\n#![allow(\n    clippy::too_many_arguments,\n    clippy::too_many_lines,\n    clippy::trivially_copy_pass_by_ref,\n    clippy::items_after_statements\n)]\n\nuse std::borrow::Cow;\nuse std::collections::{BTreeMap, HashSet};\n\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main_helpers::{\n    extract_head_metadata, format_metadata_frontmatter, has_custom_element_tags, repair_with_html5ever,\n    trim_line_end_whitespace, trim_trailing_whitespace,\n};\nuse crate::converter::plain_text::extract_plain_text;\nuse crate::converter::preprocessing_helpers::{has_inline_block_misnest, should_drop_for_preprocessing};\nuse crate::converter::utility::caching::build_dom_context;\nuse crate::converter::utility::content::normalized_tag_name;\nuse crate::converter::utility::preprocessing::{preprocess_html, strip_hidden_elements, strip_script_and_style_tags};\nuse crate::converter::utility::serialization::serialize_tag_to_html;\nuse crate::options::OutputFormat;\n\nuse crate::converter::handlers::{handle_blockquote, handle_code, handle_graphic, handle_img, handle_link, handle_pre};\nuse crate::error::Result;\nuse crate::options::ConversionOptions;\n\nuse crate::converter::context::{Context, InlineCollectorHandle};\nuse crate::types::structure_collector::StructureCollectorHandle;\n\n/// Internal implementation of HTML to Markdown conversion.\n///\n/// Returns `(markdown, Option<DocumentStructure>)`.  The structure is populated when\n/// `options.include_document_structure == true` and a `structure_collector` handle is provided.\n#[cfg_attr(\n    any(not(feature = \"inline-images\"), not(feature = \"metadata\"), not(feature = \"visitor\")),\n    allow(unused_variables)\n)]\n#[allow(clippy::too_many_lines)]\npub fn convert_html_impl(\n    html: &str,\n    options: &ConversionOptions,\n    inline_collector: Option<InlineCollectorHandle>,\n    #[cfg(feature = \"metadata\")] metadata_collector: Option<crate::metadata::MetadataCollectorHandle>,\n    #[cfg(not(feature = \"metadata\"))] _metadata_collector: Option<()>,\n    #[cfg(feature = \"visitor\")] visitor: Option<crate::visitor::VisitorHandle>,\n    #[cfg(not(feature = \"visitor\"))] _visitor: Option<()>,\n    structure_collector: Option<StructureCollectorHandle>,\n) -> Result<(\n    String,\n    Option<crate::types::DocumentStructure>,\n    Vec<crate::types::TableData>,\n)> {\n    // Strip script and style tags completely to prevent parser confusion from HTML-like content\n    // inside script/style elements. This preserves JSON-LD for metadata extraction.\n    let stripped = strip_script_and_style_tags(html);\n    // Strip elements with the `hidden` attribute before parsing.\n    let stripped = strip_hidden_elements(&stripped);\n    let mut preprocessed = preprocess_html(&stripped).into_owned();\n    let mut preprocessed_len = preprocessed.len();\n\n    if has_custom_element_tags(&preprocessed) {\n        if let Some(repaired_html) = repair_with_html5ever(&preprocessed) {\n            let stripped = strip_script_and_style_tags(&repaired_html);\n            let stripped = strip_hidden_elements(&stripped);\n            let repaired = preprocess_html(&stripped).into_owned();\n            preprocessed = repaired;\n            preprocessed_len = preprocessed.len();\n        }\n    }\n    let parser_options = tl::ParserOptions::default();\n    let mut dom = loop {\n        if let Ok(dom) = tl::parse(&preprocessed, parser_options) {\n            break dom;\n        }\n        if let Some(repaired_html) = repair_with_html5ever(&preprocessed) {\n            let stripped = strip_script_and_style_tags(&repaired_html);\n            let stripped = strip_hidden_elements(&stripped);\n            preprocessed = preprocess_html(&stripped).into_owned();\n            preprocessed_len = preprocessed.len();\n            continue;\n        }\n        return Err(crate::error::ConversionError::ParseError(\n            \"Failed to parse HTML\".to_string(),\n        ));\n    };\n    let mut parser = dom.parser();\n    let mut output = String::with_capacity(preprocessed_len.saturating_add(preprocessed_len / 4));\n\n    let mut dom_ctx = build_dom_context(&dom, parser, preprocessed_len);\n\n    // Check for inline-block misnesting and repair if needed\n    if has_inline_block_misnest(&dom_ctx, parser) {\n        if let Some(repaired_html) = repair_with_html5ever(&preprocessed) {\n            // Drop dom to release borrow on preprocessed\n            drop(dom);\n            let stripped = strip_script_and_style_tags(&repaired_html);\n            let stripped = strip_hidden_elements(&stripped);\n            preprocessed = preprocess_html(&stripped).into_owned();\n            preprocessed_len = preprocessed.len();\n            // Re-parse with repaired HTML\n            dom = tl::parse(&preprocessed, parser_options)\n                .map_err(|_| crate::error::ConversionError::ParseError(\"Failed to parse repaired HTML\".to_string()))?;\n            parser = dom.parser();\n            dom_ctx = build_dom_context(&dom, parser, preprocessed_len);\n            output = String::with_capacity(preprocessed_len.saturating_add(preprocessed_len / 4));\n        }\n    }\n\n    // Plain text output: run the full pipeline (for metadata + visitor callbacks),\n    // then return plain text instead of markdown.\n    let is_plain_text = options.output_format == OutputFormat::Plain;\n\n    let wants_frontmatter = options.extract_metadata && !options.convert_as_inline;\n    #[cfg(feature = \"metadata\")]\n    let wants_document = metadata_collector\n        .as_ref()\n        .is_some_and(|collector| collector.borrow().wants_document());\n    #[cfg(not(feature = \"metadata\"))]\n    let wants_document = false;\n\n    if wants_frontmatter || wants_document {\n        let mut head_metadata: Option<BTreeMap<String, String>> = None;\n        #[cfg(feature = \"metadata\")]\n        let mut document_lang: Option<String> = None;\n        #[cfg(feature = \"metadata\")]\n        let mut document_dir: Option<String> = None;\n\n        for child_handle in dom.children() {\n            if head_metadata.is_none() {\n                let metadata = extract_head_metadata(child_handle, parser, options);\n                if !metadata.is_empty() {\n                    head_metadata = Some(metadata);\n                }\n            }\n\n            #[cfg(feature = \"metadata\")]\n            if wants_document {\n                if let Some(tl::Node::Tag(tag)) = child_handle.get(parser) {\n                    let tag_name = tag.name().as_utf8_str();\n                    if tag_name == \"html\" || tag_name == \"body\" {\n                        if document_lang.is_none() {\n                            if let Some(Some(lang_bytes)) = tag.attributes().get(\"lang\") {\n                                document_lang = Some(lang_bytes.as_utf8_str().to_string());\n                            }\n                        }\n                        if document_dir.is_none() {\n                            if let Some(Some(dir_bytes)) = tag.attributes().get(\"dir\") {\n                                document_dir = Some(dir_bytes.as_utf8_str().to_string());\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        if wants_frontmatter {\n            if let Some(metadata) = head_metadata.as_ref() {\n                if !metadata.is_empty() {\n                    let metadata_frontmatter = format_metadata_frontmatter(metadata);\n                    output.push_str(&metadata_frontmatter);\n                }\n            }\n        }\n\n        #[cfg(feature = \"metadata\")]\n        if wants_document {\n            if let Some(ref collector) = metadata_collector {\n                if let Some(metadata) = head_metadata {\n                    if !metadata.is_empty() {\n                        collector.borrow_mut().set_head_metadata(metadata);\n                    }\n                }\n                if let Some(lang) = document_lang {\n                    collector.borrow_mut().set_language(lang);\n                }\n                if let Some(dir) = document_dir {\n                    collector.borrow_mut().set_text_direction(dir);\n                }\n            }\n        }\n    }\n\n    let reference_collector = if options.link_style == crate::options::LinkStyle::Reference {\n        Some(std::rc::Rc::new(std::cell::RefCell::new(\n            crate::converter::reference_collector::ReferenceCollector::new(),\n        )))\n    } else {\n        None\n    };\n\n    #[cfg(all(feature = \"metadata\", feature = \"visitor\"))]\n    let mut ctx = Context::new(\n        options,\n        inline_collector,\n        metadata_collector,\n        visitor,\n        structure_collector.as_ref().map(std::rc::Rc::clone),\n        reference_collector.as_ref().map(std::rc::Rc::clone),\n    );\n    #[cfg(all(feature = \"metadata\", not(feature = \"visitor\")))]\n    let mut ctx = Context::new(\n        options,\n        inline_collector,\n        metadata_collector,\n        _visitor,\n        structure_collector.as_ref().map(std::rc::Rc::clone),\n        reference_collector.as_ref().map(std::rc::Rc::clone),\n    );\n    #[cfg(all(not(feature = \"metadata\"), feature = \"visitor\"))]\n    let mut ctx = Context::new(\n        options,\n        inline_collector,\n        _metadata_collector,\n        visitor,\n        structure_collector.as_ref().map(std::rc::Rc::clone),\n        reference_collector.as_ref().map(std::rc::Rc::clone),\n    );\n    #[cfg(all(not(feature = \"metadata\"), not(feature = \"visitor\")))]\n    let mut ctx = Context::new(\n        options,\n        inline_collector,\n        _metadata_collector,\n        _visitor,\n        structure_collector.as_ref().map(std::rc::Rc::clone),\n        reference_collector.as_ref().map(std::rc::Rc::clone),\n    );\n\n    // Pre-compute node IDs matching exclude_selectors so walk_node can skip them in O(1).\n    // Invalid or unsupported selectors are silently skipped.\n    if !options.exclude_selectors.is_empty() {\n        let mut excluded: HashSet<u32> = HashSet::new();\n        for selector in &options.exclude_selectors {\n            if let Some(iter) = dom.query_selector(selector) {\n                for handle in iter {\n                    excluded.insert(handle.get_inner());\n                }\n            }\n        }\n        ctx.set_excluded_node_ids(excluded);\n    }\n\n    for child_handle in dom.children() {\n        walk_node(child_handle, parser, &mut output, options, &ctx, 0, &dom_ctx);\n    }\n\n    #[cfg(feature = \"visitor\")]\n    if let Some(err) = ctx.visitor_error.borrow().as_ref() {\n        return Err(crate::error::ConversionError::Visitor(err.clone()));\n    }\n\n    // Drop ctx before unwrapping the structure collector Rc — ctx holds a cloned Rc\n    // reference to the same collector, and Rc::try_unwrap requires exactly one reference.\n    drop(ctx);\n\n    // Append reference-style link definitions if any were collected\n    if let Some(rc) = reference_collector {\n        if let Ok(collector) = std::rc::Rc::try_unwrap(rc) {\n            let ref_section = collector.into_inner().finish();\n            if !ref_section.is_empty() {\n                let trimmed_len = output.trim_end_matches('\\n').len();\n                output.truncate(trimmed_len);\n                output.push_str(\"\\n\\n\");\n                output.push_str(&ref_section);\n            }\n        }\n    }\n\n    // If plain text was requested, discard the markdown output and return plain text.\n    // The full pipeline was still run above so that metadata + visitor callbacks fire.\n    if is_plain_text {\n        let plain = extract_plain_text(&dom, parser, options);\n        let (document, tables) = finish_structure_collector(structure_collector);\n        return Ok((plain, document, tables));\n    }\n\n    trim_line_end_whitespace(&mut output);\n    let trimmed = output.trim_end_matches('\\n');\n    let markdown = if trimmed.is_empty() {\n        String::new()\n    } else {\n        format!(\"{trimmed}\\n\")\n    };\n\n    // Finish the structure collector if present.\n    let (document, tables) = finish_structure_collector(structure_collector);\n\n    Ok((markdown, document, tables))\n}\n\n/// Consume the structure collector and return the [`DocumentStructure`] and extracted\n/// [`TableData`] entries.  Returns `(None, vec![])` when no collector was provided.\nfn finish_structure_collector(\n    sc: Option<StructureCollectorHandle>,\n) -> (Option<crate::types::DocumentStructure>, Vec<crate::types::TableData>) {\n    match sc.and_then(|rc| std::rc::Rc::try_unwrap(rc).ok()) {\n        Some(cell) => {\n            let (doc, tables) = cell.into_inner().finish();\n            (Some(doc), tables)\n        }\n        None => (None, Vec::new()),\n    }\n}\n\n// has_more_than_one_char moved to main_helpers\n// is_inline_element available from utility::content\n\n/// Recursively walk DOM nodes and convert to Markdown.\n#[allow(clippy::only_used_in_recursion)]\n#[allow(clippy::trivially_copy_pass_by_ref)]\n#[allow(clippy::cast_possible_truncation)]\npub fn walk_node(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let Some(node) = node_handle.get(parser) else { return };\n\n    if let Some(max) = options.max_depth {\n        if depth >= max {\n            return;\n        }\n    }\n\n    match node {\n        tl::Node::Raw(bytes) => {\n            let raw = bytes.as_utf8_str();\n            crate::converter::text_node::process_text_node(\n                raw.as_ref(),\n                node_handle,\n                parser,\n                output,\n                options,\n                ctx,\n                depth,\n                dom_ctx,\n            );\n        }\n\n        tl::Node::Tag(tag) => {\n            let tag_name = match dom_ctx.tag_info(node_handle.get_inner(), parser) {\n                Some(info) => Cow::Borrowed(info.name.as_str()),\n                None => normalized_tag_name(tag.name().as_utf8_str()),\n            };\n\n            #[cfg(feature = \"visitor\")]\n            if let Some(ref visitor_handle) = ctx.visitor {\n                use crate::converter::visitor_hooks::{VisitAction, handle_visitor_element_start};\n\n                let action = handle_visitor_element_start(\n                    visitor_handle,\n                    tag_name.as_ref(),\n                    node_handle,\n                    tag,\n                    parser,\n                    output,\n                    ctx,\n                    depth,\n                    dom_ctx,\n                );\n\n                match action {\n                    VisitAction::Continue => {}\n                    VisitAction::Skip => return,\n                    VisitAction::Custom => return,\n                    VisitAction::Error => return,\n                }\n            }\n\n            if should_drop_for_preprocessing(tag_name.as_ref(), tag, options) {\n                trim_trailing_whitespace(output);\n                return;\n            }\n\n            // Drop elements matching exclude_selectors, including all their descendants.\n            if !ctx.excluded_node_ids.is_empty() && ctx.excluded_node_ids.contains(&node_handle.get_inner()) {\n                trim_trailing_whitespace(output);\n                return;\n            }\n\n            if ctx.strip_tags.contains(tag_name.as_ref()) {\n                let children = tag.children();\n                {\n                    for child_handle in children.top().iter() {\n                        walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                    }\n                }\n                return;\n            }\n\n            if ctx.preserve_tags.contains(tag_name.as_ref()) {\n                let html = serialize_tag_to_html(node_handle, parser);\n                output.push_str(&html);\n                return;\n            }\n\n            #[cfg(feature = \"metadata\")]\n            if matches!(tag_name.as_ref(), \"html\" | \"head\" | \"body\") && ctx.metadata_wants_document {\n                if let Some(ref collector) = ctx.metadata_collector {\n                    let mut c = collector.borrow_mut();\n\n                    if let Some(lang) = tag.attributes().get(\"lang\").flatten() {\n                        c.set_language(lang.as_utf8_str().to_string());\n                    }\n\n                    if let Some(dir) = tag.attributes().get(\"dir\").flatten() {\n                        c.set_text_direction(dir.as_utf8_str().to_string());\n                    }\n                }\n            }\n\n            #[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\n            let element_output_start = output.len();\n\n            match tag_name.as_ref() {\n                \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" => {\n                    crate::converter::block::heading::handle(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                \"p\" => {\n                    crate::converter::block::paragraph::handle(\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // All inline elements routed to inline dispatcher\n                \"strong\" | \"b\" | \"em\" | \"i\" | \"mark\" | \"del\" | \"s\" | \"ins\" | \"u\" | \"small\" | \"sub\" | \"sup\" | \"kbd\"\n                | \"samp\" | \"var\" | \"dfn\" | \"abbr\" | \"ruby\" | \"rb\" | \"rt\" | \"rp\" | \"rtc\" | \"span\" => {\n                    crate::converter::inline::dispatch_inline_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                \"a\" => handle_link(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n                \"img\" => handle_img(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n                \"graphic\" => handle_graphic(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n                \"code\" => handle_code(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n                \"pre\" => handle_pre(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n                \"blockquote\" => handle_blockquote(node_handle, tag, parser, output, options, ctx, depth, dom_ctx),\n\n                \"time\" | \"data\" => {\n                    crate::converter::block::container::handle_passthrough(\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Noop elements that produce no output\n                \"wbr\" | \"thead\" | \"tbody\" | \"tfoot\" | \"tr\" | \"th\" | \"td\" | \"source\" => {\n                    crate::converter::block::container::handle_noop(\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                \"br\" => crate::converter::block::line_break::handle(\n                    node_handle,\n                    parser,\n                    output,\n                    options,\n                    ctx,\n                    depth,\n                    dom_ctx,\n                ),\n                \"hr\" => crate::converter::block::horizontal_rule::handle(\n                    node_handle,\n                    parser,\n                    output,\n                    options,\n                    ctx,\n                    depth,\n                    dom_ctx,\n                ),\n                \"div\" => {\n                    crate::converter::block::div::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n                \"caption\" => crate::converter::block::table::handle_caption(\n                    node_handle,\n                    parser,\n                    output,\n                    options,\n                    ctx,\n                    depth,\n                    dom_ctx,\n                ),\n                \"table\" => crate::converter::block::table::handle_table_with_context(\n                    node_handle,\n                    parser,\n                    output,\n                    options,\n                    ctx,\n                    dom_ctx,\n                    depth,\n                ),\n\n                // List elements routed to list dispatcher\n                \"ul\" | \"ol\" | \"li\" | \"dl\" | \"dt\" | \"dd\" => {\n                    crate::converter::list::dispatch_list_handler(\n                        &tag_name,\n                        node_handle,\n                        tag,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Sectioning elements routed to semantic dispatcher\n                \"article\" | \"section\" | \"nav\" | \"aside\" | \"header\" | \"footer\" | \"main\" => {\n                    crate::converter::semantic::dispatch_semantic_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Quote element routed to semantic dispatcher\n                \"q\" => {\n                    crate::converter::semantic::dispatch_semantic_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Figure elements routed to semantic dispatcher\n                \"figure\" | \"figcaption\" => {\n                    crate::converter::semantic::dispatch_semantic_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Semantic interactive elements routed to semantic dispatcher\n                \"details\" | \"summary\" | \"dialog\" | \"menu\" => {\n                    crate::converter::semantic::dispatch_semantic_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Media elements routed to media dispatcher\n                \"audio\" | \"video\" | \"picture\" | \"iframe\" | \"svg\" | \"math\" => {\n                    crate::converter::media::dispatch_media_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Form elements routed to form dispatcher\n                \"form\" | \"fieldset\" | \"legend\" | \"label\" | \"input\" | \"textarea\" | \"select\" | \"option\" | \"optgroup\"\n                | \"button\" | \"progress\" | \"meter\" | \"output\" | \"datalist\" => {\n                    crate::converter::form::dispatch_form_handler(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                // Metadata elements routed to metadata handler\n                \"head\" | \"script\" | \"style\" => {\n                    crate::converter::metadata::handle(\n                        &tag_name,\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                \"body\" | \"html\" => {\n                    crate::converter::block::container::handle_structural_container(\n                        node_handle,\n                        parser,\n                        output,\n                        options,\n                        ctx,\n                        depth,\n                        dom_ctx,\n                    );\n                }\n\n                _ => {\n                    crate::converter::block::unknown::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n\n            #[cfg(feature = \"visitor\")]\n            if let Some(ref visitor_handle) = ctx.visitor {\n                use crate::converter::visitor_hooks::handle_visitor_element_end;\n\n                handle_visitor_element_end(\n                    visitor_handle,\n                    tag_name.as_ref(),\n                    node_handle,\n                    tag,\n                    parser,\n                    output,\n                    element_output_start,\n                    ctx,\n                    depth,\n                    dom_ctx,\n                );\n            }\n        }\n\n        tl::Node::Comment(_) => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/main_helpers.rs",
    "content": "//! Helper functions for HTML to Markdown conversion.\n//!\n//! This module contains utility functions used by the main conversion pipeline,\n//! including preprocessing helpers, HTML repair, and metadata formatting.\n\nuse std::collections::BTreeMap;\n\nuse crate::options::ConversionOptions;\n\n/// Compare two tag names case-insensitively.\npub fn tag_name_eq(a: impl AsRef<str>, b: &str) -> bool {\n    a.as_ref().eq_ignore_ascii_case(b)\n}\n\n/// Remove trailing spaces and tabs from a string.\npub fn trim_trailing_whitespace(output: &mut String) {\n    while output.ends_with(' ') || output.ends_with('\\t') {\n        output.pop();\n    }\n}\n\n/// Remove trailing spaces/tabs from every line while preserving newlines.\npub fn trim_line_end_whitespace(output: &mut String) {\n    if output.is_empty() {\n        return;\n    }\n\n    let mut cleaned = String::with_capacity(output.len());\n    for (idx, line) in output.split('\\n').enumerate() {\n        if idx > 0 {\n            cleaned.push('\\n');\n        }\n\n        let has_soft_break = line.ends_with(\"  \");\n        let trimmed = line.trim_end_matches([' ', '\\t']);\n\n        cleaned.push_str(trimmed);\n        if has_soft_break {\n            cleaned.push_str(\"  \");\n        }\n    }\n\n    cleaned.push('\\n');\n    *output = cleaned;\n}\n\n// has_inline_block_misnest and should_drop_for_preprocessing moved back to main.rs\n// due to DomContext circular dependency\n\n/// Check if HTML contains custom element tags.\npub fn has_custom_element_tags(html: &str) -> bool {\n    // Custom elements must have a hyphen in their TAG NAME, not in attributes\n    // Look for patterns like <foo-bar> or </foo-bar>\n    let bytes = html.as_bytes();\n    let len = bytes.len();\n    let mut i = 0;\n\n    while i < len {\n        if bytes[i] == b'<' {\n            i += 1;\n            if i >= len {\n                break;\n            }\n\n            // Skip closing tag marker\n            if bytes[i] == b'/' {\n                i += 1;\n                if i >= len {\n                    break;\n                }\n            }\n\n            // Skip whitespace\n            while i < len && bytes[i].is_ascii_whitespace() {\n                i += 1;\n            }\n\n            // Now we're at the start of a tag name - check if it contains a hyphen\n            let tag_start = i;\n            while i < len {\n                let ch = bytes[i];\n                if ch == b'>' || ch == b'/' || ch.is_ascii_whitespace() {\n                    // End of tag name\n                    let tag_name = &bytes[tag_start..i];\n                    if tag_name.contains(&b'-') {\n                        return true;\n                    }\n                    break;\n                }\n                i += 1;\n            }\n        } else {\n            i += 1;\n        }\n    }\n\n    false\n}\n\n/// HTML5 void elements that are self-closing by spec and must NOT be expanded.\n///\n/// These elements are always void in HTML5: they have no end tag, and `<br />` is\n/// equivalent to `<br>`.  We must leave them as-is when pre-processing XML-style\n/// self-closing syntax so that `repair_with_html5ever` can parse them correctly.\nconst HTML5_VOID_ELEMENTS: &[&str] = &[\n    \"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"param\", \"source\", \"track\", \"wbr\",\n];\n\n/// Expand XML-style self-closing tags to explicit open+close pairs.\n///\n/// HTML5 does not honour the `/>` self-close syntax for non-void elements.  When\n/// `repair_with_html5ever` re-parses content that contains custom / namespaced tags\n/// written as `<ac:parameter name=\"foo\" />`, the HTML5 parser treats the `/>` as `>`\n/// and leaves the element open.  Subsequent siblings then nest inside it, breaking\n/// visitor pre-order/post-order start/end pairing.\n///\n/// This function scans the input byte-by-byte and rewrites any `<tag ... />` where\n/// `tag` is not a known HTML5 void element into `<tag ...></tag>`.  Known void\n/// elements are left unchanged because they must not receive an explicit close tag.\n///\n/// # Correctness guarantees\n/// - Non-ASCII bytes are never interpreted as structural characters; all multi-byte\n///   UTF-8 sequences pass through unmodified via `&input[byte_offset..]` slicing.\n/// - Attribute values containing `/>` are skipped correctly (the scanner tracks\n///   whether it is inside a quoted attribute).\n/// - `</closing>` tags are never modified.\n/// - The function is pure and returns a new `String`; if no substitution is needed\n///   the allocation is still performed (cheap given repair is already rare).\npub fn expand_xml_self_closing_tags(input: &str) -> String {\n    let bytes = input.as_bytes();\n    let len = bytes.len();\n    let mut output = String::with_capacity(len);\n    // `copy_start` tracks the beginning of a contiguous span of unmodified input\n    // that should be copied verbatim to `output`.\n    let mut copy_start = 0usize;\n    let mut i = 0;\n\n    while i < len {\n        if bytes[i] != b'<' {\n            i += 1;\n            continue;\n        }\n\n        // We are at `<`. Flush the unmodified span up to (but not including) this `<`.\n        let tag_open = i;\n        i += 1;\n\n        // Skip closing tags entirely — they must not be modified.\n        if i < len && bytes[i] == b'/' {\n            // Scan to the matching `>`.\n            while i < len && bytes[i] != b'>' {\n                i += 1;\n            }\n            if i < len {\n                i += 1; // consume `>`\n            }\n            continue;\n        }\n\n        // Skip leading whitespace after `<` (unusual but tolerated).\n        while i < len && bytes[i].is_ascii_whitespace() {\n            i += 1;\n        }\n\n        // Collect the tag name (byte-aligned; tag names are always ASCII).\n        let name_start = i;\n        while i < len {\n            let ch = bytes[i];\n            if ch == b'>' || ch == b'/' || ch.is_ascii_whitespace() {\n                break;\n            }\n            i += 1;\n        }\n        let tag_name_bytes = &bytes[name_start..i];\n\n        // Empty tag name — emit verbatim and continue.\n        if tag_name_bytes.is_empty() {\n            continue;\n        }\n\n        // Check whether this is a known HTML5 void element (case-insensitive).\n        let tag_name_lower = tag_name_bytes.iter().map(u8::to_ascii_lowercase).collect::<Vec<_>>();\n        let is_void = HTML5_VOID_ELEMENTS\n            .iter()\n            .any(|v| v.as_bytes() == tag_name_lower.as_slice());\n\n        // Scan the rest of the tag to find `/>` or `>`, skipping quoted attrs.\n        let attrs_start = i;\n        let mut in_single_quote = false;\n        let mut in_double_quote = false;\n        let mut self_closing = false;\n\n        while i < len {\n            match bytes[i] {\n                b'\"' if !in_single_quote => {\n                    in_double_quote = !in_double_quote;\n                    i += 1;\n                }\n                b'\\'' if !in_double_quote => {\n                    in_single_quote = !in_single_quote;\n                    i += 1;\n                }\n                b'/' if !in_single_quote && !in_double_quote => {\n                    if i + 1 < len && bytes[i + 1] == b'>' {\n                        self_closing = true;\n                        break;\n                    }\n                    i += 1;\n                }\n                b'>' if !in_single_quote && !in_double_quote => {\n                    break;\n                }\n                _ => {\n                    i += 1;\n                }\n            }\n        }\n\n        if self_closing && !is_void {\n            // Flush unchanged input up to (not including) this tag.\n            output.push_str(&input[copy_start..tag_open]);\n\n            let tag_name_str = std::str::from_utf8(tag_name_bytes).unwrap_or(\"\");\n            // attrs_part covers everything between the end of the tag name and `/>`,\n            // i.e. `&input[attrs_start..i]` (the `/` at `i` is the start of `/>`)\n            let attrs_part = &input[attrs_start..i];\n\n            // Non-void: expand `<tag attrs/>` → `<tag attrs></tag>`.\n            output.push('<');\n            output.push_str(tag_name_str);\n            output.push_str(attrs_part);\n            output.push('>');\n            output.push('<');\n            output.push('/');\n            output.push_str(tag_name_str);\n            output.push('>');\n\n            i += 2; // consume `/>`\n            copy_start = i;\n        } else {\n            // Not a self-closing non-void tag: advance past `/>` or `>`.\n            if i < len && bytes[i] == b'/' {\n                i += 2; // skip `/>`\n            } else if i < len && bytes[i] == b'>' {\n                i += 1;\n            }\n        }\n    }\n\n    // Flush the remaining unchanged tail.\n    output.push_str(&input[copy_start..]);\n    output\n}\n\n/// Try to repair HTML using html5ever parser.\n///\n/// Returns Some(repaired_html) if repair was successful, None otherwise.\n///\n/// Before feeding the input to the HTML5 parser, XML-style self-closing tags on\n/// non-void elements (e.g. `<ac:parameter name=\"foo\" />`) are expanded to explicit\n/// open+close pairs.  This preserves the intended document structure because HTML5\n/// semantics do not honour `/>` on unknown elements — without the expansion, the\n/// element would be left open and subsequent siblings would nest inside it, breaking\n/// visitor start/end event pairing (issue #331).\npub fn repair_with_html5ever(input: &str) -> Option<String> {\n    use crate::rcdom::{RcDom, SerializableHandle};\n    use html5ever::serialize::{SerializeOpts, serialize};\n    use html5ever::tendril::TendrilSink;\n\n    // Expand XML-style self-closing on non-void elements before the HTML5 parse so\n    // that `<ac:parameter ... />` is not silently left open by the HTML5 parser.\n    let expanded = expand_xml_self_closing_tags(input);\n\n    let dom = html5ever::parse_document(RcDom::default(), Default::default())\n        .from_utf8()\n        .read_from(&mut expanded.as_bytes())\n        .ok()?;\n\n    let mut buf = Vec::with_capacity(input.len());\n    let handle = SerializableHandle::from(dom.document);\n    serialize(&mut buf, &handle, SerializeOpts::default()).ok()?;\n    String::from_utf8(buf).ok()\n}\n\n/// Format metadata as YAML frontmatter.\npub fn format_metadata_frontmatter(metadata: &BTreeMap<String, String>) -> String {\n    let mut result = String::from(\"---\\n\");\n    for (key, value) in metadata {\n        use std::fmt::Write as _;\n        let _ = writeln!(&mut result, \"{}: {}\", key, value);\n    }\n    result.push_str(\"---\\n\");\n    result\n}\n\n// should_drop_for_preprocessing moved back to main.rs due to DomContext dependency\n\n/// Extract metadata from the head element.\npub fn extract_head_metadata(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    options: &ConversionOptions,\n) -> BTreeMap<String, String> {\n    let mut metadata = BTreeMap::new();\n\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // Check if this is a head tag\n        if tag.name().as_utf8_str().eq_ignore_ascii_case(\"head\") {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    // Look for meta tags\n                    if child_tag.name().as_utf8_str().eq_ignore_ascii_case(\"meta\")\n                        && !options.strip_tags.iter().any(|t| t == \"meta\")\n                        && !options.preserve_tags.iter().any(|t| t == \"meta\")\n                    {\n                        if let (Some(name), Some(content)) = (\n                            child_tag.attributes().get(\"name\").flatten(),\n                            child_tag.attributes().get(\"content\").flatten(),\n                        ) {\n                            let name_str = name.as_utf8_str();\n                            let content_str = content.as_utf8_str();\n                            metadata.insert(format!(\"meta-{}\", name_str), content_str.to_string());\n                        }\n                        // Also check for property attribute (Open Graph, etc.)\n                        if let (Some(property), Some(content)) = (\n                            child_tag.attributes().get(\"property\").flatten(),\n                            child_tag.attributes().get(\"content\").flatten(),\n                        ) {\n                            let property_str = property.as_utf8_str();\n                            let content_str = content.as_utf8_str();\n                            metadata.insert(format!(\"meta-{}\", property_str), content_str.to_string());\n                        }\n                    }\n                    // Look for title tag\n                    if child_tag.name().as_utf8_str().eq_ignore_ascii_case(\"title\")\n                        && !options.strip_tags.iter().any(|t| t == \"title\")\n                        && !options.preserve_tags.iter().any(|t| t == \"title\")\n                    {\n                        // Extract text content from title tag\n                        let mut title_content = String::new();\n                        let title_children = child_tag.children();\n                        for title_child in title_children.top().iter() {\n                            if let Some(tl::Node::Raw(raw)) = title_child.get(parser) {\n                                title_content.push_str(raw.as_utf8_str().as_ref());\n                            }\n                        }\n                        title_content = title_content.trim().to_string();\n                        if !title_content.is_empty() {\n                            metadata.insert(\"title\".to_string(), title_content);\n                        }\n                    }\n                    // Look for link tags with rel attribute (e.g., canonical)\n                    if child_tag.name().as_utf8_str().eq_ignore_ascii_case(\"link\") {\n                        if let Some(rel_attr) = child_tag.attributes().get(\"rel\").flatten() {\n                            let rel_str = rel_attr.as_utf8_str();\n                            // Check for canonical link\n                            if rel_str.contains(\"canonical\") {\n                                if let Some(href_attr) = child_tag.attributes().get(\"href\").flatten() {\n                                    let href_str = href_attr.as_utf8_str();\n                                    metadata.insert(\"canonical\".to_string(), href_str.to_string());\n                                }\n                            }\n                        }\n                    }\n                    // Look for base tag with href attribute\n                    if child_tag.name().as_utf8_str().eq_ignore_ascii_case(\"base\") {\n                        if let Some(href_attr) = child_tag.attributes().get(\"href\").flatten() {\n                            let href_str = href_attr.as_utf8_str();\n                            // Store as \"base\" which will be mapped to base_href in extract_document_metadata\n                            metadata.insert(\"base\".to_string(), href_str.to_string());\n                        }\n                    }\n                }\n            }\n        } else {\n            // If this is not a head tag, recursively search children for head tag\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                let child_metadata = extract_head_metadata(child_handle, parser, options);\n                if !child_metadata.is_empty() {\n                    metadata.extend(child_metadata);\n                    break; // Only process first head tag found\n                }\n            }\n        }\n    }\n\n    metadata\n}\n\n/// Check if text has more than one character.\npub fn has_more_than_one_char(text: &str) -> bool {\n    let mut chars = text.chars();\n    chars.next().is_some() && chars.next().is_some()\n}\n\n/// Check if an element is inline (not block-level).\npub fn is_inline_element(tag_name: &str) -> bool {\n    matches!(\n        tag_name,\n        \"a\" | \"abbr\"\n            | \"b\"\n            | \"bdi\"\n            | \"bdo\"\n            | \"br\"\n            | \"cite\"\n            | \"code\"\n            | \"data\"\n            | \"dfn\"\n            | \"em\"\n            | \"i\"\n            | \"kbd\"\n            | \"mark\"\n            | \"q\"\n            | \"rp\"\n            | \"rt\"\n            | \"ruby\"\n            | \"s\"\n            | \"samp\"\n            | \"small\"\n            | \"span\"\n            | \"strong\"\n            | \"sub\"\n            | \"sup\"\n            | \"time\"\n            | \"u\"\n            | \"var\"\n            | \"wbr\"\n            | \"del\"\n            | \"ins\"\n            | \"img\"\n            | \"map\"\n            | \"area\"\n            | \"audio\"\n            | \"video\"\n            | \"picture\"\n            | \"source\"\n            | \"track\"\n            | \"embed\"\n            | \"object\"\n            | \"param\"\n            | \"input\"\n            | \"label\"\n            | \"button\"\n            | \"select\"\n            | \"textarea\"\n            | \"output\"\n            | \"progress\"\n            | \"meter\"\n    )\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/media/embedded.rs",
    "content": "//! Embedded media element handling (iframe, video, audio, source).\n//!\n//! Converts various embedded media elements:\n//! - **iframe**: Embedded content frames, outputs src as a link\n//! - **video**: Video elements with src or nested source elements\n//! - **audio**: Audio elements with src or nested source elements\n//! - **source**: Media source elements (handled within parent elements)\n//! - **picture**: Picture elements with responsive image sources\n\nuse std::borrow::Cow;\nuse tl::{HTMLTag, NodeHandle, Parser};\n\nuse crate::converter::Context;\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main_helpers::tag_name_eq;\nuse crate::options::ConversionOptions;\n\n/// Extract src attribute from media element (audio, video, iframe).\npub fn extract_media_src<'a>(tag: &'a HTMLTag<'a>) -> Cow<'a, str> {\n    tag.attributes()\n        .get(\"src\")\n        .flatten()\n        .map(|v| v.as_utf8_str())\n        .unwrap_or_else(|| Cow::Borrowed(\"\"))\n}\n\n/// Try to find source src from nested source element.\n///\n/// Used by audio and video elements to extract src from child <source> elements\n/// when the parent doesn't have a src attribute.\npub fn find_source_src<'a, T>(children: T, parser: &'a Parser) -> Option<Cow<'a, str>>\nwhere\n    T: IntoIterator<Item = &'a NodeHandle>,\n{\n    for child_handle in children {\n        if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            if tag_name_eq(child_tag.name().as_utf8_str(), \"source\") {\n                return child_tag.attributes().get(\"src\").flatten().map(|v| v.as_utf8_str());\n            }\n        }\n    }\n    None\n}\n\n/// Check if tag is a source element.\npub fn is_source_element(tag: &HTMLTag) -> bool {\n    tag_name_eq(tag.name().as_utf8_str(), \"source\")\n}\n\n/// Determine if media should output source link in markdown.\n///\n/// Returns true if src is non-empty.\npub fn should_output_media_link(src: &str) -> bool {\n    !src.is_empty()\n}\n\n/// Handle audio element conversion to Markdown.\n///\n/// Extracts src from audio tag or nested source elements, outputs as a link,\n/// and processes fallback content (e.g., browser compatibility text).\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_audio(\n    node_handle: &NodeHandle,\n    tag: &HTMLTag,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::main::walk_node;\n\n    let children = tag.children();\n    let src = if extract_media_src(tag).is_empty() {\n        find_source_src(children.top().iter(), parser).unwrap_or(Cow::Borrowed(\"\"))\n    } else {\n        extract_media_src(tag)\n    };\n    let src_opt: Option<&str> = if src.is_empty() { None } else { Some(src.as_ref()) };\n\n    // Dispatch the visitor callback when a visitor is attached.\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::converter::utility::content::collect_tag_attributes;\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes = collect_tag_attributes(tag);\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n        let node_ctx = NodeContext {\n            node_type: NodeType::Element,\n            tag_name: \"audio\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: false,\n        };\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_audio(&node_ctx, src_opt)\n        };\n        match visit_result {\n            VisitResult::Continue => {}\n            VisitResult::Skip => return,\n            VisitResult::Custom(custom) => {\n                output.push_str(&custom);\n                if !ctx.in_paragraph && !ctx.convert_as_inline && !custom.ends_with('\\n') {\n                    output.push('\\n');\n                }\n                return;\n            }\n            VisitResult::PreserveHtml => {\n                use crate::converter::utility::serialization::serialize_node;\n                output.push_str(&serialize_node(node_handle, parser));\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    if should_output_media_link(&src) {\n        if let Some(ref collector) = ctx.reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(&src, None);\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"][\");\n            output.push_str(&ref_num.to_string());\n            output.push(']');\n        } else {\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"](\");\n            output.push_str(&src);\n            output.push(')');\n        }\n        if !ctx.in_paragraph && !ctx.convert_as_inline {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n\n    let mut fallback = String::new();\n    for child_handle in tag.children().top().iter() {\n        let is_source = if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            is_source_element(child_tag)\n        } else {\n            false\n        };\n\n        if !is_source {\n            walk_node(child_handle, parser, &mut fallback, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n    if !fallback.is_empty() {\n        output.push_str(fallback.trim());\n        if !ctx.in_paragraph && !ctx.convert_as_inline {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handle video element conversion to Markdown.\n///\n/// Extracts src from video tag or nested source elements, outputs as a link,\n/// and processes fallback content (e.g., browser compatibility text).\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_video(\n    node_handle: &NodeHandle,\n    tag: &HTMLTag,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::main::walk_node;\n\n    let children = tag.children();\n    let src = if extract_media_src(tag).is_empty() {\n        find_source_src(children.top().iter(), parser).unwrap_or(Cow::Borrowed(\"\"))\n    } else {\n        extract_media_src(tag)\n    };\n    let src_opt: Option<&str> = if src.is_empty() { None } else { Some(src.as_ref()) };\n\n    // Dispatch the visitor callback when a visitor is attached.\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::converter::utility::content::collect_tag_attributes;\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes = collect_tag_attributes(tag);\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n        let node_ctx = NodeContext {\n            node_type: NodeType::Element,\n            tag_name: \"video\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: false,\n        };\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_video(&node_ctx, src_opt)\n        };\n        match visit_result {\n            VisitResult::Continue => {}\n            VisitResult::Skip => return,\n            VisitResult::Custom(custom) => {\n                output.push_str(&custom);\n                if !ctx.in_paragraph && !ctx.convert_as_inline && !custom.ends_with('\\n') {\n                    output.push('\\n');\n                }\n                return;\n            }\n            VisitResult::PreserveHtml => {\n                use crate::converter::utility::serialization::serialize_node;\n                output.push_str(&serialize_node(node_handle, parser));\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    if should_output_media_link(&src) {\n        if let Some(ref collector) = ctx.reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(&src, None);\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"][\");\n            output.push_str(&ref_num.to_string());\n            output.push(']');\n        } else {\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"](\");\n            output.push_str(&src);\n            output.push(')');\n        }\n        if !ctx.in_paragraph && !ctx.convert_as_inline {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n\n    let mut fallback = String::new();\n    for child_handle in tag.children().top().iter() {\n        let is_source = if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            is_source_element(child_tag)\n        } else {\n            false\n        };\n\n        if !is_source {\n            walk_node(child_handle, parser, &mut fallback, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n    if !fallback.is_empty() {\n        output.push_str(fallback.trim());\n        if !ctx.in_paragraph && !ctx.convert_as_inline {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handle picture element conversion to Markdown.\n///\n/// Finds and processes the first child img element, skipping source elements.\npub fn handle_picture(\n    _node_handle: &NodeHandle,\n    tag: &HTMLTag,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::main::walk_node;\n\n    for child_handle in tag.children().top().iter() {\n        if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            if tag_name_eq(child_tag.name().as_utf8_str(), \"img\") {\n                walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                break;\n            }\n        }\n    }\n}\n\n/// Handle iframe element conversion to Markdown.\n///\n/// Extracts src attribute from iframe and outputs as a markdown link.\n/// iframes cannot be embedded in markdown, so we just provide a link to the source.\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn handle_iframe(\n    node_handle: &NodeHandle,\n    tag: &HTMLTag,\n    output: &mut String,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n    parser: &Parser,\n) {\n    let src = tag\n        .attributes()\n        .get(\"src\")\n        .flatten()\n        .map_or(Cow::Borrowed(\"\"), |v| v.as_utf8_str());\n    let src_opt: Option<&str> = if src.is_empty() { None } else { Some(src.as_ref()) };\n\n    // Dispatch the visitor callback when a visitor is attached.\n    #[cfg(feature = \"visitor\")]\n    if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::converter::utility::content::collect_tag_attributes;\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n\n        let attributes = collect_tag_attributes(tag);\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n        let node_ctx = NodeContext {\n            node_type: NodeType::Element,\n            tag_name: \"iframe\".to_string(),\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: false,\n        };\n        let visit_result = {\n            let mut visitor = visitor_handle.borrow_mut();\n            visitor.visit_iframe(&node_ctx, src_opt)\n        };\n        match visit_result {\n            VisitResult::Continue => {}\n            VisitResult::Skip => return,\n            VisitResult::Custom(custom) => {\n                output.push_str(&custom);\n                if !ctx.in_paragraph && !ctx.convert_as_inline && !custom.ends_with('\\n') {\n                    output.push('\\n');\n                }\n                return;\n            }\n            VisitResult::PreserveHtml => {\n                use crate::converter::utility::serialization::serialize_node;\n                output.push_str(&serialize_node(node_handle, parser));\n                return;\n            }\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n        }\n    }\n\n    if !src.is_empty() {\n        if let Some(ref collector) = ctx.reference_collector {\n            let ref_num = collector.borrow_mut().get_or_insert(&src, None);\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"][\");\n            output.push_str(&ref_num.to_string());\n            output.push(']');\n        } else {\n            output.push('[');\n            output.push_str(&src);\n            output.push_str(\"](\");\n            output.push_str(&src);\n            output.push(')');\n        }\n        if !ctx.in_paragraph && !ctx.convert_as_inline {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/media/graphic.rs",
    "content": "//! Graphic element handling (custom graphic elements with alternative source attributes).\n//!\n//! The `<graphic>` element is a custom XML element used in publishing formats like EPUB.\n//! Conversion logic lives in `crate::converter::handlers::graphic`.\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/media/image.rs",
    "content": "//! Image element handling (img, data URIs, inline image collection).\n\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\n\n#[cfg(feature = \"inline-images\")]\nuse crate::inline_images::{InlineImageCollector, InlineImageFormat, InlineImageSource};\n\n#[cfg(feature = \"inline-images\")]\ntype InlineCollectorHandle = std::rc::Rc<std::cell::RefCell<InlineImageCollector>>;\n\n/// Handle inline data URIs in img elements with base64 encoding validation.\n///\n/// # Features\n/// - Base64 decoding with size limits\n/// - MIME type validation\n/// - Metadata extraction (width, height, alt text)\n/// - Format detection (PNG, JPEG, GIF, BMP, WebP, SVG)\n#[cfg(feature = \"inline-images\")]\n#[allow(clippy::items_after_statements)]\n#[allow(clippy::manual_let_else)]\npub fn handle_inline_data_image(\n    collector_ref: &InlineCollectorHandle,\n    src: &str,\n    alt: &str,\n    title: Option<&str>,\n    attributes: BTreeMap<String, String>,\n) {\n    let trimmed_src = src.trim();\n    if !trimmed_src.starts_with(\"data:\") {\n        return;\n    }\n\n    let mut collector = collector_ref.borrow_mut();\n    let index = collector.next_index();\n\n    let Some((meta, payload)) = trimmed_src.split_once(',') else {\n        collector.warn_skip(index, \"missing data URI separator\");\n        return;\n    };\n\n    if payload.trim().is_empty() {\n        collector.warn_skip(index, \"empty data URI payload\");\n        return;\n    }\n\n    if !meta.starts_with(\"data:\") {\n        collector.warn_skip(index, \"invalid data URI scheme\");\n        return;\n    }\n\n    let header = &meta[\"data:\".len()..];\n    if header.is_empty() {\n        collector.warn_skip(index, \"missing MIME type\");\n        return;\n    }\n\n    let mut segments = header.split(';');\n    let mime = segments.next().unwrap_or(\"\");\n    let Some((top_level, subtype_raw)) = mime.split_once('/') else {\n        collector.warn_skip(index, \"missing MIME subtype\");\n        return;\n    };\n\n    if !top_level.eq_ignore_ascii_case(\"image\") {\n        collector.warn_skip(index, format!(\"unsupported MIME type {mime}\"));\n        return;\n    }\n\n    let subtype_raw = subtype_raw.trim();\n    if subtype_raw.is_empty() {\n        collector.warn_skip(index, \"missing MIME subtype\");\n        return;\n    }\n\n    let mut is_base64 = false;\n    let mut inline_name: Option<String> = None;\n    for segment in segments {\n        if segment.eq_ignore_ascii_case(\"base64\") {\n            is_base64 = true;\n        } else if let Some(value) = segment.strip_prefix(\"name=\") {\n            inline_name = non_empty_trimmed(value.trim_matches('\"'));\n        } else if let Some(value) = segment.strip_prefix(\"filename=\") {\n            inline_name = non_empty_trimmed(value.trim_matches('\"'));\n        }\n    }\n\n    if !is_base64 {\n        collector.warn_skip(index, \"missing base64 encoding marker\");\n        return;\n    }\n\n    use base64::{Engine as _, engine::general_purpose::STANDARD};\n\n    let payload_clean = payload.trim();\n    let max_size = collector.max_decoded_size();\n    let max_encoded = max_size.saturating_div(3).saturating_mul(4).saturating_add(4);\n    if payload_clean.len() as u64 > max_encoded {\n        collector.warn_skip(\n            index,\n            format!(\n                \"encoded payload ({} bytes) exceeds configured max ({})\",\n                payload_clean.len(),\n                max_size\n            ),\n        );\n        return;\n    }\n\n    let decoded = if let Ok(bytes) = STANDARD.decode(payload_clean) {\n        bytes\n    } else {\n        collector.warn_skip(index, \"invalid base64 payload\");\n        return;\n    };\n\n    if decoded.is_empty() {\n        collector.warn_skip(index, \"empty base64 payload\");\n        return;\n    }\n\n    if decoded.len() as u64 > max_size {\n        collector.warn_skip(\n            index,\n            format!(\n                \"decoded payload ({} bytes) exceeds configured max ({})\",\n                decoded.len(),\n                max_size\n            ),\n        );\n        return;\n    }\n\n    let format = if subtype_raw.eq_ignore_ascii_case(\"png\") {\n        InlineImageFormat::Png\n    } else if subtype_raw.eq_ignore_ascii_case(\"jpeg\") || subtype_raw.eq_ignore_ascii_case(\"jpg\") {\n        InlineImageFormat::Jpeg\n    } else if subtype_raw.eq_ignore_ascii_case(\"gif\") {\n        InlineImageFormat::Gif\n    } else if subtype_raw.eq_ignore_ascii_case(\"bmp\") {\n        InlineImageFormat::Bmp\n    } else if subtype_raw.eq_ignore_ascii_case(\"webp\") {\n        InlineImageFormat::Webp\n    } else if subtype_raw.eq_ignore_ascii_case(\"svg+xml\") {\n        InlineImageFormat::Svg\n    } else {\n        InlineImageFormat::Other(subtype_raw.to_ascii_lowercase())\n    };\n\n    let description = non_empty_trimmed(alt).or_else(|| title.and_then(non_empty_trimmed));\n\n    let filename_candidate = attributes\n        .get(\"data-filename\")\n        .cloned()\n        .or_else(|| attributes.get(\"filename\").cloned())\n        .or_else(|| attributes.get(\"data-name\").cloned())\n        .or(inline_name);\n\n    let dimensions = collector.infer_dimensions(index, &decoded, &format);\n\n    let image = collector.build_image(\n        decoded,\n        format,\n        filename_candidate,\n        description,\n        dimensions,\n        InlineImageSource::ImgDataUri,\n        attributes,\n    );\n\n    collector.push_image(index, image);\n}\n\n/// Extract non-empty trimmed string or return None.\n#[cfg(feature = \"inline-images\")]\nfn non_empty_trimmed(value: &str) -> Option<String> {\n    let trimmed = value.trim();\n    if trimmed.is_empty() {\n        None\n    } else {\n        Some(trimmed.to_string())\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/media/mod.rs",
    "content": "//! Media element handlers for HTML-to-Markdown conversion.\n//!\n//! This module provides specialized handling for various media elements:\n//! - **Image**: img tags with inline data URI and metadata collection\n//! - **Graphic**: Custom graphic elements with multiple source attributes\n//! - **SVG**: SVG and MathML elements with serialization and base64 encoding\n//! - **Embedded**: iframe, video, audio, and source elements\n\npub mod embedded;\npub mod graphic;\npub mod image;\npub mod svg;\n\n// Re-export types from parent module for submodule access\npub use super::{Context, DomContext};\n\n#[cfg(feature = \"inline-images\")]\npub use image::handle_inline_data_image;\n\n/// Dispatches media element handling to the appropriate handler.\n///\n/// This function routes media-related HTML elements to their specialized handlers\n/// based on tag name. It is designed to be called from the main `walk_node`\n/// function in `converter.rs`.\n///\n/// # Routing Table\n///\n/// The following tag routes are supported:\n///\n/// | Tag(s) | Handler | Description |\n/// |--------|---------|-------------|\n/// | `iframe` | embedded | Embedded content frames |\n/// | `video` | embedded | Video elements |\n/// | `audio` | embedded | Audio elements |\n/// | `picture` | embedded | Responsive image containers |\n/// | `svg` | svg | SVG image elements |\n/// | `math` | svg | MathML elements |\n///\n/// # Return Value\n///\n/// Returns `true` if the tag was recognized and handled, `false` otherwise.\npub fn dispatch_media_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) -> bool {\n    let Some(node) = node_handle.get(parser) else {\n        return false;\n    };\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return false,\n    };\n\n    match tag_name {\n        \"iframe\" => {\n            embedded::handle_iframe(node_handle, tag, output, ctx, depth, dom_ctx, parser);\n            true\n        }\n        \"video\" => {\n            embedded::handle_video(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"audio\" => {\n            embedded::handle_audio(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"picture\" => {\n            embedded::handle_picture(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"svg\" => {\n            svg::handle_svg(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        \"math\" => {\n            svg::handle_math(node_handle, tag, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/media/svg.rs",
    "content": "//! SVG and MathML element handling with serialization and base64 encoding.\n\nuse crate::converter::main_helpers::tag_name_eq;\nuse crate::converter::utility::content::normalized_tag_name;\n#[allow(unused_imports)]\nuse std::collections::BTreeMap;\nuse tl::{NodeHandle, Parser};\n\n#[cfg(feature = \"inline-images\")]\nuse crate::inline_images::{InlineImageCollector, InlineImageFormat, InlineImageSource};\n\n#[cfg(feature = \"inline-images\")]\ntype InlineCollectorHandle = std::rc::Rc<std::cell::RefCell<InlineImageCollector>>;\n\n/// Handle inline SVG elements with size limits and base64 encoding.\n///\n/// # Features\n/// - SVG serialization to HTML string\n/// - Size validation with configurable limits\n/// - Base64 encoding for data URI\n/// - Metadata extraction (aria-label, title, dimensions)\n#[cfg(feature = \"inline-images\")]\n#[allow(clippy::trivially_copy_pass_by_ref)]\n#[allow(clippy::needless_pass_by_value)]\n#[allow(clippy::option_if_let_else)]\npub fn handle_inline_svg(\n    collector_ref: &InlineCollectorHandle,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    title_opt: Option<String>,\n    attributes: BTreeMap<String, String>,\n) {\n    let max_size = {\n        let borrow = collector_ref.borrow();\n        if !borrow.capture_svg() {\n            return;\n        }\n        borrow.max_decoded_size()\n    };\n\n    if max_size == 0 {\n        let mut collector = collector_ref.borrow_mut();\n        let index = collector.next_index();\n        collector.warn_skip(index, \"max SVG payload size is zero\");\n        return;\n    }\n\n    let mut collector = collector_ref.borrow_mut();\n    let index = collector.next_index();\n\n    let serialized = serialize_element(node_handle, parser);\n    if serialized.is_empty() {\n        collector.warn_skip(index, \"unable to serialize SVG element\");\n        return;\n    }\n\n    let data = serialized.into_bytes();\n    if data.len() as u64 > max_size {\n        collector.warn_skip(\n            index,\n            format!(\n                \"serialized SVG payload ({} bytes) exceeds configured max ({})\",\n                data.len(),\n                max_size\n            ),\n        );\n        return;\n    }\n\n    let description = attributes\n        .get(\"aria-label\")\n        .and_then(|value| non_empty_trimmed(value))\n        .or_else(|| title_opt.as_deref().and_then(non_empty_trimmed));\n\n    let filename_candidate = attributes\n        .get(\"data-filename\")\n        .cloned()\n        .or_else(|| attributes.get(\"filename\").cloned())\n        .or_else(|| attributes.get(\"data-name\").cloned());\n\n    let image = collector.build_image(\n        data,\n        InlineImageFormat::Svg,\n        filename_candidate,\n        description,\n        None,\n        InlineImageSource::SvgElement,\n        attributes,\n    );\n\n    collector.push_image(index, image);\n}\n\n/// Serialize an element to HTML string (for SVG and Math elements).\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn serialize_element(node_handle: &NodeHandle, parser: &Parser) -> String {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let tag_name = normalized_tag_name(tag.name().as_utf8_str());\n        let mut html = String::with_capacity(256);\n        html.push('<');\n        html.push_str(&tag_name);\n\n        for (key, value_opt) in tag.attributes().iter() {\n            html.push(' ');\n            html.push_str(&key);\n            if let Some(value) = value_opt {\n                html.push_str(\"=\\\"\");\n                html.push_str(&value);\n                html.push('\"');\n            }\n        }\n\n        let has_children = !tag.children().top().is_empty();\n        if has_children {\n            html.push('>');\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    html.push_str(&serialize_node(child_handle, parser));\n                }\n            }\n            html.push_str(\"</\");\n            html.push_str(&tag_name);\n            html.push('>');\n        } else {\n            html.push_str(\" />\");\n        }\n        return html;\n    }\n    String::new()\n}\n\n/// Serialize a node to HTML string.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn serialize_node(node_handle: &NodeHandle, parser: &Parser) -> String {\n    if let Some(node) = node_handle.get(parser) {\n        match node {\n            tl::Node::Raw(bytes) => bytes.as_utf8_str().to_string(),\n            tl::Node::Tag(_) => serialize_element(node_handle, parser),\n            _ => String::new(),\n        }\n    } else {\n        String::new()\n    }\n}\n\n/// Extract non-empty trimmed string or return None.\n#[cfg(feature = \"inline-images\")]\nfn non_empty_trimmed(value: &str) -> Option<String> {\n    let trimmed = value.trim();\n    if trimmed.is_empty() {\n        None\n    } else {\n        Some(trimmed.to_string())\n    }\n}\n\n/// Handle SVG element conversion to Markdown.\n///\n/// Extracts title from child elements, handles inline image collection,\n/// and outputs either the title text (in inline mode) or a base64-encoded image.\n#[allow(clippy::too_many_arguments)]\npub fn handle_svg(\n    node_handle: &NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    _depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    use crate::converter::utility::content::get_text_content;\n\n    let mut title = String::from(\"SVG Image\");\n    let children = tag.children();\n    for child_handle in children.top().iter() {\n        if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n            if tag_name_eq(child_tag.name().as_utf8_str(), \"title\") {\n                title = get_text_content(child_handle, parser, dom_ctx).trim().to_string();\n                break;\n            }\n        }\n    }\n\n    #[cfg(feature = \"inline-images\")]\n    if let Some(ref collector_ref) = ctx.inline_collector {\n        let title_opt = if title == \"SVG Image\" {\n            None\n        } else {\n            Some(title.clone())\n        };\n        let mut attributes_map = BTreeMap::new();\n        for (key, value_opt) in tag.attributes().iter() {\n            let key_str = key.to_string();\n            let keep = key_str == \"width\"\n                || key_str == \"height\"\n                || key_str == \"filename\"\n                || key_str == \"aria-label\"\n                || key_str.starts_with(\"data-\");\n            if keep {\n                let value = value_opt.map(|value| value.to_string()).unwrap_or_default();\n                attributes_map.insert(key_str, value);\n            }\n        }\n        handle_inline_svg(collector_ref, node_handle, parser, title_opt, attributes_map);\n    }\n\n    if options.skip_images {\n        return;\n    }\n\n    if ctx.convert_as_inline {\n        output.push_str(&title);\n    } else {\n        use base64::{Engine as _, engine::general_purpose::STANDARD};\n\n        let svg_html = serialize_element(node_handle, parser);\n        let base64_svg = STANDARD.encode(svg_html.as_bytes());\n\n        output.push_str(\"![\");\n        output.push_str(&title);\n        output.push_str(\"](data:image/svg+xml;base64,\");\n        output.push_str(&base64_svg);\n        output.push(')');\n    }\n}\n\n/// Handle MathML element conversion to Markdown.\n///\n/// Serializes MathML to HTML comment and outputs text content with escaping.\n#[allow(clippy::too_many_arguments)]\npub fn handle_math(\n    node_handle: &NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    _depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    use crate::converter::utility::content::get_text_content;\n    use crate::text;\n\n    let text_content = get_text_content(node_handle, parser, dom_ctx).trim().to_string();\n\n    if text_content.is_empty() {\n        return;\n    }\n\n    let math_html = serialize_element(node_handle, parser);\n\n    let escaped_text = text::escape(\n        &text_content,\n        options.escape_misc,\n        options.escape_asterisks,\n        options.escape_underscores,\n        options.escape_ascii,\n    );\n\n    let is_display_block = tag\n        .attributes()\n        .get(\"display\")\n        .flatten()\n        .is_some_and(|v| v.as_utf8_str() == \"block\");\n\n    if is_display_block && !ctx.in_paragraph && !ctx.convert_as_inline {\n        output.push_str(\"\\n\\n\");\n    }\n\n    output.push_str(\"<!-- MathML: \");\n    output.push_str(&math_html);\n    output.push_str(\" --> \");\n    output.push_str(&escaped_text);\n\n    if is_display_block && !ctx.in_paragraph && !ctx.convert_as_inline {\n        output.push_str(\"\\n\\n\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/metadata.rs",
    "content": "//! Handler for metadata and script elements (head, script, style, math).\n//!\n//! Converts various metadata-related elements:\n//! - **head**: Document metadata container; processes script[type=\"application/ld+json\"]\n//! - **script**: Script elements; extracts JSON-LD structured data when appropriate\n//! - **style**: CSS stylesheet elements; skipped in conversion\n//! - **math**: MathML elements with serialization and HTML comments for preservation\n\nuse crate::converter::media::svg::serialize_element;\nuse crate::options::ConversionOptions;\n#[cfg(feature = \"metadata\")]\nuse crate::text::decode_html_entities;\nuse crate::text::escape;\nuse tl::{NodeHandle, Parser};\n\n// Type aliases for Context and DomContext to avoid circular imports\ntype Context = crate::converter::Context;\ntype DomContext = crate::converter::DomContext;\n\n/// Handles metadata elements: head, script, style, math.\n///\n/// Processes various metadata-related elements:\n/// - head: Scans for structured data in script[type=\"application/ld+json\"]\n/// - script: Extracts JSON-LD for structured data collection\n/// - style: Skipped (CSS not relevant in markdown)\n/// - math: Preserves MathML as HTML comments with text content\npub fn handle(\n    tag_name: &str,\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    match tag_name {\n        \"head\" => {\n            handle_head(node_handle, parser, output, options, ctx, depth, dom_ctx);\n        }\n        \"script\" => {\n            handle_script(node_handle, parser, output, options, ctx);\n        }\n        \"style\" => {\n            // Style elements are skipped - no output\n        }\n        \"math\" => {\n            handle_math(node_handle, parser, output, options, ctx, dom_ctx);\n        }\n        _ => {}\n    }\n}\n\n/// Handle head element.\n///\n/// Head elements contain metadata. We process them to extract structured data from\n/// nested script[type=\"application/ld+json\"] elements if metadata collection is enabled.\nfn handle_head(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    use crate::converter::walk_node;\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let children = tag.children();\n    let has_body_like = children.top().iter().any(|child_handle| {\n        if let Some(child_name) = dom_ctx.tag_name_for(*child_handle, parser) {\n            matches!(\n                child_name.as_ref(),\n                \"body\" | \"main\" | \"article\" | \"section\" | \"div\" | \"p\"\n            )\n        } else {\n            false\n        }\n    });\n\n    #[cfg(feature = \"metadata\")]\n    if ctx.metadata_wants_structured_data {\n        if let Some(ref collector) = ctx.metadata_collector {\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    let child_name = dom_ctx\n                        .tag_name_for(*child_handle, parser)\n                        .unwrap_or_else(|| crate::converter::normalized_tag_name(child_tag.name().as_utf8_str()));\n                    if child_name.as_ref() == \"script\" {\n                        if let Some(type_attr) = child_tag.attributes().get(\"type\").flatten() {\n                            let type_value = type_attr.as_utf8_str();\n                            let type_value = type_value.as_ref();\n                            let type_value = type_value.split(';').next().unwrap_or(type_value);\n                            if type_value.trim().eq_ignore_ascii_case(\"application/ld+json\") {\n                                let json = child_tag.inner_text(parser);\n                                let json = json.trim();\n                                if !json.is_empty() {\n                                    let json = decode_html_entities(json);\n                                    if !json.is_empty() {\n                                        collector.borrow_mut().add_json_ld(json);\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // If head contains body-like elements (malformed HTML), process them\n    if has_body_like {\n        for child_handle in children.top().iter() {\n            walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n        }\n    }\n}\n\n/// Handle script element.\n///\n/// Script elements are processed to extract JSON-LD structured data when\n/// the type is \"application/ld+json\" and metadata collection is enabled.\n#[cfg_attr(not(feature = \"metadata\"), allow(unused_variables))]\nfn handle_script(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    _output: &mut String,\n    _options: &ConversionOptions,\n    ctx: &Context,\n) {\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    #[cfg(feature = \"metadata\")]\n    if let Some(type_attr) = tag.attributes().get(\"type\").flatten() {\n        let type_value = type_attr.as_utf8_str();\n        let type_value = type_value.as_ref();\n        let type_value = type_value.split(';').next().unwrap_or(type_value);\n        if type_value.trim().eq_ignore_ascii_case(\"application/ld+json\") && ctx.metadata_wants_structured_data {\n            if let Some(ref collector) = ctx.metadata_collector {\n                let json = tag.inner_text(parser);\n                let json = json.trim();\n                if !json.is_empty() {\n                    let json = decode_html_entities(json);\n                    if !json.is_empty() {\n                        collector.borrow_mut().add_json_ld(json);\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Handle math element.\n///\n/// MathML elements are serialized to HTML and wrapped in a comment to preserve them.\n/// The text content of the element is also output as plain text.\nfn handle_math(\n    node_handle: &NodeHandle,\n    parser: &Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    dom_ctx: &DomContext,\n) {\n    let text_content = crate::converter::get_text_content(node_handle, parser, dom_ctx)\n        .trim()\n        .to_string();\n\n    if text_content.is_empty() {\n        return;\n    }\n\n    let math_html = serialize_element(node_handle, parser);\n\n    let escaped_text = escape(\n        &text_content,\n        options.escape_misc,\n        options.escape_asterisks,\n        options.escape_underscores,\n        options.escape_ascii,\n    );\n\n    let Some(node) = node_handle.get(parser) else { return };\n\n    let tag = match node {\n        tl::Node::Tag(tag) => tag,\n        _ => return,\n    };\n\n    let is_display_block = tag\n        .attributes()\n        .get(\"display\")\n        .flatten()\n        .is_some_and(|v| v.as_utf8_str() == \"block\");\n\n    if is_display_block && !ctx.in_paragraph && !ctx.convert_as_inline {\n        output.push_str(\"\\n\\n\");\n    }\n\n    output.push_str(\"<!-- MathML: \");\n    output.push_str(&math_html);\n    output.push_str(\" --> \");\n    output.push_str(&escaped_text);\n\n    if is_display_block && !ctx.in_paragraph && !ctx.convert_as_inline {\n        output.push_str(\"\\n\\n\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/mod.rs",
    "content": "//! HTML to Markdown conversion engine with modular architecture.\n//!\n//! This module provides the complete conversion pipeline for transforming HTML documents\n//! into Markdown format. It follows a modular, type-safe design where HTML element handling\n//! is organized by semantic category (block, inline, list, table, etc.) with dispatch functions\n//! routing elements to their specialized handlers.\n//!\n//! # Module Organization\n//!\n//! The converter module is organized into semantic categories:\n//!\n//! - **[block]**: Block-level elements (headings, paragraphs, blockquotes, preformatted text, tables)\n//! - **[inline]**: Inline formatting (emphasis, links, code, semantic formatting)\n//! - **[list]**: List structures (ordered, unordered, definition lists)\n//! - **[table]**: Accessible via `block::table` submodule\n//! - **[media]**: Media elements (images, video, audio, embedded content, SVG)\n//! - **[semantic]**: Semantic HTML5 elements (sectioning, figures, interactive elements)\n//! - **[form]**: Form elements (inputs, selects, buttons, fieldsets)\n//! - **[utility]**: Helper functions (DOM traversal, caching, serialization, attributes)\n//! - **[text]**: Text processing and escaping (via crate::text module)\n//!\n//! # Public Types\n//!\n//! The main context types used across the conversion pipeline:\n//!\n//! - **[Context]**: Stateful conversion context tracking (e.g., list nesting, code blocks, in_heading)\n//! - **[DomContext]**: DOM relationship cache for efficient tree navigation\n//!\n//! # Conversion Flow\n//!\n//! The conversion process follows these steps:\n//!\n//! 1. **Parse HTML**: Input HTML is parsed into a DOM tree using the astral-tl parser\n//! 2. **Walk Tree**: Recursive tree walk starting from the root document node\n//! 3. **Dispatch**: Each element is dispatched to its handler based on tag name\n//! 4. **Convert**: Handler transforms the element to Markdown representation\n//! 5. **Post-process**: Text escaping and whitespace normalization\n//!\n//! # Handler Pattern\n//!\n//! Each submodule (block, inline, list, etc.) follows a consistent pattern:\n//!\n//! ```text\n//! // Module declares handlers for specific element types\n//! pub fn dispatch_<category>_handler(\n//!     tag_name: &str,\n//!     node_handle: &NodeHandle,\n//!     parser: &Parser,\n//!     output: &mut String,\n//!     options: &ConversionOptions,\n//!     ctx: &Context,\n//!     depth: usize,\n//!     dom_ctx: &DomContext,\n//! ) -> bool {\n//!     // Route to appropriate handler, return true if handled\n//! }\n//! ```\n//!\n//! # Visibility Rules\n//!\n//! - **Context & DomContext**: Public types for external module coordination\n//! - **Dispatch functions**: Public for main walk_node caller\n//! - **Individual handlers**: Typically pub for direct access if needed\n//! - **Internal utilities**: pub(crate) or pub(super) for module-internal use\n//!\n//! # Feature Support\n//!\n//! - Inline image extraction (`inline-images` feature)\n//! - Metadata collection (`metadata` feature)\n//! - Custom visitor callbacks (`visitor` feature)\n//!\n//! # Example Integration\n//!\n//! Once `converter.rs` is refactored to use `converter/main.rs`, the walk_node function\n//! will use dispatch functions like:\n//!\n//! ```text\n//! use crate::converter::{block, inline, list, media, semantic, form};\n//!\n//! fn walk_node(...) {\n//!     // Try each dispatcher in order\n//!     if block::dispatch_block_handler(&tag, ...) { return; }\n//!     if inline::dispatch_inline_handler(&tag, ...) { return; }\n//!     if list::dispatch_list_handler(&tag, ...) { return; }\n//!     if media::dispatch_media_handler(&tag, ...) { return; }\n//!     if semantic::dispatch_semantic_handler(&tag, ...) { return; }\n//!     if form::dispatch_form_handler(&tag, ...) { return; }\n//!     // Default handling for unrecognized tags\n//! }\n//! ```\n\npub mod block;\npub mod context;\npub mod dom_context;\npub mod form;\npub mod format;\npub mod handlers;\npub mod inline;\npub mod list;\npub mod main;\nmod main_helpers;\npub mod media;\nmod metadata;\npub mod plain_text;\npub mod preprocessing_helpers;\npub mod reference_collector;\npub mod semantic;\npub mod text;\nmod text_node;\npub mod utility;\n\n#[cfg(feature = \"visitor\")]\npub mod visitor_hooks;\n\n// Import and re-export public types and functions from the main module\npub use self::context::Context;\npub use self::dom_context::DomContext;\n\n// Import the tree walker and utility functions from main and main_helpers\npub use self::main::{convert_html_impl, walk_node};\npub use self::main_helpers::trim_trailing_whitespace;\n\n// Re-export helper functions from utility modules (migrated from converter_legacy)\npub use crate::converter::utility::content::{chomp_inline, get_text_content, normalized_tag_name};\n#[allow(unused_imports)]\npub use crate::converter::utility::serialization::{serialize_node, serialize_node_to_html};\n\n// Helper functions migrated to utility modules\npub use crate::converter::utility::siblings::append_inline_suffix;\n\n// Caching functions migrated to utility/caching\n\n// Content functions migrated to utility/content\n\n// Heading functions migrated to block/heading\npub use crate::converter::block::heading::find_single_heading_child;\n\n// Link functions migrated to inline/link\n\n// Re-export dispatch functions for routing elements to handlers\n// Media module doesn't have a dispatcher - it exports utility functions\n\n// Re-export utility submodules for public access to their types\n// NOTE: utility::preprocessing is deliberately not re-exported to avoid naming conflict\n// with preprocessing_helpers module. Users should access utility::preprocessing directly.\n\n// Re-export format renderer types\n\n// Block and inline handlers are internal - only dispatchers are exposed\n// Individual handlers are pub(crate) and not meant to be part of the public API\n\n// Re-export media utilities for internal use (crate-private)\n\n// Re-export list utilities for internal use (crate-private)\n\n// Semantic and form handlers are also internal (pub(crate))\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/plain_text.rs",
    "content": "//! Plain text extraction from parsed HTML DOM.\n//!\n//! Provides a fast-path text extractor that walks the DOM tree collecting only\n//! visible text content with structural whitespace, bypassing the full\n//! Markdown/Djot conversion pipeline.\n\nuse std::collections::HashSet;\nuse std::fmt::Write;\n\nuse crate::converter::preprocessing_helpers::should_drop_for_preprocessing;\nuse crate::options::ConversionOptions;\nuse crate::text;\n\n/// Tracks list context for proper marker emission on `<li>` elements.\n#[derive(Clone, Debug)]\nenum ListContext {\n    /// Not inside any list.\n    None,\n    /// Inside `<ul>` — each `<li>` gets a `- ` prefix.\n    Unordered,\n    /// Inside `<ol>` — each `<li>` gets a sequential `N. ` prefix.\n    /// The `next_index` is incremented after each `<li>`.\n    Ordered { next_index: u32 },\n}\n\n/// Tags whose content should be skipped entirely.\nconst SKIP_TAGS: &[&str] = &[\"script\", \"style\", \"head\", \"template\", \"noscript\", \"svg\", \"math\"];\n\n/// Block-level tags that should be separated by blank lines.\nconst BLOCK_TAGS: &[&str] = &[\n    \"p\",\n    \"div\",\n    \"h1\",\n    \"h2\",\n    \"h3\",\n    \"h4\",\n    \"h5\",\n    \"h6\",\n    \"blockquote\",\n    \"section\",\n    \"article\",\n    \"aside\",\n    \"main\",\n    \"nav\",\n    \"header\",\n    \"footer\",\n    \"figure\",\n    \"figcaption\",\n    \"details\",\n    \"summary\",\n    \"address\",\n    \"hgroup\",\n    \"search\",\n];\n\n/// Extract plain text from a parsed DOM tree.\n///\n/// Walks the tree collecting visible text with structural whitespace:\n/// - Block elements get blank-line separation\n/// - `<br>` becomes a newline, `<hr>` a blank line\n/// - `<pre>` preserves internal whitespace\n/// - `<img>` outputs alt text (unless `skip_images` is set)\n/// - `<script>`, `<style>`, `<head>`, `<template>`, `<noscript>` are skipped\n/// - Tables: cells separated by tab, rows by newline\n/// - Inline elements are recursed without markers\n/// - Nodes matching `excluded_node_ids` (from `exclude_selectors`) are dropped entirely\npub fn extract_plain_text(dom: &tl::VDom, parser: &tl::Parser, options: &ConversionOptions) -> String {\n    let mut buf = String::with_capacity(1024);\n    let mut list_ctx = ListContext::None;\n\n    // Pre-compute excluded node IDs from exclude_selectors.\n    let excluded_node_ids: HashSet<u32> = if options.exclude_selectors.is_empty() {\n        HashSet::new()\n    } else {\n        let mut ids = HashSet::new();\n        for selector in &options.exclude_selectors {\n            if let Some(iter) = dom.query_selector(selector) {\n                for handle in iter {\n                    ids.insert(handle.get_inner());\n                }\n            }\n        }\n        ids\n    };\n\n    for child_handle in dom.children() {\n        walk_plain(\n            child_handle,\n            parser,\n            &mut buf,\n            options,\n            false,\n            &mut list_ctx,\n            &excluded_node_ids,\n        );\n    }\n\n    post_process(&mut buf);\n    buf\n}\n\n/// Recursive plain-text walker.\nfn walk_plain(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    buf: &mut String,\n    options: &ConversionOptions,\n    in_pre: bool,\n    list_ctx: &mut ListContext,\n    excluded_node_ids: &HashSet<u32>,\n) {\n    let Some(node) = node_handle.get(parser) else {\n        return;\n    };\n\n    match node {\n        tl::Node::Raw(bytes) => {\n            let raw = bytes.as_utf8_str();\n            let decoded = text::decode_html_entities_cow(raw.as_ref());\n            if in_pre {\n                buf.push_str(&decoded);\n            } else {\n                let normalized = text::normalize_whitespace_cow(&decoded);\n                if !normalized.is_empty() {\n                    // Avoid leading space at start of a new line\n                    if normalized.as_ref() == \" \" && buf.ends_with('\\n') {\n                        return;\n                    }\n                    buf.push_str(&normalized);\n                }\n            }\n        }\n        tl::Node::Tag(tag) => {\n            // Drop elements matching exclude_selectors, including all their descendants.\n            if !excluded_node_ids.is_empty() && excluded_node_ids.contains(&node_handle.get_inner()) {\n                return;\n            }\n\n            let tag_name = tag.name().as_utf8_str().to_ascii_lowercase();\n            let tag_str = tag_name.as_str();\n\n            // Skip invisible content\n            if SKIP_TAGS.contains(&tag_str) {\n                return;\n            }\n\n            // Apply preprocessing: drop nav/footer/aside/noise elements\n            // (shared logic with the markdown path).\n            if should_drop_for_preprocessing(tag_str, tag, options) {\n                return;\n            }\n\n            match tag_str {\n                \"br\" => {\n                    buf.push('\\n');\n                }\n                \"hr\" => {\n                    ensure_blank_line(buf);\n                }\n                \"pre\" => {\n                    ensure_blank_line(buf);\n                    walk_children(tag, parser, buf, options, true, list_ctx, excluded_node_ids);\n                    ensure_blank_line(buf);\n                }\n                \"img\" => {\n                    if !options.skip_images {\n                        if let Some(Some(alt)) = tag.attributes().get(\"alt\") {\n                            let alt_text = alt.as_utf8_str();\n                            if !alt_text.is_empty() {\n                                buf.push_str(alt_text.as_ref());\n                            }\n                        }\n                    }\n                }\n                \"table\" => {\n                    ensure_blank_line(buf);\n                    walk_table(tag, parser, buf, options, excluded_node_ids);\n                    ensure_blank_line(buf);\n                }\n                \"ul\" => {\n                    ensure_newline(buf);\n                    let mut child_ctx = ListContext::Unordered;\n                    walk_children(tag, parser, buf, options, false, &mut child_ctx, excluded_node_ids);\n                    ensure_newline(buf);\n                }\n                \"ol\" => {\n                    let start = tag\n                        .attributes()\n                        .get(\"start\")\n                        .flatten()\n                        .and_then(|v| v.as_utf8_str().parse::<u32>().ok())\n                        .unwrap_or(1);\n                    ensure_newline(buf);\n                    let mut child_ctx = ListContext::Ordered { next_index: start };\n                    walk_children(tag, parser, buf, options, false, &mut child_ctx, excluded_node_ids);\n                    ensure_newline(buf);\n                }\n                \"li\" => {\n                    ensure_newline(buf);\n                    match list_ctx {\n                        ListContext::Unordered => {\n                            buf.push_str(\"- \");\n                        }\n                        ListContext::Ordered { next_index } => {\n                            let _ = write!(buf, \"{}. \", next_index);\n                            *next_index += 1;\n                        }\n                        ListContext::None => {\n                            // <li> outside a list — emit with bullet as fallback\n                            buf.push_str(\"- \");\n                        }\n                    }\n                    walk_children(tag, parser, buf, options, false, list_ctx, excluded_node_ids);\n                    ensure_newline(buf);\n                }\n                _ if BLOCK_TAGS.contains(&tag_str) => {\n                    ensure_blank_line(buf);\n                    walk_children(tag, parser, buf, options, in_pre, list_ctx, excluded_node_ids);\n                    ensure_blank_line(buf);\n                }\n                _ => {\n                    // Inline elements and structural containers (html, body, etc.)\n                    walk_children(tag, parser, buf, options, in_pre, list_ctx, excluded_node_ids);\n                }\n            }\n        }\n        tl::Node::Comment(_) => {}\n    }\n}\n\n/// Walk all children of a tag.\nfn walk_children(\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    buf: &mut String,\n    options: &ConversionOptions,\n    in_pre: bool,\n    list_ctx: &mut ListContext,\n    excluded_node_ids: &HashSet<u32>,\n) {\n    let children = tag.children();\n    let top = children.top();\n    for child in top.iter() {\n        walk_plain(child, parser, buf, options, in_pre, list_ctx, excluded_node_ids);\n    }\n}\n\n/// Walk a `<table>` element, extracting cells as tab-separated, rows as newline-separated.\nfn walk_table(\n    table_tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    buf: &mut String,\n    options: &ConversionOptions,\n    excluded_node_ids: &HashSet<u32>,\n) {\n    // Collect all <tr> node handles by recursing into the table\n    let mut row_handles = Vec::new();\n    collect_descendant_handles(table_tag, parser, \"tr\", &mut row_handles);\n\n    for (row_idx, row_handle) in row_handles.iter().enumerate() {\n        if row_idx > 0 {\n            buf.push('\\n');\n        }\n        let Some(tl::Node::Tag(row_tag)) = row_handle.get(parser) else {\n            continue;\n        };\n\n        // Collect direct <th>/<td> children\n        let mut cell_handles = Vec::new();\n        let row_children = row_tag.children();\n        let row_top = row_children.top();\n        for child in row_top.iter() {\n            if let Some(tl::Node::Tag(child_tag)) = child.get(parser) {\n                let name = child_tag.name().as_utf8_str();\n                if name.eq_ignore_ascii_case(\"th\") || name.eq_ignore_ascii_case(\"td\") {\n                    cell_handles.push(*child);\n                }\n            }\n        }\n\n        for (cell_idx, cell_handle) in cell_handles.iter().enumerate() {\n            if cell_idx > 0 {\n                buf.push('\\t');\n            }\n            let mut cell_buf = String::new();\n            if let Some(tl::Node::Tag(cell_tag)) = cell_handle.get(parser) {\n                let mut cell_list_ctx = ListContext::None;\n                walk_children(\n                    cell_tag,\n                    parser,\n                    &mut cell_buf,\n                    options,\n                    false,\n                    &mut cell_list_ctx,\n                    excluded_node_ids,\n                );\n            }\n            buf.push_str(cell_buf.trim());\n        }\n    }\n}\n\n/// Recursively collect all descendant `NodeHandle`s matching `target_tag` (by cloning handles).\nfn collect_descendant_handles(\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    target_tag: &str,\n    result: &mut Vec<tl::NodeHandle>,\n) {\n    let children = tag.children();\n    let top = children.top();\n    for child in top.iter() {\n        if let Some(tl::Node::Tag(child_tag)) = child.get(parser) {\n            if child_tag.name().as_utf8_str().eq_ignore_ascii_case(target_tag) {\n                result.push(*child);\n            } else {\n                collect_descendant_handles(child_tag, parser, target_tag, result);\n            }\n        }\n    }\n}\n\n/// Ensure the buffer ends with a blank line (two newlines).\nfn ensure_blank_line(buf: &mut String) {\n    if buf.is_empty() {\n        return;\n    }\n    // Strip trailing horizontal whitespace\n    while buf.ends_with(' ') || buf.ends_with('\\t') {\n        buf.pop();\n    }\n    let current_newlines = buf.chars().rev().take_while(|&c| c == '\\n').count();\n    for _ in current_newlines..2 {\n        buf.push('\\n');\n    }\n}\n\n/// Ensure the buffer ends with at least one newline.\nfn ensure_newline(buf: &mut String) {\n    if buf.is_empty() {\n        return;\n    }\n    if !buf.ends_with('\\n') {\n        buf.push('\\n');\n    }\n}\n\n/// Collapse runs of 3 or more consecutive newlines to exactly 2 in a single pass.\nfn collapse_triple_newlines(buf: &mut String) {\n    let bytes = buf.as_bytes();\n    let mut result = String::with_capacity(buf.len());\n    let mut newline_count = 0usize;\n    for &b in bytes {\n        if b == b'\\n' {\n            newline_count += 1;\n            if newline_count <= 2 {\n                result.push('\\n');\n            }\n        } else {\n            newline_count = 0;\n            result.push(b as char);\n        }\n    }\n    *buf = result;\n}\n\n/// Trim trailing whitespace from every line in a buffer without allocating per-line strings.\n///\n/// Uses a single allocation of the same capacity, writing each line's trimmed content\n/// and inserting newline separators directly.\nfn trim_line_ends(buf: &mut String) {\n    let mut result = String::with_capacity(buf.len());\n    for line in buf.lines() {\n        if !result.is_empty() {\n            result.push('\\n');\n        }\n        result.push_str(line.trim_end());\n    }\n    *buf = result;\n}\n\n/// Post-process: collapse 3+ newlines to 2, trim line-end whitespace, ensure single trailing newline.\nfn post_process(buf: &mut String) {\n    // Collapse runs of 3+ newlines to exactly 2\n    collapse_triple_newlines(buf);\n\n    // Trim trailing whitespace from each line in-place\n    trim_line_ends(buf);\n\n    // Trim to single trailing newline\n    let keep = buf.trim_end_matches('\\n').len();\n    if keep == 0 {\n        buf.clear();\n    } else {\n        buf.truncate(keep);\n        buf.push('\\n');\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/preprocessing_helpers.rs",
    "content": "//! HTML preprocessing and validation helpers.\n//!\n//! This module contains helper functions for preprocessing HTML before conversion,\n//! including validation and normalization checks.\n\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main_helpers::is_inline_element;\nuse crate::converter::utility::attributes::{attribute_matches_any, element_has_navigation_hint};\nuse crate::converter::utility::content::normalized_tag_name;\nuse crate::options::ConversionOptions;\n\n/// Check if an inline ancestor element is allowed to contain block-level elements.\npub fn inline_ancestor_allows_block(tag_name: &str) -> bool {\n    matches!(tag_name, \"a\" | \"ins\" | \"del\")\n}\n\n/// Detect block elements that were incorrectly nested under inline ancestors.\n///\n/// Excludes elements inside `<pre>` or `<code>` blocks, as they have special\n/// whitespace preservation rules and should not be repaired.\npub fn has_inline_block_misnest(dom_ctx: &DomContext, parser: &tl::Parser) -> bool {\n    for handle in dom_ctx.node_map.iter().flatten() {\n        if let Some(tl::Node::Tag(_tag)) = handle.get(parser) {\n            let is_block = dom_ctx\n                .tag_info(handle.get_inner(), parser)\n                .map(|info| info.is_block)\n                .unwrap_or(false);\n            if is_block {\n                // Check if this block element or any ancestor is pre/code\n                let mut check_parent = Some(handle.get_inner());\n                let mut inside_preformatted = false;\n                while let Some(node_id) = check_parent {\n                    if let Some(info) = dom_ctx.tag_info(node_id, parser) {\n                        if matches!(info.name.as_str(), \"pre\" | \"code\") {\n                            inside_preformatted = true;\n                            break;\n                        }\n                    }\n                    check_parent = dom_ctx.parent_of(node_id);\n                }\n\n                // Skip misnesting check for elements inside pre/code blocks\n                if inside_preformatted {\n                    continue;\n                }\n\n                let mut current = dom_ctx.parent_of(handle.get_inner());\n                while let Some(parent_id) = current {\n                    if let Some(parent_info) = dom_ctx.tag_info(parent_id, parser) {\n                        if is_inline_element(&parent_info.name) && !inline_ancestor_allows_block(&parent_info.name) {\n                            return true;\n                        }\n                    } else if let Some(parent_handle) = dom_ctx.node_handle(parent_id) {\n                        if let Some(tl::Node::Tag(parent_tag)) = parent_handle.get(parser) {\n                            let parent_name = normalized_tag_name(parent_tag.name().as_utf8_str());\n                            if is_inline_element(&parent_name) && !inline_ancestor_allows_block(&parent_name) {\n                                return true;\n                            }\n                        }\n                    }\n                    current = dom_ctx.parent_of(parent_id);\n                }\n            }\n        }\n    }\n\n    false\n}\n\n/// Determine if a node should be dropped during preprocessing.\n///\n/// Behavior depends on the [`PreprocessingPreset`]:\n///\n/// - **Minimal**: Only scripts/styles are stripped (handled elsewhere). This function\n///   drops nothing — all structural elements are preserved.\n/// - **Standard** (default): Drops `<nav>` unconditionally. Drops `<header>`, `<footer>`,\n///   and `<aside>` only when they have navigation hints (class/role/aria attributes\n///   indicating site chrome). Drops `<form>` when `remove_forms` is enabled.\n/// - **Aggressive**: All of Standard, plus: drops `<footer>`, `<aside>`, `<noscript>`\n///   unconditionally. Drops ANY element with navigation hints in class/id/role\n///   (e.g. `<div class=\"sidebar\">`). Drops elements with noise-related classes/roles.\npub fn should_drop_for_preprocessing(tag_name: &str, tag: &tl::HTMLTag, options: &ConversionOptions) -> bool {\n    use crate::options::PreprocessingPreset;\n\n    if !options.preprocessing.enabled {\n        return false;\n    }\n\n    let preset = options.preprocessing.preset;\n\n    // Minimal preset: drop nothing here (scripts/styles handled in earlier pipeline stage).\n    if preset == PreprocessingPreset::Minimal {\n        return false;\n    }\n\n    // Form removal — applies to both Standard and Aggressive when enabled.\n    if options.preprocessing.remove_forms && tag_name == \"form\" {\n        return true;\n    }\n\n    let is_aggressive = preset == PreprocessingPreset::Aggressive;\n\n    // Aggressive: drop <noscript> — its content is fallback for no-JS browsers.\n    if is_aggressive && tag_name == \"noscript\" {\n        return true;\n    }\n\n    // Navigation removal — only when the flag is enabled.\n    if !options.preprocessing.remove_navigation {\n        return false;\n    }\n\n    let has_nav_hint = element_has_navigation_hint(tag);\n\n    // <nav> is always navigation — drop in both Standard and Aggressive.\n    if tag_name == \"nav\" {\n        return true;\n    }\n\n    if tag_name == \"header\" {\n        // Drop <header> only with navigation hints (e.g. class=\"site-header\",\n        // role=\"navigation\"). A plain <header> often wraps article titles like\n        // <header><h1>Title</h1></header> — dropping it loses content.\n        return has_nav_hint;\n    }\n\n    if tag_name == \"footer\" || tag_name == \"aside\" {\n        // Standard: drop only with navigation hints.\n        // Aggressive: drop unconditionally.\n        return is_aggressive || has_nav_hint;\n    }\n\n    // Aggressive: drop ANY element that has navigation hints in class/id/role.\n    // This catches <div class=\"sidebar\">, <div class=\"menu\">, <section class=\"navigation\">,\n    // and similar non-semantic navigation containers.\n    if is_aggressive && has_nav_hint {\n        return true;\n    }\n\n    // Aggressive: drop elements with noise-related roles.\n    if is_aggressive {\n        if element_has_noise_hint(tag) {\n            return true;\n        }\n    }\n\n    false\n}\n\n/// Check if an element has noise-related hints (ads, cookie banners, social sharing).\nfn element_has_noise_hint(tag: &tl::HTMLTag) -> bool {\n    const NOISE_KEYWORDS: &[&str] = &[\n        \"cookie\",\n        \"consent\",\n        \"gdpr\",\n        \"banner\",\n        \"advertisement\",\n        \"ad-container\",\n        \"advert\",\n        \"social-share\",\n        \"share-buttons\",\n        \"popup\",\n        \"modal-overlay\",\n        \"newsletter-signup\",\n    ];\n\n    attribute_matches_any(tag, \"class\", NOISE_KEYWORDS) || attribute_matches_any(tag, \"id\", NOISE_KEYWORDS)\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/reference_collector.rs",
    "content": "//! Collector for reference-style link definitions.\n\nuse std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::rc::Rc;\n\n/// Shared handle for passing the collector through the conversion context.\npub type ReferenceCollectorHandle = Rc<RefCell<ReferenceCollector>>;\n\n#[derive(Debug, Clone, Hash, Eq, PartialEq)]\nstruct ReferenceKey {\n    url: String,\n    title: Option<String>,\n}\n\n/// Collects link/image references during conversion and produces a reference\n/// definitions section at the end of the document.\n#[derive(Debug, Default)]\npub struct ReferenceCollector {\n    map: HashMap<ReferenceKey, usize>,\n    entries: Vec<(usize, String, Option<String>)>,\n}\n\nimpl ReferenceCollector {\n    /// Create a new, empty reference collector.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Register a URL (and optional title) and return its 1-based reference number.\n    ///\n    /// If the same URL+title pair was already registered, the existing number is returned.\n    pub fn get_or_insert(&mut self, url: &str, title: Option<&str>) -> usize {\n        let key = ReferenceKey {\n            url: url.to_string(),\n            title: title.map(String::from),\n        };\n        if let Some(&num) = self.map.get(&key) {\n            return num;\n        }\n        let num = self.entries.len() + 1;\n        self.map.insert(key, num);\n        self.entries.push((num, url.to_string(), title.map(String::from)));\n        num\n    }\n\n    /// Produce the reference definitions section.\n    ///\n    /// Returns an empty string when no references were collected.\n    pub fn finish(&self) -> String {\n        if self.entries.is_empty() {\n            return String::new();\n        }\n        let mut out = String::new();\n        for (num, url, title) in &self.entries {\n            out.push('[');\n            out.push_str(&num.to_string());\n            out.push_str(\"]: \");\n            out.push_str(url);\n            if let Some(t) = title {\n                out.push_str(\" \\\"\");\n                out.push_str(&t.replace('\"', \"\\\\\\\"\"));\n                out.push('\"');\n            }\n            out.push('\\n');\n        }\n        out\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/attributes.rs",
    "content": "//! Handlers for semantic inline elements with attributes.\n//!\n//! Processes semantic inline elements that often carry semantic meaning through attributes:\n//! - `<cite>` - Citation/reference to a source\n//! - `<q>` - Inline quotation\n//! - `<abbr>` - Abbreviation with optional title explanation\n//! - `<dfn>` - Definition of a term\n//! - `<time>` - Machine-readable date/time\n//! - `<data>` - Machine-readable data value\n//!\n//! These elements carry semantic meaning in HTML5 but often have minimal\n//! visual distinction in rendered Markdown. Some are formatted with emphasis\n//! or have their attributes included in the output.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\n\nuse crate::converter::utility::content::chomp_inline;\n\n/// Handles the `<dfn>` element.\n///\n/// A dfn element marks a term that is being defined. The content represents\n/// the term, and its definition would typically appear in surrounding context.\n/// It is rendered as emphasized (italic) text.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Non-empty content is wrapped with the configured emphasis symbol (default: `*`)\n/// - Inline suffix handling is applied (e.g., footnote references)\npub fn handle_dfn(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let (prefix, suffix, trimmed) = chomp_inline(&content);\n        if !trimmed.is_empty() {\n            output.push_str(prefix);\n            output.push(options.strong_em_symbol);\n            output.push_str(trimmed);\n            output.push(options.strong_em_symbol);\n            append_inline_suffix(output, suffix, !trimmed.is_empty(), node_handle, parser, dom_ctx);\n        }\n    }\n}\n\n/// Handles the `<abbr>` element.\n///\n/// An abbr element marks an abbreviation or acronym. The `title` attribute\n/// provides the expansion of the abbreviation, which is appended in parentheses\n/// if present.\n///\n/// # Behavior\n///\n/// - Content is collected from children\n/// - Non-empty content is output as-is\n/// - If `title` attribute exists, it is appended in parentheses: `abbr (title)`\n///\n/// # Example\n///\n/// ```html\n/// <abbr title=\"HyperText Markup Language\">HTML</abbr>\n/// ```\n///\n/// Produces: `HTML (HyperText Markup Language)`\npub fn handle_abbr(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            output.push_str(trimmed);\n\n            // Append title attribute if present\n            if let Some(title) = tag.attributes().get(\"title\").flatten().map(|v| v.as_utf8_str()) {\n                let trimmed_title = title.trim();\n                if !trimmed_title.is_empty() {\n                    output.push_str(\" (\");\n                    output.push_str(trimmed_title);\n                    output.push(')');\n                }\n            }\n        }\n    }\n}\n\n/// Handles the `<time>` and `<data>` elements.\n///\n/// Time and data elements contain machine-readable content in their attributes\n/// and human-readable content in their text. For Markdown purposes, we output\n/// only the human-readable text content, as Markdown doesn't have a way to\n/// preserve machine-readable metadata.\n///\n/// # Behavior\n///\n/// - Content is extracted from children and output as-is\n/// - Attributes (datetime, value) are not rendered in Markdown output\npub fn handle_time_data(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n    }\n}\n\n/// Handles the `<cite>` element.\n///\n/// A cite element marks the title of a cited work (book, article, website, etc.).\n/// It is rendered as emphasized (italic) text in block mode, or as plain text in inline mode.\n///\n/// # Behavior\n///\n/// - **Block mode**: Content is wrapped with emphasis markers (default: `*`)\n/// - **Inline mode**: Content is output as-is without formatting\npub fn handle_cite(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            if ctx.convert_as_inline {\n                output.push_str(trimmed);\n            } else {\n                output.push('*');\n                output.push_str(trimmed);\n                output.push('*');\n            }\n        }\n    }\n}\n\n/// Handles the `<q>` element.\n///\n/// A q element marks an inline quotation. In Markdown, it is rendered as\n/// quoted text enclosed in double quotes. Backslashes and quotes within\n/// the content are escaped.\n///\n/// # Behavior\n///\n/// - **Block mode**: Content is wrapped in escaped double quotes: `\"content\"`\n/// - **Inline mode**: Content is output as-is without quotes\n///\n/// # Escaping\n///\n/// Internal backslashes and double quotes are escaped:\n/// - `\\` → `\\\\`\n/// - `\"` → `\\\"`\npub fn handle_q(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(32);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            output.push('\"');\n            output.push_str(trimmed);\n            output.push('\"');\n        }\n    }\n}\n\n/// Dispatcher for semantic inline attribute elements.\n///\n/// Routes `<cite>`, `<q>`, `<abbr>`, `<dfn>`, `<time>`, and `<data>` elements\n/// to their respective handlers.\npub fn handle(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    match tag_name {\n        \"dfn\" => handle_dfn(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"abbr\" => handle_abbr(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"time\" | \"data\" => handle_time_data(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"cite\" => handle_cite(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"q\" => handle_q(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        _ => {}\n    }\n}\n\n/// Appends inline suffix to the output.\n///\n/// This is a placeholder for integrating with other inline formatting systems\n/// (e.g., footnote references). For now, it simply outputs the suffix.\nfn append_inline_suffix(\n    output: &mut String,\n    suffix: &str,\n    _is_nonempty: bool,\n    _node_handle: &tl::NodeHandle,\n    _parser: &tl::Parser,\n    _dom_ctx: &super::DomContext,\n) {\n    output.push_str(suffix);\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/definition_list.rs",
    "content": "//! Handlers for HTML5 definition list and heading group elements.\n//!\n//! Processes list and heading semantic elements:\n//! - `<hgroup>` - Groups related headings together\n//! - `<dl>` - Definition list container\n//! - `<dt>` - Definition term\n//! - `<dd>` - Definition description\n//! - `<menu>` - Semantic list (typically unordered)\n//!\n//! These elements have special formatting requirements for proper Markdown output.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\nuse super::walk_node;\n\n/// Handles the `<hgroup>` element.\n///\n/// An hgroup element groups related headings together (e.g., a title and subtitle).\n/// In Markdown, we simply process all children sequentially, allowing nested\n/// headings to maintain their individual formatting.\n///\n/// # Behavior\n///\n/// - Children are processed sequentially in the current context\n/// - No special formatting is applied at the hgroup level\npub fn handle_hgroup(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n            }\n        }\n    }\n}\n\n/// Handles the `<dl>` element.\n///\n/// A definition list contains terms and their definitions. Terms and definitions\n/// are output as plain blocks without Pandoc-style colon syntax, since standard\n/// Markdown and GFM do not support definition lists.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is collected and wrapped with proper spacing\npub fn handle_dl(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Collect content from children\n        let mut content = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        // Output collected content with proper spacing\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                output.push_str(\"\\n\\n\");\n            }\n            output.push_str(trimmed);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<dt>` element.\n///\n/// A dt element contains a term being defined. Terms are output on their own line,\n/// with definitions following on subsequent lines.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Content is output as-is\n/// - **Block mode**: Content is followed by a newline\npub fn handle_dt(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(64);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            if ctx.convert_as_inline {\n                output.push_str(trimmed);\n            } else {\n                output.push_str(trimmed);\n                output.push('\\n');\n            }\n        }\n    }\n}\n\n/// Handles the `<dd>` element.\n///\n/// A dd element contains the definition for a term. It is output as a plain\n/// block since standard Markdown and GFM do not support definition list syntax.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Content is output as-is\n/// - **Block mode**: Content is output as a block\npub fn handle_dd(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(128);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n\n        if ctx.convert_as_inline {\n            if !trimmed.is_empty() {\n                output.push_str(trimmed);\n            }\n        } else if !trimmed.is_empty() {\n            output.push_str(trimmed);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<menu>` element.\n///\n/// A menu element is a semantic list, typically used for command menus or\n/// navigation. It is rendered as an unordered list with dashes.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without list formatting\n/// - **Block mode**: Content is rendered as an unordered list\n/// - Uses `-` as the list bullet (overrides configured bullets)\n/// - Proper blank-line spacing is maintained\npub fn handle_menu(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let content_start = output.len();\n\n        // Create options with menu-specific bullet style\n        let menu_options = crate::options::ConversionOptions {\n            bullets: \"-\".to_string(),\n            ..options.clone()\n        };\n\n        // Create context for list rendering\n        let list_ctx = super::Context {\n            in_ordered_list: false,\n            list_counter: 0,\n            in_list: true,\n            list_depth: ctx.list_depth,\n            ..ctx.clone()\n        };\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, output, &menu_options, &list_ctx, depth, dom_ctx);\n            }\n        }\n\n        // Ensure proper spacing after menu\n        if !ctx.convert_as_inline && output.len() > content_start {\n            if !output.ends_with(\"\\n\\n\") {\n                if output.ends_with('\\n') {\n                    output.push('\\n');\n                } else {\n                    output.push_str(\"\\n\\n\");\n                }\n            }\n        } else if ctx.convert_as_inline {\n            // In inline mode, remove trailing newlines\n            while output.ends_with('\\n') {\n                output.pop();\n            }\n        }\n    }\n}\n\n/// Dispatcher for definition list and related elements.\n///\n/// Routes `<hgroup>`, `<dl>`, `<dt>`, `<dd>`, and `<menu>` elements\n/// to their respective handlers.\npub fn handle(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    match tag_name {\n        \"hgroup\" => handle_hgroup(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"dl\" => handle_dl(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"dt\" => handle_dt(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"dd\" => handle_dd(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"menu\" => handle_menu(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/figure.rs",
    "content": "//! Handlers for HTML5 figure elements.\n//!\n//! Processes figure-related semantic elements:\n//! - `<figure>` - Self-contained illustration, diagram, photo, code listing, etc.\n//! - `<figcaption>` - Caption or legend for a figure\n//!\n//! Figure elements group an image (or other media) with its associated caption.\n//! The Markdown output preserves this relationship through content organization.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\n\n/// Handles the `<figure>` element.\n///\n/// A figure element contains content (typically images) and optionally a figcaption.\n/// The handler collects all content and cleans up extra line breaks.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is collected, line breaks normalized, and wrapped with blank lines\n/// - **Image normalization**: Removes extra spaces before `![` to improve Markdown formatting\n///\n/// # Implementation Details\n///\n/// The handler performs the following on the collected content:\n/// 1. Normalizes newline + image sequences: `\\n![` → `![`\n/// 2. Normalizes space + image sequences: ` ![` → `![`\n/// 3. Trims the final content and wraps it with blank lines\npub fn handle_figure(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Ensure spacing before the figure\n        if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n            output.push_str(\"\\n\\n\");\n        }\n\n        // Collect content in a separate buffer\n        let mut figure_content = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut figure_content, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        // Normalize image syntax\n        figure_content = figure_content.replace(\"\\n![\", \"![\");\n        figure_content = figure_content.replace(\" ![\", \"![\");\n\n        // Trim and output\n        let trimmed = figure_content.trim_matches(|c| c == '\\n' || c == ' ' || c == '\\t');\n        if !trimmed.is_empty() {\n            output.push_str(trimmed);\n            if !output.ends_with('\\n') {\n                output.push('\\n');\n            }\n            if !output.ends_with(\"\\n\\n\") {\n                output.push('\\n');\n            }\n        }\n    }\n}\n\n/// Handles the `<figcaption>` element.\n///\n/// A figcaption element contains text that describes or supplements the figure.\n/// It is rendered as emphasized (italic) text to distinguish it from regular content.\n///\n/// # Behavior\n///\n/// - Content is collected and trimmed\n/// - Non-empty content is wrapped in `*text*` (emphasis) markers\n/// - Proper spacing is maintained around the caption\n///\n/// # Implementation Details\n///\n/// The handler:\n/// 1. Collects and processes all children\n/// 2. Checks for existing output and adds spacing as needed\n/// 3. Wraps content in emphasis markers: `*caption*`\n/// 4. Ensures proper blank-line spacing after the caption\npub fn handle_figcaption(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut text = String::new();\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        let text = text.trim();\n        if !text.is_empty() {\n            // Add spacing before caption if needed\n            if !output.is_empty() {\n                if output.ends_with(\"```\\n\") {\n                    output.push('\\n');\n                } else {\n                    // Trim trailing whitespace and ensure single blank line\n                    while output.ends_with(' ') || output.ends_with('\\t') {\n                        output.pop();\n                    }\n                    if output.ends_with('\\n') && !output.ends_with(\"\\n\\n\") {\n                        output.push('\\n');\n                    } else if !output.ends_with('\\n') {\n                        output.push_str(\"\\n\\n\");\n                    }\n                }\n            }\n\n            // Output caption as emphasized text\n            output.push('*');\n            output.push_str(text);\n            output.push_str(\"*\\n\\n\");\n        }\n    }\n}\n\n/// Dispatcher for figure-related elements.\n///\n/// Routes `<figure>` and `<figcaption>` elements to their respective handlers.\npub fn handle(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    match tag_name {\n        \"figure\" => handle_figure(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"figcaption\" => handle_figcaption(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        _ => {}\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn figure_caption_separated_from_image() {\n        let html = r#\"<figure><img src=\"photo.jpg\" alt=\"Photo\"><figcaption>A nice photo</figcaption></figure>\"#;\n        let result = crate::convert(html, None).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(\n            content.contains(\"![Photo](photo.jpg)\"),\n            \"image should be present: {}\",\n            content\n        );\n        assert!(\n            content.contains(\"A nice photo\"),\n            \"caption should be present: {}\",\n            content\n        );\n        // Image and caption should not be on the same line\n        let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();\n        let img_line = lines.iter().position(|l| l.contains(\"![\")).unwrap_or(999);\n        let cap_line = lines.iter().position(|l| l.contains(\"A nice photo\")).unwrap_or(999);\n        assert!(\n            cap_line > img_line,\n            \"caption should be on a separate line after image, lines: {:?}\",\n            lines\n        );\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/mod.rs",
    "content": "//! Semantic HTML5 element handlers for HTML to Markdown conversion.\n//!\n//! This module provides specialized handlers for semantic HTML5 elements:\n//! - Sectioning elements (article, section, nav, aside, header, footer, main)\n//! - Figure elements (figure, figcaption)\n//! - Interactive elements (details, summary, dialog)\n//! - Semantic inline attributes (cite, q, abbr, dfn, time, data)\n//!\n//! These handlers are designed to be extracted from the main `converter.rs`\n//! file and integrated through the dispatcher function.\n//!\n//! **Integration Pattern:**\n//! Each handler function takes the same signature:\n//! - `tag_name: &str` - The HTML tag being processed\n//! - `node_handle: &NodeHandle` - The DOM node handle\n//! - `parser: &Parser` - The HTML parser reference\n//! - `output: &mut String` - The output buffer to write to\n//! - `options: &ConversionOptions` - Conversion configuration\n//! - `ctx: &Context` - Processing context (state tracking)\n//! - `depth: usize` - Current DOM tree depth\n//! - `dom_ctx: &DomContext` - DOM context for tree relationships\n//!\n//! The main dispatcher function `dispatch_semantic_handler` routes tags to\n//! their appropriate handlers and returns a boolean indicating success.\n\npub mod attributes;\npub mod definition_list;\npub mod figure;\npub mod sectioning;\npub mod summary;\n\n// Re-export types from parent module for submodule access\npub use super::walk_node;\npub use super::{Context, DomContext};\n\n// Re-export handler functions for direct use\npub use attributes::handle as handle_attributes;\npub use definition_list::handle as handle_definition_list;\npub use figure::handle as handle_figure;\npub use sectioning::handle as handle_sectioning;\npub use summary::handle as handle_summary;\n\n// Re-exports are done via the dispatch function parameter types\n\n/// Dispatches semantic element handling to the appropriate handler.\n///\n/// This function routes semantic HTML5 elements to their specialized handlers\n/// based on tag name. It is designed to be called from the main `walk_node`\n/// function in `converter.rs`.\n///\n/// # Routing Table\n///\n/// The following tag routes are supported:\n/// - **Sectioning**: article, section, nav, aside, header, footer, main\n/// - **Figure**: figure, figcaption\n/// - **Summary**: details, summary, dialog\n/// - **Definition List**: hgroup, dl, dt, dd, menu\n/// - **Attributes**: cite, q, abbr, dfn, time, data\n///\n/// # Returns\n///\n/// Returns `true` if the tag was successfully handled by a semantic handler,\n/// `false` if the tag is not a semantic element and requires other handling.\n///\n/// # Example\n///\n/// ```text\n/// if dispatch_semantic_handler(tag_name, &node_handle, &parser, output, options, ctx, depth, dom_ctx) {\n///     // Tag was handled\n/// } else {\n///     // Continue with other handlers\n/// }\n/// ```\npub fn dispatch_semantic_handler(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) -> bool {\n    match tag_name {\n        // Sectioning elements\n        \"article\" | \"section\" | \"nav\" | \"aside\" | \"header\" | \"footer\" | \"main\" => {\n            handle_sectioning(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Figure elements\n        \"figure\" | \"figcaption\" => {\n            handle_figure(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Summary and interactive elements\n        \"details\" | \"summary\" | \"dialog\" => {\n            handle_summary(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Definition list and related elements\n        \"hgroup\" | \"dl\" | \"dt\" | \"dd\" | \"menu\" => {\n            handle_definition_list(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        // Semantic inline attributes\n        \"cite\" | \"q\" | \"abbr\" | \"dfn\" | \"time\" | \"data\" => {\n            handle_attributes(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);\n            true\n        }\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/sectioning.rs",
    "content": "//! Handlers for HTML5 sectioning elements.\n//!\n//! Processes semantic sectioning elements:\n//! - `<article>` - Independent, self-contained content\n//! - `<section>` - Generic grouping of thematic content\n//! - `<nav>` - Navigation links (typically rendered inline or in sidebars)\n//! - `<aside>` - Peripheral content (sidebars, callouts)\n//! - `<header>` - Introductory content (page headers)\n//! - `<footer>` - End content (page footers)\n//! - `<main>` - Primary content area\n//!\n//! All these elements are treated as block-level containers.\n//! Their content is extracted and formatted with proper spacing.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\n\n/// Handles sectioning elements (article, section, nav, aside, header, footer, main).\n///\n/// Sectioning elements are rendered as block-level containers. When in inline\n/// conversion mode, their content is rendered inline without block spacing.\n/// Otherwise, content is wrapped with proper blank lines to separate from other blocks.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline; block spacing is skipped\n/// - **Block mode**: Content is collected, trimmed, and formatted with blank lines\n/// - **Empty content**: Empty sections are skipped entirely\n///\n/// # Implementation Note\n///\n/// Sectioning elements act as transparent containers—their presence doesn't\n/// add any special formatting beyond structural grouping.\npub fn handle(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Collect content in a separate buffer\n        let mut content = String::with_capacity(256);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);\n            }\n        }\n\n        // Skip if content is empty\n        if content.trim().is_empty() {\n            return;\n        }\n\n        // Add spacing before the content\n        if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n            output.push_str(\"\\n\\n\");\n        }\n\n        // Append the content\n        output.push_str(&content);\n\n        // Ensure proper spacing after the content\n        if content.ends_with('\\n') && !content.ends_with(\"\\n\\n\") {\n            output.push('\\n');\n        } else if !content.ends_with('\\n') {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/semantic/summary.rs",
    "content": "//! Handlers for HTML5 interactive elements.\n//!\n//! Processes interactive disclosure and dialog semantic elements:\n//! - `<details>` - Expandable/collapsible disclosure widget\n//! - `<summary>` - Summary or caption for a details element\n//! - `<dialog>` - Dialog box overlay widget\n//!\n//! These elements are treated as block-level content containers\n//! with special formatting for the summary element.\n\n// Note: Context and DomContext are defined in converter.rs\n// walk_node is also defined there and must be called via the parent module\nuse super::walk_node;\n\n/// Handles the `<details>` element.\n///\n/// A details element represents a disclosure widget that can be toggled\n/// to show/hide additional content. In Markdown, it's rendered as a block\n/// with all content visible.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is collected and wrapped with proper blank-line spacing\n/// - **Empty content**: Skipped entirely\npub fn handle_details(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Collect content\n        let mut content = String::with_capacity(256);\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            // Add spacing before if needed\n            if !output.is_empty() && !output.ends_with(\"\\n\\n\") {\n                output.push_str(\"\\n\\n\");\n            }\n\n            // Output content\n            output.push_str(trimmed);\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Handles the `<summary>` element.\n///\n/// A summary element contains a caption for a details element.\n/// It is rendered as strong (bold) text to distinguish it from regular content.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Content is rendered inline without emphasis\n/// - **Block mode**: Content is wrapped in strong markers (e.g., `**text**`)\n/// - Uses the configured strong/emphasis symbol from ConversionOptions\n///\n/// # Implementation Details\n///\n/// The handler:\n/// 1. Creates a context with `in_strong: true` for nested formatting\n/// 2. Collects content from all children\n/// 3. Wraps non-empty content in strong markers (repeated twice per Markdown spec)\npub fn handle_summary(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let mut content = String::with_capacity(64);\n\n        // Set strong context for nested content\n        let mut summary_ctx = ctx.clone();\n        if !ctx.convert_as_inline {\n            summary_ctx.in_strong = true;\n        }\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(\n                    child_handle,\n                    parser,\n                    &mut content,\n                    options,\n                    &summary_ctx,\n                    depth + 1,\n                    dom_ctx,\n                );\n            }\n        }\n\n        let trimmed = content.trim();\n        if !trimmed.is_empty() {\n            if ctx.convert_as_inline {\n                // Inline mode: output without formatting\n                output.push_str(trimmed);\n            } else {\n                // Block mode: output with strong markers\n                let mut symbol = String::with_capacity(2);\n                symbol.push(options.strong_em_symbol);\n                symbol.push(options.strong_em_symbol);\n                output.push_str(&symbol);\n                output.push_str(trimmed);\n                output.push_str(&symbol);\n                output.push_str(\"\\n\\n\");\n            }\n        }\n    }\n}\n\n/// Handles the `<dialog>` element.\n///\n/// A dialog element represents a modal dialog box. In Markdown, it's rendered\n/// as a block container with content visible.\n///\n/// # Behavior\n///\n/// - **Inline mode**: Children are processed inline without block spacing\n/// - **Block mode**: Content is processed and wrapped with proper blank lines\n/// - Trailing whitespace is removed from collected content\n///\n/// # Implementation Details\n///\n/// The handler:\n/// 1. Marks the position in output before processing children\n/// 2. Processes all children in the normal context\n/// 3. Removes trailing spaces and tabs from the output\n/// 4. Ensures proper blank-line spacing after the dialog\npub fn handle_dialog(\n    _tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        // In inline context, just process children inline\n        if ctx.convert_as_inline {\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n                }\n            }\n            return;\n        }\n\n        // Mark position before processing children\n        let content_start = output.len();\n\n        let children = tag.children();\n        {\n            for child_handle in children.top().iter() {\n                super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);\n            }\n        }\n\n        // Remove trailing whitespace from dialog content\n        while output.len() > content_start && (output.ends_with(' ') || output.ends_with('\\t')) {\n            output.pop();\n        }\n\n        // Ensure proper spacing after dialog\n        if output.len() > content_start && !output.ends_with(\"\\n\\n\") {\n            output.push_str(\"\\n\\n\");\n        }\n    }\n}\n\n/// Dispatcher for interactive elements.\n///\n/// Routes `<details>`, `<summary>`, and `<dialog>` elements to their respective handlers.\npub fn handle(\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &crate::options::ConversionOptions,\n    ctx: &super::Context,\n    depth: usize,\n    dom_ctx: &super::DomContext,\n) {\n    match tag_name {\n        \"details\" => handle_details(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"summary\" => handle_summary(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        \"dialog\" => handle_dialog(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/text/mod.rs",
    "content": "//! Text processing module for HTML to Markdown conversion.\n//!\n//! This module provides utilities for normalizing, escaping, and processing text content\n//! extracted from HTML documents during the conversion to Markdown format.\n\nmod processing;\n\npub use processing::dedent_code_block;\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/text/processing.rs",
    "content": "//! Text processing utilities for HTML to Markdown conversion.\n//!\n//! This module provides functions for processing text content, including\n//! code block dedentation and special character handling.\n\n/// Remove common leading whitespace from all lines in a code block.\n///\n/// This is useful when HTML authors indent `<pre>` content for readability,\n/// so we can strip the shared indentation without touching meaningful spacing.\n///\n/// # Examples\n///\n/// ```text\n/// \"    line1\\n    line2\" → \"line1\\nline2\"\n/// \"  indent1\\n    indent2\" → \"indent1\\n  indent2\" (removes 2 chars, minimum indent)\n/// \"  \\n  code\" → \"\\ncode\"\n/// ```\npub fn dedent_code_block(content: &str) -> String {\n    let lines: Vec<&str> = content.lines().collect();\n    if lines.is_empty() {\n        return String::new();\n    }\n\n    let min_indent = lines\n        .iter()\n        .filter(|line| !line.trim().is_empty())\n        .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())\n        .min()\n        .unwrap_or(0);\n\n    lines.iter().fold(String::new(), |mut acc, line| {\n        if !acc.is_empty() {\n            acc.push('\\n');\n        }\n        let processed = if line.trim().is_empty() {\n            *line\n        } else {\n            let mut remaining = min_indent;\n            let mut cut = 0;\n            for (idx, ch) in line.char_indices() {\n                if remaining == 0 {\n                    break;\n                }\n                if ch.is_whitespace() {\n                    remaining -= 1;\n                    cut = idx + ch.len_utf8();\n                } else {\n                    break;\n                }\n            }\n            &line[cut..]\n        };\n        acc.push_str(processed);\n        acc\n    })\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/text_node.rs",
    "content": "//! Text node processing for HTML to Markdown conversion.\n//!\n//! Handles raw text nodes with:\n//! - HTML entity decoding\n//! - Whitespace normalization and stripping\n//! - Text escaping with configurable escape modes\n//! - Visitor callbacks (when feature enabled)\n//! - List item indentation\n\nuse std::borrow::Cow;\n\nuse crate::converter::dom_context::DomContext;\nuse crate::converter::main_helpers::{has_more_than_one_char, is_inline_element};\nuse crate::converter::utility::siblings::{\n    get_next_sibling_tag, next_sibling_is_inline_tag, previous_sibling_is_inline_tag,\n};\nuse crate::options::ConversionOptions;\nuse crate::text;\n\n// Type aliases for Context to avoid circular imports\ntype Context = crate::converter::Context;\n\n/// Process a raw text node during HTML to Markdown conversion.\n///\n/// Handles:\n/// - HTML entity decoding\n/// - Whitespace normalization and stripping\n/// - Text escaping with configurable escape modes\n/// - Visitor callbacks (when feature enabled)\n/// - List item indentation\n#[allow(clippy::too_many_lines)]\n#[cfg_attr(not(feature = \"visitor\"), allow(unused_variables))]\npub fn process_text_node(\n    raw: &str,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    output: &mut String,\n    options: &ConversionOptions,\n    ctx: &Context,\n    depth: usize,\n    dom_ctx: &DomContext,\n) {\n    let mut text = text::decode_html_entities_cow(raw);\n\n    if text.is_empty() {\n        return;\n    }\n\n    let text_ref = text.as_ref();\n    let had_newlines = text_ref.contains('\\n');\n    let has_double_newline = text_ref.contains(\"\\n\\n\") || text_ref.contains(\"\\r\\n\\r\\n\");\n\n    if options.strip_newlines && (text.contains('\\r') || text.contains('\\n')) {\n        text = Cow::Owned(text.replace(['\\r', '\\n'], \" \"));\n    }\n\n    if text.trim().is_empty() {\n        if ctx.in_code {\n            output.push_str(text.as_ref());\n            return;\n        }\n\n        if options.whitespace_mode == crate::options::WhitespaceMode::Strict {\n            if ctx.convert_as_inline || ctx.in_table_cell || ctx.in_list_item {\n                output.push_str(text.as_ref());\n                return;\n            }\n            if has_double_newline {\n                if !output.ends_with(\"\\n\\n\") {\n                    output.push('\\n');\n                }\n                return;\n            }\n            output.push_str(text.as_ref());\n            return;\n        }\n\n        if had_newlines {\n            if output.is_empty() {\n                return;\n            }\n            if !output.ends_with(\"\\n\\n\") {\n                if let Some(next_tag) = get_next_sibling_tag(node_handle, parser, dom_ctx) {\n                    if is_inline_element(next_tag) {\n                        // Newlines between inline elements collapse to a single space\n                        // in HTML rendering (per CSS white-space: normal). Preserve\n                        // this word boundary so adjacent inline content doesn't merge.\n                        if !output.ends_with(' ') && !output.ends_with('\\n') {\n                            output.push(' ');\n                        }\n                        return;\n                    }\n                }\n            }\n            return;\n        }\n\n        if previous_sibling_is_inline_tag(node_handle, parser, dom_ctx)\n            && next_sibling_is_inline_tag(node_handle, parser, dom_ctx)\n        {\n            if has_more_than_one_char(text.as_ref()) {\n                if !output.ends_with(' ') {\n                    output.push(' ');\n                }\n            } else {\n                output.push_str(text.as_ref());\n            }\n        } else {\n            output.push_str(text.as_ref());\n        }\n        return;\n    }\n\n    let processed_text = if ctx.in_code || ctx.in_ruby {\n        text.into_owned()\n    } else if ctx.in_table_cell {\n        let escaped = if options.whitespace_mode == crate::options::WhitespaceMode::Normalized {\n            let normalized_text = text::normalize_whitespace_cow(text.as_ref());\n            let escaped_result = text::escape(\n                normalized_text.as_ref(),\n                options.escape_misc,\n                options.escape_asterisks,\n                options.escape_underscores,\n                options.escape_ascii,\n            );\n            escaped_result.into_owned()\n        } else {\n            text::escape(\n                text.as_ref(),\n                options.escape_misc,\n                options.escape_asterisks,\n                options.escape_underscores,\n                options.escape_ascii,\n            )\n            .into_owned()\n        };\n        if options.escape_misc {\n            escaped\n        } else {\n            escaped.replace('|', r\"\\|\")\n        }\n    } else if options.whitespace_mode == crate::options::WhitespaceMode::Strict {\n        text::escape(\n            text.as_ref(),\n            options.escape_misc,\n            options.escape_asterisks,\n            options.escape_underscores,\n            options.escape_ascii,\n        )\n        .into_owned()\n    } else {\n        let has_double_newline = text.contains(\"\\n\\n\") || text.contains(\"\\r\\n\\r\\n\");\n        let has_trailing_single_newline =\n            text.ends_with('\\n') && !text.ends_with(\"\\n\\n\") && !text.ends_with(\"\\r\\n\\r\\n\");\n\n        let normalized_text = text::normalize_whitespace_cow(text.as_ref());\n\n        let (prefix, suffix, core) = text::chomp(normalized_text.as_ref());\n\n        let skip_prefix = output.ends_with(\"\\n\\n\")\n            || output.ends_with(\"* \")\n            || output.ends_with(\"- \")\n            || output.ends_with(\". \")\n            || output.ends_with(\"] \")\n            || (output.ends_with('\\n') && prefix == \" \")\n            || (output.ends_with(' ')\n                && prefix == \" \"\n                && !previous_sibling_is_inline_tag(node_handle, parser, dom_ctx));\n\n        let mut final_text = String::with_capacity(prefix.len() + core.len() + suffix.len() + 2);\n        if !skip_prefix && !prefix.is_empty() {\n            final_text.push_str(prefix);\n        }\n\n        let escaped_core = text::escape(\n            core,\n            options.escape_misc,\n            options.escape_asterisks,\n            options.escape_underscores,\n            options.escape_ascii,\n        );\n        final_text.push_str(&escaped_core);\n\n        if !suffix.is_empty() {\n            final_text.push_str(suffix);\n        } else if has_trailing_single_newline {\n            // Check if the \"\\n\\n\" at the end of the output buffer came from within\n            // the current block's content, not from a previous block's closing.\n            // Without this distinction, the second paragraph after a \"\\n\\n\" boundary\n            // would incorrectly suppress the trailing space before inline elements.\n            let safe_start = ctx.block_content_start.min(output.len());\n            let safe_start = crate::converter::utility::content::floor_char_boundary(output, safe_start);\n            let current_block_output = &output[safe_start..];\n            let at_paragraph_break = current_block_output.ends_with(\"\\n\\n\");\n            if !at_paragraph_break {\n                if has_double_newline {\n                    final_text.push('\\n');\n                } else if let Some(next_tag) = get_next_sibling_tag(node_handle, parser, dom_ctx) {\n                    if matches!(next_tag, \"span\") {\n                    } else if ctx.inline_depth > 0 || ctx.convert_as_inline || ctx.in_paragraph {\n                        final_text.push(' ');\n                    } else {\n                        final_text.push('\\n');\n                    }\n                } else if ctx.inline_depth > 0 || ctx.convert_as_inline || ctx.in_paragraph {\n                    final_text.push(' ');\n                } else {\n                    final_text.push('\\n');\n                }\n            }\n        }\n\n        final_text\n    };\n\n    #[cfg(feature = \"visitor\")]\n    let final_text = if let Some(ref visitor_handle) = ctx.visitor {\n        use crate::visitor::{NodeContext, NodeType, VisitResult};\n        use std::collections::BTreeMap;\n\n        let node_id = node_handle.get_inner();\n        let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n        let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n        let node_ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline: true,\n        };\n\n        let mut visitor = visitor_handle.borrow_mut();\n        match visitor.visit_text(&node_ctx, &processed_text) {\n            VisitResult::Continue => processed_text,\n            VisitResult::Custom(custom) => {\n                if ctx.inline_depth > 0 || ctx.in_heading {\n                    processed_text\n                } else {\n                    custom\n                }\n            }\n            VisitResult::Skip => return,\n            VisitResult::Error(err) => {\n                if ctx.visitor_error.borrow().is_none() {\n                    *ctx.visitor_error.borrow_mut() = Some(err);\n                }\n                return;\n            }\n            VisitResult::PreserveHtml => processed_text,\n        }\n    } else {\n        processed_text\n    };\n\n    #[cfg(not(feature = \"visitor\"))]\n    let final_text = processed_text;\n\n    if ctx.in_list_item && final_text.contains(\"\\n\\n\") {\n        let indent = \" \".repeat(4 * ctx.list_depth);\n        let mut first = true;\n        for part in final_text.split(\"\\n\\n\") {\n            if !first {\n                output.push_str(\"\\n\\n\");\n                output.push_str(&indent);\n            }\n            first = false;\n            output.push_str(part.trim());\n        }\n    } else {\n        output.push_str(&final_text);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/attributes.rs",
    "content": "//! Attribute handling and extraction utilities.\n//!\n//! Functions for working with element attributes, semantic detection, and hOCR document detection.\n\nuse crate::converter::DomContext;\nuse crate::converter::utility::content::normalized_tag_name;\n\n/// Check if a tag has main content semantics based on role or class.\npub fn tag_has_main_semantics(tag: &tl::HTMLTag) -> bool {\n    if let Some(Some(role)) = tag.attributes().get(\"role\") {\n        let lowered = role.as_utf8_str().to_ascii_lowercase();\n        if matches!(lowered.as_str(), \"main\" | \"article\" | \"document\" | \"region\") {\n            return true;\n        }\n    }\n\n    if let Some(Some(class_bytes)) = tag.attributes().get(\"class\") {\n        let class_value = class_bytes.as_utf8_str().to_ascii_lowercase();\n        const MAIN_CLASS_HINTS: &[&str] = &[\n            \"mw-body\",\n            \"mw-parser-output\",\n            \"content-body\",\n            \"content-container\",\n            \"article-body\",\n            \"article-content\",\n            \"main-content\",\n            \"page-content\",\n            \"entry-content\",\n            \"post-content\",\n            \"document-body\",\n        ];\n        if MAIN_CLASS_HINTS.iter().any(|hint| class_value.contains(hint)) {\n            return true;\n        }\n    }\n\n    false\n}\n\n/// Check if an element has navigation-related hints in its attributes.\npub fn element_has_navigation_hint(tag: &tl::HTMLTag) -> bool {\n    if attribute_matches_any(tag, \"role\", &[\"navigation\", \"menubar\", \"tablist\", \"toolbar\"]) {\n        return true;\n    }\n\n    if attribute_contains_any(\n        tag,\n        \"aria-label\",\n        &[\"navigation\", \"menu\", \"contents\", \"table of contents\", \"toc\"],\n    ) {\n        return true;\n    }\n\n    const NAV_KEYWORDS: &[&str] = &[\n        \"nav\",\n        \"navigation\",\n        \"navbar\",\n        \"breadcrumbs\",\n        \"breadcrumb\",\n        \"toc\",\n        \"sidebar\",\n        \"sidenav\",\n        \"menu\",\n        \"menubar\",\n        \"mainmenu\",\n        \"subnav\",\n        \"tabs\",\n        \"tablist\",\n        \"toolbar\",\n        \"pager\",\n        \"pagination\",\n        \"skipnav\",\n        \"skip-link\",\n        \"skiplinks\",\n        \"site-nav\",\n        \"site-menu\",\n        \"site-header\",\n        \"site-footer\",\n        \"topbar\",\n        \"bottombar\",\n        \"masthead\",\n        \"vector-nav\",\n        \"vector-header\",\n        \"vector-footer\",\n    ];\n\n    attribute_matches_any(tag, \"class\", NAV_KEYWORDS) || attribute_matches_any(tag, \"id\", NAV_KEYWORDS)\n}\n\n/// Check if an attribute value matches any of the given keywords (space or custom-separator aware).\npub fn attribute_matches_any(tag: &tl::HTMLTag, attr: &str, keywords: &[&str]) -> bool {\n    let Some(attr_value) = tag.attributes().get(attr) else {\n        return false;\n    };\n    let Some(value) = attr_value else {\n        return false;\n    };\n    let raw = value.as_utf8_str();\n    raw.split_whitespace()\n        .map(|token| {\n            token\n                .chars()\n                .map(|c| match c {\n                    '_' | ':' | '.' | '/' => '-',\n                    _ => c,\n                })\n                .collect::<String>()\n                .to_ascii_lowercase()\n        })\n        .filter(|token| !token.is_empty())\n        .any(|token| keywords.iter().any(|kw| token == *kw))\n}\n\n/// Check if an attribute contains any of the given keywords (substring match).\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn attribute_contains_any(tag: &tl::HTMLTag, attr: &str, keywords: &[&str]) -> bool {\n    let Some(attr_value) = tag.attributes().get(attr) else {\n        return false;\n    };\n    let Some(value) = attr_value else {\n        return false;\n    };\n    let lower = value.as_utf8_str().to_ascii_lowercase();\n    keywords.iter().any(|kw| lower.contains(*kw))\n}\n\n/// Check if a node has a semantic content ancestor (main, article, section).\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn has_semantic_content_ancestor(node_handle: &tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {\n    let mut current_id = node_handle.get_inner();\n    while let Some(parent_id) = dom_ctx.parent_of(current_id) {\n        if let Some(parent_info) = dom_ctx.tag_info(parent_id, parser) {\n            if matches!(parent_info.name.as_str(), \"main\" | \"article\" | \"section\") {\n                return true;\n            }\n        }\n        if let Some(parent_handle) = dom_ctx.node_handle(parent_id) {\n            if let Some(tl::Node::Tag(parent_tag)) = parent_handle.get(parser) {\n                let parent_name = normalized_tag_name(parent_tag.name().as_utf8_str());\n                if matches!(parent_name.as_ref(), \"main\" | \"article\" | \"section\") {\n                    return true;\n                }\n                if tag_has_main_semantics(parent_tag) {\n                    return true;\n                }\n            }\n        }\n        current_id = parent_id;\n    }\n    false\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/caching.rs",
    "content": "//! Performance caching utilities.\n//!\n//! Caching mechanisms for expensive operations during conversion, including\n//! DOM context building and cache capacity management.\n\nuse crate::converter::DomContext;\nuse std::num::NonZeroUsize;\n\n/// Build a DOM context with hierarchical node information.\n///\n/// Pre-computes parent-child relationships, sibling indices, and caches\n/// tag information for efficient DOM navigation during conversion.\npub fn build_dom_context(dom: &tl::VDom, parser: &tl::Parser, input_len: usize) -> DomContext {\n    let cache_capacity = text_cache_capacity_for_input(input_len);\n    let mut ctx = DomContext {\n        parent_map: Vec::new(),\n        children_map: Vec::new(),\n        sibling_index_map: Vec::new(),\n        root_children: dom.children().to_vec(),\n        node_map: Vec::new(),\n        tag_info_map: Vec::new(),\n        prev_inline_like_map: Vec::new(),\n        next_inline_like_map: Vec::new(),\n        next_tag_map: Vec::new(),\n        next_whitespace_map: Vec::new(),\n        text_cache: std::cell::RefCell::new(lru::LruCache::new(cache_capacity)),\n    };\n\n    for (index, child_handle) in dom.children().iter().enumerate() {\n        let id = child_handle.get_inner();\n        ctx.ensure_capacity(id);\n        ctx.sibling_index_map[id as usize] = Some(index);\n        record_node_hierarchy(*child_handle, None, parser, &mut ctx);\n    }\n\n    ctx\n}\n\n/// Calculate appropriate cache capacity based on input size.\n///\n/// Returns a cache capacity between 32 and TEXT_CACHE_CAPACITY,\n/// scaled proportionally to input size (1KB = 1 slot).\npub fn text_cache_capacity_for_input(input_len: usize) -> NonZeroUsize {\n    const TEXT_CACHE_CAPACITY: usize = 256;\n    // `clamp(32, TEXT_CACHE_CAPACITY)` guarantees `target >= 32 > 0`, so `new` always returns Some.\n    let target = (input_len / 1024).clamp(32, TEXT_CACHE_CAPACITY);\n    NonZeroUsize::new(target).unwrap_or(NonZeroUsize::MIN)\n}\n\n/// Recursively record node hierarchy into DOM context.\n///\n/// Builds the complete parent-child relationship map for efficient tree traversal.\npub fn record_node_hierarchy(\n    node_handle: tl::NodeHandle,\n    parent: Option<u32>,\n    parser: &tl::Parser,\n    ctx: &mut DomContext,\n) {\n    let id = node_handle.get_inner();\n    ctx.ensure_capacity(id);\n    ctx.parent_map[id as usize] = parent;\n    ctx.node_map[id as usize] = Some(node_handle);\n\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let children: Vec<_> = tag.children().top().iter().copied().collect();\n        for (index, child) in children.iter().enumerate() {\n            let child_id = child.get_inner();\n            ctx.ensure_capacity(child_id);\n            ctx.sibling_index_map[child_id as usize] = Some(index);\n            record_node_hierarchy(*child, Some(id), parser, ctx);\n        }\n        ctx.children_map[id as usize] = Some(children);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/content.rs",
    "content": "//! Content extraction and manipulation utilities.\n//!\n//! Functions for extracting and processing element content, including text collection\n//! and empty element detection.\n\nuse crate::text;\nuse std::borrow::Cow;\n#[cfg(feature = \"visitor\")]\nuse std::collections::BTreeMap;\n\n// Forward declare DomContext from parent module to avoid circular imports\npub use crate::converter::DomContext;\n\n/// Collect all attributes from an HTML tag as a `BTreeMap<String, String>`.\n///\n/// Boolean attributes (those with `None` as the value) are skipped; only\n/// attributes that carry an explicit value are included.\n#[cfg(feature = \"visitor\")]\npub fn collect_tag_attributes(tag: &tl::HTMLTag) -> BTreeMap<String, String> {\n    tag.attributes()\n        .iter()\n        .filter_map(|(k, v)| v.as_ref().map(|val| (k.to_string(), val.to_string())))\n        .collect()\n}\n\n/// Chomp whitespace from inline element content, preserving line breaks.\n///\n/// Similar to `text::chomp` but handles line breaks from `<br>` tags specially.\n/// Line breaks are extracted as suffix to be placed outside formatting.\n/// Returns (prefix, suffix, `trimmed_text`).\npub fn chomp_inline(text: &str) -> (&str, &str, &str) {\n    if text.is_empty() {\n        return (\"\", \"\", \"\");\n    }\n\n    let prefix = if text.starts_with(&[' ', '\\t'][..]) { \" \" } else { \"\" };\n\n    let has_trailing_linebreak = text.ends_with(\"  \\n\") || text.ends_with(\"\\\\\\n\");\n\n    let suffix = if has_trailing_linebreak {\n        if text.ends_with(\"  \\n\") { \"  \\n\" } else { \"\\\\\\n\" }\n    } else if text.ends_with(&[' ', '\\t'][..]) {\n        \" \"\n    } else {\n        \"\"\n    };\n\n    let trimmed = if has_trailing_linebreak {\n        text.strip_suffix(\"  \\n\").map_or_else(\n            || text.strip_suffix(\"\\\\\\n\").map_or_else(|| text.trim(), |s| s.trim()),\n            |s| s.trim(),\n        )\n    } else {\n        text.trim()\n    };\n\n    (prefix, suffix, trimmed)\n}\n\n/// Get the text content of a node and its children.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn get_text_content(node_handle: &tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> String {\n    dom_ctx.text_content(*node_handle, parser)\n}\n\n/// Collect inline text for link labels, skipping block-level descendants.\n#[allow(clippy::match_wildcard_for_single_variants)]\npub fn collect_link_label_text(\n    children: &[tl::NodeHandle],\n    parser: &tl::Parser,\n    dom_ctx: &DomContext,\n) -> (String, Vec<tl::NodeHandle>, bool) {\n    let mut text = String::new();\n    let mut saw_block = false;\n    let mut block_nodes = Vec::new();\n    let mut stack: Vec<_> = children.iter().rev().copied().collect();\n\n    while let Some(handle) = stack.pop() {\n        if let Some(node) = handle.get(parser) {\n            match node {\n                tl::Node::Raw(bytes) => {\n                    let raw = bytes.as_utf8_str();\n                    let decoded = text::decode_html_entities_cow(raw.as_ref());\n                    text.push_str(decoded.as_ref());\n                }\n                tl::Node::Tag(tag) => {\n                    let is_block = dom_ctx.tag_info(handle.get_inner(), parser).map_or_else(\n                        || {\n                            let tag_name = normalized_tag_name(tag.name().as_utf8_str());\n                            is_block_level_element(tag_name.as_ref())\n                        },\n                        |info| info.is_block,\n                    );\n                    if is_block {\n                        saw_block = true;\n                        block_nodes.push(handle);\n                        continue;\n                    }\n\n                    if let Some(children) = dom_ctx.children_of(handle.get_inner()) {\n                        for child in children.iter().rev() {\n                            stack.push(*child);\n                        }\n                    } else {\n                        let tag_children = tag.children();\n                        let mut child_nodes: Vec<_> = tag_children.top().iter().copied().collect();\n                        child_nodes.reverse();\n                        stack.extend(child_nodes);\n                    }\n                }\n                _ => {}\n            }\n        }\n    }\n\n    (text, block_nodes, saw_block)\n}\n\n/// Normalize a link label by collapsing newlines and normalizing whitespace.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn normalize_link_label(label: &str) -> String {\n    let mut needs_collapse = false;\n    for ch in label.chars() {\n        if ch == '\\n' || ch == '\\r' {\n            needs_collapse = true;\n            break;\n        }\n    }\n\n    let collapsed = if needs_collapse {\n        let mut collapsed = String::with_capacity(label.len());\n        for ch in label.chars() {\n            if ch == '\\n' || ch == '\\r' {\n                collapsed.push(' ');\n            } else {\n                collapsed.push(ch);\n            }\n        }\n        Cow::Owned(collapsed)\n    } else {\n        Cow::Borrowed(label)\n    };\n\n    let normalized = text::normalize_whitespace_cow(collapsed.as_ref());\n    normalized.as_ref().trim().to_string()\n}\n\n/// Normalize a tag name to lowercase, preserving borrowed input when possible.\npub fn normalized_tag_name(raw: Cow<'_, str>) -> Cow<'_, str> {\n    if raw.as_bytes().iter().any(u8::is_ascii_uppercase) {\n        let mut owned = raw.into_owned();\n        owned.make_ascii_lowercase();\n        Cow::Owned(owned)\n    } else {\n        raw\n    }\n}\n\n/// Check if an element is block-level (not inline).\npub fn is_block_level_element(tag_name: &str) -> bool {\n    is_block_level_name(tag_name, crate::converter::main_helpers::is_inline_element(tag_name))\n}\n\n/// Returns the largest valid char boundary index at or before `index`.\n///\n/// If `index` is already a char boundary it is returned unchanged.\n/// Otherwise it walks backwards to find one.  Returns 0 if no boundary\n/// is found before `index`.\npub fn floor_char_boundary(s: &str, index: usize) -> usize {\n    if index >= s.len() {\n        s.len()\n    } else {\n        let mut i = index;\n        while i > 0 && !s.is_char_boundary(i) {\n            i -= 1;\n        }\n        i\n    }\n}\n\n/// Escape special Markdown characters in a link label.\n///\n/// Handles bracket escaping to prevent unintended link label termination.\n/// Tracks bracket depth and escapes closing brackets when depth is zero.\n///\n/// # Examples\n/// ```text\n/// Input:  \"[link]\"\n/// Output: \"[link\\\\]\"\n///\n/// Input:  \"[outer [inner]]\"\n/// Output: \"[outer [inner]]\"\n/// ```\npub fn escape_link_label(text: &str) -> String {\n    if text.is_empty() {\n        return String::new();\n    }\n\n    let mut result = String::with_capacity(text.len());\n    let mut backslash_count = 0usize;\n    let mut bracket_depth = 0usize;\n\n    for ch in text.chars() {\n        if ch == '\\\\' {\n            result.push('\\\\');\n            backslash_count += 1;\n            continue;\n        }\n\n        let is_escaped = backslash_count % 2 == 1;\n        backslash_count = 0;\n\n        match ch {\n            '[' if !is_escaped => {\n                bracket_depth = bracket_depth.saturating_add(1);\n                result.push('[');\n            }\n            ']' if !is_escaped => {\n                if bracket_depth == 0 {\n                    result.push('\\\\');\n                } else {\n                    bracket_depth -= 1;\n                }\n                result.push(']');\n            }\n            _ => result.push(ch),\n        }\n    }\n\n    result\n}\n\n/// Helper for block-level element detection.\npub fn is_block_level_name(tag_name: &str, is_inline: bool) -> bool {\n    !is_inline\n        && matches!(\n            tag_name,\n            \"address\"\n                | \"article\"\n                | \"aside\"\n                | \"blockquote\"\n                | \"canvas\"\n                | \"dd\"\n                | \"div\"\n                | \"dl\"\n                | \"dt\"\n                | \"fieldset\"\n                | \"figcaption\"\n                | \"figure\"\n                | \"footer\"\n                | \"form\"\n                | \"h1\"\n                | \"h2\"\n                | \"h3\"\n                | \"h4\"\n                | \"h5\"\n                | \"h6\"\n                | \"header\"\n                | \"hr\"\n                | \"li\"\n                | \"main\"\n                | \"nav\"\n                | \"ol\"\n                | \"p\"\n                | \"pre\"\n                | \"section\"\n                | \"table\"\n                | \"tfoot\"\n                | \"ul\"\n        )\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/mod.rs",
    "content": "//! Utility module: helper functions for common operations.\n//!\n//! This module contains utility functions used across conversion logic\n//! including sibling handling, content extraction, serialization, preprocessing,\n//! caching, and attribute processing.\n//!\n//! These functions are re-exported from the main converter module to provide\n//! organized access to utility functions by category.\n\npub mod attributes;\npub mod caching;\npub mod content;\npub mod preprocessing;\npub mod serialization;\npub mod siblings;\n\n// Re-export commonly used functions for convenience\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/preprocessing.rs",
    "content": "//! HTML preprocessing and normalization.\n//!\n//! Functions for preprocessing HTML before conversion, including script/style stripping,\n//! tag repair, and malformed HTML handling.\n\nuse std::borrow::Cow;\nuse std::str;\n\n/// Strip script and style tags and their content from HTML.\npub fn strip_script_and_style_tags(input: &str) -> Cow<'_, str> {\n    let bytes = input.as_bytes();\n    let len = bytes.len();\n\n    if len == 0 {\n        return Cow::Borrowed(input);\n    }\n\n    let mut idx = 0;\n    let mut last = 0;\n    let mut output: Option<String> = None;\n    let mut svg_depth = 0usize;\n\n    // Fast-path: check if there are any < characters at all\n    if !bytes.contains(&b'<') {\n        return Cow::Borrowed(input);\n    }\n\n    while idx < len {\n        if bytes[idx] == b'<' && idx + 1 < len {\n            if matches_tag_start(bytes, idx + 1, b\"svg\") {\n                if let Some(open_end) = find_tag_end(bytes, idx + 1 + b\"svg\".len()) {\n                    svg_depth += 1;\n                    idx = open_end;\n                    continue;\n                }\n            } else if matches_end_tag_start(bytes, idx + 1, b\"svg\") {\n                if let Some(close_end) = find_tag_end(bytes, idx + 2 + b\"svg\".len()) {\n                    if svg_depth > 0 {\n                        svg_depth = svg_depth.saturating_sub(1);\n                    }\n                    idx = close_end;\n                    continue;\n                }\n            }\n\n            if svg_depth > 0 {\n                idx += 1;\n                continue;\n            }\n\n            // Check for </script or </style (closing tags first for safety)\n            if bytes[idx + 1] == b'/' && idx + 2 < len {\n                // Match </script>\n                if idx + 9 <= len && eq_ascii_insensitive(&bytes[idx..idx + 9], b\"</script>\") {\n                    idx += 9;\n                    continue;\n                }\n\n                // Match </style>\n                if idx + 8 <= len && eq_ascii_insensitive(&bytes[idx..idx + 8], b\"</style>\") {\n                    idx += 8;\n                    continue;\n                }\n            }\n\n            // Check for <script or <style (opening tags)\n            // Match <script (case insensitive)\n            if idx + 7 < len && eq_ascii_insensitive(&bytes[idx..idx + 7], b\"<script\") {\n                // Check if this is actually \"<script\" followed by whitespace, >, or attribute\n                let after_tag = bytes[idx + 7];\n                if after_tag == b'>'\n                    || after_tag == b' '\n                    || after_tag == b'\\t'\n                    || after_tag == b'\\n'\n                    || after_tag == b'\\r'\n                {\n                    // Find the opening tag end\n                    let mut tag_end = idx + 7;\n                    while tag_end < len && bytes[tag_end] != b'>' {\n                        tag_end += 1;\n                    }\n\n                    if tag_end < len {\n                        tag_end += 1; // Include the '>'\n\n                        // Check if this is a JSON-LD script tag\n                        let tag_content = &input[idx..tag_end];\n                        if !is_json_ld_script_open_tag(tag_content) {\n                            // Find the closing </script> tag\n                            let close_tag = find_closing_tag_bytes(bytes, tag_end, b\"script\");\n                            if let Some(close_idx) = close_tag {\n                                let out = output.get_or_insert_with(|| String::with_capacity(len));\n                                out.push_str(&input[last..idx]);\n                                if idx > 0\n                                    && close_idx < len\n                                    && !bytes[idx - 1].is_ascii_whitespace()\n                                    && !bytes[close_idx].is_ascii_whitespace()\n                                {\n                                    out.push(' ');\n                                }\n                                last = close_idx;\n                                idx = close_idx;\n                                continue;\n                            }\n                        }\n                    }\n                }\n            }\n            // Match <style (case insensitive)\n            else if idx + 6 < len && eq_ascii_insensitive(&bytes[idx..idx + 6], b\"<style\") {\n                // Check if this is actually \"<style\" followed by whitespace, >, or attribute\n                let after_tag = bytes[idx + 6];\n                if after_tag == b'>'\n                    || after_tag == b' '\n                    || after_tag == b'\\t'\n                    || after_tag == b'\\n'\n                    || after_tag == b'\\r'\n                {\n                    // Find the opening tag end\n                    let mut tag_end = idx + 6;\n                    while tag_end < len && bytes[tag_end] != b'>' {\n                        tag_end += 1;\n                    }\n\n                    if tag_end < len {\n                        tag_end += 1; // Include the '>'\n\n                        // Find the closing </style> tag\n                        let close_tag = find_closing_tag_bytes(bytes, tag_end, b\"style\");\n                        if let Some(close_idx) = close_tag {\n                            let out = output.get_or_insert_with(|| String::with_capacity(len));\n                            out.push_str(&input[last..idx]);\n                            if idx > 0\n                                && close_idx < len\n                                && !bytes[idx - 1].is_ascii_whitespace()\n                                && !bytes[close_idx].is_ascii_whitespace()\n                            {\n                                out.push(' ');\n                            }\n                            last = close_idx;\n                            idx = close_idx;\n                            continue;\n                        }\n                    }\n                }\n            }\n        }\n\n        idx += 1;\n    }\n\n    if let Some(mut out) = output {\n        if last < len {\n            out.push_str(&input[last..]);\n        }\n        Cow::Owned(out)\n    } else {\n        Cow::Borrowed(input)\n    }\n}\n\n/// Find the position of a closing tag in bytes.\n/// Returns the position AFTER the closing tag (including the '>').\n/// This is highly optimized for performance and uses a fast-path scan.\n#[inline]\npub fn find_closing_tag_bytes(bytes: &[u8], start: usize, tag: &[u8]) -> Option<usize> {\n    let len = bytes.len();\n    let tag_len = tag.len();\n\n    // Fast path: look for the closing tag pattern byte-by-byte\n    // We use a simple byte scan to find '</' then validate the tag name\n    let mut idx = start;\n\n    // Limit search to prevent stack overflow on large files\n    // Look for closing tag within reasonable bounds\n    const MAX_SCAN: usize = 100_000_000; // 100MB limit per tag - prevents pathological cases\n\n    while idx < len && (idx - start) < MAX_SCAN {\n        // Optimization: skip forward to next '<' quickly using memchr\n        if bytes[idx] != b'<' {\n            if let Some(pos) = memchr::memchr(b'<', &bytes[idx..]) {\n                idx += pos;\n            } else {\n                break;\n            }\n        }\n\n        // Check for </ pattern\n        if idx + 2 < len && bytes[idx + 1] == b'/' {\n            // Check if tag name matches\n            if idx + 2 + tag_len <= len && eq_ascii_insensitive(&bytes[idx + 2..idx + 2 + tag_len], tag) {\n                // Ensure it's followed by > or whitespace\n                let after_tag = idx + 2 + tag_len;\n                if after_tag < len && (bytes[after_tag] == b'>' || bytes[after_tag].is_ascii_whitespace()) {\n                    // Find the >\n                    let mut close_idx = after_tag;\n                    while close_idx < len && bytes[close_idx] != b'>' {\n                        close_idx += 1;\n                    }\n                    if close_idx < len {\n                        return Some(close_idx + 1); // Include the '>'\n                    }\n                }\n            }\n        }\n\n        idx += 1;\n    }\n\n    None\n}\n\n/// Compare bytes ignoring ASCII case.\n#[inline]\npub fn eq_ascii_insensitive(a: &[u8], b: &[u8]) -> bool {\n    if a.len() != b.len() {\n        return false;\n    }\n    a.iter().zip(b.iter()).all(|(x, y)| x.eq_ignore_ascii_case(y))\n}\n\n/// Preprocess HTML to normalize tags and fix common issues.\npub fn preprocess_html(input: &str) -> Cow<'_, str> {\n    const SELF_CLOSING: [(&[u8], &str); 3] = [(b\"<br/>\", \"<br>\"), (b\"<hr/>\", \"<hr>\"), (b\"<img/>\", \"<img>\")];\n    const TAGS: [&[u8]; 2] = [b\"script\", b\"style\"];\n    const SVG: &[u8] = b\"svg\";\n    const DOCTYPE: &[u8] = b\"doctype\";\n    const EMPTY_COMMENT: &[u8] = b\"<!---->\";\n\n    let bytes = input.as_bytes();\n    let len = bytes.len();\n    if len == 0 {\n        return Cow::Borrowed(input);\n    }\n\n    let mut idx = 0;\n    let mut last = 0;\n    let mut output: Option<String> = None;\n    let mut svg_depth = 0usize;\n\n    while idx < len {\n        if bytes[idx] == b'<' {\n            if bytes[idx..].starts_with(EMPTY_COMMENT) {\n                let out = output.get_or_insert_with(|| String::with_capacity(input.len()));\n                out.push_str(&input[last..idx]);\n                out.push_str(\"<!-- -->\");\n                idx += EMPTY_COMMENT.len();\n                last = idx;\n                continue;\n            }\n\n            let mut replaced = false;\n            for (pattern, replacement) in &SELF_CLOSING {\n                if bytes[idx..].starts_with(pattern) {\n                    let out = output.get_or_insert_with(|| String::with_capacity(input.len()));\n                    out.push_str(&input[last..idx]);\n                    out.push_str(replacement);\n                    idx += pattern.len();\n                    last = idx;\n                    replaced = true;\n                    break;\n                }\n            }\n            if replaced {\n                continue;\n            }\n\n            if matches_tag_start(bytes, idx + 1, SVG) {\n                if let Some(open_end) = find_tag_end(bytes, idx + 1 + SVG.len()) {\n                    svg_depth += 1;\n                    idx = open_end;\n                    continue;\n                }\n            } else if matches_end_tag_start(bytes, idx + 1, SVG) {\n                if let Some(close_end) = find_tag_end(bytes, idx + 2 + SVG.len()) {\n                    if svg_depth > 0 {\n                        svg_depth = svg_depth.saturating_sub(1);\n                    }\n                    idx = close_end;\n                    continue;\n                }\n            }\n\n            if svg_depth == 0 {\n                let mut handled = false;\n                for tag in TAGS {\n                    if matches_tag_start(bytes, idx + 1, tag) {\n                        if let Some(open_end) = find_tag_end(bytes, idx + 1 + tag.len()) {\n                            if tag == b\"script\" && is_json_ld_script_open_tag(&input[idx..open_end]) {\n                                continue;\n                            }\n                            let remove_end = find_closing_tag(bytes, open_end, tag).unwrap_or(open_end);\n                            let out = output.get_or_insert_with(|| String::with_capacity(input.len()));\n                            out.push_str(&input[last..idx]);\n                            out.push_str(&input[idx..open_end]);\n                            out.push_str(\"</\");\n                            // `TAGS` contains only ASCII byte literals (`b\"script\"`, `b\"style\"`),\n                            // which are always valid UTF-8; `from_utf8` cannot fail here.\n                            if let Ok(tag_str) = str::from_utf8(tag) {\n                                out.push_str(tag_str);\n                            }\n                            out.push('>');\n\n                            last = remove_end;\n                            idx = remove_end;\n                            handled = true;\n                        }\n                    }\n\n                    if handled {\n                        break;\n                    }\n                }\n\n                if handled {\n                    continue;\n                }\n\n                if idx + 2 < len && bytes[idx + 1] == b'!' {\n                    let mut cursor = idx + 2;\n                    while cursor < len && bytes[cursor].is_ascii_whitespace() {\n                        cursor += 1;\n                    }\n\n                    if cursor + DOCTYPE.len() <= len\n                        && bytes[cursor..cursor + DOCTYPE.len()].eq_ignore_ascii_case(DOCTYPE)\n                    {\n                        if let Some(end) = find_tag_end(bytes, cursor + DOCTYPE.len()) {\n                            let out = output.get_or_insert_with(|| String::with_capacity(input.len()));\n                            out.push_str(&input[last..idx]);\n                            last = end;\n                            idx = end;\n                            continue;\n                        }\n                    }\n                }\n            }\n\n            let is_valid_tag = if idx + 1 < len {\n                match bytes[idx + 1] {\n                    b'!' => {\n                        idx + 2 < len\n                            && (bytes[idx + 2] == b'-'\n                                || bytes[idx + 2].is_ascii_alphabetic()\n                                || bytes[idx + 2].is_ascii_uppercase())\n                    }\n                    b'/' => {\n                        idx + 2 < len && (bytes[idx + 2].is_ascii_alphabetic() || bytes[idx + 2].is_ascii_uppercase())\n                    }\n                    b'?' => true,\n                    c if c.is_ascii_alphabetic() || c.is_ascii_uppercase() => true,\n                    _ => false,\n                }\n            } else {\n                false\n            };\n\n            if !is_valid_tag {\n                let out = output.get_or_insert_with(|| String::with_capacity(input.len() + 4));\n                out.push_str(&input[last..idx]);\n                out.push_str(\"&lt;\");\n                idx += 1;\n                last = idx;\n                continue;\n            }\n        }\n\n        idx += 1;\n    }\n\n    if let Some(mut out) = output {\n        if last < len {\n            out.push_str(&input[last..]);\n        }\n        Cow::Owned(out)\n    } else {\n        Cow::Borrowed(input)\n    }\n}\n\n/// Check if a script tag is a JSON-LD script.\npub fn is_json_ld_script_open_tag(tag: &str) -> bool {\n    let bytes = tag.as_bytes();\n    let mut idx = 0;\n    while idx + 4 <= bytes.len() {\n        if eq_ascii_case_insensitive(&bytes[idx..], b\"type\") {\n            let before_ok = idx == 0\n                || bytes\n                    .get(idx.saturating_sub(1))\n                    .is_some_and(|b| b.is_ascii_whitespace() || *b == b'<' || *b == b'/');\n            let after_ok = bytes\n                .get(idx + 4)\n                .is_some_and(|b| b.is_ascii_whitespace() || *b == b'=');\n            if !before_ok || !after_ok {\n                idx += 4;\n                continue;\n            }\n\n            let mut i = idx + 4;\n            while bytes.get(i).is_some_and(u8::is_ascii_whitespace) {\n                i += 1;\n            }\n            if bytes.get(i) != Some(&b'=') {\n                idx += 4;\n                continue;\n            }\n            i += 1;\n            while bytes.get(i).is_some_and(u8::is_ascii_whitespace) {\n                i += 1;\n            }\n            if i >= bytes.len() {\n                return false;\n            }\n\n            let (value_start, value_end) = match bytes[i] {\n                b'\"' | b'\\'' => {\n                    let quote = bytes[i];\n                    let start = i + 1;\n                    let mut end = start;\n                    while end < bytes.len() && bytes[end] != quote {\n                        end += 1;\n                    }\n                    (start, end)\n                }\n                _ => {\n                    let start = i;\n                    let mut end = start;\n                    while end < bytes.len() && !bytes[end].is_ascii_whitespace() && bytes[end] != b'>' {\n                        end += 1;\n                    }\n                    (start, end)\n                }\n            };\n\n            let value = &tag[value_start..value_end];\n            let media_type = value.split(';').next().unwrap_or(value).trim();\n            return eq_ascii_case_insensitive(media_type.as_bytes(), b\"application/ld+json\");\n        }\n        idx += 1;\n    }\n    false\n}\n\n/// Case-insensitive byte comparison for ASCII.\n#[inline]\npub fn eq_ascii_case_insensitive(haystack: &[u8], needle: &[u8]) -> bool {\n    if haystack.len() < needle.len() {\n        return false;\n    }\n    haystack\n        .iter()\n        .zip(needle.iter())\n        .all(|(a, b)| a.eq_ignore_ascii_case(b))\n}\n\n/// Check if bytes match a tag start pattern.\npub fn matches_tag_start(bytes: &[u8], mut start: usize, tag: &[u8]) -> bool {\n    if start >= bytes.len() {\n        return false;\n    }\n\n    if start + tag.len() > bytes.len() {\n        return false;\n    }\n\n    if !bytes[start..start + tag.len()].eq_ignore_ascii_case(tag) {\n        return false;\n    }\n\n    start += tag.len();\n\n    match bytes.get(start) {\n        Some(b'>' | b'/' | b' ' | b'\\t' | b'\\n' | b'\\r') => true,\n        Some(_) => false,\n        None => true,\n    }\n}\n\n/// Find the end of an HTML tag (the position of '>').\npub fn find_tag_end(bytes: &[u8], mut idx: usize) -> Option<usize> {\n    let len = bytes.len();\n    let mut in_quote: Option<u8> = None;\n\n    while idx < len {\n        match bytes[idx] {\n            b'\"' | b'\\'' => {\n                if let Some(current) = in_quote {\n                    if current == bytes[idx] {\n                        in_quote = None;\n                    }\n                } else {\n                    in_quote = Some(bytes[idx]);\n                }\n            }\n            b'>' if in_quote.is_none() => return Some(idx + 1),\n            _ => {}\n        }\n        idx += 1;\n    }\n\n    None\n}\n\n/// Find the closing tag for a given tag name.\npub fn find_closing_tag(bytes: &[u8], mut idx: usize, tag: &[u8]) -> Option<usize> {\n    let len = bytes.len();\n    let mut depth = 1usize;\n\n    while idx < len {\n        if bytes[idx] == b'<' {\n            if matches_tag_start(bytes, idx + 1, tag) {\n                if let Some(next) = find_tag_end(bytes, idx + 1 + tag.len()) {\n                    depth += 1;\n                    idx = next;\n                    continue;\n                }\n            } else if matches_end_tag_start(bytes, idx + 1, tag) {\n                if let Some(close) = find_tag_end(bytes, idx + 2 + tag.len()) {\n                    depth -= 1;\n                    if depth == 0 {\n                        return Some(close);\n                    }\n                    idx = close;\n                    continue;\n                }\n            }\n        }\n\n        idx += 1;\n    }\n\n    None\n}\n\n/// Check if bytes match an end tag pattern.\npub fn matches_end_tag_start(bytes: &[u8], start: usize, tag: &[u8]) -> bool {\n    if start >= bytes.len() || bytes[start] != b'/' {\n        return false;\n    }\n    matches_tag_start(bytes, start + 1, tag)\n}\n\n/// Sanitize malformed markdown-like URLs in HTML attributes.\n///\n/// Handles cases like: `//[domain.com/path](http://domain.com/path)`\n/// Extracts the actual URL from parentheses.\n///\n/// This is an internal function used during preprocessing to extract valid URLs\n/// from malformed HTML that contains markdown-like syntax.\n///\n/// # Arguments\n/// * `url` - The URL string to sanitize\n///\n/// # Returns\n/// * `Cow<str>` - Either the borrowed original URL or an owned sanitized version\npub fn sanitize_markdown_url(url: &str) -> Cow<'_, str> {\n    // Pattern: ...[text](actual_url) or similar markdown-like syntax\n    // This handles malformed HTML where markdown syntax wasn't properly converted\n    // and prevents downstream URL parsing errors (e.g., bracketed \"IPv6\" hosts).\n\n    // Fast-path: we only care about markdown-like link syntax.\n    let Some(mid) = url.find(\"](\") else {\n        return Cow::Borrowed(url);\n    };\n\n    // Ensure there is an opening '[' before the \"](...\" sequence.\n    if !url[..mid].contains('[') {\n        return Cow::Borrowed(url);\n    }\n\n    let paren_start = mid + 2;\n    let Some(rel_end) = url[paren_start..].find(')') else {\n        return Cow::Borrowed(url);\n    };\n    let paren_end = paren_start + rel_end;\n    if paren_start >= paren_end {\n        return Cow::Borrowed(url);\n    }\n\n    Cow::Owned(url[paren_start..paren_end].to_string())\n}\n\n/// Strip elements with the `hidden` attribute from HTML.\n///\n/// Scans for opening tags containing the `hidden` attribute, finds their\n/// matching closing tag, and removes the entire element (tag + content).\n/// Self-closing tags with `hidden` are also removed.\npub fn strip_hidden_elements(input: &str) -> Cow<'_, str> {\n    let bytes = input.as_bytes();\n    let len = bytes.len();\n\n    if len == 0 || !bytes.contains(&b'<') {\n        return Cow::Borrowed(input);\n    }\n\n    let mut idx = 0;\n    let mut last = 0;\n    let mut output: Option<String> = None;\n\n    while idx < len {\n        if bytes[idx] == b'<' && idx + 1 < len && bytes[idx + 1] != b'/' && bytes[idx + 1] != b'!' {\n            // Find the end of this opening tag\n            if let Some(tag_end) = find_tag_end(bytes, idx + 1) {\n                let tag_slice = &input[idx..tag_end];\n                if tag_has_hidden_attribute(tag_slice) {\n                    // Extract the tag name\n                    let name_start = idx + 1;\n                    let mut name_end = name_start;\n                    while name_end < len\n                        && !bytes[name_end].is_ascii_whitespace()\n                        && bytes[name_end] != b'>'\n                        && bytes[name_end] != b'/'\n                    {\n                        name_end += 1;\n                    }\n                    let tag_name = &bytes[name_start..name_end];\n\n                    // Check if it's a self-closing tag (e.g., <br hidden> or <br hidden/>)\n                    let is_self_closing = tag_slice.ends_with(\"/>\")\n                        || tag_name.eq_ignore_ascii_case(b\"br\")\n                        || tag_name.eq_ignore_ascii_case(b\"hr\")\n                        || tag_name.eq_ignore_ascii_case(b\"img\")\n                        || tag_name.eq_ignore_ascii_case(b\"input\");\n\n                    let remove_end = if is_self_closing {\n                        tag_end\n                    } else {\n                        // Find the closing tag\n                        find_closing_tag_bytes(bytes, tag_end, tag_name).unwrap_or(tag_end)\n                    };\n\n                    let out = output.get_or_insert_with(|| String::with_capacity(len));\n                    out.push_str(&input[last..idx]);\n                    last = remove_end;\n                    idx = remove_end;\n                    continue;\n                }\n            }\n        }\n        idx += 1;\n    }\n\n    if let Some(mut out) = output {\n        if last < len {\n            out.push_str(&input[last..]);\n        }\n        Cow::Owned(out)\n    } else {\n        Cow::Borrowed(input)\n    }\n}\n\n/// Check if an opening tag string contains the `hidden` attribute.\n///\n/// Handles: `hidden`, `hidden=\"\"`, `hidden=\"hidden\"`, `hidden=\"true\"`.\n/// Does NOT match attributes like `data-hidden` or `aria-hidden`.\nfn tag_has_hidden_attribute(tag: &str) -> bool {\n    let bytes = tag.as_bytes();\n    let len = bytes.len();\n    let needle = b\"hidden\";\n    let nlen = needle.len();\n\n    let mut i = 0;\n    // Skip past the tag name\n    while i < len && bytes[i] != b' ' && bytes[i] != b'\\t' && bytes[i] != b'\\n' && bytes[i] != b'>' {\n        i += 1;\n    }\n\n    while i + nlen <= len {\n        if bytes[i..i + nlen].eq_ignore_ascii_case(needle) {\n            // Check that the character before is whitespace (attribute boundary)\n            let before_ok = i == 0 || bytes[i - 1].is_ascii_whitespace();\n            // Check that the character after is whitespace, '>', '=', or '/'\n            let after = bytes.get(i + nlen).copied();\n            let after_ok = matches!(after, None | Some(b' ' | b'\\t' | b'\\n' | b'\\r' | b'>' | b'=' | b'/'));\n            if before_ok && after_ok {\n                return true;\n            }\n        }\n        i += 1;\n    }\n    false\n}\n\n#[cfg(test)]\nmod tests {\n    use super::sanitize_markdown_url;\n\n    #[test]\n    fn sanitize_markdown_url_extracts_scheme_relative_markdown_like_url() {\n        let input = \"//[p1.zemanta.com/v2/p/ns/45625/PAGE\\\\_VIEW/](http://p1.zemanta.com/v2/p/ns/45625/PAGE_VIEW/)\";\n        let sanitized = sanitize_markdown_url(input);\n        assert_eq!(sanitized, \"http://p1.zemanta.com/v2/p/ns/45625/PAGE_VIEW/\");\n    }\n\n    #[test]\n    fn sanitize_markdown_url_extracts_standard_markdown_like_url() {\n        let input = \"[label](https://example.com/path?q=1)\";\n        let sanitized = sanitize_markdown_url(input);\n        assert_eq!(sanitized, \"https://example.com/path?q=1\");\n    }\n\n    #[test]\n    fn sanitize_markdown_url_leaves_normal_urls_unchanged() {\n        let input = \"https://example.com/normal\";\n        let sanitized = sanitize_markdown_url(input);\n        assert_eq!(sanitized, input);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/serialization.rs",
    "content": "//! Output serialization and formatting.\n//!\n//! Utilities for serializing HTML elements back to string format, used for preserving\n//! original HTML for elements like SVG, math, and custom elements.\n\nuse crate::converter::utility::content::normalized_tag_name;\n\n/// Serialize an element to HTML string (for SVG and Math elements).\n#[allow(clippy::trivially_copy_pass_by_ref)]\n#[allow(dead_code)] // used with visitor feature\npub fn serialize_element(node_handle: &tl::NodeHandle, parser: &tl::Parser) -> String {\n    if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {\n        let tag_name = normalized_tag_name(tag.name().as_utf8_str());\n        let mut html = String::with_capacity(256);\n        html.push('<');\n        html.push_str(&tag_name);\n\n        for (key, value_opt) in tag.attributes().iter() {\n            html.push(' ');\n            html.push_str(&key);\n            if let Some(value) = value_opt {\n                html.push_str(\"=\\\"\");\n                html.push_str(&value);\n                html.push('\"');\n            }\n        }\n\n        let has_children = !tag.children().top().is_empty();\n        if has_children {\n            html.push('>');\n            let children = tag.children();\n            {\n                for child_handle in children.top().iter() {\n                    html.push_str(&serialize_node(child_handle, parser));\n                }\n            }\n            html.push_str(\"</\");\n            html.push_str(&tag_name);\n            html.push('>');\n        } else {\n            html.push_str(\" />\");\n        }\n        return html;\n    }\n    String::new()\n}\n\n/// Serialize a node to HTML string.\n#[allow(clippy::trivially_copy_pass_by_ref)]\n#[allow(dead_code)] // used with visitor feature\npub fn serialize_node(node_handle: &tl::NodeHandle, parser: &tl::Parser) -> String {\n    if let Some(node) = node_handle.get(parser) {\n        match node {\n            tl::Node::Raw(bytes) => bytes.as_utf8_str().to_string(),\n            tl::Node::Tag(_) => serialize_element(node_handle, parser),\n            _ => String::new(),\n        }\n    } else {\n        String::new()\n    }\n}\n\n/// Serialize a tag to HTML, wrapping serialize_node_to_html.\npub fn serialize_tag_to_html(handle: &tl::NodeHandle, parser: &tl::Parser) -> String {\n    let mut html = String::new();\n    serialize_node_to_html(handle, parser, &mut html);\n    html\n}\n\n/// Recursively serialize a node to HTML.\n#[allow(clippy::trivially_copy_pass_by_ref)]\n#[allow(dead_code)] // used with visitor feature\npub fn serialize_node_to_html(handle: &tl::NodeHandle, parser: &tl::Parser, output: &mut String) {\n    match handle.get(parser) {\n        Some(tl::Node::Tag(tag)) => {\n            let tag_name = normalized_tag_name(tag.name().as_utf8_str());\n\n            output.push('<');\n            output.push_str(&tag_name);\n\n            for (key, value) in tag.attributes().iter() {\n                output.push(' ');\n                output.push_str(&key);\n                if let Some(val) = value {\n                    output.push_str(\"=\\\"\");\n                    output.push_str(&val);\n                    output.push('\"');\n                }\n            }\n\n            output.push('>');\n\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                serialize_node_to_html(child_handle, parser, output);\n            }\n\n            if !matches!(\n                tag_name.as_ref(),\n                \"br\" | \"hr\"\n                    | \"img\"\n                    | \"input\"\n                    | \"meta\"\n                    | \"link\"\n                    | \"area\"\n                    | \"base\"\n                    | \"col\"\n                    | \"embed\"\n                    | \"param\"\n                    | \"source\"\n                    | \"track\"\n                    | \"wbr\"\n            ) {\n                output.push_str(\"</\");\n                output.push_str(&tag_name);\n                output.push('>');\n            }\n        }\n        Some(tl::Node::Raw(bytes)) => {\n            if let Ok(text) = std::str::from_utf8(bytes.as_bytes()) {\n                output.push_str(text);\n            }\n        }\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/utility/siblings.rs",
    "content": "//! Sibling node navigation and handling.\n//!\n//! Utilities for working with sibling nodes in the DOM tree, including navigation functions\n//! and inline/block element detection for whitespace handling.\n\nuse crate::converter::DomContext;\n\n/// Get the tag name of the next sibling element.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn get_next_sibling_tag<'a>(\n    node_handle: &tl::NodeHandle,\n    parser: &'a tl::Parser,\n    dom_ctx: &'a DomContext,\n) -> Option<&'a str> {\n    dom_ctx.next_tag_name(*node_handle, parser)\n}\n\n/// Get the tag name of the previous sibling element.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn get_previous_sibling_tag<'a>(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &'a DomContext,\n) -> Option<&'a str> {\n    let id = node_handle.get_inner();\n    let parent = dom_ctx.parent_of(id);\n\n    let siblings = if let Some(parent_id) = parent {\n        dom_ctx.children_of(parent_id)?\n    } else {\n        &dom_ctx.root_children\n    };\n\n    let position = dom_ctx.sibling_index(id).or_else(|| {\n        siblings\n            .iter()\n            .position(|handle: &tl::NodeHandle| handle.get_inner() == id)\n    })?;\n\n    for sibling in siblings.iter().take(position).rev() {\n        if let Some(info) = dom_ctx.tag_info(sibling.get_inner(), parser) {\n            return Some(info.name.as_str());\n        }\n        if let Some(tl::Node::Raw(raw)) = sibling.get(parser) {\n            if !raw.as_utf8_str().trim().is_empty() {\n                return None;\n            }\n        }\n    }\n\n    None\n}\n\n/// Check if the previous sibling is an inline tag.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn previous_sibling_is_inline_tag(node_handle: &tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {\n    dom_ctx.previous_inline_like(*node_handle, parser)\n}\n\n/// Check if the next sibling is whitespace-only text.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn next_sibling_is_whitespace_text(\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &DomContext,\n) -> bool {\n    dom_ctx.next_whitespace_text(*node_handle, parser)\n}\n\n/// Check if the next sibling is an inline tag.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn next_sibling_is_inline_tag(node_handle: &tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {\n    dom_ctx.next_inline_like(*node_handle, parser)\n}\n\n/// Append an inline suffix to output, with smart whitespace handling.\n///\n/// Avoids adding spaces before siblings that are already whitespace.\n#[allow(clippy::trivially_copy_pass_by_ref)]\npub fn append_inline_suffix(\n    output: &mut String,\n    suffix: &str,\n    has_core_content: bool,\n    node_handle: &tl::NodeHandle,\n    parser: &tl::Parser,\n    dom_ctx: &DomContext,\n) {\n    if suffix.is_empty() {\n        return;\n    }\n\n    if suffix == \" \" && has_core_content && next_sibling_is_whitespace_text(node_handle, parser, dom_ctx) {\n        return;\n    }\n\n    output.push_str(suffix);\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/converter/visitor_hooks.rs",
    "content": "//! Visitor callback hooks for custom HTML traversal during conversion.\n//!\n//! This module contains the visitor pattern implementation hooks that are called\n//! before and after element processing during the HTML to Markdown conversion tree walk.\n//! These hooks enable custom processing, analysis, or modification of elements during conversion.\n\nuse std::collections::BTreeMap;\n\n#[cfg(feature = \"visitor\")]\nuse crate::converter::utility::content::collect_tag_attributes;\nuse crate::converter::utility::content::is_block_level_element;\nuse crate::visitor::{NodeContext, NodeType, VisitResult};\n\n/// Handles visitor callback for element start (before processing).\n///\n/// This function is called when entering an element during tree traversal,\n/// before the element's content is processed. The visitor can:\n/// - Continue with normal processing (Continue)\n/// - Skip the element entirely (Skip)\n/// - Provide custom output to replace the element (Custom)\n/// - Signal an error (Error)\n///\n/// # Arguments\n///\n/// * `visitor_handle` - Reference to the visitor for callbacks\n/// * `tag_name` - The normalized tag name being processed\n/// * `node_handle` - Handle to the DOM node\n/// * `tag` - Reference to the tag object\n/// * `parser` - Reference to the tl parser\n/// * `output` - Mutable reference to output string\n/// * `ctx` - Reference to the conversion context\n/// * `depth` - Current tree depth\n/// * `dom_ctx` - Reference to DOM context for tree navigation\n///\n/// # Returns\n///\n/// `VisitAction` enum indicating what should happen next:\n/// - `VisitAction::Continue` - Process element normally\n/// - `VisitAction::Skip` - Skip element, don't process or call visit_element_end\n/// - `VisitAction::Custom(output)` - Use custom output, skip normal processing\n/// - `VisitAction::Error` - Stop processing with error\npub fn handle_visitor_element_start(\n    visitor_handle: &crate::visitor::VisitorHandle,\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser<'_>,\n    output: &mut String,\n    _ctx: &crate::converter::Context,\n    depth: usize,\n    dom_ctx: &crate::converter::DomContext,\n) -> VisitAction {\n    let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n    let node_id = node_handle.get_inner();\n    let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n    let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n    let node_ctx = NodeContext {\n        node_type: NodeType::Element,\n        tag_name: tag_name.to_string(),\n        attributes,\n        depth,\n        index_in_parent,\n        parent_tag,\n        is_inline: !is_block_level_element(tag_name),\n    };\n\n    let visitor_start_result = {\n        let mut visitor = visitor_handle.borrow_mut();\n        visitor.visit_element_start(&node_ctx)\n    };\n\n    match visitor_start_result {\n        crate::visitor::VisitResult::Continue => VisitAction::Continue,\n        crate::visitor::VisitResult::Skip => VisitAction::Skip,\n        crate::visitor::VisitResult::Custom(custom_output) => {\n            output.push_str(&custom_output);\n\n            // For custom output, still call visit_element_end (except for tables)\n            if !matches!(tag_name, \"table\") {\n                let element_content = &custom_output;\n                let mut visitor = visitor_handle.borrow_mut();\n                let _ = visitor.visit_element_end(&node_ctx, element_content);\n            }\n\n            VisitAction::Custom\n        }\n        crate::visitor::VisitResult::Error(_msg) => VisitAction::Error,\n        _ => VisitAction::Continue,\n    }\n}\n\n/// Handles visitor callback for element end (after processing).\n///\n/// This function is called when exiting an element after its content has been processed.\n/// The visitor can:\n/// - Accept the output normally (Continue)\n/// - Replace the output with custom content (Custom)\n/// - Remove the output entirely (Skip)\n/// - Signal an error (Error)\n///\n/// # Arguments\n///\n/// * `visitor_handle` - Reference to the visitor for callbacks\n/// * `tag_name` - The normalized tag name that was processed\n/// * `node_handle` - Handle to the DOM node\n/// * `tag` - Reference to the tag object\n/// * `parser` - Reference to the tl parser\n/// * `output` - Mutable reference to output string\n/// * `element_output_start` - Byte position where this element's output started\n/// * `ctx` - Reference to the conversion context\n/// * `depth` - Current tree depth\n/// * `dom_ctx` - Reference to DOM context for tree navigation\npub fn handle_visitor_element_end(\n    visitor_handle: &crate::visitor::VisitorHandle,\n    tag_name: &str,\n    node_handle: &tl::NodeHandle,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser<'_>,\n    output: &mut String,\n    element_output_start: usize,\n    ctx: &crate::converter::Context,\n    depth: usize,\n    dom_ctx: &crate::converter::DomContext,\n) {\n    // Skip visitor callback for table elements\n    if matches!(tag_name, \"table\") {\n        return;\n    }\n\n    let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);\n\n    let node_id = node_handle.get_inner();\n    let parent_tag = dom_ctx.parent_tag_name(node_id, parser);\n    let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);\n\n    let node_ctx = NodeContext {\n        node_type: NodeType::Element,\n        tag_name: tag_name.to_string(),\n        attributes,\n        depth,\n        index_in_parent,\n        parent_tag,\n        is_inline: !is_block_level_element(tag_name),\n    };\n\n    // The saved `element_output_start` can become stale in two ways:\n    // 1. A child visitor returning Custom/Skip truncates `output`, making the\n    //    saved position point past the end of the now-shorter string.\n    // 2. Element handlers (e.g. div) trim trailing whitespace that was present\n    //    when the position was captured, then append new multi-byte content.\n    //    The old position can land inside a multi-byte character.\n    // Clamp to output length, then retreat to a valid char boundary.\n    let safe_start = element_output_start.min(output.len());\n    let safe_start = crate::converter::utility::content::floor_char_boundary(output, safe_start);\n    let element_content = &output[safe_start..];\n\n    let mut visitor = visitor_handle.borrow_mut();\n    match visitor.visit_element_end(&node_ctx, element_content) {\n        VisitResult::Continue => {}\n        VisitResult::Custom(custom) => {\n            output.truncate(safe_start);\n            output.push_str(&custom);\n        }\n        VisitResult::Skip => {\n            output.truncate(safe_start);\n        }\n        VisitResult::Error(err) => {\n            if ctx.visitor_error.borrow().is_none() {\n                *ctx.visitor_error.borrow_mut() = Some(err);\n            }\n        }\n        VisitResult::PreserveHtml => {}\n    }\n}\n\n/// Result of visitor element start callback indicating what should happen next.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum VisitAction {\n    /// Continue with normal element processing\n    Continue,\n    /// Skip the element entirely (don't process children or call visit_element_end)\n    Skip,\n    /// Custom output was provided, skip normal processing\n    Custom,\n    /// Error occurred during visitor callback\n    Error,\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/error.rs",
    "content": "//! Error types for HTML to Markdown conversion.\n\nuse thiserror::Error;\n\n/// Result type for conversion operations.\npub type Result<T> = std::result::Result<T, ConversionError>;\n\n/// Errors that can occur during HTML to Markdown conversion.\n#[derive(Error, Debug)]\npub enum ConversionError {\n    /// HTML parsing error\n    #[error(\"HTML parsing error: {0}\")]\n    ParseError(String),\n\n    /// HTML sanitization error\n    #[error(\"Sanitization error: {0}\")]\n    SanitizationError(String),\n\n    /// Invalid configuration\n    #[error(\"Invalid configuration: {0}\")]\n    ConfigError(String),\n\n    /// I/O error\n    #[error(\"I/O error: {0}\")]\n    IoError(#[from] std::io::Error),\n\n    /// Panic caught during conversion to prevent unwinding across FFI boundaries\n    #[error(\"Internal panic: {0}\")]\n    Panic(String),\n\n    /// Invalid input data\n    #[error(\"Invalid input: {0}\")]\n    InvalidInput(String),\n\n    /// Visitor callback error\n    #[cfg(feature = \"visitor\")]\n    #[error(\"Visitor error: {0}\")]\n    Visitor(String),\n\n    /// Generic conversion error\n    #[error(\"Conversion error: {0}\")]\n    Other(String),\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/exports.rs",
    "content": "//! Public API re-exports from submodules.\n//!\n//! This module centralizes all public type and function exports,\n//! making the crate's public interface clear and organized.\n\npub use crate::error::{ConversionError, Result};\n\n#[cfg(feature = \"inline-images\")]\npub use crate::inline_images::{\n    DEFAULT_INLINE_IMAGE_LIMIT, HtmlExtraction, InlineImage, InlineImageConfig, InlineImageConfigUpdate,\n    InlineImageFormat, InlineImageSource, InlineImageWarning,\n};\n\n#[cfg(feature = \"metadata\")]\npub use crate::metadata::{\n    DEFAULT_MAX_STRUCTURED_DATA_SIZE, DocumentMetadata, HeaderMetadata, HtmlMetadata, ImageMetadata, ImageType,\n    LinkMetadata, LinkType, MetadataConfig, MetadataConfigUpdate, StructuredData, StructuredDataType, TextDirection,\n};\n\npub use crate::options::{\n    CodeBlockStyle, ConversionOptions, ConversionOptionsBuilder, ConversionOptionsUpdate, HeadingStyle, HighlightStyle,\n    LinkStyle, ListIndentType, NewlineStyle, OutputFormat, PreprocessingOptions, PreprocessingOptionsUpdate,\n    PreprocessingPreset, WhitespaceMode,\n};\n"
  },
  {
    "path": "crates/html-to-markdown/src/inline_images.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\nuse std::collections::BTreeMap;\n\nuse crate::error::ConversionError;\n\n/// Configuration for capturing inline images during conversion.\n#[derive(Debug, Clone)]\npub struct InlineImageConfig {\n    /// Maximum allowed decoded size in bytes; larger payloads are rejected.\n    pub max_decoded_size_bytes: u64,\n    /// Optional prefix for generated filenames (defaults to \"`embedded_image`\").\n    pub filename_prefix: Option<String>,\n    /// Whether to capture inline SVG elements (defaults to true).\n    pub capture_svg: bool,\n    /// Whether to decode raster images to infer dimensions (defaults to false).\n    pub infer_dimensions: bool,\n}\n\n/// Default maximum size for inline image extraction (5 MB).\npub const DEFAULT_INLINE_IMAGE_LIMIT: u64 = 5 * 1024 * 1024;\n\n/// Partial update for `InlineImageConfig`.\n///\n/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n/// Only specified fields (Some values) will override existing options; None values leave the\n/// corresponding fields unchanged when applied via [`InlineImageConfig::apply_update`].\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), derive(serde::Deserialize))]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(deny_unknown_fields))]\npub struct InlineImageConfigUpdate {\n    /// Optional maximum decoded size override in bytes.\n    pub max_decoded_size_bytes: Option<u64>,\n    /// Optional filename prefix override for generated filenames.\n    pub filename_prefix: Option<String>,\n    /// Optional inline SVG capture enablement override.\n    pub capture_svg: Option<bool>,\n    /// Optional dimension inference override for raster images.\n    pub infer_dimensions: Option<bool>,\n}\n\nimpl InlineImageConfig {\n    /// Create a new configuration with required maximum decoded size.\n    #[must_use]\n    pub const fn new(max_decoded_size_bytes: u64) -> Self {\n        Self {\n            max_decoded_size_bytes,\n            filename_prefix: None,\n            capture_svg: true,\n            infer_dimensions: false,\n        }\n    }\n\n    /// Apply a partial update to this inline image configuration.\n    ///\n    /// Any specified fields in the update will override the current values.\n    /// Unspecified fields (None) are left unchanged.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial inline image options update with fields to override\n    pub fn apply_update(&mut self, update: InlineImageConfigUpdate) {\n        if let Some(max_decoded_size_bytes) = update.max_decoded_size_bytes {\n            self.max_decoded_size_bytes = max_decoded_size_bytes;\n        }\n        if let Some(filename_prefix) = update.filename_prefix {\n            self.filename_prefix = Some(filename_prefix);\n        }\n        if let Some(capture_svg) = update.capture_svg {\n            self.capture_svg = capture_svg;\n        }\n        if let Some(infer_dimensions) = update.infer_dimensions {\n            self.infer_dimensions = infer_dimensions;\n        }\n    }\n\n    /// Create new inline image configuration from a partial update.\n    ///\n    /// Creates a new `InlineImageConfig` struct with defaults, then applies the update.\n    /// Fields not specified in the update keep their default values.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial inline image options update with fields to set\n    ///\n    /// # Returns\n    ///\n    /// New `InlineImageConfig` with specified updates applied to defaults\n    #[must_use]\n    pub fn from_update(update: InlineImageConfigUpdate) -> Self {\n        let mut config = Self::new(DEFAULT_INLINE_IMAGE_LIMIT);\n        config.apply_update(update);\n        config\n    }\n}\n\n/// Supported inline image formats derived from the MIME subtype.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum InlineImageFormat {\n    /// PNG (Portable Network Graphics) raster image format.\n    Png,\n    /// JPEG (Joint Photographic Experts Group) raster image format.\n    Jpeg,\n    /// GIF (Graphics Interchange Format) raster image format.\n    Gif,\n    /// BMP (Bitmap) raster image format.\n    Bmp,\n    /// WebP modern raster image format.\n    Webp,\n    /// SVG (Scalable Vector Graphics) vector format.\n    Svg,\n    /// Custom or unrecognized image format; contains the MIME subtype.\n    Other(String),\n}\n\nimpl std::fmt::Display for InlineImageFormat {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Png => write!(f, \"png\"),\n            Self::Jpeg => write!(f, \"jpeg\"),\n            Self::Gif => write!(f, \"gif\"),\n            Self::Bmp => write!(f, \"bmp\"),\n            Self::Webp => write!(f, \"webp\"),\n            Self::Svg => write!(f, \"svg\"),\n            Self::Other(custom) => write!(f, \"{custom}\"),\n        }\n    }\n}\n\n/// Source of the inline image.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum InlineImageSource {\n    /// Image sourced from an `<img>` tag's `data:` URI.\n    ImgDataUri,\n    /// Image sourced from an inline `<svg>` element.\n    SvgElement,\n}\n\nimpl std::fmt::Display for InlineImageSource {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::ImgDataUri => write!(f, \"img_data_uri\"),\n            Self::SvgElement => write!(f, \"svg_element\"),\n        }\n    }\n}\n\n/// Information about an extracted inline image.\n#[derive(Debug, Clone)]\npub struct InlineImage {\n    /// Raw image data as bytes (encoded in its original format).\n    pub data: Vec<u8>,\n    /// Detected or inferred image format.\n    pub format: InlineImageFormat,\n    /// Generated or extracted filename for the image.\n    pub filename: Option<String>,\n    /// Alt text or other descriptive metadata from the source HTML.\n    pub description: Option<String>,\n    /// Image dimensions in pixels (width, height); only present if inferred.\n    pub dimensions: Option<(u32, u32)>,\n    /// Where the image originated (data URI or SVG element).\n    pub source: InlineImageSource,\n    /// Additional HTML attributes from the source element.\n    pub attributes: BTreeMap<String, String>,\n}\n\nimpl std::fmt::Display for InlineImage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{self:?}\")\n    }\n}\n\n/// Human-friendly warning emitted during inline image extraction.\n#[derive(Debug, Clone)]\npub struct InlineImageWarning {\n    /// The index of the image that triggered the warning.\n    pub index: usize,\n    /// Descriptive message explaining the warning or issue.\n    pub message: String,\n}\n\n/// Output containing extracted inline images from `convert()` when `extract_images` is enabled.\n#[derive(Debug, Clone)]\npub struct HtmlExtraction {\n    /// Converted markdown output.\n    pub markdown: String,\n    /// Extracted inline images found in the HTML.\n    pub inline_images: Vec<InlineImage>,\n    /// Non-fatal warnings encountered during extraction.\n    pub warnings: Vec<InlineImageWarning>,\n}\n\n/// Internal collector that maintains inline image state during traversal.\n#[derive(Debug)]\npub struct InlineImageCollector {\n    config: InlineImageConfig,\n    prefix: String,\n    next_index: usize,\n    images: Vec<InlineImage>,\n    warnings: Vec<InlineImageWarning>,\n}\n\nimpl InlineImageCollector {\n    pub(crate) fn new(config: InlineImageConfig) -> Result<Self, ConversionError> {\n        if config.max_decoded_size_bytes == 0 {\n            return Err(ConversionError::ConfigError(\n                \"inline image max_decoded_size_bytes must be greater than zero\".to_string(),\n            ));\n        }\n\n        let prefix = config\n            .filename_prefix\n            .as_deref()\n            .filter(|value| !value.trim().is_empty())\n            .unwrap_or(\"embedded_image\")\n            .to_string();\n\n        Ok(Self {\n            config,\n            prefix,\n            next_index: 0,\n            images: Vec::new(),\n            warnings: Vec::new(),\n        })\n    }\n\n    pub(crate) const fn capture_svg(&self) -> bool {\n        self.config.capture_svg\n    }\n\n    pub(crate) const fn should_infer_dimensions(&self) -> bool {\n        self.config.infer_dimensions\n    }\n\n    pub(crate) const fn max_decoded_size(&self) -> u64 {\n        self.config.max_decoded_size_bytes\n    }\n\n    pub(crate) const fn next_index(&mut self) -> usize {\n        self.next_index += 1;\n        self.next_index\n    }\n\n    pub(crate) fn finalize_filename(&self, provided: Option<&str>, index: usize, format: &InlineImageFormat) -> String {\n        if let Some(name) = provided {\n            return name.to_string();\n        }\n\n        let extension = match format {\n            InlineImageFormat::Png => \"png\",\n            InlineImageFormat::Jpeg => \"jpeg\",\n            InlineImageFormat::Gif => \"gif\",\n            InlineImageFormat::Bmp => \"bmp\",\n            InlineImageFormat::Webp => \"webp\",\n            InlineImageFormat::Svg => \"svg\",\n            // ~keep: Split on MIME type delimiters (+, ., ;) to extract base subtype\n            InlineImageFormat::Other(custom) => custom\n                .split(['+', '.', ';'])\n                .next()\n                .filter(|s| !s.is_empty())\n                .unwrap_or(\"bin\"),\n        };\n\n        format!(\"{}_{}.{}\", self.prefix, index, extension)\n    }\n\n    pub(crate) fn warn_skip(&mut self, index: usize, reason: impl Into<String>) {\n        let message = format!(\"Skipped inline image {}: {}\", index, reason.into());\n        self.warnings.push(InlineImageWarning { index, message });\n    }\n\n    pub(crate) fn warn_info(&mut self, index: usize, reason: impl Into<String>) {\n        let message = format!(\"Inline image {}: {}\", index, reason.into());\n        self.warnings.push(InlineImageWarning { index, message });\n    }\n\n    pub(crate) fn push_image(&mut self, index: usize, mut image: InlineImage) {\n        if image.filename.is_none() {\n            let derived = self.finalize_filename(None, index, &image.format);\n            image.filename = Some(derived);\n        }\n        self.images.push(image);\n    }\n\n    #[allow(clippy::too_many_arguments)]\n    pub(crate) const fn build_image(\n        &self,\n        data: Vec<u8>,\n        format: InlineImageFormat,\n        filename: Option<String>,\n        description: Option<String>,\n        dimensions: Option<(u32, u32)>,\n        source: InlineImageSource,\n        attributes: BTreeMap<String, String>,\n    ) -> InlineImage {\n        InlineImage {\n            data,\n            format,\n            filename,\n            description,\n            dimensions,\n            source,\n            attributes,\n        }\n    }\n\n    pub(crate) fn infer_dimensions(\n        &mut self,\n        index: usize,\n        data: &[u8],\n        format: &InlineImageFormat,\n    ) -> Option<(u32, u32)> {\n        if !self.should_infer_dimensions() {\n            return None;\n        }\n\n        match format {\n            InlineImageFormat::Svg | InlineImageFormat::Other(_) => return None,\n            _ => {}\n        }\n\n        match image::load_from_memory(data) {\n            Ok(img) => Some((img.width(), img.height())),\n            Err(err) => {\n                self.warn_info(\n                    index,\n                    format!(\"unable to decode raster data for dimension inference ({err})\"),\n                );\n                None\n            }\n        }\n    }\n\n    pub(crate) fn finish(self) -> (Vec<InlineImage>, Vec<InlineImageWarning>) {\n        (self.images, self.warnings)\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/lib.rs",
    "content": "#![allow(\n    clippy::too_many_lines,\n    clippy::option_if_let_else,\n    clippy::match_wildcard_for_single_variants,\n    clippy::needless_pass_by_value,\n    clippy::struct_excessive_bools,\n    clippy::fn_params_excessive_bools,\n    clippy::branches_sharing_code,\n    clippy::match_same_arms,\n    clippy::missing_errors_doc,\n    clippy::items_after_statements,\n    clippy::doc_markdown,\n    clippy::cast_sign_loss,\n    clippy::default_trait_access,\n    clippy::unused_self,\n    clippy::cast_precision_loss,\n    clippy::collapsible_if,\n    clippy::too_many_arguments,\n    clippy::collapsible_else_if,\n    clippy::extra_unused_lifetimes,\n    clippy::unnecessary_lazy_evaluations,\n    clippy::must_use_candidate,\n    clippy::trivially_copy_pass_by_ref,\n    clippy::explicit_iter_loop,\n    clippy::missing_const_for_fn,\n    clippy::manual_assert,\n    clippy::return_self_not_must_use,\n    clippy::collapsible_match,\n    clippy::cast_possible_truncation,\n    clippy::map_unwrap_or,\n    clippy::manual_let_else,\n    clippy::used_underscore_binding,\n    clippy::assigning_clones,\n    clippy::uninlined_format_args\n)]\n\n//! High-performance HTML to Markdown converter.\n//!\n//! Built with html5ever for fast, memory-efficient HTML parsing.\n//!\n//! ## Optional inline image extraction\n//!\n//! Enable the `inline-images` Cargo feature to collect embedded data URI images and inline SVG\n//! assets alongside the produced Markdown.\n\n// ============================================================================\n// Module Declarations\n// ============================================================================\n\npub mod error;\n#[cfg(feature = \"metadata\")]\npub mod metadata;\npub mod options;\npub mod types;\n#[cfg(feature = \"visitor\")]\npub mod visitor;\n\n// Internal modules (not part of public API)\nmod convert_api;\n#[allow(dead_code)]\npub(crate) mod converter;\nmod exports;\n#[cfg(feature = \"inline-images\")]\nmod inline_images;\npub(crate) mod prelude;\nmod rcdom;\npub(crate) mod text;\nmod validation;\n#[cfg(feature = \"visitor\")]\n#[allow(clippy::ref_option)]\npub(crate) mod visitor_helpers;\npub(crate) mod wrapper;\n\n// ============================================================================\n// Public Re-exports (from exports module)\n// ============================================================================\n\npub use exports::*;\npub use types::{\n    AnnotationKind, ConversionResult, DocumentNode, DocumentStructure, GridCell, NodeContent, ProcessingWarning,\n    TableData, TableGrid, TextAnnotation, WarningKind,\n};\n#[cfg(feature = \"visitor\")]\npub use visitor::{NodeContext, NodeType, VisitResult};\n\n// ============================================================================\n// Main Public API Functions\n// ============================================================================\n\npub use convert_api::convert;\n\n// Tests\n// ============================================================================\n\n#[cfg(test)]\nmod basic_tests {\n    use super::*;\n\n    #[test]\n    fn test_binary_input_rejected() {\n        let html = format!(\"abc{}def\", \"\\0\".repeat(20));\n        let result = convert(&html, None);\n        assert!(matches!(result, Err(ConversionError::InvalidInput(_))));\n    }\n\n    #[test]\n    fn test_binary_magic_rejected() {\n        let html = \"%PDF-1.7\";\n        let result = convert(html, None);\n        assert!(matches!(result, Err(ConversionError::InvalidInput(_))));\n    }\n\n    #[test]\n    fn test_utf16_hint_recovered() {\n        let html = String::from_utf8_lossy(b\"\\xFF\\xFE<\\0h\\0t\\0m\\0l\\0>\\0\").to_string();\n        let result = convert(&html, None);\n        assert!(result.is_ok(), \"UTF-16 input should be recovered instead of rejected\");\n    }\n\n    #[test]\n    fn test_plain_text_allowed() {\n        let result = convert(\"Just text\", None).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(content.contains(\"Just text\"));\n    }\n\n    #[test]\n    fn test_plain_text_escaped_when_enabled() {\n        let options = ConversionOptions {\n            escape_asterisks: true,\n            escape_underscores: true,\n            ..ConversionOptions::default()\n        };\n        let result = convert(\"Text *asterisks* _underscores_\", Some(options)).unwrap();\n        let content = result.content.unwrap_or_default();\n        assert!(content.contains(r\"\\*asterisks\\*\"));\n        assert!(content.contains(r\"\\_underscores\\_\"));\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/metadata/collector.rs",
    "content": "//! Metadata collector for single-pass extraction.\n\nuse super::config::MetadataConfig;\nuse super::extraction::{extract_document_metadata, extract_structured_data};\nuse super::types::{HtmlMetadata, ImageMetadata, ImageType, LinkMetadata};\nuse std::collections::BTreeMap;\n\n/// Internal metadata collector for single-pass extraction.\n///\n/// Follows a pattern for efficient metadata extraction during tree traversal.\n/// Maintains state for:\n/// - Document metadata from head elements\n/// - Header hierarchy tracking\n/// - Link accumulation\n/// - Structured data collection\n/// - Language and directionality attributes\n///\n/// # Architecture\n///\n/// The collector is designed to be:\n/// - **Performant**: Pre-allocated collections, minimal cloning\n/// - **Single-pass**: Collects during main tree walk without separate passes\n/// - **Optional**: Zero overhead when disabled via feature flags\n/// - **Type-safe**: Strict separation of collection and result types\n#[derive(Debug)]\n#[allow(dead_code)]\npub struct MetadataCollector {\n    pub(super) head_metadata: BTreeMap<String, String>,\n    pub(super) headers: Vec<super::types::HeaderMetadata>,\n    pub(super) header_stack: Vec<usize>,\n    pub(super) links: Vec<LinkMetadata>,\n    pub(super) images: Vec<ImageMetadata>,\n    pub(super) json_ld: Vec<String>,\n    pub(super) structured_data_size: usize,\n    pub(super) config: MetadataConfig,\n    pub(super) lang: Option<String>,\n    pub(super) dir: Option<String>,\n}\n\n#[allow(dead_code)]\nimpl MetadataCollector {\n    /// Create a new metadata collector with configuration.\n    ///\n    /// Pre-allocates collections based on typical document sizes\n    /// for efficient append operations during traversal.\n    ///\n    /// # Arguments\n    ///\n    /// * `config` - Extraction configuration specifying which types to collect\n    ///\n    /// # Returns\n    ///\n    /// A new collector ready for use during tree traversal.\n    pub(crate) fn new(config: MetadataConfig) -> Self {\n        Self {\n            head_metadata: BTreeMap::new(),\n            headers: Vec::with_capacity(32),\n            header_stack: Vec::with_capacity(6),\n            links: Vec::with_capacity(64),\n            images: Vec::with_capacity(16),\n            json_ld: Vec::with_capacity(4),\n            structured_data_size: 0,\n            config,\n            lang: None,\n            dir: None,\n        }\n    }\n\n    /// Add a header element to the collection.\n    ///\n    /// Validates that level is in range 1-6 and tracks hierarchy via depth.\n    ///\n    /// # Arguments\n    ///\n    /// * `level` - Header level (1-6)\n    /// * `text` - Normalized header text content\n    /// * `id` - Optional HTML id attribute\n    /// * `depth` - Current document nesting depth\n    /// * `html_offset` - Byte offset in original HTML\n    pub(crate) fn add_header(&mut self, level: u8, text: String, id: Option<String>, depth: usize, html_offset: usize) {\n        if !self.config.extract_headers {\n            return;\n        }\n\n        if !(1..=6).contains(&level) {\n            return;\n        }\n\n        let header = super::types::HeaderMetadata {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        };\n\n        self.headers.push(header);\n    }\n\n    /// Add a link element to the collection.\n    ///\n    /// Classifies the link based on href value and stores with metadata.\n    pub(crate) fn add_link(\n        &mut self,\n        href: String,\n        text: String,\n        title: Option<String>,\n        rel: Option<String>,\n        attributes: BTreeMap<String, String>,\n    ) {\n        if !self.config.extract_links {\n            return;\n        }\n\n        let link_type = super::types::LinkMetadata::classify_link(&href);\n\n        let rel_vec = rel\n            .map(|r| {\n                r.split_whitespace()\n                    .map(std::string::ToString::to_string)\n                    .collect::<Vec<_>>()\n            })\n            .unwrap_or_default();\n\n        let link = LinkMetadata {\n            href,\n            text,\n            title,\n            link_type,\n            rel: rel_vec,\n            attributes,\n        };\n\n        self.links.push(link);\n    }\n\n    /// Add an image element to the collection.\n    ///\n    /// # Arguments\n    ///\n    /// * `src` - Image source (URL or data URI)\n    /// * `alt` - Optional alt text\n    /// * `title` - Optional title attribute\n    /// * `dimensions` - Optional (width, height) tuple\n    pub(crate) fn add_image(\n        &mut self,\n        src: String,\n        alt: Option<String>,\n        title: Option<String>,\n        dimensions: Option<(u32, u32)>,\n        attributes: BTreeMap<String, String>,\n    ) {\n        if !self.config.extract_images {\n            return;\n        }\n\n        let image_type = if src.starts_with(\"data:\") {\n            ImageType::DataUri\n        } else if src.starts_with(\"http://\") || src.starts_with(\"https://\") {\n            ImageType::External\n        } else if src.starts_with('<') && src.contains(\"svg\") {\n            ImageType::InlineSvg\n        } else {\n            ImageType::Relative\n        };\n\n        let image = ImageMetadata {\n            src,\n            alt,\n            title,\n            dimensions,\n            image_type,\n            attributes,\n        };\n\n        self.images.push(image);\n    }\n\n    /// Add a JSON-LD structured data block.\n    ///\n    /// Accumulates JSON content with size validation against configured limits.\n    pub(crate) fn add_json_ld(&mut self, json_content: String) {\n        if !self.config.extract_structured_data {\n            return;\n        }\n\n        let content_size = json_content.len();\n        if content_size > self.config.max_structured_data_size {\n            return;\n        }\n        if self.structured_data_size + content_size > self.config.max_structured_data_size {\n            return;\n        }\n\n        self.structured_data_size += content_size;\n        self.json_ld.push(json_content);\n    }\n\n    /// Set document head metadata from extracted head section.\n    ///\n    /// Merges metadata pairs from head elements (meta, title, link, etc.)\n    /// into the collector's head metadata store.\n    pub(crate) fn set_head_metadata(&mut self, metadata: BTreeMap<String, String>) {\n        if !self.config.extract_document {\n            return;\n        }\n        self.head_metadata.extend(metadata);\n    }\n\n    /// Set document language attribute.\n    ///\n    /// Usually from `lang` attribute on `<html>` or `<body>` tag.\n    /// Only sets if not already set (first occurrence wins).\n    pub(crate) fn set_language(&mut self, lang: String) {\n        if !self.config.extract_document {\n            return;\n        }\n        if self.lang.is_none() {\n            self.lang = Some(lang);\n        }\n    }\n\n    /// Set document text direction attribute.\n    ///\n    /// Usually from `dir` attribute on `<html>` or `<body>` tag.\n    /// Only sets if not already set (first occurrence wins).\n    pub(crate) fn set_text_direction(&mut self, dir: String) {\n        if !self.config.extract_document {\n            return;\n        }\n        if self.dir.is_none() {\n            self.dir = Some(dir);\n        }\n    }\n\n    pub(crate) const fn wants_document(&self) -> bool {\n        self.config.extract_document\n    }\n\n    pub(crate) const fn wants_headers(&self) -> bool {\n        self.config.extract_headers\n    }\n\n    pub(crate) const fn wants_links(&self) -> bool {\n        self.config.extract_links\n    }\n\n    pub(crate) const fn wants_images(&self) -> bool {\n        self.config.extract_images\n    }\n\n    pub(crate) const fn wants_structured_data(&self) -> bool {\n        self.config.extract_structured_data\n    }\n\n    /// Finish collection and return all extracted metadata.\n    ///\n    /// Performs final processing, validation, and consolidation of all\n    /// collected data into the [`HtmlMetadata`] output structure.\n    #[allow(dead_code)]\n    pub(crate) fn finish(self) -> HtmlMetadata {\n        let structured_data = extract_structured_data(self.json_ld);\n        let document = extract_document_metadata(self.head_metadata, self.lang, self.dir);\n\n        HtmlMetadata {\n            document,\n            headers: self.headers,\n            links: self.links,\n            images: self.images,\n            structured_data,\n        }\n    }\n\n    /// Categorize links by type for analysis and filtering.\n    ///\n    /// Separates collected links into groups by [`LinkType`](super::types::LinkType).\n    #[allow(dead_code)]\n    pub(crate) fn categorize_links(&self) -> BTreeMap<String, Vec<&LinkMetadata>> {\n        let mut categorized: BTreeMap<String, Vec<&LinkMetadata>> = BTreeMap::new();\n\n        for link in &self.links {\n            let category = link.link_type.to_string();\n            categorized.entry(category).or_default().push(link);\n        }\n\n        categorized\n    }\n\n    /// Count headers by level for structural analysis.\n    ///\n    /// Returns count of headers at each level (1-6).\n    #[allow(dead_code)]\n    pub(crate) fn header_counts(&self) -> BTreeMap<String, usize> {\n        let mut counts: BTreeMap<String, usize> = BTreeMap::new();\n\n        for header in &self.headers {\n            *counts.entry(header.level.to_string()).or_insert(0) += 1;\n        }\n\n        counts\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_metadata_collector_new() {\n        let config = MetadataConfig::default();\n        let collector = MetadataCollector::new(config);\n\n        assert_eq!(collector.headers.capacity(), 32);\n        assert_eq!(collector.links.capacity(), 64);\n        assert_eq!(collector.images.capacity(), 16);\n        assert_eq!(collector.json_ld.capacity(), 4);\n    }\n\n    #[test]\n    fn test_metadata_collector_add_header() {\n        let config = MetadataConfig::default();\n        let mut collector = MetadataCollector::new(config);\n\n        collector.add_header(1, \"Title\".to_string(), Some(\"title\".to_string()), 0, 100);\n        assert_eq!(collector.headers.len(), 1);\n\n        let header = &collector.headers[0];\n        assert_eq!(header.level, 1);\n        assert_eq!(header.text, \"Title\");\n        assert_eq!(header.id, Some(\"title\".to_string()));\n\n        collector.add_header(7, \"Invalid\".to_string(), None, 0, 200);\n        assert_eq!(collector.headers.len(), 1);\n    }\n\n    #[test]\n    fn test_metadata_collector_add_link() {\n        let config = MetadataConfig::default();\n        let mut collector = MetadataCollector::new(config);\n\n        collector.add_link(\n            \"https://example.com\".to_string(),\n            \"Example\".to_string(),\n            Some(\"Visit\".to_string()),\n            Some(\"nofollow external\".to_string()),\n            BTreeMap::from([(\"data-id\".to_string(), \"example\".to_string())]),\n        );\n\n        assert_eq!(collector.links.len(), 1);\n\n        let link = &collector.links[0];\n        assert_eq!(link.href, \"https://example.com\");\n        assert_eq!(link.text, \"Example\");\n    }\n\n    #[test]\n    fn test_metadata_collector_respects_config() {\n        let config = MetadataConfig {\n            extract_document: false,\n            extract_headers: false,\n            extract_links: false,\n            extract_images: false,\n            extract_structured_data: false,\n            max_structured_data_size: 1_000_000,\n        };\n        let mut collector = MetadataCollector::new(config);\n\n        collector.add_header(1, \"Title\".to_string(), None, 0, 100);\n        collector.add_link(\n            \"https://example.com\".to_string(),\n            \"Link\".to_string(),\n            None,\n            None,\n            BTreeMap::new(),\n        );\n        collector.add_image(\n            \"https://example.com/img.jpg\".to_string(),\n            None,\n            None,\n            None,\n            BTreeMap::new(),\n        );\n        collector.add_json_ld(\"{}\".to_string());\n\n        assert!(collector.headers.is_empty());\n        assert!(collector.links.is_empty());\n        assert!(collector.images.is_empty());\n        assert!(collector.json_ld.is_empty());\n    }\n\n    #[test]\n    fn test_metadata_collector_finish() {\n        let config = MetadataConfig::default();\n        let mut collector = MetadataCollector::new(config);\n\n        collector.set_language(\"en\".to_string());\n        collector.add_header(1, \"Main Title\".to_string(), None, 0, 100);\n        collector.add_link(\n            \"https://example.com\".to_string(),\n            \"Example\".to_string(),\n            None,\n            None,\n            BTreeMap::new(),\n        );\n\n        let metadata = collector.finish();\n\n        assert_eq!(metadata.document.language, Some(\"en\".to_string()));\n        assert_eq!(metadata.headers.len(), 1);\n        assert_eq!(metadata.links.len(), 1);\n    }\n\n    #[test]\n    fn test_categorize_links() {\n        let config = MetadataConfig::default();\n        let mut collector = MetadataCollector::new(config);\n\n        collector.add_link(\"#anchor\".to_string(), \"Anchor\".to_string(), None, None, BTreeMap::new());\n        collector.add_link(\n            \"https://example.com\".to_string(),\n            \"External\".to_string(),\n            None,\n            None,\n            BTreeMap::new(),\n        );\n        collector.add_link(\n            \"mailto:test@example.com\".to_string(),\n            \"Email\".to_string(),\n            None,\n            None,\n            BTreeMap::new(),\n        );\n\n        let categorized = collector.categorize_links();\n\n        assert_eq!(categorized.get(\"anchor\").map(std::vec::Vec::len), Some(1));\n        assert_eq!(categorized.get(\"external\").map(std::vec::Vec::len), Some(1));\n        assert_eq!(categorized.get(\"email\").map(std::vec::Vec::len), Some(1));\n    }\n\n    #[test]\n    fn test_header_counts() {\n        let config = MetadataConfig::default();\n        let mut collector = MetadataCollector::new(config);\n\n        collector.add_header(1, \"H1\".to_string(), None, 0, 100);\n        collector.add_header(2, \"H2\".to_string(), None, 1, 200);\n        collector.add_header(2, \"H2b\".to_string(), None, 1, 300);\n        collector.add_header(3, \"H3\".to_string(), None, 2, 400);\n\n        let counts = collector.header_counts();\n\n        assert_eq!(counts.get(\"1\").copied(), Some(1));\n        assert_eq!(counts.get(\"2\").copied(), Some(2));\n        assert_eq!(counts.get(\"3\").copied(), Some(1));\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/metadata/config.rs",
    "content": "//! Metadata extraction configuration.\n\n/// Default maximum size for structured data extraction (1 MB)\npub const DEFAULT_MAX_STRUCTURED_DATA_SIZE: usize = 1_000_000;\n\n/// Configuration for metadata extraction granularity.\n///\n/// Controls which metadata types are extracted and size limits for safety.\n/// Enables selective extraction of different metadata categories from HTML documents,\n/// allowing fine-grained control over which types of information to collect during\n/// the HTML-to-Markdown conversion process.\n///\n/// # Fields\n///\n/// - `extract_document`: Enable document-level metadata extraction (title, description, author, Open Graph, Twitter Card, etc.)\n/// - `extract_headers`: Enable heading element extraction (h1-h6) with hierarchy tracking\n/// - `extract_links`: Enable anchor element extraction with link type classification\n/// - `extract_images`: Enable image element extraction with source and dimension metadata\n/// - `extract_structured_data`: Enable structured data extraction (JSON-LD, Microdata, RDFa)\n/// - `max_structured_data_size`: Safety limit on total structured data size in bytes\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::MetadataConfig;\n/// let config = MetadataConfig {\n///     extract_document: true,\n///     extract_headers: true,\n///     extract_links: true,\n///     extract_images: true,\n///     extract_structured_data: true,\n///     max_structured_data_size: 1_000_000,\n/// };\n///\n/// assert!(config.extract_headers);\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct MetadataConfig {\n    /// Extract document-level metadata (title, description, author, etc.).\n    ///\n    /// When enabled, collects metadata from `<head>` section including:\n    /// - `<title>` element content\n    /// - `<meta name=\"description\">` and other standard meta tags\n    /// - Open Graph (og:*) properties for social media optimization\n    /// - Twitter Card (twitter:*) properties\n    /// - Language and text direction attributes\n    /// - Canonical URL and base href references\n    pub extract_document: bool,\n\n    /// Extract h1-h6 header elements and their hierarchy.\n    ///\n    /// When enabled, collects all heading elements with:\n    /// - Header level (1-6)\n    /// - Text content (normalized)\n    /// - HTML id attribute if present\n    /// - Document tree depth for hierarchy tracking\n    /// - Byte offset in original HTML for positioning\n    pub extract_headers: bool,\n\n    /// Extract anchor (a) elements as links with type classification.\n    ///\n    /// When enabled, collects all hyperlinks with:\n    /// - href attribute value\n    /// - Link text content\n    /// - Title attribute (tooltip text)\n    /// - Automatic link type classification (anchor, internal, external, email, phone, other)\n    /// - Rel attribute values\n    /// - Additional custom attributes\n    pub extract_links: bool,\n\n    /// Extract image elements and data URIs.\n    ///\n    /// When enabled, collects all image elements with:\n    /// - Source URL or data URI\n    /// - Alt text for accessibility\n    /// - Title attribute\n    /// - Dimensions (width, height) if available\n    /// - Automatic image type classification (data URI, external, relative, inline SVG)\n    /// - Additional custom attributes\n    pub extract_images: bool,\n\n    /// Extract structured data (JSON-LD, Microdata, RDFa).\n    ///\n    /// When enabled, collects machine-readable structured data including:\n    /// - JSON-LD script blocks with schema detection\n    /// - Microdata attributes (itemscope, itemtype, itemprop)\n    /// - RDFa markup\n    /// - Extracted schema type if detectable\n    pub extract_structured_data: bool,\n\n    /// Maximum total size of structured data to collect (bytes).\n    ///\n    /// Prevents memory exhaustion attacks on malformed or adversarial documents\n    /// containing excessively large structured data blocks. When the accumulated\n    /// size of structured data exceeds this limit, further collection stops.\n    /// Default: `1_000_000` bytes (1 MB)\n    pub max_structured_data_size: usize,\n}\n\n/// Partial update for `MetadataConfig`.\n///\n/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n/// Only specified fields (Some values) will override existing config; None values leave the\n/// corresponding fields unchanged when applied via [`MetadataConfig::apply_update`].\n///\n/// # Fields\n///\n/// - `extract_document`: Optional override for document-level metadata extraction\n/// - `extract_headers`: Optional override for heading element extraction\n/// - `extract_links`: Optional override for link element extraction\n/// - `extract_images`: Optional override for image element extraction\n/// - `extract_structured_data`: Optional override for structured data extraction\n/// - `max_structured_data_size`: Optional override for structured data size limit\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{MetadataConfig, MetadataConfigUpdate};\n/// let update = MetadataConfigUpdate {\n///     extract_document: Some(false),\n///     extract_headers: Some(true),\n///     extract_links: None,  // No change\n///     extract_images: None,  // No change\n///     extract_structured_data: None,  // No change\n///     max_structured_data_size: None,  // No change\n/// };\n///\n/// let mut config = MetadataConfig::default();\n/// config.apply_update(update);\n/// assert!(!config.extract_document);\n/// assert!(config.extract_headers);\n/// ```\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), derive(serde::Deserialize))]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(deny_unknown_fields))]\npub struct MetadataConfigUpdate {\n    /// Optional override for extracting document-level metadata.\n    ///\n    /// When Some(true), enables document metadata extraction; Some(false) disables it.\n    /// None leaves the current setting unchanged.\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(alias = \"extract_document\"))]\n    pub extract_document: Option<bool>,\n\n    /// Optional override for extracting heading elements (h1-h6).\n    ///\n    /// When Some(true), enables header extraction; Some(false) disables it.\n    /// None leaves the current setting unchanged.\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(alias = \"extract_headers\"))]\n    pub extract_headers: Option<bool>,\n\n    /// Optional override for extracting anchor (link) elements.\n    ///\n    /// When Some(true), enables link extraction; Some(false) disables it.\n    /// None leaves the current setting unchanged.\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(alias = \"extract_links\"))]\n    pub extract_links: Option<bool>,\n\n    /// Optional override for extracting image elements.\n    ///\n    /// When Some(true), enables image extraction; Some(false) disables it.\n    /// None leaves the current setting unchanged.\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(alias = \"extract_images\"))]\n    pub extract_images: Option<bool>,\n\n    /// Optional override for extracting structured data (JSON-LD, Microdata, RDFa).\n    ///\n    /// When Some(true), enables structured data extraction; Some(false) disables it.\n    /// None leaves the current setting unchanged.\n    #[cfg_attr(\n        any(feature = \"serde\", feature = \"metadata\"),\n        serde(alias = \"extract_structured_data\")\n    )]\n    pub extract_structured_data: Option<bool>,\n\n    /// Optional override for maximum structured data collection size in bytes.\n    ///\n    /// When Some(size), sets the new size limit. None leaves the current limit unchanged.\n    /// Use this to adjust safety thresholds for different documents.\n    #[cfg_attr(\n        any(feature = \"serde\", feature = \"metadata\"),\n        serde(alias = \"max_structured_data_size\")\n    )]\n    pub max_structured_data_size: Option<usize>,\n}\n\nimpl Default for MetadataConfig {\n    /// Create default metadata configuration.\n    ///\n    /// Defaults to extracting all metadata types with 1MB limit on structured data.\n    fn default() -> Self {\n        Self {\n            extract_document: true,\n            extract_headers: true,\n            extract_links: true,\n            extract_images: true,\n            extract_structured_data: true,\n            max_structured_data_size: DEFAULT_MAX_STRUCTURED_DATA_SIZE,\n        }\n    }\n}\n\nimpl MetadataConfig {\n    /// Check if any metadata extraction is enabled.\n    ///\n    /// Returns `true` if at least one extraction category is enabled, `false` if all are disabled.\n    /// This is useful for early exit optimization when the application doesn't need metadata.\n    ///\n    /// # Returns\n    ///\n    /// `true` if any of the extraction flags are enabled, `false` if all are disabled.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::MetadataConfig;\n    /// // All enabled\n    /// let config = MetadataConfig::default();\n    /// assert!(config.any_enabled());\n    ///\n    /// // Selectively enabled\n    /// let config = MetadataConfig {\n    ///     extract_headers: true,\n    ///     extract_document: false,\n    ///     extract_links: false,\n    ///     extract_images: false,\n    ///     extract_structured_data: false,\n    ///     max_structured_data_size: 1_000_000,\n    /// };\n    /// assert!(config.any_enabled());\n    ///\n    /// // All disabled\n    /// let config = MetadataConfig {\n    ///     extract_document: false,\n    ///     extract_headers: false,\n    ///     extract_links: false,\n    ///     extract_images: false,\n    ///     extract_structured_data: false,\n    ///     max_structured_data_size: 1_000_000,\n    /// };\n    /// assert!(!config.any_enabled());\n    /// ```\n    #[must_use]\n    pub const fn any_enabled(&self) -> bool {\n        self.extract_document\n            || self.extract_headers\n            || self.extract_links\n            || self.extract_images\n            || self.extract_structured_data\n    }\n\n    /// Apply a partial update to this metadata configuration.\n    ///\n    /// Any specified fields in the update (Some values) will override the current values.\n    /// Unspecified fields (None) are left unchanged. This allows selective modification\n    /// of configuration without affecting unrelated settings.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial metadata config update with fields to override\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::{MetadataConfig, MetadataConfigUpdate};\n    /// let mut config = MetadataConfig::default();\n    /// // config starts with all extraction enabled\n    ///\n    /// let update = MetadataConfigUpdate {\n    ///     extract_document: Some(false),\n    ///     extract_images: Some(false),\n    ///     // All other fields are None, so they won't change\n    ///     ..Default::default()\n    /// };\n    ///\n    /// config.apply_update(update);\n    ///\n    /// assert!(!config.extract_document);\n    /// assert!(!config.extract_images);\n    /// assert!(config.extract_headers);  // Unchanged\n    /// assert!(config.extract_links);    // Unchanged\n    /// ```\n    pub const fn apply_update(&mut self, update: MetadataConfigUpdate) {\n        if let Some(extract_document) = update.extract_document {\n            self.extract_document = extract_document;\n        }\n        if let Some(extract_headers) = update.extract_headers {\n            self.extract_headers = extract_headers;\n        }\n        if let Some(extract_links) = update.extract_links {\n            self.extract_links = extract_links;\n        }\n        if let Some(extract_images) = update.extract_images {\n            self.extract_images = extract_images;\n        }\n        if let Some(extract_structured_data) = update.extract_structured_data {\n            self.extract_structured_data = extract_structured_data;\n        }\n        if let Some(max_structured_data_size) = update.max_structured_data_size {\n            self.max_structured_data_size = max_structured_data_size;\n        }\n    }\n\n    /// Create new metadata configuration from a partial update.\n    ///\n    /// Creates a new `MetadataConfig` struct with defaults, then applies the update.\n    /// Fields not specified in the update (None) keep their default values.\n    /// This is a convenience method for constructing a configuration from a partial specification\n    /// without needing to explicitly call `.default()` first.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial metadata config update with fields to set\n    ///\n    /// # Returns\n    ///\n    /// New `MetadataConfig` with specified updates applied to defaults\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::{MetadataConfig, MetadataConfigUpdate};\n    /// let update = MetadataConfigUpdate {\n    ///     extract_document: Some(false),\n    ///     extract_headers: Some(true),\n    ///     extract_links: Some(true),\n    ///     extract_images: None,  // Will use default (true)\n    ///     extract_structured_data: None,  // Will use default (true)\n    ///     max_structured_data_size: None,  // Will use default (1MB)\n    /// };\n    ///\n    /// let config = MetadataConfig::from_update(update);\n    ///\n    /// assert!(!config.extract_document);\n    /// assert!(config.extract_headers);\n    /// assert!(config.extract_links);\n    /// assert!(config.extract_images);  // Default\n    /// assert!(config.extract_structured_data);  // Default\n    /// ```\n    #[must_use]\n    pub fn from_update(update: MetadataConfigUpdate) -> Self {\n        let mut config = Self::default();\n        config.apply_update(update);\n        config\n    }\n}\n\nimpl From<MetadataConfigUpdate> for MetadataConfig {\n    fn from(update: MetadataConfigUpdate) -> Self {\n        Self::from_update(update)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_metadata_config_default() {\n        let config = MetadataConfig::default();\n\n        assert!(config.extract_headers);\n        assert!(config.extract_links);\n        assert!(config.extract_images);\n        assert!(config.extract_structured_data);\n        assert_eq!(config.max_structured_data_size, DEFAULT_MAX_STRUCTURED_DATA_SIZE);\n    }\n\n    #[test]\n    fn test_metadata_config_any_enabled() {\n        let all_enabled = MetadataConfig::default();\n        assert!(all_enabled.any_enabled());\n\n        let some_enabled = MetadataConfig {\n            extract_headers: true,\n            extract_document: false,\n            extract_links: false,\n            extract_images: false,\n            extract_structured_data: false,\n            max_structured_data_size: 1_000_000,\n        };\n        assert!(some_enabled.any_enabled());\n\n        let none_enabled = MetadataConfig {\n            extract_document: false,\n            extract_headers: false,\n            extract_links: false,\n            extract_images: false,\n            extract_structured_data: false,\n            max_structured_data_size: 1_000_000,\n        };\n        assert!(!none_enabled.any_enabled());\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/metadata/extraction.rs",
    "content": "//! Metadata extraction utilities and helpers.\n\nuse super::types::{DocumentMetadata, StructuredData, StructuredDataType, TextDirection};\nuse std::collections::BTreeMap;\n\n/// Extract document metadata from collected head metadata.\n///\n/// Parses head metadata into structured document metadata,\n/// handling special cases like Open Graph, Twitter Card, keywords, etc.\npub(crate) fn extract_document_metadata(\n    head_metadata: BTreeMap<String, String>,\n    lang: Option<String>,\n    dir: Option<String>,\n) -> DocumentMetadata {\n    let mut doc = DocumentMetadata::default();\n\n    for (raw_key, value) in head_metadata {\n        let mut key = raw_key.as_str();\n        let mut replaced_key: Option<String> = None;\n\n        if let Some(stripped) = key.strip_prefix(\"meta-\") {\n            key = stripped;\n        }\n\n        if key.as_bytes().contains(&b':') {\n            replaced_key = Some(key.replace(':', \"-\"));\n            key = replaced_key.as_deref().unwrap_or(key);\n        }\n\n        // Normalize to lowercase for case-insensitive matching (per HTML spec,\n        // meta name attributes are compared ASCII case-insensitively).\n        let lower_key = key.to_ascii_lowercase();\n\n        match lower_key.as_str() {\n            \"title\" => doc.title = Some(value),\n            \"description\" => doc.description = Some(value),\n            \"author\" | \"creator\" | \"publisher\" => {\n                if doc.author.is_none() {\n                    doc.author = Some(value);\n                }\n            }\n            \"canonical\" => doc.canonical_url = Some(value),\n            \"base\" | \"base-href\" => doc.base_href = Some(value),\n            k if k.starts_with(\"og-\") => {\n                let og_key = k.trim_start_matches(\"og-\").replace('-', \"_\");\n                doc.open_graph.insert(og_key, value);\n            }\n            k if k.starts_with(\"twitter-\") => {\n                let tw_key = k.trim_start_matches(\"twitter-\").replace('-', \"_\");\n                doc.twitter_card.insert(tw_key, value);\n            }\n            // Dublin Core: DC.* and DCTERMS.* prefixes (dot becomes part of key after meta- strip).\n            // Map DC/DCTERMS fields to dedicated struct fields where applicable.\n            k if k.starts_with(\"dc.\") || k.starts_with(\"dc-\") => {\n                let dc_field = k.trim_start_matches(\"dc.\").trim_start_matches(\"dc-\");\n                match dc_field {\n                    \"title\" => {\n                        if doc.title.is_none() {\n                            doc.title = Some(value);\n                        }\n                    }\n                    \"description\" => {\n                        if doc.description.is_none() {\n                            doc.description = Some(value);\n                        }\n                    }\n                    \"creator\" | \"contributor\" | \"publisher\" => {\n                        if doc.author.is_none() {\n                            doc.author = Some(value);\n                        }\n                    }\n                    \"subject\" | \"keywords\" => {\n                        if doc.keywords.is_empty() {\n                            doc.keywords = split_keywords(&value);\n                        }\n                    }\n                    _ => {\n                        let meta_key = format!(\"dc_{}\", dc_field.replace('-', \"_\"));\n                        doc.meta_tags.insert(meta_key, value);\n                    }\n                }\n            }\n            k if k.starts_with(\"dcterms.\") || k.starts_with(\"dcterms-\") => {\n                let dc_field = k.trim_start_matches(\"dcterms.\").trim_start_matches(\"dcterms-\");\n                match dc_field {\n                    \"title\" | \"alternative\" => {\n                        if doc.title.is_none() {\n                            doc.title = Some(value);\n                        }\n                    }\n                    \"description\" | \"abstract\" => {\n                        if doc.description.is_none() {\n                            doc.description = Some(value);\n                        }\n                    }\n                    \"creator\" | \"contributor\" | \"publisher\" => {\n                        if doc.author.is_none() {\n                            doc.author = Some(value);\n                        }\n                    }\n                    \"subject\" | \"keywords\" => {\n                        if doc.keywords.is_empty() {\n                            doc.keywords = split_keywords(&value);\n                        }\n                    }\n                    _ => {\n                        let meta_key = format!(\"dcterms_{}\", dc_field.replace('-', \"_\"));\n                        doc.meta_tags.insert(meta_key, value);\n                    }\n                }\n            }\n            // All keyword-bearing meta tag variants\n            \"keywords\" | \"news_keywords\" | \"citation_keywords\" | \"subject\" | \"topic\" | \"category\"\n            | \"classification\" => {\n                if doc.keywords.is_empty() {\n                    doc.keywords = split_keywords(&value);\n                }\n            }\n            _ => {\n                let meta_key = if key.as_ptr() == raw_key.as_ptr() && key.len() == raw_key.len() {\n                    raw_key\n                } else if let Some(replaced) = replaced_key {\n                    replaced\n                } else {\n                    key.to_string()\n                };\n                doc.meta_tags.insert(meta_key, value);\n            }\n        }\n    }\n\n    if let Some(lang) = lang {\n        doc.language = Some(lang);\n    }\n\n    if let Some(dir) = dir {\n        if let Some(parsed_dir) = TextDirection::parse(&dir) {\n            doc.text_direction = Some(parsed_dir);\n        }\n    }\n\n    doc\n}\n\n/// Split a comma-separated keywords string into a `Vec<String>`.\nfn split_keywords(value: &str) -> Vec<String> {\n    value\n        .split(',')\n        .map(|s| s.trim().to_string())\n        .filter(|s| !s.is_empty())\n        .collect()\n}\n\n/// Extract structured data blocks into `StructuredData` items.\npub(crate) fn extract_structured_data(json_ld: Vec<String>) -> Vec<StructuredData> {\n    let mut result = Vec::with_capacity(json_ld.len());\n\n    for json_str in json_ld {\n        let schema_type = scan_schema_type(&json_str)\n            .or_else(|| {\n                if json_str.contains(\"\\\"@type\\\"\") {\n                    serde_json::from_str::<serde_json::Value>(&json_str).ok().and_then(|v| {\n                        v.get(\"@type\")\n                            .and_then(|t| t.as_str().map(std::string::ToString::to_string))\n                    })\n                } else {\n                    None\n                }\n            })\n            .or_else(|| {\n                if !json_str.contains(\"\\\"@graph\\\"\") {\n                    return None;\n                }\n\n                let value = serde_json::from_str::<serde_json::Value>(&json_str).ok()?;\n                let graph = value.get(\"@graph\")?;\n                let items = graph.as_array()?;\n                items.iter().find_map(|item| {\n                    item.get(\"@type\")\n                        .and_then(|t| t.as_str().map(std::string::ToString::to_string))\n                })\n            });\n\n        result.push(StructuredData {\n            data_type: StructuredDataType::JsonLd,\n            raw_json: json_str,\n            schema_type,\n        });\n    }\n\n    result\n}\n\n/// Scan for @type in JSON string without full parsing.\nfn scan_schema_type(json_str: &str) -> Option<String> {\n    let needle = \"\\\"@type\\\"\";\n    let start = json_str.find(needle)? + needle.len();\n    let bytes = json_str.as_bytes();\n    let mut i = start;\n\n    while i < bytes.len() && bytes[i].is_ascii_whitespace() {\n        i += 1;\n    }\n    if i >= bytes.len() || bytes[i] != b':' {\n        return None;\n    }\n    i += 1;\n    while i < bytes.len() && bytes[i].is_ascii_whitespace() {\n        i += 1;\n    }\n    if i >= bytes.len() {\n        return None;\n    }\n\n    if bytes[i] == b'[' {\n        i += 1;\n        while i < bytes.len() && bytes[i].is_ascii_whitespace() {\n            i += 1;\n        }\n        if i >= bytes.len() || bytes[i] != b'\"' {\n            return None;\n        }\n    } else if bytes[i] != b'\"' {\n        return None;\n    }\n\n    let start_quote = i;\n    i += 1;\n    let mut escaped = false;\n    while i < bytes.len() {\n        let byte = bytes[i];\n        if escaped {\n            escaped = false;\n            i += 1;\n            continue;\n        }\n        if byte == b'\\\\' {\n            escaped = true;\n            i += 1;\n            continue;\n        }\n        if byte == b'\"' {\n            let end_quote = i;\n            let slice = &json_str[start_quote..=end_quote];\n            return serde_json::from_str::<String>(slice).ok();\n        }\n        i += 1;\n    }\n\n    None\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_extract_document_metadata() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"title\".to_string(), \"Test Title\".to_string());\n        head_metadata.insert(\"description\".to_string(), \"Test Description\".to_string());\n        head_metadata.insert(\"keywords\".to_string(), \"rust, testing\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, Some(\"en\".to_string()), Some(\"ltr\".to_string()));\n\n        assert_eq!(doc.title, Some(\"Test Title\".to_string()));\n        assert_eq!(doc.description, Some(\"Test Description\".to_string()));\n        assert_eq!(doc.keywords, vec![\"rust\", \"testing\"]);\n        assert_eq!(doc.language, Some(\"en\".to_string()));\n    }\n\n    #[test]\n    fn test_extract_document_metadata_with_og() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"og-title\".to_string(), \"OG Title\".to_string());\n        head_metadata.insert(\"og-description\".to_string(), \"OG Description\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n\n        assert_eq!(doc.open_graph.get(\"title\"), Some(&\"OG Title\".to_string()));\n        assert_eq!(doc.open_graph.get(\"description\"), Some(&\"OG Description\".to_string()));\n    }\n\n    #[test]\n    fn test_keywords_case_insensitive() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-Keywords\".to_string(), \"Rust, HTML, Markdown\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"Rust\", \"HTML\", \"Markdown\"]);\n    }\n\n    #[test]\n    fn test_keywords_dc_subject() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-DC.subject\".to_string(), \"weather, forecast\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"weather\", \"forecast\"]);\n    }\n\n    #[test]\n    fn test_keywords_dc_keywords() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-DC.keywords\".to_string(), \"climate, data\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"climate\", \"data\"]);\n    }\n\n    #[test]\n    fn test_keywords_dcterms_subject() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-DCTERMS.subject\".to_string(), \"science, research\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"science\", \"research\"]);\n    }\n\n    #[test]\n    fn test_keywords_news_keywords() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-news_keywords\".to_string(), \"breaking, world\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"breaking\", \"world\"]);\n    }\n\n    #[test]\n    fn test_keywords_citation_keywords() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-citation_keywords\".to_string(), \"biology, genetics\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.keywords, vec![\"biology\", \"genetics\"]);\n    }\n\n    #[test]\n    fn test_dc_title_and_description_fallback() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-DC.title\".to_string(), \"DC Title\".to_string());\n        head_metadata.insert(\"meta-DC.description\".to_string(), \"DC Description\".to_string());\n        head_metadata.insert(\"meta-DC.creator\".to_string(), \"DC Author\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.title, Some(\"DC Title\".to_string()));\n        assert_eq!(doc.description, Some(\"DC Description\".to_string()));\n        assert_eq!(doc.author, Some(\"DC Author\".to_string()));\n    }\n\n    #[test]\n    fn test_dc_does_not_override_standard_fields() {\n        let mut head_metadata = BTreeMap::new();\n        // Standard fields come first alphabetically in BTreeMap\n        head_metadata.insert(\"description\".to_string(), \"Standard Description\".to_string());\n        head_metadata.insert(\"meta-DC.description\".to_string(), \"DC Description\".to_string());\n        head_metadata.insert(\"title\".to_string(), \"Standard Title\".to_string());\n        head_metadata.insert(\"meta-DC.title\".to_string(), \"DC Title\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.title, Some(\"Standard Title\".to_string()));\n        assert_eq!(doc.description, Some(\"Standard Description\".to_string()));\n    }\n\n    #[test]\n    fn test_case_insensitive_title_description_author() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-Title\".to_string(), \"My Title\".to_string());\n        head_metadata.insert(\"meta-Description\".to_string(), \"My Desc\".to_string());\n        head_metadata.insert(\"meta-Author\".to_string(), \"My Author\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.title, Some(\"My Title\".to_string()));\n        assert_eq!(doc.description, Some(\"My Desc\".to_string()));\n        assert_eq!(doc.author, Some(\"My Author\".to_string()));\n    }\n\n    #[test]\n    fn test_dcterms_remaining_go_to_meta_tags() {\n        let mut head_metadata = BTreeMap::new();\n        head_metadata.insert(\"meta-DCTERMS.license\".to_string(), \"MIT\".to_string());\n\n        let doc = extract_document_metadata(head_metadata, None, None);\n        assert_eq!(doc.meta_tags.get(\"dcterms_license\"), Some(&\"MIT\".to_string()));\n    }\n\n    #[test]\n    fn test_scan_schema_type() {\n        let json = r#\"{\"@type\":\"Article\",\"title\":\"Test\"}\"#;\n        assert_eq!(scan_schema_type(json), Some(\"Article\".to_string()));\n\n        let json_array = r#\"{\"@type\":[\"Article\",\"NewsArticle\"]}\"#;\n        assert_eq!(scan_schema_type(json_array), Some(\"Article\".to_string()));\n\n        let no_type = r#\"{\"title\":\"Test\"}\"#;\n        assert_eq!(scan_schema_type(no_type), None);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/metadata/mod.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\n//! Metadata extraction for HTML to Markdown conversion.\n//!\n//! This module provides comprehensive, type-safe metadata extraction during HTML-to-Markdown\n//! conversion, enabling content analysis, SEO optimization, and document indexing workflows.\n//! Metadata includes:\n//! - **Document metadata**: Title, description, author, language, canonical URL, Open Graph, Twitter Card\n//! - **Headers**: Heading elements (h1-h6) with hierarchy, IDs, and positions\n//! - **Links**: Hyperlinks with type classification (anchor, internal, external, email, phone)\n//! - **Images**: Image elements with source, alt text, dimensions, and type (data URI, external, etc.)\n//! - **Structured data**: JSON-LD, Microdata, and RDFa blocks\n//!\n//! The implementation follows a single-pass collector pattern for zero-overhead extraction\n//! when metadata features are disabled.\n//!\n//! # Architecture\n//!\n//! Metadata extraction uses the [`MetadataCollector`] pattern:\n//! - **Single-pass collection**: Metadata is gathered during the primary tree traversal without additional passes\n//! - **Zero overhead when disabled**: Entire module can be compiled out via feature flags\n//! - **Configurable granularity**: Use [`MetadataConfig`] to select which metadata types to extract\n//! - **Type-safe APIs**: All metadata types are enum-based with exhaustive matching\n//! - **Memory-bounded**: Size limits prevent memory exhaustion from adversarial documents\n//! - **Pre-allocated buffers**: Typical documents (32 headers, 64 links, 16 images) handled efficiently\n//!\n//! # Type Overview\n//!\n//! ## Enumerations\n//!\n//! - [`TextDirection`]: Document directionality (LTR, RTL, Auto)\n//! - [`LinkType`]: Link classification (Anchor, Internal, External, Email, Phone, Other)\n//! - [`ImageType`]: Image source type (DataUri, External, Relative, InlineSvg)\n//! - [`StructuredDataType`]: Structured data format (JsonLd, Microdata, RDFa)\n//!\n//! ## Structures\n//!\n//! - [`DocumentMetadata`]: Head-level metadata with maps for Open Graph and Twitter Card\n//! - [`HeaderMetadata`]: Heading element with level (1-6), text, ID, hierarchy depth, and position\n//! - [`LinkMetadata`]: Hyperlink with href, text, title, type, rel attributes, and custom attributes\n//! - [`ImageMetadata`]: Image element with src, alt, title, dimensions, type, and attributes\n//! - [`StructuredData`]: Structured data block with type and raw JSON\n//! - [`MetadataConfig`]: Configuration controlling extraction granularity and size limits\n//! - [`HtmlMetadata`]: Top-level result containing all extracted metadata\n//!\n//! # Examples\n//!\n//! ## Basic Usage with `convert()`\n//!\n//! ```text\n//! use html_to_markdown_rs::convert;\n//!\n//! let html = r#\"\n//!   <html lang=\"en\">\n//!     <head>\n//!       <title>My Article</title>\n//!       <meta name=\"description\" content=\"An interesting read\">\n//!     </head>\n//!     <body>\n//!       <h1 id=\"main\">Title</h1>\n//!       <a href=\"https://example.com\">External Link</a>\n//!       <img src=\"photo.jpg\" alt=\"A photo\">\n//!     </body>\n//!   </html>\n//! \"#;\n//!\n//! let result = convert(html, None)?;\n//! let metadata = result.metadata.unwrap();\n//!\n//! // Access document metadata\n//! assert_eq!(metadata.document.title, Some(\"My Article\".to_string()));\n//! assert_eq!(metadata.document.language, Some(\"en\".to_string()));\n//!\n//! // Access headers\n//! assert_eq!(metadata.headers.len(), 1);\n//! assert_eq!(metadata.headers[0].level, 1);\n//! assert_eq!(metadata.headers[0].id, Some(\"main\".to_string()));\n//!\n//! // Access links\n//! assert_eq!(metadata.links.len(), 1);\n//! assert_eq!(metadata.links[0].link_type, LinkType::External);\n//!\n//! // Access images\n//! assert_eq!(metadata.images.len(), 1);\n//! assert_eq!(metadata.images[0].image_type, ImageType::Relative);\n//! # Ok::<(), html_to_markdown_rs::ConversionError>(())\n//! ```\n//!\n//! ## Selective Extraction\n//!\n//! ```text\n//! use html_to_markdown_rs::{convert, ConversionOptions};\n//!\n//! let options = ConversionOptions {\n//!     extract_metadata: false,  // Disable metadata extraction\n//!     ..Default::default()\n//! };\n//!\n//! let result = convert(html, Some(options))?;\n//! assert!(result.metadata.is_none());  // Metadata not extracted\n//! # Ok::<(), html_to_markdown_rs::ConversionError>(())\n//! ```\n//!\n//! ## Analyzing Link Types\n//!\n//! ```text\n//! use html_to_markdown_rs::convert;\n//! use html_to_markdown_rs::metadata::LinkType;\n//!\n//! let result = convert(html, None)?;\n//! let metadata = result.metadata.unwrap();\n//!\n//! for link in &metadata.links {\n//!     match link.link_type {\n//!         LinkType::External => println!(\"External: {}\", link.href),\n//!         LinkType::Internal => println!(\"Internal: {}\", link.href),\n//!         LinkType::Anchor => println!(\"Anchor: {}\", link.href),\n//!         LinkType::Email => println!(\"Email: {}\", link.href),\n//!         _ => {}\n//!     }\n//! }\n//! # Ok::<(), html_to_markdown_rs::ConversionError>(())\n//! ```\n//!\n//! # Serialization\n//!\n//! All types in this module support serialization via `serde` when the `metadata` feature is enabled.\n//! This enables easy export to JSON, YAML, or other formats:\n//!\n//! ```text\n//! use html_to_markdown_rs::convert;\n//!\n//! let result = convert(html, None)?;\n//! if let Some(metadata) = &result.metadata {\n//!     let json = serde_json::to_string_pretty(metadata)?;\n//!     println!(\"{}\", json);\n//! }\n//! # Ok::<(), Box<dyn std::error::Error>>(())\n//! ```\n\npub mod collector;\npub mod config;\npub mod extraction;\npub mod types;\n\n// Re-export public types\npub use collector::MetadataCollector;\npub use config::{DEFAULT_MAX_STRUCTURED_DATA_SIZE, MetadataConfig, MetadataConfigUpdate};\npub use types::{\n    DocumentMetadata, HeaderMetadata, HtmlMetadata, ImageMetadata, ImageType, LinkMetadata, LinkType, StructuredData,\n    StructuredDataType, TextDirection,\n};\n\n// Internal handle type for shared mutable access during tree traversal\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\n/// Handle to a metadata collector via reference-counted mutable cell.\n///\n/// Used internally for sharing collector state across the tree traversal.\n///\n/// # Examples\n///\n/// ```text\n/// let collector = MetadataCollector::new(MetadataConfig::default());\n/// let handle = Rc::new(RefCell::new(collector));\n///\n/// // In tree walk, can be passed and borrowed\n/// handle.borrow_mut().add_header(1, \"Title\".to_string(), None, 0, 100);\n///\n/// let metadata = handle.take().finish();\n/// ```\n#[allow(dead_code)]\npub(crate) type MetadataCollectorHandle = Rc<RefCell<MetadataCollector>>;\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_text_direction_parse() {\n        assert_eq!(TextDirection::parse(\"ltr\"), Some(TextDirection::LeftToRight));\n        assert_eq!(TextDirection::parse(\"rtl\"), Some(TextDirection::RightToLeft));\n        assert_eq!(TextDirection::parse(\"auto\"), Some(TextDirection::Auto));\n        assert_eq!(TextDirection::parse(\"invalid\"), None);\n        assert_eq!(TextDirection::parse(\"LTR\"), Some(TextDirection::LeftToRight));\n    }\n\n    #[test]\n    fn test_text_direction_display() {\n        assert_eq!(TextDirection::LeftToRight.to_string(), \"ltr\");\n        assert_eq!(TextDirection::RightToLeft.to_string(), \"rtl\");\n        assert_eq!(TextDirection::Auto.to_string(), \"auto\");\n    }\n\n    #[test]\n    fn test_link_classification() {\n        assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n        assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n        assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n        assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n        assert_eq!(LinkMetadata::classify_link(\"http://example.com\"), LinkType::External);\n        assert_eq!(LinkMetadata::classify_link(\"/path/to/page\"), LinkType::Internal);\n        assert_eq!(LinkMetadata::classify_link(\"../relative\"), LinkType::Internal);\n        assert_eq!(LinkMetadata::classify_link(\"./same\"), LinkType::Internal);\n    }\n\n    #[test]\n    fn test_header_validation() {\n        let valid = HeaderMetadata {\n            level: 3,\n            text: \"Title\".to_string(),\n            id: None,\n            depth: 2,\n            html_offset: 100,\n        };\n        assert!(valid.is_valid());\n\n        let invalid_high = HeaderMetadata {\n            level: 7,\n            text: \"Title\".to_string(),\n            id: None,\n            depth: 2,\n            html_offset: 100,\n        };\n        assert!(!invalid_high.is_valid());\n\n        let invalid_low = HeaderMetadata {\n            level: 0,\n            text: \"Title\".to_string(),\n            id: None,\n            depth: 2,\n            html_offset: 100,\n        };\n        assert!(!invalid_low.is_valid());\n    }\n\n    #[test]\n    fn test_document_metadata_default() {\n        let doc = DocumentMetadata::default();\n\n        assert!(doc.title.is_none());\n        assert!(doc.description.is_none());\n        assert!(doc.keywords.is_empty());\n        assert!(doc.open_graph.is_empty());\n        assert!(doc.twitter_card.is_empty());\n        assert!(doc.meta_tags.is_empty());\n    }\n\n    #[test]\n    fn test_image_type_classification() {\n        let data_uri = ImageMetadata {\n            src: \"data:image/png;base64,iVBORw0KG...\".to_string(),\n            alt: None,\n            title: None,\n            dimensions: None,\n            image_type: ImageType::DataUri,\n            attributes: Default::default(),\n        };\n        assert_eq!(data_uri.image_type, ImageType::DataUri);\n\n        let external = ImageMetadata {\n            src: \"https://example.com/image.jpg\".to_string(),\n            alt: None,\n            title: None,\n            dimensions: None,\n            image_type: ImageType::External,\n            attributes: Default::default(),\n        };\n        assert_eq!(external.image_type, ImageType::External);\n    }\n\n    #[test]\n    fn test_link_type_display() {\n        assert_eq!(LinkType::Anchor.to_string(), \"anchor\");\n        assert_eq!(LinkType::Internal.to_string(), \"internal\");\n        assert_eq!(LinkType::External.to_string(), \"external\");\n        assert_eq!(LinkType::Email.to_string(), \"email\");\n        assert_eq!(LinkType::Phone.to_string(), \"phone\");\n        assert_eq!(LinkType::Other.to_string(), \"other\");\n    }\n\n    #[test]\n    fn test_structured_data_type_display() {\n        assert_eq!(StructuredDataType::JsonLd.to_string(), \"json_ld\");\n        assert_eq!(StructuredDataType::Microdata.to_string(), \"microdata\");\n        assert_eq!(StructuredDataType::RDFa.to_string(), \"rdfa\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/metadata/types.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\n//! Type definitions for metadata extraction.\n\nuse std::collections::BTreeMap;\n\n/// Text directionality of document content.\n///\n/// Corresponds to the HTML `dir` attribute and `bdi` element directionality.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub enum TextDirection {\n    /// Left-to-right text flow (default for Latin scripts)\n    #[cfg_attr(feature = \"metadata\", serde(rename = \"ltr\"))]\n    LeftToRight,\n    /// Right-to-left text flow (Hebrew, Arabic, Urdu, etc.)\n    #[cfg_attr(feature = \"metadata\", serde(rename = \"rtl\"))]\n    RightToLeft,\n    /// Automatic directionality detection\n    #[cfg_attr(feature = \"metadata\", serde(rename = \"auto\"))]\n    Auto,\n}\n\nimpl std::fmt::Display for TextDirection {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::LeftToRight => write!(f, \"ltr\"),\n            Self::RightToLeft => write!(f, \"rtl\"),\n            Self::Auto => write!(f, \"auto\"),\n        }\n    }\n}\n\nimpl TextDirection {\n    /// Parse a text direction from string value.\n    ///\n    /// # Arguments\n    ///\n    /// * `s` - Direction string (\"ltr\", \"rtl\", or \"auto\")\n    ///\n    /// # Returns\n    ///\n    /// `Some(TextDirection)` if valid, `None` otherwise.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::TextDirection;\n    /// assert_eq!(TextDirection::parse(\"ltr\"), Some(TextDirection::LeftToRight));\n    /// assert_eq!(TextDirection::parse(\"rtl\"), Some(TextDirection::RightToLeft));\n    /// assert_eq!(TextDirection::parse(\"auto\"), Some(TextDirection::Auto));\n    /// assert_eq!(TextDirection::parse(\"invalid\"), None);\n    /// ```\n    #[must_use]\n    pub fn parse(s: &str) -> Option<Self> {\n        if s.eq_ignore_ascii_case(\"ltr\") {\n            return Some(Self::LeftToRight);\n        }\n        if s.eq_ignore_ascii_case(\"rtl\") {\n            return Some(Self::RightToLeft);\n        }\n        if s.eq_ignore_ascii_case(\"auto\") {\n            return Some(Self::Auto);\n        }\n        None\n    }\n}\n\n/// Link classification based on href value and document context.\n///\n/// Used to categorize links during extraction for filtering and analysis.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\n#[cfg_attr(feature = \"metadata\", serde(rename_all = \"snake_case\"))]\npub enum LinkType {\n    /// Anchor link within same document (href starts with #)\n    Anchor,\n    /// Internal link within same domain\n    Internal,\n    /// External link to different domain\n    External,\n    /// Email link (mailto:)\n    Email,\n    /// Phone link (tel:)\n    Phone,\n    /// Other protocol or unclassifiable\n    Other,\n}\n\nimpl std::fmt::Display for LinkType {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Anchor => write!(f, \"anchor\"),\n            Self::Internal => write!(f, \"internal\"),\n            Self::External => write!(f, \"external\"),\n            Self::Email => write!(f, \"email\"),\n            Self::Phone => write!(f, \"phone\"),\n            Self::Other => write!(f, \"other\"),\n        }\n    }\n}\n\n/// Image source classification for proper handling and processing.\n///\n/// Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\n#[cfg_attr(feature = \"metadata\", serde(rename_all = \"snake_case\"))]\npub enum ImageType {\n    /// Data URI embedded image (base64 or other encoding)\n    DataUri,\n    /// Inline SVG element\n    InlineSvg,\n    /// External image URL (http/https)\n    External,\n    /// Relative image path\n    Relative,\n}\n\nimpl std::fmt::Display for ImageType {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::DataUri => write!(f, \"data_uri\"),\n            Self::InlineSvg => write!(f, \"inline_svg\"),\n            Self::External => write!(f, \"external\"),\n            Self::Relative => write!(f, \"relative\"),\n        }\n    }\n}\n\n/// Structured data format type.\n///\n/// Identifies the schema/format used for structured data markup.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\n#[cfg_attr(feature = \"metadata\", serde(rename_all = \"snake_case\"))]\npub enum StructuredDataType {\n    /// JSON-LD (JSON for Linking Data) script blocks\n    #[cfg_attr(feature = \"metadata\", serde(rename = \"json_ld\"))]\n    JsonLd,\n    /// HTML5 Microdata attributes (itemscope, itemtype, itemprop)\n    Microdata,\n    /// RDF in Attributes (RDFa) markup\n    #[cfg_attr(feature = \"metadata\", serde(rename = \"rdfa\"))]\n    RDFa,\n}\n\nimpl std::fmt::Display for StructuredDataType {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::JsonLd => write!(f, \"json_ld\"),\n            Self::Microdata => write!(f, \"microdata\"),\n            Self::RDFa => write!(f, \"rdfa\"),\n        }\n    }\n}\n\n/// Document-level metadata extracted from `<head>` and top-level elements.\n///\n/// Contains all metadata typically used by search engines, social media platforms,\n/// and browsers for document indexing and presentation.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::DocumentMetadata;\n/// let doc = DocumentMetadata {\n///     title: Some(\"My Article\".to_string()),\n///     description: Some(\"A great article about Rust\".to_string()),\n///     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n///     ..Default::default()\n/// };\n///\n/// assert_eq!(doc.title, Some(\"My Article\".to_string()));\n/// ```\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct DocumentMetadata {\n    /// Document title from `<title>` tag\n    pub title: Option<String>,\n\n    /// Document description from `<meta name=\"description\">` tag\n    pub description: Option<String>,\n\n    /// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n    pub keywords: Vec<String>,\n\n    /// Document author from `<meta name=\"author\">` tag\n    pub author: Option<String>,\n\n    /// Canonical URL from `<link rel=\"canonical\">` tag\n    pub canonical_url: Option<String>,\n\n    /// Base URL from `<base href=\"\">` tag for resolving relative URLs\n    pub base_href: Option<String>,\n\n    /// Document language from `lang` attribute\n    pub language: Option<String>,\n\n    /// Document text direction from `dir` attribute\n    pub text_direction: Option<TextDirection>,\n\n    /// Open Graph metadata (og:* properties) for social media\n    /// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n    pub open_graph: BTreeMap<String, String>,\n\n    /// Twitter Card metadata (twitter:* properties)\n    /// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n    pub twitter_card: BTreeMap<String, String>,\n\n    /// Additional meta tags not covered by specific fields\n    /// Keys are meta name/property attributes, values are content\n    pub meta_tags: BTreeMap<String, String>,\n}\n\n/// Header element metadata with hierarchy tracking.\n///\n/// Captures heading elements (h1-h6) with their text content, identifiers,\n/// and position in the document structure.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HeaderMetadata;\n/// let header = HeaderMetadata {\n///     level: 1,\n///     text: \"Main Title\".to_string(),\n///     id: Some(\"main-title\".to_string()),\n///     depth: 0,\n///     html_offset: 145,\n/// };\n///\n/// assert_eq!(header.level, 1);\n/// assert!(header.is_valid());\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct HeaderMetadata {\n    /// Header level: 1 (h1) through 6 (h6)\n    pub level: u8,\n\n    /// Normalized text content of the header\n    pub text: String,\n\n    /// HTML id attribute if present\n    pub id: Option<String>,\n\n    /// Document tree depth at the header element\n    pub depth: usize,\n\n    /// Byte offset in original HTML document\n    pub html_offset: usize,\n}\n\nimpl HeaderMetadata {\n    /// Validate that the header level is within valid range (1-6).\n    ///\n    /// # Returns\n    ///\n    /// `true` if level is 1-6, `false` otherwise.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::HeaderMetadata;\n    /// let valid = HeaderMetadata {\n    ///     level: 3,\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(valid.is_valid());\n    ///\n    /// let invalid = HeaderMetadata {\n    ///     level: 7,  // Invalid\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(!invalid.is_valid());\n    /// ```\n    #[must_use]\n    pub const fn is_valid(&self) -> bool {\n        self.level >= 1 && self.level <= 6\n    }\n}\n\n/// Hyperlink metadata with categorization and attributes.\n///\n/// Represents `<a>` elements with parsed href values, text content, and link type classification.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n/// let link = LinkMetadata {\n///     href: \"https://example.com\".to_string(),\n///     text: \"Example\".to_string(),\n///     title: Some(\"Visit Example\".to_string()),\n///     link_type: LinkType::External,\n///     rel: vec![\"nofollow\".to_string()],\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(link.link_type, LinkType::External);\n/// assert_eq!(link.text, \"Example\");\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct LinkMetadata {\n    /// The href URL value\n    pub href: String,\n\n    /// Link text content (normalized, concatenated if mixed with elements)\n    pub text: String,\n\n    /// Optional title attribute (often shown as tooltip)\n    pub title: Option<String>,\n\n    /// Link type classification\n    pub link_type: LinkType,\n\n    /// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n    pub rel: Vec<String>,\n\n    /// Additional HTML attributes\n    pub attributes: BTreeMap<String, String>,\n}\n\nimpl LinkMetadata {\n    /// Classify a link based on href value.\n    ///\n    /// # Arguments\n    ///\n    /// * `href` - The href attribute value\n    ///\n    /// # Returns\n    ///\n    /// Appropriate [`LinkType`] based on protocol and content.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n    /// assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n    /// assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n    /// assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n    /// assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n    /// ```\n    #[must_use]\n    pub fn classify_link(href: &str) -> LinkType {\n        if href.starts_with('#') {\n            LinkType::Anchor\n        } else if href.starts_with(\"mailto:\") {\n            LinkType::Email\n        } else if href.starts_with(\"tel:\") {\n            LinkType::Phone\n        } else if href.starts_with(\"http://\") || href.starts_with(\"https://\") {\n            LinkType::External\n        } else if href.starts_with('/') || href.starts_with(\"../\") || href.starts_with(\"./\") {\n            LinkType::Internal\n        } else {\n            LinkType::Other\n        }\n    }\n}\n\n/// Image metadata with source and dimensions.\n///\n/// Captures `<img>` elements and inline `<svg>` elements with metadata\n/// for image analysis and optimization.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n/// let img = ImageMetadata {\n///     src: \"https://example.com/image.jpg\".to_string(),\n///     alt: Some(\"An example image\".to_string()),\n///     title: Some(\"Example\".to_string()),\n///     dimensions: Some((800, 600)),\n///     image_type: ImageType::External,\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(img.image_type, ImageType::External);\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct ImageMetadata {\n    /// Image source (URL, data URI, or SVG content identifier)\n    pub src: String,\n\n    /// Alternative text from alt attribute (for accessibility)\n    pub alt: Option<String>,\n\n    /// Title attribute (often shown as tooltip)\n    pub title: Option<String>,\n\n    /// Image dimensions as (width, height) if available\n    pub dimensions: Option<(u32, u32)>,\n\n    /// Image type classification\n    pub image_type: ImageType,\n\n    /// Additional HTML attributes\n    pub attributes: BTreeMap<String, String>,\n}\n\n/// Structured data block (JSON-LD, Microdata, or RDFa).\n///\n/// Represents machine-readable structured data found in the document.\n/// JSON-LD blocks are collected as raw JSON strings for flexibility.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n/// let schema = StructuredData {\n///     data_type: StructuredDataType::JsonLd,\n///     raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n///     schema_type: Some(\"Article\".to_string()),\n/// };\n///\n/// assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct StructuredData {\n    /// Type of structured data (JSON-LD, Microdata, RDFa)\n    pub data_type: StructuredDataType,\n\n    /// Raw JSON string (for JSON-LD) or serialized representation\n    pub raw_json: String,\n\n    /// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n    pub schema_type: Option<String>,\n}\n\n/// Comprehensive metadata extraction result from HTML document.\n///\n/// Contains all extracted metadata types in a single structure,\n/// suitable for serialization and transmission across language boundaries.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HtmlMetadata;\n/// let metadata = HtmlMetadata {\n///     document: Default::default(),\n///     headers: Vec::new(),\n///     links: Vec::new(),\n///     images: Vec::new(),\n///     structured_data: Vec::new(),\n/// };\n///\n/// assert!(metadata.headers.is_empty());\n/// ```\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(feature = \"metadata\", derive(serde::Serialize, serde::Deserialize))]\npub struct HtmlMetadata {\n    /// Document-level metadata (title, description, canonical, etc.)\n    pub document: DocumentMetadata,\n\n    /// Extracted header elements with hierarchy\n    pub headers: Vec<HeaderMetadata>,\n\n    /// Extracted hyperlinks with type classification\n    pub links: Vec<LinkMetadata>,\n\n    /// Extracted images with source and dimensions\n    pub images: Vec<ImageMetadata>,\n\n    /// Extracted structured data blocks\n    pub structured_data: Vec<StructuredData>,\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/options/conversion.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\n\n//! Main conversion options with builder pattern.\n\nuse crate::options::preprocessing::PreprocessingOptions;\nuse crate::options::validation::{\n    CodeBlockStyle, HeadingStyle, HighlightStyle, LinkStyle, ListIndentType, NewlineStyle, OutputFormat, WhitespaceMode,\n};\n\n/// Main conversion options for HTML to Markdown conversion.\n///\n/// Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::ConversionOptions;\n///\n/// let options = ConversionOptions::builder()\n///     .heading_style(HeadingStyle::Atx)\n///     .wrap(true)\n///     .wrap_width(100)\n///     .build();\n/// ```\n#[derive(Debug, Clone)]\n#[cfg_attr(\n    any(feature = \"serde\", feature = \"metadata\"),\n    derive(serde::Serialize, serde::Deserialize)\n)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(default, deny_unknown_fields))]\npub struct ConversionOptions {\n    /// Heading style to use in Markdown output (ATX `#` or Setext underline).\n    pub heading_style: HeadingStyle,\n    /// How to indent nested list items (spaces or tab).\n    pub list_indent_type: ListIndentType,\n    /// Number of spaces (or tabs) to use for each level of list indentation.\n    pub list_indent_width: usize,\n    /// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n    pub bullets: String,\n    /// Character used for bold/italic emphasis markers (`*` or `_`).\n    pub strong_em_symbol: char,\n    /// Escape `*` characters in plain text to avoid unintended bold/italic.\n    pub escape_asterisks: bool,\n    /// Escape `_` characters in plain text to avoid unintended bold/italic.\n    pub escape_underscores: bool,\n    /// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n    pub escape_misc: bool,\n    /// Escape ASCII characters that have special meaning in certain Markdown dialects.\n    pub escape_ascii: bool,\n    /// Default language annotation for fenced code blocks that have no language hint.\n    pub code_language: String,\n    /// Automatically convert bare URLs into Markdown autolinks.\n    pub autolinks: bool,\n    /// Emit a default title when no `<title>` tag is present.\n    pub default_title: bool,\n    /// Render `<br>` elements inside table cells as literal line breaks.\n    pub br_in_tables: bool,\n    /// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n    pub highlight_style: HighlightStyle,\n    /// Extract `<meta>` and `<head>` information into the result metadata.\n    pub extract_metadata: bool,\n    /// Controls how whitespace is normalised during conversion.\n    pub whitespace_mode: WhitespaceMode,\n    /// Strip all newlines from the output, producing a single-line result.\n    pub strip_newlines: bool,\n    /// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n    pub wrap: bool,\n    /// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n    pub wrap_width: usize,\n    /// Treat the entire document as inline content (no block-level wrappers).\n    pub convert_as_inline: bool,\n    /// Markdown notation for subscript text (e.g. `\"~\"`).\n    pub sub_symbol: String,\n    /// Markdown notation for superscript text (e.g. `\"^\"`).\n    pub sup_symbol: String,\n    /// How to encode hard line breaks (`<br>`) in Markdown.\n    pub newline_style: NewlineStyle,\n    /// Style used for fenced code blocks (backticks or tilde).\n    pub code_block_style: CodeBlockStyle,\n    /// HTML tag names whose `<img>` children are kept inline instead of block.\n    pub keep_inline_images_in: Vec<String>,\n    /// Pre-processing options applied to the HTML before conversion.\n    pub preprocessing: PreprocessingOptions,\n    /// Expected character encoding of the input HTML (default `\"utf-8\"`).\n    pub encoding: String,\n    /// Emit debug information during conversion.\n    pub debug: bool,\n    /// HTML tag names whose content is stripped from the output entirely.\n    pub strip_tags: Vec<String>,\n    /// HTML tag names that are preserved verbatim in the output.\n    pub preserve_tags: Vec<String>,\n    /// Skip conversion of `<img>` elements (omit images from output).\n    pub skip_images: bool,\n    /// Link rendering style (inline or reference).\n    pub link_style: LinkStyle,\n    /// Target output format (Markdown, plain text, etc.).\n    pub output_format: OutputFormat,\n    /// Include structured document tree in result.\n    pub include_document_structure: bool,\n    /// Extract inline images from data URIs and SVGs.\n    pub extract_images: bool,\n    /// Maximum decoded image size in bytes (default 5MB).\n    pub max_image_size: u64,\n    /// Capture SVG elements as images.\n    pub capture_svg: bool,\n    /// Infer image dimensions from data.\n    pub infer_dimensions: bool,\n    /// Maximum DOM traversal depth. `None` means unlimited.\n    /// When set, subtrees beyond this depth are silently truncated.\n    pub max_depth: Option<usize>,\n    /// CSS selectors for elements to exclude entirely (element + all content).\n    ///\n    /// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n    /// excluded elements and all their descendants are dropped from the output.\n    /// Supports any CSS selector that `tl` supports: tag names, `.class`,\n    /// `#id`, `[attribute]`, etc.\n    ///\n    /// Invalid selectors are silently skipped at conversion time.\n    ///\n    /// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(default))]\n    pub exclude_selectors: Vec<String>,\n\n    /// Optional visitor for custom traversal logic.\n    ///\n    /// When set, the visitor's callbacks are invoked for matching HTML elements\n    /// during conversion, allowing custom output, skipping, or HTML preservation.\n    /// See [`crate::visitor::HtmlVisitor`].\n    #[cfg(feature = \"visitor\")]\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(skip))]\n    pub visitor: Option<crate::visitor::VisitorHandle>,\n}\n\nimpl Default for ConversionOptions {\n    fn default() -> Self {\n        Self {\n            heading_style: HeadingStyle::default(),\n            list_indent_type: ListIndentType::default(),\n            list_indent_width: 2,\n            bullets: \"-*+\".to_string(),\n            strong_em_symbol: '*',\n            escape_asterisks: false,\n            escape_underscores: false,\n            escape_misc: false,\n            escape_ascii: false,\n            code_language: String::new(),\n            autolinks: true,\n            default_title: false,\n            br_in_tables: false,\n            highlight_style: HighlightStyle::default(),\n            extract_metadata: true,\n            whitespace_mode: WhitespaceMode::default(),\n            strip_newlines: false,\n            wrap: false,\n            wrap_width: 80,\n            convert_as_inline: false,\n            sub_symbol: String::new(),\n            sup_symbol: String::new(),\n            newline_style: NewlineStyle::Spaces,\n            code_block_style: CodeBlockStyle::default(),\n            keep_inline_images_in: Vec::new(),\n            preprocessing: PreprocessingOptions::default(),\n            encoding: \"utf-8\".to_string(),\n            debug: false,\n            strip_tags: Vec::new(),\n            preserve_tags: Vec::new(),\n            skip_images: false,\n            link_style: LinkStyle::default(),\n            output_format: OutputFormat::default(),\n            include_document_structure: false,\n            extract_images: false,\n            max_image_size: 5_242_880,\n            capture_svg: false,\n            infer_dimensions: true,\n            max_depth: None,\n            exclude_selectors: Vec::new(),\n            #[cfg(feature = \"visitor\")]\n            visitor: None,\n        }\n    }\n}\n\nimpl ConversionOptions {\n    /// Create a new builder with default values.\n    #[must_use]\n    pub fn builder() -> ConversionOptionsBuilder {\n        ConversionOptionsBuilder(Self::default())\n    }\n}\n\n// ── Builder ─────────────────────────────────────────────────────────────────\n\n/// Builder for [`ConversionOptions`].\n///\n/// All fields start with default values. Call `.build()` to produce the final options.\n#[derive(Debug, Clone)]\npub struct ConversionOptionsBuilder(ConversionOptions);\n\nmacro_rules! builder_setter {\n    ($name:ident, $ty:ty) => {\n        /// Set the value.\n        #[must_use]\n        pub fn $name(mut self, value: $ty) -> Self {\n            self.0.$name = value;\n            self\n        }\n    };\n}\n\nmacro_rules! builder_setter_into {\n    ($name:ident, $ty:ty) => {\n        /// Set the value.\n        #[must_use]\n        pub fn $name(mut self, value: impl Into<$ty>) -> Self {\n            self.0.$name = value.into();\n            self\n        }\n    };\n}\n\nimpl ConversionOptionsBuilder {\n    // Output control\n    builder_setter!(output_format, OutputFormat);\n    builder_setter!(include_document_structure, bool);\n    builder_setter!(extract_metadata, bool);\n    builder_setter!(extract_images, bool);\n\n    // Markdown formatting\n    builder_setter!(heading_style, HeadingStyle);\n    builder_setter!(list_indent_type, ListIndentType);\n    builder_setter!(list_indent_width, usize);\n    builder_setter_into!(bullets, String);\n    builder_setter!(strong_em_symbol, char);\n    builder_setter!(code_block_style, CodeBlockStyle);\n    builder_setter!(newline_style, NewlineStyle);\n    builder_setter!(highlight_style, HighlightStyle);\n    builder_setter_into!(code_language, String);\n    builder_setter!(link_style, LinkStyle);\n    builder_setter!(autolinks, bool);\n    builder_setter!(default_title, bool);\n    builder_setter!(br_in_tables, bool);\n    builder_setter_into!(sub_symbol, String);\n    builder_setter_into!(sup_symbol, String);\n\n    // Escaping\n    builder_setter!(escape_asterisks, bool);\n    builder_setter!(escape_underscores, bool);\n    builder_setter!(escape_misc, bool);\n    builder_setter!(escape_ascii, bool);\n\n    // Whitespace / wrapping\n    builder_setter!(whitespace_mode, WhitespaceMode);\n    builder_setter!(strip_newlines, bool);\n    builder_setter!(wrap, bool);\n    builder_setter!(wrap_width, usize);\n\n    // Element handling\n    builder_setter!(convert_as_inline, bool);\n    builder_setter!(skip_images, bool);\n\n    /// Set the list of HTML tag names whose content is stripped from output.\n    #[must_use]\n    pub fn strip_tags(mut self, tags: Vec<String>) -> Self {\n        self.0.strip_tags = tags;\n        self\n    }\n\n    /// Set the list of HTML tag names that are preserved verbatim in output.\n    #[must_use]\n    pub fn preserve_tags(mut self, tags: Vec<String>) -> Self {\n        self.0.preserve_tags = tags;\n        self\n    }\n\n    /// Set the list of HTML tag names whose `<img>` children are kept inline.\n    #[must_use]\n    pub fn keep_inline_images_in(mut self, tags: Vec<String>) -> Self {\n        self.0.keep_inline_images_in = tags;\n        self\n    }\n\n    // Image extraction config\n    builder_setter!(max_image_size, u64);\n    builder_setter!(capture_svg, bool);\n    builder_setter!(infer_dimensions, bool);\n    builder_setter!(max_depth, Option<usize>);\n\n    /// Set the list of CSS selectors for elements to exclude entirely from output.\n    #[must_use]\n    pub fn exclude_selectors(mut self, selectors: Vec<String>) -> Self {\n        self.0.exclude_selectors = selectors;\n        self\n    }\n\n    /// Set the visitor used during conversion.\n    #[cfg(feature = \"visitor\")]\n    #[must_use]\n    pub fn visitor(mut self, visitor: Option<crate::visitor::VisitorHandle>) -> Self {\n        self.0.visitor = visitor;\n        self\n    }\n\n    // Preprocessing\n    /// Set the pre-processing options applied to the HTML before conversion.\n    #[must_use]\n    pub fn preprocessing(mut self, preprocessing: PreprocessingOptions) -> Self {\n        self.0.preprocessing = preprocessing;\n        self\n    }\n\n    // Encoding\n    builder_setter_into!(encoding, String);\n\n    // Debug\n    builder_setter!(debug, bool);\n\n    /// Build the final [`ConversionOptions`].\n    #[must_use]\n    pub fn build(self) -> ConversionOptions {\n        self.0\n    }\n}\n\n// ── ConversionOptionsUpdate (for binding crate compatibility) ────────────\n\nuse crate::options::preprocessing::PreprocessingOptionsUpdate;\n\n/// Partial update for `ConversionOptions`.\n///\n/// Uses `Option<T>` fields for selective updates. Bindings use this to construct\n/// options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(\n    any(feature = \"serde\", feature = \"metadata\"),\n    derive(serde::Serialize, serde::Deserialize)\n)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(deny_unknown_fields))]\npub struct ConversionOptionsUpdate {\n    /// Optional override for [`ConversionOptions::heading_style`].\n    pub heading_style: Option<HeadingStyle>,\n    /// Optional override for [`ConversionOptions::list_indent_type`].\n    pub list_indent_type: Option<ListIndentType>,\n    /// Optional override for [`ConversionOptions::list_indent_width`].\n    pub list_indent_width: Option<usize>,\n    /// Optional override for [`ConversionOptions::bullets`].\n    pub bullets: Option<String>,\n    /// Optional override for [`ConversionOptions::strong_em_symbol`].\n    pub strong_em_symbol: Option<char>,\n    /// Optional override for [`ConversionOptions::escape_asterisks`].\n    pub escape_asterisks: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_underscores`].\n    pub escape_underscores: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_misc`].\n    pub escape_misc: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_ascii`].\n    pub escape_ascii: Option<bool>,\n    /// Optional override for [`ConversionOptions::code_language`].\n    pub code_language: Option<String>,\n    /// Optional override for [`ConversionOptions::autolinks`].\n    pub autolinks: Option<bool>,\n    /// Optional override for [`ConversionOptions::default_title`].\n    pub default_title: Option<bool>,\n    /// Optional override for [`ConversionOptions::br_in_tables`].\n    pub br_in_tables: Option<bool>,\n    /// Optional override for [`ConversionOptions::highlight_style`].\n    pub highlight_style: Option<HighlightStyle>,\n    /// Optional override for [`ConversionOptions::extract_metadata`].\n    pub extract_metadata: Option<bool>,\n    /// Optional override for [`ConversionOptions::whitespace_mode`].\n    pub whitespace_mode: Option<WhitespaceMode>,\n    /// Optional override for [`ConversionOptions::strip_newlines`].\n    pub strip_newlines: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap`].\n    pub wrap: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap_width`].\n    pub wrap_width: Option<usize>,\n    /// Optional override for [`ConversionOptions::convert_as_inline`].\n    pub convert_as_inline: Option<bool>,\n    /// Optional override for [`ConversionOptions::sub_symbol`].\n    pub sub_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::sup_symbol`].\n    pub sup_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::newline_style`].\n    pub newline_style: Option<NewlineStyle>,\n    /// Optional override for [`ConversionOptions::code_block_style`].\n    pub code_block_style: Option<CodeBlockStyle>,\n    /// Optional override for [`ConversionOptions::keep_inline_images_in`].\n    pub keep_inline_images_in: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preprocessing`].\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    /// Optional override for [`ConversionOptions::encoding`].\n    pub encoding: Option<String>,\n    /// Optional override for [`ConversionOptions::debug`].\n    pub debug: Option<bool>,\n    /// Optional override for [`ConversionOptions::strip_tags`].\n    pub strip_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preserve_tags`].\n    pub preserve_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::skip_images`].\n    pub skip_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::link_style`].\n    pub link_style: Option<LinkStyle>,\n    /// Optional override for [`ConversionOptions::output_format`].\n    pub output_format: Option<OutputFormat>,\n    /// Optional override for [`ConversionOptions::include_document_structure`].\n    pub include_document_structure: Option<bool>,\n    /// Optional override for [`ConversionOptions::extract_images`].\n    pub extract_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_image_size`].\n    pub max_image_size: Option<u64>,\n    /// Optional override for [`ConversionOptions::capture_svg`].\n    pub capture_svg: Option<bool>,\n    /// Optional override for [`ConversionOptions::infer_dimensions`].\n    pub infer_dimensions: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_depth`].\n    pub max_depth: Option<Option<usize>>,\n    /// Optional override for [`ConversionOptions::exclude_selectors`].\n    pub exclude_selectors: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::visitor`].\n    #[cfg(feature = \"visitor\")]\n    #[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(skip))]\n    pub visitor: Option<crate::visitor::VisitorHandle>,\n}\n\nimpl ConversionOptions {\n    /// Apply a partial update to these conversion options.\n    pub fn apply_update(&mut self, update: ConversionOptionsUpdate) {\n        macro_rules! apply {\n            ($field:ident) => {\n                if let Some(v) = update.$field {\n                    self.$field = v;\n                }\n            };\n        }\n        apply!(heading_style);\n        apply!(list_indent_type);\n        apply!(list_indent_width);\n        apply!(bullets);\n        apply!(strong_em_symbol);\n        apply!(escape_asterisks);\n        apply!(escape_underscores);\n        apply!(escape_misc);\n        apply!(escape_ascii);\n        apply!(code_language);\n        apply!(autolinks);\n        apply!(default_title);\n        apply!(br_in_tables);\n        apply!(highlight_style);\n        apply!(extract_metadata);\n        apply!(whitespace_mode);\n        apply!(strip_newlines);\n        apply!(wrap);\n        apply!(wrap_width);\n        apply!(convert_as_inline);\n        apply!(sub_symbol);\n        apply!(sup_symbol);\n        apply!(newline_style);\n        apply!(code_block_style);\n        apply!(keep_inline_images_in);\n        apply!(encoding);\n        apply!(debug);\n        apply!(strip_tags);\n        apply!(preserve_tags);\n        apply!(skip_images);\n        apply!(link_style);\n        apply!(output_format);\n        apply!(include_document_structure);\n        apply!(extract_images);\n        apply!(max_image_size);\n        apply!(capture_svg);\n        apply!(infer_dimensions);\n        apply!(max_depth);\n        apply!(exclude_selectors);\n        #[cfg(feature = \"visitor\")]\n        if let Some(visitor) = update.visitor {\n            self.visitor = Some(visitor);\n        }\n        if let Some(preprocessing) = update.preprocessing {\n            self.preprocessing.apply_update(preprocessing);\n        }\n    }\n\n    /// Create from a partial update, applying to defaults.\n    #[must_use]\n    pub fn from_update(update: ConversionOptionsUpdate) -> Self {\n        let mut options = Self::default();\n        options.apply_update(update);\n        options\n    }\n}\n\nimpl From<ConversionOptionsUpdate> for ConversionOptions {\n    fn from(update: ConversionOptionsUpdate) -> Self {\n        Self::from_update(update)\n    }\n}\n\n// ── Tests ───────────────────────────────────────────────────────────────────\n\n#[cfg(all(test, any(feature = \"serde\", feature = \"metadata\")))]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_conversion_options_serde() {\n        let options = ConversionOptions::builder()\n            .heading_style(HeadingStyle::AtxClosed)\n            .list_indent_width(4)\n            .bullets(\"*\")\n            .escape_asterisks(true)\n            .whitespace_mode(WhitespaceMode::Strict)\n            .build();\n\n        let json = serde_json::to_string(&options).expect(\"Failed to serialize\");\n        let deserialized: ConversionOptions = serde_json::from_str(&json).expect(\"Failed to deserialize\");\n\n        assert_eq!(deserialized.list_indent_width, 4);\n        assert_eq!(deserialized.bullets, \"*\");\n        assert!(deserialized.escape_asterisks);\n        assert_eq!(deserialized.heading_style, HeadingStyle::AtxClosed);\n        assert_eq!(deserialized.whitespace_mode, WhitespaceMode::Strict);\n    }\n\n    #[test]\n    fn test_conversion_options_partial_deserialization() {\n        let partial_json = r#\"{\n            \"heading_style\": \"atxclosed\",\n            \"list_indent_width\": 4,\n            \"bullets\": \"*\"\n        }\"#;\n\n        let deserialized: ConversionOptions =\n            serde_json::from_str(partial_json).expect(\"Failed to deserialize partial JSON\");\n\n        assert_eq!(deserialized.heading_style, HeadingStyle::AtxClosed);\n        assert_eq!(deserialized.list_indent_width, 4);\n        assert_eq!(deserialized.bullets, \"*\");\n        assert!(!deserialized.escape_asterisks);\n        assert!(!deserialized.escape_underscores);\n        assert_eq!(deserialized.list_indent_type, ListIndentType::Spaces);\n    }\n\n    #[test]\n    fn test_builder_pattern() {\n        let options = ConversionOptions::builder()\n            .heading_style(HeadingStyle::Underlined)\n            .wrap(true)\n            .wrap_width(100)\n            .include_document_structure(true)\n            .extract_images(true)\n            .build();\n\n        assert_eq!(options.heading_style, HeadingStyle::Underlined);\n        assert!(options.wrap);\n        assert_eq!(options.wrap_width, 100);\n        assert!(options.include_document_structure);\n        assert!(options.extract_images);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/options/inline_image.rs",
    "content": "//! Inline image configuration.\n//!\n//! This module provides configuration for controlling how images are rendered\n//! within specific HTML elements.\n\n/// Inline image configuration that specifies contexts where images remain as markdown links.\n///\n/// This is a wrapper type that provides semantic clarity for the vector of element\n/// names where inline images should be preserved.\n#[derive(Debug, Clone)]\npub struct InlineImageConfig {\n    /// HTML elements where images should remain as markdown links (not converted to alt text)\n    pub keep_inline_images_in: Vec<String>,\n}\n\nimpl InlineImageConfig {\n    /// Create a new inline image configuration with an empty list.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            keep_inline_images_in: Vec::new(),\n        }\n    }\n\n    /// Create a new inline image configuration from a list of element names.\n    ///\n    /// # Arguments\n    ///\n    /// * `elements` - A vector of HTML element names where inline images should be kept\n    #[must_use]\n    pub fn from_elements(elements: Vec<String>) -> Self {\n        Self {\n            keep_inline_images_in: elements,\n        }\n    }\n\n    /// Add an element name to the list of elements where images are kept inline.\n    ///\n    /// # Arguments\n    ///\n    /// * `element` - The HTML element name to add (e.g., \"p\", \"div\")\n    pub fn add_element(&mut self, element: String) {\n        self.keep_inline_images_in.push(element);\n    }\n\n    /// Check if a given element should keep images inline.\n    ///\n    /// # Arguments\n    ///\n    /// * `element` - The HTML element name to check\n    ///\n    /// # Returns\n    ///\n    /// `true` if the element is in the configured list, `false` otherwise\n    #[must_use]\n    pub fn should_keep_images(&self, element: &str) -> bool {\n        self.keep_inline_images_in.iter().any(|e| e == element)\n    }\n}\n\nimpl Default for InlineImageConfig {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_inline_image_config_new() {\n        let config = InlineImageConfig::new();\n        assert_eq!(config.keep_inline_images_in.len(), 0);\n    }\n\n    #[test]\n    fn test_inline_image_config_from_elements() {\n        let elements = vec![\"p\".to_string(), \"div\".to_string()];\n        let config = InlineImageConfig::from_elements(elements);\n        assert_eq!(config.keep_inline_images_in.len(), 2);\n        assert!(config.should_keep_images(\"p\"));\n        assert!(config.should_keep_images(\"div\"));\n        assert!(!config.should_keep_images(\"span\"));\n    }\n\n    #[test]\n    fn test_inline_image_config_add_element() {\n        let mut config = InlineImageConfig::new();\n        config.add_element(\"p\".to_string());\n        config.add_element(\"div\".to_string());\n\n        assert_eq!(config.keep_inline_images_in.len(), 2);\n        assert!(config.should_keep_images(\"p\"));\n        assert!(config.should_keep_images(\"div\"));\n    }\n\n    #[test]\n    fn test_inline_image_config_should_keep_images() {\n        let config = InlineImageConfig::from_elements(vec![\"figure\".to_string()]);\n        assert!(config.should_keep_images(\"figure\"));\n        assert!(!config.should_keep_images(\"p\"));\n    }\n\n    #[test]\n    fn test_inline_image_config_default() {\n        let config = InlineImageConfig::default();\n        assert_eq!(config.keep_inline_images_in.len(), 0);\n        assert!(!config.should_keep_images(\"p\"));\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/options/mod.rs",
    "content": "//! Configuration options for HTML to Markdown conversion.\n//!\n//! This module provides comprehensive configuration options for customizing\n//! HTML to Markdown conversion behavior, including output formatting, preprocessing,\n//! and metadata extraction options.\n\npub mod conversion;\npub mod inline_image;\npub mod preprocessing;\npub mod validation;\n\n// Re-exports for easy access\npub use conversion::{ConversionOptions, ConversionOptionsBuilder, ConversionOptionsUpdate};\npub use preprocessing::{PreprocessingOptions, PreprocessingOptionsUpdate, PreprocessingPreset};\npub use validation::{\n    CodeBlockStyle, HeadingStyle, HighlightStyle, LinkStyle, ListIndentType, NewlineStyle, OutputFormat, WhitespaceMode,\n};\n\n// Note: InlineImageConfig is re-exported from the inline_images module,\n// not from this options module, to maintain compatibility with existing imports.\n"
  },
  {
    "path": "crates/html-to-markdown/src/options/preprocessing.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\n\n//! HTML preprocessing configuration options.\n//!\n//! This module provides configuration for document cleanup before conversion,\n//! including preset levels and granular control over element removal.\n\nuse crate::options::validation::normalize_token;\n\n/// HTML preprocessing aggressiveness level.\n///\n/// Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum PreprocessingPreset {\n    /// Minimal cleanup. Remove only essential noise (scripts, styles).\n    Minimal,\n    /// Standard cleanup. Default. Removes navigation, forms, and other auxiliary content.\n    #[default]\n    Standard,\n    /// Aggressive cleanup. Remove extensive non-content elements and structure.\n    Aggressive,\n}\n\nimpl PreprocessingPreset {\n    /// Parse a preprocessing preset from a string.\n    ///\n    /// Accepts \"minimal\", \"aggressive\", or defaults to Standard.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"minimal\" => Self::Minimal,\n            \"aggressive\" => Self::Aggressive,\n            _ => Self::Standard,\n        }\n    }\n}\n\n/// HTML preprocessing options for document cleanup before conversion.\n#[derive(Debug, Clone)]\n#[cfg_attr(\n    any(feature = \"serde\", feature = \"metadata\"),\n    derive(serde::Serialize, serde::Deserialize)\n)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(default, deny_unknown_fields))]\npub struct PreprocessingOptions {\n    /// Enable HTML preprocessing globally\n    pub enabled: bool,\n\n    /// Preprocessing preset level (Minimal, Standard, Aggressive)\n    pub preset: PreprocessingPreset,\n\n    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n    pub remove_navigation: bool,\n\n    /// Remove form elements (forms, inputs, buttons, etc.)\n    pub remove_forms: bool,\n}\n\n/// Partial update for `PreprocessingOptions`.\n///\n/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n/// Only specified fields (Some values) will override existing options; None values leave the\n/// corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(\n    any(feature = \"serde\", feature = \"metadata\"),\n    derive(serde::Serialize, serde::Deserialize)\n)]\n#[cfg_attr(any(feature = \"serde\", feature = \"metadata\"), serde(deny_unknown_fields))]\npub struct PreprocessingOptionsUpdate {\n    /// Optional global preprocessing enablement override\n    pub enabled: Option<bool>,\n\n    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n    pub preset: Option<PreprocessingPreset>,\n\n    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n    pub remove_navigation: Option<bool>,\n\n    /// Optional form element removal override (forms, inputs, buttons, etc.)\n    pub remove_forms: Option<bool>,\n}\n\nimpl Default for PreprocessingOptions {\n    fn default() -> Self {\n        Self {\n            enabled: true,\n            preset: PreprocessingPreset::default(),\n            remove_navigation: true,\n            remove_forms: true,\n        }\n    }\n}\n\nimpl PreprocessingOptions {\n    /// Apply a partial update to these preprocessing options.\n    ///\n    /// Any specified fields in the update will override the current values.\n    /// Unspecified fields (None) are left unchanged.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    #[allow(clippy::needless_pass_by_value)]\n    pub const fn apply_update(&mut self, update: PreprocessingOptionsUpdate) {\n        if let Some(enabled) = update.enabled {\n            self.enabled = enabled;\n        }\n        if let Some(preset) = update.preset {\n            self.preset = preset;\n        }\n        if let Some(remove_navigation) = update.remove_navigation {\n            self.remove_navigation = remove_navigation;\n        }\n        if let Some(remove_forms) = update.remove_forms {\n            self.remove_forms = remove_forms;\n        }\n    }\n\n    /// Create new preprocessing options from a partial update.\n    ///\n    /// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n    /// Fields not specified in the update keep their default values.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    ///\n    /// # Returns\n    ///\n    /// New `PreprocessingOptions` with specified updates applied to defaults\n    #[must_use]\n    pub fn from_update(update: PreprocessingOptionsUpdate) -> Self {\n        let mut options = Self::default();\n        options.apply_update(update);\n        options\n    }\n}\n\nimpl From<PreprocessingOptionsUpdate> for PreprocessingOptions {\n    fn from(update: PreprocessingOptionsUpdate) -> Self {\n        Self::from_update(update)\n    }\n}\n\n#[cfg(any(feature = \"serde\", feature = \"metadata\"))]\nmod serde_impls {\n    use super::PreprocessingPreset;\n    use serde::{Deserialize, Serializer};\n\n    impl<'de> Deserialize<'de> for PreprocessingPreset {\n        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n        where\n            D: serde::Deserializer<'de>,\n        {\n            let value = String::deserialize(deserializer)?;\n            Ok(Self::parse(&value))\n        }\n    }\n\n    impl serde::Serialize for PreprocessingPreset {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Minimal => \"minimal\",\n                Self::Standard => \"standard\",\n                Self::Aggressive => \"aggressive\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n}\n\n#[cfg(all(test, any(feature = \"serde\", feature = \"metadata\")))]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_preprocessing_options_serde() {\n        let options = PreprocessingOptions {\n            enabled: true,\n            preset: PreprocessingPreset::Aggressive,\n            remove_navigation: false,\n            ..Default::default()\n        };\n\n        // Serialize to JSON\n        let json = serde_json::to_string(&options).expect(\"Failed to serialize\");\n\n        // Deserialize back\n        let deserialized: PreprocessingOptions = serde_json::from_str(&json).expect(\"Failed to deserialize\");\n\n        // Verify values\n        assert!(deserialized.enabled);\n        assert_eq!(deserialized.preset, PreprocessingPreset::Aggressive);\n        assert!(!deserialized.remove_navigation);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/options/validation.rs",
    "content": "//! Validation and parsing utilities for option enums.\n//!\n//! This module provides parsing and serialization logic for configuration\n//! enums (HeadingStyle, ListIndentType, etc.) with string conversion support.\n\n/// Heading style options for Markdown output.\n///\n/// Controls how headings (h1-h6) are rendered in the output Markdown.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum HeadingStyle {\n    /// Underlined style (=== for h1, --- for h2).\n    Underlined,\n    /// ATX style (# for h1, ## for h2, etc.). Default.\n    #[default]\n    Atx,\n    /// ATX closed style (# title #, with closing hashes).\n    AtxClosed,\n}\n\nimpl HeadingStyle {\n    /// Parse a heading style from a string.\n    ///\n    /// Accepts \"atx\", \"atxclosed\", or defaults to Underlined.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"atx\" => Self::Atx,\n            \"atxclosed\" => Self::AtxClosed,\n            _ => Self::Underlined,\n        }\n    }\n}\n\n/// List indentation character type.\n///\n/// Controls whether list items are indented with spaces or tabs.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum ListIndentType {\n    /// Use spaces for indentation. Default. Width controlled by `list_indent_width`.\n    #[default]\n    Spaces,\n    /// Use tabs for indentation.\n    Tabs,\n}\n\nimpl ListIndentType {\n    /// Parse a list indentation type from a string.\n    ///\n    /// Accepts \"tabs\" or defaults to Spaces.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"tabs\" => Self::Tabs,\n            _ => Self::Spaces,\n        }\n    }\n}\n\n/// Whitespace handling strategy during conversion.\n///\n/// Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum WhitespaceMode {\n    /// Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior.\n    #[default]\n    Normalized,\n    /// Preserve all whitespace exactly as it appears in the HTML.\n    Strict,\n}\n\nimpl WhitespaceMode {\n    /// Parse a whitespace mode from a string.\n    ///\n    /// Accepts \"strict\" or defaults to Normalized.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"strict\" => Self::Strict,\n            _ => Self::Normalized,\n        }\n    }\n}\n\n/// Line break syntax in Markdown output.\n///\n/// Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum NewlineStyle {\n    /// Two trailing spaces at end of line. Default. Standard Markdown syntax.\n    #[default]\n    Spaces,\n    /// Backslash at end of line. Alternative Markdown syntax.\n    Backslash,\n}\n\nimpl NewlineStyle {\n    /// Parse a newline style from a string.\n    ///\n    /// Accepts \"backslash\" or defaults to Spaces.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"backslash\" => Self::Backslash,\n            _ => Self::Spaces,\n        }\n    }\n}\n\n/// Code block fence style in Markdown output.\n///\n/// Determines how code blocks (`<pre><code>`) are rendered in Markdown.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum CodeBlockStyle {\n    /// Indented code blocks (4 spaces). `CommonMark` standard.\n    Indented,\n    /// Fenced code blocks with backticks (```). Default (GFM). Supports language hints.\n    #[default]\n    Backticks,\n    /// Fenced code blocks with tildes (~~~). Supports language hints.\n    Tildes,\n}\n\nimpl CodeBlockStyle {\n    /// Parse a code block style from a string.\n    ///\n    /// Accepts \"backticks\", \"tildes\", or defaults to Indented.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"backticks\" => Self::Backticks,\n            \"tildes\" => Self::Tildes,\n            _ => Self::Indented,\n        }\n    }\n}\n\n/// Highlight rendering style for `<mark>` elements.\n///\n/// Controls how highlighted text is rendered in Markdown output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum HighlightStyle {\n    /// Double equals syntax (==text==). Default. Pandoc-compatible.\n    #[default]\n    DoubleEqual,\n    /// Preserve as HTML (==text==). Original HTML tag.\n    Html,\n    /// Render as bold (**text**). Uses strong emphasis.\n    Bold,\n    /// Strip formatting, render as plain text. No markup.\n    None,\n}\n\nimpl HighlightStyle {\n    /// Parse a highlight style from a string.\n    ///\n    /// Accepts \"doubleequal\", \"html\", \"bold\", \"none\", or defaults to None.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"doubleequal\" => Self::DoubleEqual,\n            \"html\" => Self::Html,\n            \"bold\" => Self::Bold,\n            \"none\" => Self::None,\n            _ => Self::None,\n        }\n    }\n}\n\n/// Link rendering style in Markdown output.\n///\n/// Controls whether links and images use inline `[text](url)` syntax or\n/// reference-style `[text][1]` syntax with definitions collected at the end.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum LinkStyle {\n    /// Inline links: `[text](url)`. Default.\n    #[default]\n    Inline,\n    /// Reference-style links: `[text][1]` with `[1]: url` at end of document.\n    Reference,\n}\n\nimpl LinkStyle {\n    /// Parse a link style from a string.\n    ///\n    /// Accepts \"reference\" or defaults to Inline.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"reference\" => Self::Reference,\n            _ => Self::Inline,\n        }\n    }\n}\n\n/// Output format for conversion.\n///\n/// Specifies the target markup language format for the conversion output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum OutputFormat {\n    /// Standard Markdown (CommonMark compatible). Default.\n    #[default]\n    Markdown,\n    /// Djot lightweight markup language.\n    Djot,\n    /// Plain text output (no markup, visible text only).\n    Plain,\n}\n\nimpl OutputFormat {\n    /// Parse an output format from a string.\n    ///\n    /// Accepts \"djot\" or defaults to Markdown.\n    /// Input is normalized (lowercased, alphanumeric only).\n    #[must_use]\n    pub fn parse(value: &str) -> Self {\n        match normalize_token(value).as_str() {\n            \"djot\" => Self::Djot,\n            \"plain\" | \"plaintext\" | \"text\" => Self::Plain,\n            _ => Self::Markdown,\n        }\n    }\n}\n\n/// Normalize a configuration string by lowercasing and removing non-alphanumeric characters.\npub(crate) fn normalize_token(value: &str) -> String {\n    let mut out = String::with_capacity(value.len());\n    for ch in value.chars() {\n        if ch.is_ascii_alphanumeric() {\n            out.push(ch.to_ascii_lowercase());\n        }\n    }\n    out\n}\n\n#[cfg(any(feature = \"serde\", feature = \"metadata\"))]\nmod serde_impls {\n    use super::{\n        CodeBlockStyle, HeadingStyle, HighlightStyle, LinkStyle, ListIndentType, NewlineStyle, OutputFormat,\n        WhitespaceMode,\n    };\n    use serde::{Deserialize, Serialize, Serializer};\n\n    macro_rules! impl_deserialize_from_parse {\n        ($ty:ty, $parser:expr) => {\n            impl<'de> Deserialize<'de> for $ty {\n                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n                where\n                    D: serde::Deserializer<'de>,\n                {\n                    let value = String::deserialize(deserializer)?;\n                    Ok($parser(&value))\n                }\n            }\n        };\n    }\n\n    impl_deserialize_from_parse!(HeadingStyle, HeadingStyle::parse);\n    impl_deserialize_from_parse!(ListIndentType, ListIndentType::parse);\n    impl_deserialize_from_parse!(WhitespaceMode, WhitespaceMode::parse);\n    impl_deserialize_from_parse!(NewlineStyle, NewlineStyle::parse);\n    impl_deserialize_from_parse!(CodeBlockStyle, CodeBlockStyle::parse);\n    impl_deserialize_from_parse!(HighlightStyle, HighlightStyle::parse);\n    impl_deserialize_from_parse!(LinkStyle, LinkStyle::parse);\n    impl_deserialize_from_parse!(OutputFormat, OutputFormat::parse);\n\n    // Serialize implementations that convert enum variants to their string representations\n    impl Serialize for HeadingStyle {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Underlined => \"underlined\",\n                Self::Atx => \"atx\",\n                Self::AtxClosed => \"atxclosed\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for ListIndentType {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Spaces => \"spaces\",\n                Self::Tabs => \"tabs\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for WhitespaceMode {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Normalized => \"normalized\",\n                Self::Strict => \"strict\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for NewlineStyle {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Spaces => \"spaces\",\n                Self::Backslash => \"backslash\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for CodeBlockStyle {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Indented => \"indented\",\n                Self::Backticks => \"backticks\",\n                Self::Tildes => \"tildes\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for HighlightStyle {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::DoubleEqual => \"doubleequal\",\n                Self::Html => \"html\",\n                Self::Bold => \"bold\",\n                Self::None => \"none\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for LinkStyle {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Inline => \"inline\",\n                Self::Reference => \"reference\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n\n    impl Serialize for OutputFormat {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: Serializer,\n        {\n            let s = match self {\n                Self::Markdown => \"markdown\",\n                Self::Djot => \"djot\",\n                Self::Plain => \"plain\",\n            };\n            serializer.serialize_str(s)\n        }\n    }\n}\n\n#[cfg(all(test, any(feature = \"serde\", feature = \"metadata\")))]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_enum_serialization() {\n        // Test that enums serialize to lowercase strings\n        let heading = HeadingStyle::AtxClosed;\n        let json = serde_json::to_string(&heading).expect(\"Failed to serialize\");\n        assert_eq!(json, r#\"\"atxclosed\"\"#);\n\n        let list_indent = ListIndentType::Tabs;\n        let json = serde_json::to_string(&list_indent).expect(\"Failed to serialize\");\n        assert_eq!(json, r#\"\"tabs\"\"#);\n\n        let whitespace = WhitespaceMode::Strict;\n        let json = serde_json::to_string(&whitespace).expect(\"Failed to serialize\");\n        assert_eq!(json, r#\"\"strict\"\"#);\n    }\n\n    #[test]\n    fn test_enum_deserialization() {\n        // Test that enums deserialize from strings (case insensitive)\n        let heading: HeadingStyle = serde_json::from_str(r#\"\"atxclosed\"\"#).expect(\"Failed\");\n        assert_eq!(heading, HeadingStyle::AtxClosed);\n\n        let heading: HeadingStyle = serde_json::from_str(r#\"\"ATXCLOSED\"\"#).expect(\"Failed\");\n        assert_eq!(heading, HeadingStyle::AtxClosed);\n\n        let list_indent: ListIndentType = serde_json::from_str(r#\"\"tabs\"\"#).expect(\"Failed\");\n        assert_eq!(list_indent, ListIndentType::Tabs);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/prelude.rs",
    "content": "//! Prelude module for convenient internal imports.\n"
  },
  {
    "path": "crates/html-to-markdown/src/rcdom.rs",
    "content": "// Vendored from markup5ever_rcdom v0.36.0+unofficial\n// Original source: https://github.com/servo/html5ever (rcdom/)\n// Copyright (c) 2014 The html5ever Project Developers\n// Licensed under MIT OR Apache-2.0 (see ATTRIBUTIONS.md)\n//\n// Vendored to:\n// - Remove unused xml5ever transitive dependency\n// - Eliminate pinned external dependency on \"+unofficial\" crate\n// - Gain full control over this small, critical module\n//\n// Changes from upstream:\n// - Replaced `extern crate markup5ever` / `extern crate tendril` with\n//   `use` imports through `html5ever` (edition 2024 compatibility)\n// - Added module-level clippy allows for vendored code style\n\n#![allow(\n    clippy::panic,\n    clippy::expect_used,\n    clippy::missing_panics_doc,\n    clippy::must_use_candidate,\n    clippy::return_self_not_must_use,\n    clippy::module_name_repetitions,\n    clippy::redundant_else,\n    clippy::match_wildcard_for_single_variants,\n    clippy::similar_names,\n    clippy::items_after_statements,\n    clippy::use_self,\n    clippy::missing_fields_in_debug,\n    clippy::semicolon_if_nothing_returned,\n    missing_docs\n)]\n\n//! A simple reference-counted DOM.\n//!\n//! This is sufficient as a static parse tree, but don't build a\n//! web browser using it. :)\n\nuse std::borrow::Cow;\nuse std::cell::{Cell, RefCell};\nuse std::collections::{HashSet, VecDeque};\nuse std::default::Default;\nuse std::fmt;\nuse std::io;\nuse std::mem;\nuse std::rc::{Rc, Weak};\n\nuse html5ever::tendril::StrTendril;\n\nuse html5ever::Attribute;\nuse html5ever::ExpandedName;\nuse html5ever::QualName;\nuse html5ever::interface::tree_builder;\nuse html5ever::interface::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink};\nuse html5ever::serialize::TraversalScope;\nuse html5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode};\nuse html5ever::serialize::{Serialize, Serializer};\n\n/// The different kinds of nodes in the DOM.\n#[derive(Debug)]\npub enum NodeData {\n    /// The `Document` itself - the root node of a HTML document.\n    Document,\n\n    /// A `DOCTYPE` with name, public id, and system id. See\n    /// [document type declaration on wikipedia][dtd wiki].\n    ///\n    /// [dtd wiki]: https://en.wikipedia.org/wiki/Document_type_declaration\n    Doctype {\n        name: StrTendril,\n        // Fields required by html5ever's DOM model; not accessed during conversion.\n        #[allow(dead_code)]\n        public_id: StrTendril,\n        #[allow(dead_code)]\n        system_id: StrTendril,\n    },\n\n    /// A text node.\n    Text { contents: RefCell<StrTendril> },\n\n    /// A comment.\n    Comment { contents: StrTendril },\n\n    /// An element with attributes.\n    Element {\n        name: QualName,\n        attrs: RefCell<Vec<Attribute>>,\n\n        /// For HTML \\<template\\> elements, the [template contents].\n        ///\n        /// [template contents]: https://html.spec.whatwg.org/multipage/#template-contents\n        template_contents: RefCell<Option<Handle>>,\n\n        /// Whether the node is a [HTML integration point].\n        ///\n        /// [HTML integration point]: https://html.spec.whatwg.org/multipage/#html-integration-point\n        mathml_annotation_xml_integration_point: bool,\n    },\n\n    /// A Processing instruction.\n    ProcessingInstruction { target: StrTendril, contents: StrTendril },\n}\n\n/// A DOM node.\npub struct Node {\n    /// Parent node.\n    pub parent: Cell<Option<WeakHandle>>,\n    /// Child nodes of this node.\n    pub children: RefCell<Vec<Handle>>,\n    /// Represents this node's data.\n    pub data: NodeData,\n}\n\nimpl Node {\n    /// Create a new node from its contents\n    pub fn new(data: NodeData) -> Rc<Self> {\n        Rc::new(Node {\n            data,\n            parent: Cell::new(None),\n            children: RefCell::new(Vec::new()),\n        })\n    }\n}\n\nimpl Drop for Node {\n    fn drop(&mut self) {\n        let mut nodes = mem::take(&mut *self.children.borrow_mut());\n        while let Some(node) = nodes.pop() {\n            let children = mem::take(&mut *node.children.borrow_mut());\n            nodes.extend(children);\n            if let NodeData::Element {\n                ref template_contents, ..\n            } = node.data\n            {\n                if let Some(template_contents) = template_contents.borrow_mut().take() {\n                    nodes.push(template_contents);\n                }\n            }\n        }\n    }\n}\n\nimpl fmt::Debug for Node {\n    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {\n        fmt.debug_struct(\"Node\")\n            .field(\"data\", &self.data)\n            .field(\"children\", &self.children)\n            .finish()\n    }\n}\n\n/// Reference to a DOM node.\npub type Handle = Rc<Node>;\n\n/// Weak reference to a DOM node, used for parent pointers.\npub type WeakHandle = Weak<Node>;\n\n/// Append a parentless node to another nodes' children\nfn append(new_parent: &Handle, child: Handle) {\n    let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent)));\n    // Invariant: child cannot have existing parent\n    assert!(previous_parent.is_none());\n    new_parent.children.borrow_mut().push(child);\n}\n\n/// If the node has a parent, get it and this node's position in its children\nfn get_parent_and_index(target: &Handle) -> Option<(Handle, usize)> {\n    if let Some(weak) = target.parent.take() {\n        let parent = weak.upgrade().expect(\"dangling weak pointer\");\n        target.parent.set(Some(weak));\n        let i = match parent\n            .children\n            .borrow()\n            .iter()\n            .enumerate()\n            .find(|&(_, child)| Rc::ptr_eq(child, target))\n        {\n            Some((i, _)) => i,\n            None => panic!(\"have parent but couldn't find in parent's children!\"),\n        };\n        Some((parent, i))\n    } else {\n        None\n    }\n}\n\nfn append_to_existing_text(prev: &Handle, text: &str) -> bool {\n    match prev.data {\n        NodeData::Text { ref contents } => {\n            contents.borrow_mut().push_slice(text);\n            true\n        }\n        _ => false,\n    }\n}\n\nfn remove_from_parent(target: &Handle) {\n    if let Some((parent, i)) = get_parent_and_index(target) {\n        parent.children.borrow_mut().remove(i);\n        target.parent.set(None);\n    }\n}\n\n/// The DOM itself; the result of parsing.\npub struct RcDom {\n    /// The `Document` itself.\n    pub document: Handle,\n\n    /// Errors that occurred during parsing.\n    pub errors: RefCell<Vec<Cow<'static, str>>>,\n\n    /// The document's quirks mode.\n    pub quirks_mode: Cell<QuirksMode>,\n}\n\nimpl TreeSink for RcDom {\n    type Output = Self;\n    fn finish(self) -> Self {\n        self\n    }\n\n    type Handle = Handle;\n\n    type ElemName<'a>\n        = ExpandedName<'a>\n    where\n        Self: 'a;\n\n    fn parse_error(&self, msg: Cow<'static, str>) {\n        self.errors.borrow_mut().push(msg);\n    }\n\n    fn get_document(&self) -> Handle {\n        self.document.clone()\n    }\n\n    fn get_template_contents(&self, target: &Handle) -> Handle {\n        if let NodeData::Element {\n            ref template_contents, ..\n        } = target.data\n        {\n            template_contents\n                .borrow()\n                .as_ref()\n                .expect(\"not a template element!\")\n                .clone()\n        } else {\n            panic!(\"not a template element!\")\n        }\n    }\n\n    fn set_quirks_mode(&self, mode: QuirksMode) {\n        self.quirks_mode.set(mode);\n    }\n\n    fn same_node(&self, x: &Handle, y: &Handle) -> bool {\n        Rc::ptr_eq(x, y)\n    }\n\n    fn elem_name<'a>(&self, target: &'a Handle) -> ExpandedName<'a> {\n        match target.data {\n            NodeData::Element { ref name, .. } => name.expanded(),\n            _ => panic!(\"not an element!\"),\n        }\n    }\n\n    fn create_element(&self, name: QualName, attrs: Vec<Attribute>, flags: ElementFlags) -> Handle {\n        Node::new(NodeData::Element {\n            name,\n            attrs: RefCell::new(attrs),\n            template_contents: RefCell::new(if flags.template {\n                Some(Node::new(NodeData::Document))\n            } else {\n                None\n            }),\n            mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point,\n        })\n    }\n\n    fn create_comment(&self, text: StrTendril) -> Handle {\n        Node::new(NodeData::Comment { contents: text })\n    }\n\n    fn create_pi(&self, target: StrTendril, data: StrTendril) -> Handle {\n        Node::new(NodeData::ProcessingInstruction { target, contents: data })\n    }\n\n    fn append(&self, parent: &Handle, child: NodeOrText<Handle>) {\n        // Append to an existing Text node if we have one.\n        if let NodeOrText::AppendText(text) = &child {\n            if let Some(h) = parent.children.borrow().last() {\n                if append_to_existing_text(h, text) {\n                    return;\n                }\n            }\n        }\n\n        append(\n            parent,\n            match child {\n                NodeOrText::AppendText(text) => Node::new(NodeData::Text {\n                    contents: RefCell::new(text),\n                }),\n                NodeOrText::AppendNode(node) => node,\n            },\n        );\n    }\n\n    fn append_before_sibling(&self, sibling: &Handle, child: NodeOrText<Handle>) {\n        let (parent, i) = get_parent_and_index(sibling).expect(\"append_before_sibling called on node without parent\");\n\n        let child = match (child, i) {\n            // No previous node.\n            (NodeOrText::AppendText(text), 0) => Node::new(NodeData::Text {\n                contents: RefCell::new(text),\n            }),\n\n            // Look for a text node before the insertion point.\n            (NodeOrText::AppendText(text), i) => {\n                let children = parent.children.borrow();\n                let prev = &children[i - 1];\n                if append_to_existing_text(prev, &text) {\n                    return;\n                }\n                Node::new(NodeData::Text {\n                    contents: RefCell::new(text),\n                })\n            }\n\n            // The tree builder promises we won't have a text node after\n            // the insertion point.\n\n            // Any other kind of node.\n            (NodeOrText::AppendNode(node), _) => node,\n        };\n\n        remove_from_parent(&child);\n\n        child.parent.set(Some(Rc::downgrade(&parent)));\n        parent.children.borrow_mut().insert(i, child);\n    }\n\n    fn append_based_on_parent_node(\n        &self,\n        element: &Self::Handle,\n        prev_element: &Self::Handle,\n        child: NodeOrText<Self::Handle>,\n    ) {\n        let parent = element.parent.take();\n        let has_parent = parent.is_some();\n        element.parent.set(parent);\n\n        if has_parent {\n            self.append_before_sibling(element, child);\n        } else {\n            self.append(prev_element, child);\n        }\n    }\n\n    fn append_doctype_to_document(&self, name: StrTendril, public_id: StrTendril, system_id: StrTendril) {\n        append(\n            &self.document,\n            Node::new(NodeData::Doctype {\n                name,\n                public_id,\n                system_id,\n            }),\n        );\n    }\n\n    fn add_attrs_if_missing(&self, target: &Handle, attrs: Vec<Attribute>) {\n        let mut existing = if let NodeData::Element { ref attrs, .. } = target.data {\n            attrs.borrow_mut()\n        } else {\n            panic!(\"not an element\")\n        };\n\n        let existing_names = existing.iter().map(|e| e.name.clone()).collect::<HashSet<_>>();\n        existing.extend(attrs.into_iter().filter(|attr| !existing_names.contains(&attr.name)));\n    }\n\n    fn remove_from_parent(&self, target: &Handle) {\n        remove_from_parent(target);\n    }\n\n    fn reparent_children(&self, node: &Handle, new_parent: &Handle) {\n        let mut children = node.children.borrow_mut();\n        let mut new_children = new_parent.children.borrow_mut();\n        for child in children.iter() {\n            let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent)));\n            assert!(Rc::ptr_eq(\n                node,\n                &previous_parent\n                    .expect(\"invariant: child must have a parent during reparenting\")\n                    .upgrade()\n                    .expect(\"dangling weak\")\n            ))\n        }\n        new_children.extend(mem::take(&mut *children));\n    }\n\n    fn is_mathml_annotation_xml_integration_point(&self, target: &Handle) -> bool {\n        if let NodeData::Element {\n            mathml_annotation_xml_integration_point,\n            ..\n        } = target.data\n        {\n            mathml_annotation_xml_integration_point\n        } else {\n            panic!(\"not an element!\")\n        }\n    }\n}\n\nimpl Default for RcDom {\n    fn default() -> RcDom {\n        RcDom {\n            document: Node::new(NodeData::Document),\n            errors: Default::default(),\n            quirks_mode: Cell::new(tree_builder::NoQuirks),\n        }\n    }\n}\n\nenum SerializeOp {\n    Open(Handle),\n    Close(QualName),\n}\n\npub struct SerializableHandle(Handle);\n\nimpl From<Handle> for SerializableHandle {\n    fn from(h: Handle) -> SerializableHandle {\n        SerializableHandle(h)\n    }\n}\n\nimpl Serialize for SerializableHandle {\n    fn serialize<S>(&self, serializer: &mut S, traversal_scope: TraversalScope) -> io::Result<()>\n    where\n        S: Serializer,\n    {\n        let mut ops = VecDeque::new();\n        match traversal_scope {\n            IncludeNode => ops.push_back(SerializeOp::Open(self.0.clone())),\n            ChildrenOnly(_) => ops.extend(self.0.children.borrow().iter().map(|h| SerializeOp::Open(h.clone()))),\n        }\n\n        while let Some(op) = ops.pop_front() {\n            match op {\n                SerializeOp::Open(handle) => match handle.data {\n                    NodeData::Element {\n                        ref name, ref attrs, ..\n                    } => {\n                        serializer\n                            .start_elem(name.clone(), attrs.borrow().iter().map(|at| (&at.name, &at.value[..])))?;\n\n                        ops.reserve(1 + handle.children.borrow().len());\n                        ops.push_front(SerializeOp::Close(name.clone()));\n\n                        for child in handle.children.borrow().iter().rev() {\n                            ops.push_front(SerializeOp::Open(child.clone()));\n                        }\n                    }\n\n                    NodeData::Doctype { ref name, .. } => serializer.write_doctype(name)?,\n\n                    NodeData::Text { ref contents } => serializer.write_text(&contents.borrow())?,\n\n                    NodeData::Comment { ref contents } => serializer.write_comment(contents)?,\n\n                    NodeData::ProcessingInstruction {\n                        ref target,\n                        ref contents,\n                    } => serializer.write_processing_instruction(target, contents)?,\n\n                    NodeData::Document => panic!(\"Can't serialize Document node itself\"),\n                },\n\n                SerializeOp::Close(name) => {\n                    serializer.end_elem(name)?;\n                }\n            }\n        }\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/text.rs",
    "content": "#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]\n//! Text processing utilities for Markdown conversion.\n\nuse regex::Regex;\nuse std::borrow::Cow;\nuse std::sync::LazyLock;\n\n/// Regex for escaping miscellaneous characters\nstatic ESCAPE_MISC_RE: LazyLock<Regex> =\n    LazyLock::new(|| Regex::new(r\"([\\\\&<`\\[\\]>~#=+|\\-])\").expect(\"valid regex pattern\"));\n\n/// Regex for escaping numbered lists\nstatic ESCAPE_NUMBERED_LIST_RE: LazyLock<Regex> =\n    LazyLock::new(|| Regex::new(r\"([0-9])([.)])\").expect(\"valid regex pattern\"));\n\n/// Regex for escaping ASCII punctuation (CommonMark spec example 12)\n/// Matches: `! \" # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \\ ] ^ _ \\` { | } ~`\nstatic ESCAPE_ASCII_RE: LazyLock<Regex> =\n    LazyLock::new(|| Regex::new(r\"([!\\x22#$%&\\x27()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~])\").expect(\"valid regex pattern\"));\n\n/// Escape Markdown special characters in text.\n///\n/// # Arguments\n///\n/// * `text` - Text to escape\n/// * `escape_misc` - Escape miscellaneous characters (`\\` `&` `<` `` ` `` `[` `>` `~` `#` `=` `+` `|` `-`)\n/// * `escape_asterisks` - Escape asterisks (`*`)\n/// * `escape_underscores` - Escape underscores (`_`)\n/// * `escape_ascii` - Escape all ASCII punctuation (for `CommonMark` spec compliance)\n///\n/// # Returns\n///\n/// Escaped text\n#[allow(clippy::fn_params_excessive_bools)]\npub fn escape(\n    text: &str,\n    escape_misc: bool,\n    escape_asterisks: bool,\n    escape_underscores: bool,\n    escape_ascii: bool,\n) -> Cow<'_, str> {\n    if text.is_empty() {\n        return Cow::Borrowed(\"\");\n    }\n\n    if !escape_misc && !escape_asterisks && !escape_underscores && !escape_ascii {\n        return Cow::Borrowed(text);\n    }\n\n    if escape_ascii\n        && !text.as_bytes().iter().any(|b| {\n            matches!(\n                b,\n                b'!' | b'\"'\n                    | b'#'\n                    | b'$'\n                    | b'%'\n                    | b'&'\n                    | b'\\''\n                    | b'('\n                    | b')'\n                    | b'*'\n                    | b'+'\n                    | b','\n                    | b'-'\n                    | b'.'\n                    | b'/'\n                    | b':'\n                    | b';'\n                    | b'<'\n                    | b'='\n                    | b'>'\n                    | b'?'\n                    | b'@'\n                    | b'['\n                    | b'\\\\'\n                    | b']'\n                    | b'^'\n                    | b'_'\n                    | b'`'\n                    | b'{'\n                    | b'|'\n                    | b'}'\n                    | b'~'\n            )\n        })\n    {\n        return Cow::Borrowed(text);\n    }\n\n    if !escape_ascii && escape_misc && !escape_asterisks && !escape_underscores {\n        let needs_misc = text.as_bytes().iter().any(|b| {\n            matches!(\n                b,\n                b'\\\\' | b'&' | b'<' | b'`' | b'[' | b']' | b'>' | b'~' | b'#' | b'=' | b'+' | b'|' | b'-'\n            )\n        });\n        let needs_numbered = text.as_bytes().iter().any(|b| matches!(b, b'.' | b')'));\n        if !needs_misc && !needs_numbered {\n            return Cow::Borrowed(text);\n        }\n    }\n\n    let mut result: Cow<'_, str> = Cow::Borrowed(text);\n\n    if escape_ascii {\n        result = match ESCAPE_ASCII_RE.replace_all(result.as_ref(), r\"\\$1\") {\n            Cow::Borrowed(_) => result,\n            Cow::Owned(s) => Cow::Owned(s),\n        };\n        return result;\n    }\n\n    if escape_misc {\n        result = match ESCAPE_MISC_RE.replace_all(result.as_ref(), r\"\\$1\") {\n            Cow::Borrowed(_) => result,\n            Cow::Owned(s) => Cow::Owned(s),\n        };\n\n        result = match ESCAPE_NUMBERED_LIST_RE.replace_all(result.as_ref(), r\"$1\\$2\") {\n            Cow::Borrowed(_) => result,\n            Cow::Owned(s) => Cow::Owned(s),\n        };\n    }\n\n    if escape_asterisks && result.contains('*') {\n        result = Cow::Owned(result.replace('*', r\"\\*\"));\n    }\n\n    if escape_underscores && result.contains('_') {\n        result = Cow::Owned(result.replace('_', r\"\\_\"));\n    }\n\n    result\n}\n\n/// Extract boundary whitespace from text (chomp).\n///\n/// Returns (prefix, suffix, `trimmed_text`) tuple.\n/// Prefix/suffix are \" \" if original text had leading/trailing whitespace.\n/// However, suffix is \"\" if the trailing whitespace is only newlines (not spaces/tabs).\n/// This prevents trailing newlines from becoming trailing spaces in the output.\n/// The trimmed text has all leading/trailing whitespace removed.\n#[must_use]\npub fn chomp(text: &str) -> (&str, &str, &str) {\n    if text.is_empty() {\n        return (\"\", \"\", \"\");\n    }\n\n    let prefix = if text.starts_with(|c: char| c.is_whitespace()) {\n        \" \"\n    } else {\n        \"\"\n    };\n\n    let suffix = if text.ends_with(\"\\n\\n\") || text.ends_with(\"\\r\\n\\r\\n\") {\n        \"\\n\\n\"\n    } else if text.ends_with([' ', '\\t']) {\n        \" \"\n    } else {\n        \"\"\n    };\n\n    let trimmed = if suffix == \"\\n\\n\" {\n        text.trim_end_matches(\"\\n\\n\").trim_end_matches(\"\\r\\n\\r\\n\").trim()\n    } else {\n        text.trim()\n    };\n\n    (prefix, suffix, trimmed)\n}\n\n/// Normalize whitespace by collapsing consecutive spaces and tabs.\n///\n/// Multiple spaces and tabs are replaced with a single space.\n/// Newlines are preserved.\n/// Unicode spaces are normalized to ASCII spaces.\n///\n/// # Arguments\n///\n/// * `text` - The text to normalize\n///\n/// # Returns\n///\n/// Normalized text with collapsed spaces/tabs but preserved newlines\n#[must_use]\npub fn normalize_whitespace(text: &str) -> String {\n    let mut result = String::with_capacity(text.len());\n    let mut prev_was_space = false;\n\n    for ch in text.chars() {\n        let is_space = ch == ' ' || ch == '\\t' || is_unicode_space(ch);\n\n        if is_space {\n            if !prev_was_space {\n                result.push(' ');\n                prev_was_space = true;\n            }\n        } else {\n            result.push(ch);\n            prev_was_space = false;\n        }\n    }\n\n    result\n}\n\n/// Normalize whitespace in text, returning borrowed or owned result as needed.\n///\n/// This function optimizes memory by returning a borrowed reference when no normalization\n/// is needed, and only allocating a new string when whitespace changes are necessary.\n///\n/// Multiple consecutive spaces, tabs, and Unicode space characters are replaced with\n/// a single ASCII space. Newlines are preserved as-is.\n///\n/// # Arguments\n///\n/// * `text` - The text to normalize\n///\n/// # Returns\n///\n/// `Cow::Borrowed` if text is already normalized, or `Cow::Owned` with normalized text\n#[must_use]\npub fn normalize_whitespace_cow(text: &str) -> Cow<'_, str> {\n    let mut prev_was_space = false;\n\n    for ch in text.chars() {\n        let is_space = ch == ' ' || ch == '\\t' || is_unicode_space(ch);\n        if is_space {\n            if prev_was_space || ch != ' ' {\n                return Cow::Owned(normalize_whitespace(text));\n            }\n            prev_was_space = true;\n        } else {\n            prev_was_space = false;\n        }\n    }\n\n    Cow::Borrowed(text)\n}\n\n/// Decode common HTML entities.\n///\n/// Decodes the most common HTML entities to their character equivalents:\n/// - `&quot;` → `\"`\n/// - `&apos;` → `'`\n/// - `&lt;` → `<`\n/// - `&gt;` → `>`\n/// - `&amp;` → `&` (must be last to avoid double-decoding)\n///\n/// # Arguments\n///\n/// * `text` - Text containing HTML entities\n///\n/// # Returns\n///\n/// Text with entities decoded\n#[must_use]\npub fn decode_html_entities(text: &str) -> String {\n    html_escape::decode_html_entities(text).into_owned()\n}\n\n/// Decode HTML entities in text, returning borrowed or owned result as needed.\n///\n/// This function optimizes memory by returning a borrowed reference when no HTML\n/// entities are present, and only allocating a new string when entity decoding\n/// is necessary.\n///\n/// Decodes common HTML entities like:\n/// - `&quot;` → `\"`\n/// - `&apos;` → `'`\n/// - `&lt;` → `<`\n/// - `&gt;` → `>`\n/// - `&amp;` → `&` (decoded last to avoid double-decoding)\n///\n/// # Arguments\n///\n/// * `text` - Text potentially containing HTML entities\n///\n/// # Returns\n///\n/// `Cow::Borrowed` if no entities found, or `Cow::Owned` with entities decoded\n#[must_use]\npub fn decode_html_entities_cow(text: &str) -> Cow<'_, str> {\n    if !text.contains('&') {\n        return Cow::Borrowed(text);\n    }\n\n    html_escape::decode_html_entities(text)\n}\n\n/// Check if a character is a unicode space character.\n///\n/// Includes: non-breaking space, various width spaces, etc.\nconst fn is_unicode_space(ch: char) -> bool {\n    matches!(\n        ch,\n        '\\u{00A0}'\n            | '\\u{1680}'\n            | '\\u{2000}'\n            | '\\u{2001}'\n            | '\\u{2002}'\n            | '\\u{2003}'\n            | '\\u{2004}'\n            | '\\u{2005}'\n            | '\\u{2006}'\n            | '\\u{2007}'\n            | '\\u{2008}'\n            | '\\u{2009}'\n            | '\\u{200A}'\n            | '\\u{202F}'\n            | '\\u{205F}'\n            | '\\u{3000}'\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_escape_misc() {\n        assert_eq!(escape(\"foo & bar\", true, false, false, false), r\"foo \\& bar\");\n        assert_eq!(escape(\"foo [bar]\", true, false, false, false), r\"foo \\[bar\\]\");\n        assert_eq!(escape(\"1. Item\", true, false, false, false), r\"1\\. Item\");\n        assert_eq!(escape(\"1) Item\", true, false, false, false), r\"1\\) Item\");\n    }\n\n    #[test]\n    fn test_escape_asterisks() {\n        assert_eq!(escape(\"foo * bar\", false, true, false, false), r\"foo \\* bar\");\n        assert_eq!(escape(\"**bold**\", false, true, false, false), r\"\\*\\*bold\\*\\*\");\n    }\n\n    #[test]\n    fn test_escape_underscores() {\n        assert_eq!(escape(\"foo_bar\", false, false, true, false), r\"foo\\_bar\");\n        assert_eq!(escape(\"__bold__\", false, false, true, false), r\"\\_\\_bold\\_\\_\");\n    }\n\n    #[test]\n    fn test_escape_ascii() {\n        assert_eq!(escape(r##\"!\"#$%&\"##, false, false, false, true), r#\"\\!\\\"\\#\\$\\%\\&\"#);\n        assert_eq!(escape(\"*+,-./\", false, false, false, true), r\"\\*\\+\\,\\-\\.\\/\");\n        assert_eq!(escape(\"<=>?@\", false, false, false, true), r\"\\<\\=\\>\\?\\@\");\n        assert_eq!(escape(r\"[\\]^_`\", false, false, false, true), r\"\\[\\\\\\]\\^\\_\\`\");\n        assert_eq!(escape(\"{|}~\", false, false, false, true), r\"\\{\\|\\}\\~\");\n    }\n\n    #[test]\n    fn test_chomp() {\n        assert_eq!(chomp(\"  text  \"), (\" \", \" \", \"text\"));\n        assert_eq!(chomp(\"text\"), (\"\", \"\", \"text\"));\n        assert_eq!(chomp(\" text\"), (\" \", \"\", \"text\"));\n        assert_eq!(chomp(\"text \"), (\"\", \" \", \"text\"));\n        assert_eq!(chomp(\"\"), (\"\", \"\", \"\"));\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/document.rs",
    "content": "//! Structured document tree types aligned with kreuzberg's `DocumentStructure`.\n\nuse std::collections::HashMap;\n\n#[cfg(feature = \"serde\")]\nuse serde::{Deserialize, Serialize};\n\nuse super::tables::TableGrid;\n\n/// A structured document tree representing the semantic content of an HTML document.\n///\n/// Uses a flat node array with index-based parent/child references for efficient traversal.\n#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct DocumentStructure {\n    /// All nodes in document reading order.\n    pub nodes: Vec<DocumentNode>,\n    /// The source format (always \"html\" for this crate).\n    #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n    pub source_format: Option<String>,\n}\n\n/// A single node in the document tree.\n#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct DocumentNode {\n    /// Deterministic node identifier.\n    pub id: String,\n    /// The semantic content of this node.\n    pub content: NodeContent,\n    /// Index of the parent node (None for root nodes).\n    #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n    pub parent: Option<u32>,\n    /// Indices of child nodes in reading order.\n    #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Vec::is_empty\", default))]\n    pub children: Vec<u32>,\n    /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n    #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Vec::is_empty\", default))]\n    pub annotations: Vec<TextAnnotation>,\n    /// Format-specific attributes (e.g. class, id, data-* attributes).\n    #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n    pub attributes: Option<HashMap<String, String>>,\n}\n\n/// The semantic content type of a document node.\n///\n/// Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\n#[cfg_attr(feature = \"serde\", serde(tag = \"node_type\", rename_all = \"snake_case\"))]\npub enum NodeContent {\n    /// A heading element (h1-h6).\n    Heading {\n        /// Heading level (1-6).\n        level: u8,\n        /// The heading text content.\n        text: String,\n    },\n    /// A paragraph of text.\n    Paragraph {\n        /// The paragraph text content.\n        text: String,\n    },\n    /// A list container (ordered or unordered). Children are `ListItem` nodes.\n    List {\n        /// Whether this is an ordered list.\n        ordered: bool,\n    },\n    /// A single list item.\n    ListItem {\n        /// The list item text content.\n        text: String,\n    },\n    /// A table with structured cell data.\n    Table {\n        /// The table grid structure.\n        grid: TableGrid,\n    },\n    /// An image element.\n    Image {\n        /// Alt text or caption.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        description: Option<String>,\n        /// Image source URL.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        src: Option<String>,\n        /// Index into `ConversionResult.images` when image extraction is enabled.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        image_index: Option<u32>,\n    },\n    /// A code block or inline code.\n    Code {\n        /// The code text content.\n        text: String,\n        /// Programming language (from class=\"language-*\" or similar).\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        language: Option<String>,\n    },\n    /// A block quote container.\n    Quote,\n    /// A definition list container.\n    DefinitionList,\n    /// A definition list entry with term and description.\n    DefinitionItem {\n        /// The term being defined.\n        term: String,\n        /// The definition text.\n        definition: String,\n    },\n    /// A raw block preserved as-is (e.g. `<script>`, `<style>` content).\n    RawBlock {\n        /// The format of the raw content (e.g. \"html\", \"css\", \"javascript\").\n        format: String,\n        /// The raw content.\n        content: String,\n    },\n    /// A block of key-value metadata pairs (from `<head>` meta tags).\n    MetadataBlock {\n        /// Key-value metadata pairs.\n        entries: Vec<(String, String)>,\n    },\n    /// A section grouping container (auto-generated from heading hierarchy).\n    Group {\n        /// Optional section label.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        label: Option<String>,\n        /// The heading level that created this group.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        heading_level: Option<u8>,\n        /// The heading text that created this group.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        heading_text: Option<String>,\n    },\n}\n\n/// An inline text annotation with byte-range offsets.\n///\n/// Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct TextAnnotation {\n    /// Start byte offset (inclusive) into the parent node's text.\n    pub start: u32,\n    /// End byte offset (exclusive) into the parent node's text.\n    pub end: u32,\n    /// The type of annotation.\n    pub kind: AnnotationKind,\n}\n\n/// The type of an inline text annotation.\n///\n/// Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\n#[cfg_attr(feature = \"serde\", serde(tag = \"annotation_type\", rename_all = \"snake_case\"))]\npub enum AnnotationKind {\n    /// Bold / strong emphasis.\n    #[default]\n    Bold,\n    /// Italic / emphasis.\n    Italic,\n    /// Underline.\n    Underline,\n    /// Strikethrough / deleted text.\n    Strikethrough,\n    /// Inline code.\n    Code,\n    /// Subscript text.\n    Subscript,\n    /// Superscript text.\n    Superscript,\n    /// Highlighted / marked text.\n    Highlight,\n    /// A hyperlink.\n    Link {\n        /// The link URL.\n        url: String,\n        /// Optional link title attribute.\n        #[cfg_attr(feature = \"serde\", serde(skip_serializing_if = \"Option::is_none\"))]\n        title: Option<String>,\n    },\n}\n\nimpl Default for NodeContent {\n    fn default() -> Self {\n        Self::Heading {\n            level: 1,\n            text: String::new(),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/mod.rs",
    "content": "//! Core types for structured HTML extraction results.\n//!\n//! These types are aligned with kreuzberg's `DocumentStructure` model for seamless integration.\n\nmod document;\nmod result;\npub mod structure_builder;\npub mod structure_collector;\nmod tables;\nmod warnings;\n\npub use document::{AnnotationKind, DocumentNode, DocumentStructure, NodeContent, TextAnnotation};\npub use result::ConversionResult;\npub use structure_builder::build_document_structure;\npub use structure_collector::{StructureCollector, StructureCollectorHandle};\npub use tables::{GridCell, TableData, TableGrid};\npub use warnings::{ProcessingWarning, WarningKind};\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/result.rs",
    "content": "//! The primary result type for HTML conversion and extraction.\n\n#[cfg(feature = \"serde\")]\nuse serde::{Deserialize, Serialize};\n\nuse super::document::DocumentStructure;\nuse super::tables::TableData;\nuse super::warnings::ProcessingWarning;\n\n/// The primary result of HTML conversion and extraction.\n///\n/// Contains the converted text output, optional structured document tree,\n/// metadata, extracted tables, images, and processing warnings.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::{convert, ConversionOptions};\n///\n/// let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n/// assert!(result.content.is_some());\n/// assert!(result.warnings.is_empty());\n/// ```\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct ConversionResult {\n    /// Converted text output (markdown, djot, or plain text).\n    ///\n    /// `None` when `output_format` is set to `OutputFormat::None`,\n    /// indicating extraction-only mode.\n    pub content: Option<String>,\n\n    /// Structured document tree with semantic elements.\n    ///\n    /// Populated when `include_document_structure` is `true` in options.\n    pub document: Option<DocumentStructure>,\n\n    /// Extracted HTML metadata (title, OG, links, images, structured data).\n    #[cfg(feature = \"metadata\")]\n    pub metadata: crate::metadata::HtmlMetadata,\n\n    /// Extracted tables with structured cell data and markdown representation.\n    pub tables: Vec<TableData>,\n\n    /// Extracted inline images (data URIs and SVGs).\n    ///\n    /// Populated when `extract_images` is `true` in options.\n    #[cfg(feature = \"inline-images\")]\n    #[cfg_attr(feature = \"serde\", serde(skip))]\n    pub images: Vec<crate::inline_images::InlineImage>,\n\n    /// Non-fatal processing warnings.\n    pub warnings: Vec<ProcessingWarning>,\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/structure_builder.rs",
    "content": "//! Builds a [`DocumentStructure`] from a parsed `tl::VDom`.\n//!\n//! Walk the DOM once, mapping each HTML element to the appropriate [`NodeContent`] variant,\n//! collecting inline [`TextAnnotation`]s, tracking parent/child relationships, and generating\n//! heading-based [`Group`] hierarchy.\n\nuse std::collections::HashMap;\n\nuse super::document::{AnnotationKind, DocumentNode, DocumentStructure, NodeContent, TextAnnotation};\nuse super::tables::{GridCell, TableGrid};\n\n// ── Text extraction ───────────────────────────────────────────────────────────\n\n/// Extract plain text from a tag's descendants, decoding HTML entities.\nfn extract_text(tag: &tl::HTMLTag, parser: &tl::Parser) -> String {\n    let mut buf = String::new();\n    collect_text_from_tag(tag, parser, &mut buf);\n    buf\n}\n\n/// Recursively accumulate text content from a tag's children.\nfn collect_text_from_tag(tag: &tl::HTMLTag, parser: &tl::Parser, buf: &mut String) {\n    let children = tag.children();\n    for handle in children.top().iter() {\n        let Some(node) = handle.get(parser) else {\n            continue;\n        };\n        match node {\n            tl::Node::Raw(bytes) => {\n                let raw = bytes.as_utf8_str();\n                let decoded = crate::text::decode_html_entities_cow(raw.as_ref());\n                buf.push_str(&decoded);\n            }\n            tl::Node::Tag(child_tag) => {\n                let name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n                // Skip invisible elements\n                if matches!(name.as_str(), \"script\" | \"style\" | \"head\") {\n                    continue;\n                }\n                collect_text_from_tag(child_tag, parser, buf);\n            }\n            tl::Node::Comment(_) => {}\n        }\n    }\n}\n\n// ── Inline annotation extraction ─────────────────────────────────────────────\n\n/// Scan the children of `tag` and collect [`TextAnnotation`]s into `annotations`.\n///\n/// `text` is the pre-extracted full text of the enclosing block node; annotation\n/// byte offsets are computed relative to that string.\nfn collect_annotations(tag: &tl::HTMLTag, parser: &tl::Parser, text: &str, annotations: &mut Vec<TextAnnotation>) {\n    collect_annotations_from_tag(tag, parser, text, &mut 0usize, annotations);\n}\n\n/// Recursive helper.  `offset` tracks how many bytes of `full_text` have been consumed\n/// so far; it is mutated in place as we walk the tree.\nfn collect_annotations_from_tag(\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    full_text: &str,\n    offset: &mut usize,\n    annotations: &mut Vec<TextAnnotation>,\n) {\n    let children = tag.children();\n    for handle in children.top().iter() {\n        let Some(node) = handle.get(parser) else {\n            continue;\n        };\n        match node {\n            tl::Node::Raw(bytes) => {\n                let raw = bytes.as_utf8_str();\n                let decoded = crate::text::decode_html_entities_cow(raw.as_ref());\n                *offset += decoded.len();\n            }\n            tl::Node::Tag(child_tag) => {\n                let name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n                if matches!(name.as_str(), \"script\" | \"style\" | \"head\") {\n                    continue;\n                }\n\n                let start = *offset;\n                // Recurse to advance offset over the child's text span.\n                collect_annotations_from_tag(child_tag, parser, full_text, offset, annotations);\n                let end = *offset;\n\n                // Emit annotation only for non-empty spans that fit within the text.\n                if start < end && end <= full_text.len() {\n                    let kind = match name.as_str() {\n                        \"strong\" | \"b\" => Some(AnnotationKind::Bold),\n                        \"em\" | \"i\" => Some(AnnotationKind::Italic),\n                        \"u\" | \"ins\" => Some(AnnotationKind::Underline),\n                        \"s\" | \"del\" | \"strike\" => Some(AnnotationKind::Strikethrough),\n                        \"code\" | \"kbd\" | \"samp\" => Some(AnnotationKind::Code),\n                        \"sub\" => Some(AnnotationKind::Subscript),\n                        \"sup\" => Some(AnnotationKind::Superscript),\n                        \"mark\" => Some(AnnotationKind::Highlight),\n                        \"a\" => {\n                            let url = child_tag\n                                .attributes()\n                                .get(\"href\")\n                                .flatten()\n                                .map(|v| v.as_utf8_str().to_string())\n                                .unwrap_or_default();\n                            let title = child_tag\n                                .attributes()\n                                .get(\"title\")\n                                .flatten()\n                                .map(|v| v.as_utf8_str().to_string());\n                            Some(AnnotationKind::Link { url, title })\n                        }\n                        _ => None,\n                    };\n\n                    if let Some(kind) = kind {\n                        annotations.push(TextAnnotation {\n                            start: start as u32,\n                            end: end as u32,\n                            kind,\n                        });\n                    }\n                }\n            }\n            tl::Node::Comment(_) => {}\n        }\n    }\n}\n\n// ── Table extraction ──────────────────────────────────────────────────────────\n\n/// Build a [`TableGrid`] from a `<table>` element.\nfn extract_table_grid(table_tag: &tl::HTMLTag, parser: &tl::Parser) -> TableGrid {\n    // Gather all <tr> handles (recursing through thead/tbody/tfoot).\n    let mut row_handles: Vec<tl::NodeHandle> = Vec::new();\n    collect_tr_handles(table_tag, parser, &mut row_handles);\n\n    let mut cells: Vec<GridCell> = Vec::new();\n    let mut max_col: u32 = 0;\n\n    for (row_idx, row_handle) in row_handles.iter().enumerate() {\n        let Some(tl::Node::Tag(row_tag)) = row_handle.get(parser) else {\n            continue;\n        };\n\n        let mut col_idx: u32 = 0;\n        let row_children = row_tag.children();\n\n        for child_handle in row_children.top().iter() {\n            let Some(tl::Node::Tag(cell_tag)) = child_handle.get(parser) else {\n                continue;\n            };\n            let cell_name = cell_tag.name().as_utf8_str().to_ascii_lowercase();\n            let is_cell = cell_name == \"td\" || cell_name == \"th\";\n            if !is_cell {\n                continue;\n            }\n\n            let is_header = cell_name == \"th\";\n\n            let row_span = cell_tag\n                .attributes()\n                .get(\"rowspan\")\n                .flatten()\n                .and_then(|v| v.as_utf8_str().parse::<u32>().ok())\n                .unwrap_or(1)\n                .max(1);\n\n            let col_span = cell_tag\n                .attributes()\n                .get(\"colspan\")\n                .flatten()\n                .and_then(|v| v.as_utf8_str().parse::<u32>().ok())\n                .unwrap_or(1)\n                .max(1);\n\n            let content = extract_text(cell_tag, parser).trim().to_string();\n\n            cells.push(GridCell {\n                content,\n                row: row_idx as u32,\n                col: col_idx,\n                row_span,\n                col_span,\n                is_header,\n            });\n\n            col_idx += col_span;\n            if col_idx > max_col {\n                max_col = col_idx;\n            }\n        }\n    }\n\n    let rows = row_handles.len() as u32;\n    TableGrid {\n        rows,\n        cols: max_col,\n        cells,\n    }\n}\n\n/// Recursively collect all `<tr>` `NodeHandle`s from within a table element.\nfn collect_tr_handles(tag: &tl::HTMLTag, parser: &tl::Parser, result: &mut Vec<tl::NodeHandle>) {\n    let children = tag.children();\n    for handle in children.top().iter() {\n        if let Some(tl::Node::Tag(child_tag)) = handle.get(parser) {\n            let name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n            if name == \"tr\" {\n                result.push(*handle);\n            } else {\n                collect_tr_handles(child_tag, parser, result);\n            }\n        }\n    }\n}\n\n// ── Node ID generation ────────────────────────────────────────────────────────\n\n/// Generate a deterministic node ID from the node type, an excerpt of its text content,\n/// and its position (index) in the flat node list.\nfn make_node_id(node_type: &str, text: &str, index: usize) -> String {\n    use std::collections::hash_map::DefaultHasher;\n    use std::hash::{Hash, Hasher};\n\n    let mut hasher = DefaultHasher::new();\n    node_type.hash(&mut hasher);\n    // Only hash a prefix of the text to keep cost bounded.\n    let end = crate::converter::utility::content::floor_char_boundary(text, text.len().min(64));\n    text[..end].hash(&mut hasher);\n    index.hash(&mut hasher);\n    let digest = hasher.finish();\n    format!(\"{node_type}-{digest:016x}\")\n}\n\n// ── Definition list helpers ───────────────────────────────────────────────────\n\n/// Collect `<dt>`/`<dd>` pairs from a `<dl>` element.\n///\n/// Returns `(term_text, definition_text)` tuples.  Consecutive `<dt>` elements share\n/// the next `<dd>`; orphan `<dd>`s use an empty term.\nfn collect_definition_items(dl_tag: &tl::HTMLTag, parser: &tl::Parser) -> Vec<(String, String)> {\n    let mut items: Vec<(String, String)> = Vec::new();\n    let mut pending_terms: Vec<String> = Vec::new();\n\n    let children = dl_tag.children();\n    for handle in children.top().iter() {\n        let Some(tl::Node::Tag(child_tag)) = handle.get(parser) else {\n            continue;\n        };\n        let name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n        match name.as_str() {\n            \"dt\" => {\n                pending_terms.push(extract_text(child_tag, parser).trim().to_string());\n            }\n            \"dd\" => {\n                let definition = extract_text(child_tag, parser).trim().to_string();\n                if pending_terms.is_empty() {\n                    items.push((String::new(), definition));\n                } else {\n                    let mut drained: Vec<String> = std::mem::take(&mut pending_terms);\n                    let last_term = drained.pop();\n                    for term in drained {\n                        items.push((term, String::new()));\n                    }\n                    if let Some(term) = last_term {\n                        items.push((term, definition));\n                    }\n                }\n            }\n            _ => {}\n        }\n    }\n\n    // Flush trailing <dt>s without a corresponding <dd>.\n    for term in pending_terms {\n        items.push((term, String::new()));\n    }\n\n    items\n}\n\n// ── Head metadata extraction ──────────────────────────────────────────────────\n\n/// Extract `<meta name=… content=…>` and `<title>` entries from a `<head>` element.\nfn extract_head_metadata_entries(head_tag: &tl::HTMLTag, parser: &tl::Parser) -> Vec<(String, String)> {\n    let mut entries: Vec<(String, String)> = Vec::new();\n\n    let children = head_tag.children();\n    for handle in children.top().iter() {\n        let Some(tl::Node::Tag(child_tag)) = handle.get(parser) else {\n            continue;\n        };\n        let name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n        match name.as_str() {\n            \"title\" => {\n                let title = extract_text(child_tag, parser).trim().to_string();\n                if !title.is_empty() {\n                    entries.push((\"title\".to_string(), title));\n                }\n            }\n            \"meta\" => {\n                // name + content\n                if let (Some(Some(meta_name)), Some(Some(meta_content))) = (\n                    child_tag.attributes().get(\"name\"),\n                    child_tag.attributes().get(\"content\"),\n                ) {\n                    entries.push((\n                        meta_name.as_utf8_str().to_string(),\n                        meta_content.as_utf8_str().to_string(),\n                    ));\n                }\n                // property + content (Open Graph etc.)\n                if let (Some(Some(property)), Some(Some(content))) = (\n                    child_tag.attributes().get(\"property\"),\n                    child_tag.attributes().get(\"content\"),\n                ) {\n                    entries.push((property.as_utf8_str().to_string(), content.as_utf8_str().to_string()));\n                }\n            }\n            _ => {}\n        }\n    }\n\n    entries\n}\n\n// ── Main builder ──────────────────────────────────────────────────────────────\n\n/// State threaded through the recursive walk.\nstruct BuilderState {\n    /// Accumulated nodes (flat list in document order).\n    nodes: Vec<DocumentNode>,\n    /// Stack of open heading-group indices: `(heading_level, node_index)`.\n    group_stack: Vec<(u8, u32)>,\n}\n\nimpl BuilderState {\n    fn new() -> Self {\n        Self {\n            nodes: Vec::new(),\n            group_stack: Vec::new(),\n        }\n    }\n\n    /// Append a node and return its index.\n    fn push(&mut self, node: DocumentNode) -> u32 {\n        let idx = self.nodes.len() as u32;\n        self.nodes.push(node);\n        idx\n    }\n\n    /// Index of the innermost open group, if any.\n    fn current_group(&self) -> Option<u32> {\n        self.group_stack.last().map(|(_, idx)| *idx)\n    }\n\n    /// Record `child_idx` as a child of `parent_idx`.\n    fn add_child(&mut self, parent_idx: u32, child_idx: u32) {\n        if let Some(parent) = self.nodes.get_mut(parent_idx as usize) {\n            parent.children.push(child_idx);\n        }\n    }\n}\n\n/// Build a [`DocumentStructure`] from an already-parsed `tl::VDom`.\n///\n/// Walks the DOM once, mapping HTML elements to semantic [`NodeContent`] variants,\n/// tracking parent/child relationships, extracting inline [`TextAnnotation`]s, and\n/// constructing heading-based [`Group`] nodes.\npub fn build_document_structure(dom: &tl::VDom<'_>) -> DocumentStructure {\n    let parser = dom.parser();\n    let mut state = BuilderState::new();\n\n    for handle in dom.children() {\n        walk(&mut state, handle, parser, None);\n    }\n\n    DocumentStructure {\n        nodes: state.nodes,\n        source_format: Some(\"html\".to_string()),\n    }\n}\n\n/// Recursive DOM walker.\n///\n/// `parent_idx` is the flat-list index of the nearest structural parent, if any.\nfn walk(state: &mut BuilderState, handle: &tl::NodeHandle, parser: &tl::Parser, parent_idx: Option<u32>) {\n    let Some(node) = handle.get(parser) else {\n        return;\n    };\n\n    match node {\n        tl::Node::Raw(_) | tl::Node::Comment(_) => {}\n        tl::Node::Tag(tag) => {\n            let tag_name = tag.name().as_utf8_str().to_ascii_lowercase();\n            process_tag(state, tag_name.as_str(), tag, parser, parent_idx);\n        }\n    }\n}\n\n/// Decide how to handle a given tag, creating nodes and recursing as needed.\n#[allow(clippy::too_many_lines)]\nfn process_tag(\n    state: &mut BuilderState,\n    tag_name: &str,\n    tag: &tl::HTMLTag,\n    parser: &tl::Parser,\n    parent_idx: Option<u32>,\n) {\n    match tag_name {\n        // ── Headings ──────────────────────────────────────────────────────\n        \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" => {\n            let level = tag_name[1..].parse::<u8>().unwrap_or(1);\n            let text = extract_text(tag, parser).trim().to_string();\n\n            // Close any open groups at the same or deeper heading level.\n            while let Some(&(open_level, _)) = state.group_stack.last() {\n                if open_level >= level {\n                    state.group_stack.pop();\n                } else {\n                    break;\n                }\n            }\n\n            // Parent for the new group is the enclosing group or the explicit parent.\n            let group_parent = state.group_stack.last().map(|(_, idx)| *idx).or(parent_idx);\n            let group_id = make_node_id(\"group\", &text, state.nodes.len());\n            let group_idx = state.push(DocumentNode {\n                id: group_id,\n                content: NodeContent::Group {\n                    label: Some(text.clone()),\n                    heading_level: Some(level),\n                    heading_text: Some(text.clone()),\n                },\n                parent: group_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(gp) = group_parent {\n                state.add_child(gp, group_idx);\n            }\n            state.group_stack.push((level, group_idx));\n\n            // Emit the Heading node as a child of the new group.\n            let mut annotations = Vec::new();\n            collect_annotations(tag, parser, &text, &mut annotations);\n            let heading_id = make_node_id(\"heading\", &text, state.nodes.len());\n            let heading_idx = state.push(DocumentNode {\n                id: heading_id,\n                content: NodeContent::Heading { level, text },\n                parent: Some(group_idx),\n                children: Vec::new(),\n                annotations,\n                attributes: None,\n            });\n            state.add_child(group_idx, heading_idx);\n        }\n\n        // ── Paragraph ────────────────────────────────────────────────────\n        \"p\" => {\n            let text = extract_text(tag, parser).trim().to_string();\n            if text.is_empty() {\n                return;\n            }\n            let effective_parent = state.current_group().or(parent_idx);\n            let mut annotations = Vec::new();\n            collect_annotations(tag, parser, &text, &mut annotations);\n            let id = make_node_id(\"paragraph\", &text, state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Paragraph { text },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations,\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Lists ─────────────────────────────────────────────────────────\n        \"ul\" | \"ol\" => {\n            let ordered = tag_name == \"ol\";\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"list\", if ordered { \"ordered\" } else { \"unordered\" }, state.nodes.len());\n            let list_idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::List { ordered },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, list_idx);\n            }\n            // Recurse with the list node as the parent so <li>s attach to it.\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk(state, child_handle, parser, Some(list_idx));\n            }\n        }\n\n        // ── List item ─────────────────────────────────────────────────────\n        \"li\" => {\n            let text = extract_text(tag, parser).trim().to_string();\n            let effective_parent = parent_idx.or_else(|| state.current_group());\n            let mut annotations = Vec::new();\n            collect_annotations(tag, parser, &text, &mut annotations);\n            let id = make_node_id(\"list_item\", &text, state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::ListItem { text },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations,\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Table ─────────────────────────────────────────────────────────\n        \"table\" => {\n            let grid = extract_table_grid(tag, parser);\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"table\", &grid.rows.to_string(), state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Table { grid },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Image ─────────────────────────────────────────────────────────\n        \"img\" => {\n            let src = tag\n                .attributes()\n                .get(\"src\")\n                .flatten()\n                .map(|v| v.as_utf8_str().to_string());\n            let description = tag\n                .attributes()\n                .get(\"alt\")\n                .flatten()\n                .map(|v| v.as_utf8_str().to_string())\n                .filter(|s| !s.is_empty());\n            let effective_parent = state.current_group().or(parent_idx);\n            let label = src.as_deref().unwrap_or(\"img\");\n            let id = make_node_id(\"image\", label, state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Image {\n                    description,\n                    src,\n                    image_index: None,\n                },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Code block (<pre><code …>) ────────────────────────────────────\n        \"pre\" => {\n            let mut language: Option<String> = None;\n            let mut code_text: Option<String> = None;\n\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {\n                    let child_name = child_tag.name().as_utf8_str().to_ascii_lowercase();\n                    if child_name == \"code\" {\n                        // Extract language from class=\"language-*\"\n                        if let Some(Some(class_val)) = child_tag.attributes().get(\"class\") {\n                            let class_str = class_val.as_utf8_str();\n                            for token in class_str.split_whitespace() {\n                                if let Some(lang) = token.strip_prefix(\"language-\") {\n                                    language = Some(lang.to_string());\n                                    break;\n                                }\n                            }\n                        }\n                        code_text = Some(extract_text(child_tag, parser));\n                        break;\n                    }\n                }\n            }\n\n            let text = code_text.unwrap_or_else(|| extract_text(tag, parser));\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"code\", &text, state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Code { text, language },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Blockquote ────────────────────────────────────────────────────\n        \"blockquote\" => {\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"quote\", \"blockquote\", state.nodes.len());\n            let quote_idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Quote,\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, quote_idx);\n            }\n            // Recurse into blockquote children under the Quote node.\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk(state, child_handle, parser, Some(quote_idx));\n            }\n        }\n\n        // ── Definition list ───────────────────────────────────────────────\n        \"dl\" => {\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"definition_list\", \"dl\", state.nodes.len());\n            let dl_idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::DefinitionList,\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, dl_idx);\n            }\n\n            for (term, definition) in collect_definition_items(tag, parser) {\n                let item_id = make_node_id(\"definition_item\", &term, state.nodes.len());\n                let item_idx = state.push(DocumentNode {\n                    id: item_id,\n                    content: NodeContent::DefinitionItem { term, definition },\n                    parent: Some(dl_idx),\n                    children: Vec::new(),\n                    annotations: Vec::new(),\n                    attributes: None,\n                });\n                state.add_child(dl_idx, item_idx);\n            }\n        }\n\n        // ── Script / Style → RawBlock ─────────────────────────────────────\n        \"script\" | \"style\" => {\n            let format = if tag_name == \"script\" {\n                tag.attributes()\n                    .get(\"type\")\n                    .flatten()\n                    .map(|v| v.as_utf8_str().to_string())\n                    .unwrap_or_else(|| \"javascript\".to_string())\n            } else {\n                \"css\".to_string()\n            };\n            let content = extract_text(tag, parser);\n            if content.trim().is_empty() {\n                return;\n            }\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"raw_block\", &format, state.nodes.len());\n            let idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::RawBlock { format, content },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, idx);\n            }\n        }\n\n        // ── Head → MetadataBlock ──────────────────────────────────────────\n        \"head\" => {\n            let entries = extract_head_metadata_entries(tag, parser);\n            if entries.is_empty() {\n                return;\n            }\n            let id = make_node_id(\"metadata_block\", \"head\", state.nodes.len());\n            // Metadata blocks sit at the root level.\n            state.push(DocumentNode {\n                id,\n                content: NodeContent::MetadataBlock { entries },\n                parent: None,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: None,\n            });\n        }\n\n        // ── Semantic containers → Group node ──────────────────────────────\n        \"main\" | \"article\" | \"section\" | \"header\" | \"footer\" | \"nav\" | \"aside\" => {\n            let label = tag\n                .attributes()\n                .get(\"aria-label\")\n                .flatten()\n                .map(|v| v.as_utf8_str().to_string());\n            let effective_parent = state.current_group().or(parent_idx);\n            let id = make_node_id(\"group\", tag_name, state.nodes.len());\n            let group_idx = state.push(DocumentNode {\n                id,\n                content: NodeContent::Group {\n                    label,\n                    heading_level: None,\n                    heading_text: None,\n                },\n                parent: effective_parent,\n                children: Vec::new(),\n                annotations: Vec::new(),\n                attributes: collect_attributes(tag),\n            });\n            if let Some(ep) = effective_parent {\n                state.add_child(ep, group_idx);\n            }\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk(state, child_handle, parser, Some(group_idx));\n            }\n        }\n\n        // ── Transparent structural containers ─────────────────────────────\n        \"html\" | \"body\" | \"div\" | \"figure\" | \"figcaption\" | \"details\" | \"summary\" | \"address\" | \"hgroup\" | \"search\"\n        | \"form\" | \"fieldset\" => {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk(state, child_handle, parser, parent_idx);\n            }\n        }\n\n        // ── Everything else: recurse transparently ────────────────────────\n        _ => {\n            let children = tag.children();\n            for child_handle in children.top().iter() {\n                walk(state, child_handle, parser, parent_idx);\n            }\n        }\n    }\n}\n\n/// Collect a safe subset of attributes into a `HashMap`.\n///\n/// Only `id`, `class`, `lang`, `dir`, and `data-*` attributes are kept.\n/// Event handlers (`on*`) and other potentially unsafe attributes are dropped.\nfn collect_attributes(tag: &tl::HTMLTag) -> Option<HashMap<String, String>> {\n    let raw = tag.attributes().clone();\n    let mut map: HashMap<String, String> = HashMap::new();\n\n    for (key_cow, val_opt) in raw.iter() {\n        let key = key_cow.to_ascii_lowercase();\n        // Drop event handlers.\n        if key.starts_with(\"on\") {\n            continue;\n        }\n        if matches!(key.as_str(), \"id\" | \"class\" | \"lang\" | \"dir\") || key.starts_with(\"data-\") {\n            if let Some(val) = val_opt {\n                map.insert(key, val.to_string());\n            }\n        }\n    }\n\n    if map.is_empty() { None } else { Some(map) }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/structure_collector.rs",
    "content": "//! Collector that builds a [`DocumentStructure`] during the converter's HTML DOM walk.\n//!\n//! Follows the same single-pass collector pattern used by [`crate::metadata::MetadataCollector`]:\n//! an `Rc<RefCell<StructureCollector>>` handle is threaded through the [`crate::converter::Context`]\n//! and individual element handlers call `push_*` methods as they encounter content.\n//!\n//! # Design\n//!\n//! - **Flat node array** with index-based parent/child links (matches [`DocumentStructure`]).\n//! - **section_stack** tracks the currently-open heading groups (`(level, group_node_index)`).\n//! - **container_stack** tracks open blockquote containers.\n//! - **list_stack** tracks open list containers so `push_list_item` attaches items to the right list.\n//! - IDs are deterministic hashes of `(node_type, text_prefix, index)`.\n\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\nuse super::document::{DocumentNode, DocumentStructure, NodeContent};\nuse super::tables::{TableData, TableGrid};\n\n/// Shared mutable handle used in [`crate::converter::Context`].\npub type StructureCollectorHandle = Rc<RefCell<StructureCollector>>;\n\n/// Incremental builder for [`DocumentStructure`] during a single DOM walk.\npub struct StructureCollector {\n    /// Accumulated nodes in document order.\n    nodes: Vec<DocumentNode>,\n    /// Open heading-group stack: `(heading_level, node_index)`.\n    /// Mirrors the `group_stack` in `structure_builder`.\n    section_stack: Vec<(u8, u32)>,\n    /// Open blockquote container indices (innermost last).\n    container_stack: Vec<u32>,\n    /// Open list container indices (innermost last).\n    list_stack: Vec<u32>,\n    /// Extracted tables with both structured grid data and markdown rendering.\n    ///\n    /// Populated by [`push_table_data`] when document structure extraction is enabled.\n    tables: Vec<TableData>,\n}\n\nimpl StructureCollector {\n    /// Create a new empty collector.\n    pub fn new() -> Self {\n        Self {\n            nodes: Vec::new(),\n            section_stack: Vec::new(),\n            container_stack: Vec::new(),\n            list_stack: Vec::new(),\n            tables: Vec::new(),\n        }\n    }\n\n    // ── Public push methods ──────────────────────────────────────────────────\n\n    /// Record a heading element.\n    ///\n    /// Creates a [`NodeContent::Group`] (which owns all subsequent sibling content until a\n    /// heading of equal or higher rank closes it) followed by a [`NodeContent::Heading`] child.\n    ///\n    /// Returns the index of the **heading** node (the group node is one before it).\n    pub fn push_heading(&mut self, level: u8, text: &str, id: Option<&str>) -> u32 {\n        // Close any open groups at the same or deeper heading level.\n        while let Some(&(open_level, _)) = self.section_stack.last() {\n            if open_level >= level {\n                self.section_stack.pop();\n            } else {\n                break;\n            }\n        }\n\n        // The group's parent is the surrounding open group or a container/list (if any).\n        let group_parent = self.current_structural_parent();\n\n        // Create the Group node.\n        let group_id = Self::generate_id(\"group\", text, self.nodes.len() as u32);\n        let group_idx = self.raw_push(DocumentNode {\n            id: group_id,\n            content: NodeContent::Group {\n                label: Some(text.to_string()),\n                heading_level: Some(level),\n                heading_text: Some(text.to_string()),\n            },\n            parent: group_parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(gp) = group_parent {\n            self.add_child(gp, group_idx);\n        }\n        self.section_stack.push((level, group_idx));\n\n        // Create the Heading node as a child of the new group.\n        let heading_id = Self::generate_id(\"heading\", text, self.nodes.len() as u32);\n        let heading_idx = self.raw_push(DocumentNode {\n            id: heading_id,\n            content: NodeContent::Heading {\n                level,\n                text: text.to_string(),\n            },\n            parent: Some(group_idx),\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: id.map(|v| {\n                let mut m = std::collections::HashMap::new();\n                m.insert(\"id\".to_string(), v.to_string());\n                m\n            }),\n        });\n        self.add_child(group_idx, heading_idx);\n        heading_idx\n    }\n\n    /// Record a paragraph element.\n    ///\n    /// Returns the node index.\n    pub fn push_paragraph(&mut self, text: &str) -> u32 {\n        if text.is_empty() {\n            return u32::MAX;\n        }\n        let parent = self.current_structural_parent();\n        let id = Self::generate_id(\"paragraph\", text, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Paragraph { text: text.to_string() },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Open a list container.\n    ///\n    /// Returns the node index; call [`push_list_end`] to close it.\n    pub fn push_list_start(&mut self, ordered: bool) -> u32 {\n        let parent = self.current_structural_parent();\n        let label = if ordered { \"ordered\" } else { \"unordered\" };\n        let id = Self::generate_id(\"list\", label, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::List { ordered },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        self.list_stack.push(idx);\n        idx\n    }\n\n    /// Close the innermost open list container.\n    pub fn push_list_end(&mut self) {\n        self.list_stack.pop();\n    }\n\n    /// Record a list item under the current open list.\n    ///\n    /// If there is no open list, the item is parented under the current section/container.\n    /// Returns the node index.\n    pub fn push_list_item(&mut self, text: &str) -> u32 {\n        let parent = self\n            .list_stack\n            .last()\n            .copied()\n            .or_else(|| self.current_structural_parent());\n        let id = Self::generate_id(\"list_item\", text, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::ListItem { text: text.to_string() },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Record a table with both structured grid data and its markdown rendering.\n    ///\n    /// Adds the table to the document tree as a [`NodeContent::Table`] node and also\n    /// appends a [`TableData`] entry (grid + markdown) to the flat tables list that is\n    /// exposed via [`ConversionResult::tables`].\n    ///\n    /// Returns the node index.\n    pub fn push_table_data(&mut self, grid: TableGrid, markdown: String) -> u32 {\n        let parent = self.current_structural_parent();\n        let label = grid.rows.to_string();\n        let id = Self::generate_id(\"table\", &label, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Table { grid: grid.clone() },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        self.tables.push(TableData { grid, markdown });\n        idx\n    }\n\n    /// Record a table (grid only, no markdown rendering).\n    ///\n    /// Prefer [`push_table_data`] when the markdown rendering is available; use this\n    /// method only when the markdown is not yet computed.\n    ///\n    /// Returns the node index.\n    pub fn push_table(&mut self, grid: TableGrid) -> u32 {\n        let parent = self.current_structural_parent();\n        let label = grid.rows.to_string();\n        let id = Self::generate_id(\"table\", &label, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Table { grid },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Record an image element.\n    ///\n    /// Returns the node index.\n    pub fn push_image(&mut self, src: Option<&str>, alt: Option<&str>) -> u32 {\n        let parent = self.current_structural_parent();\n        let label = src.unwrap_or(\"img\");\n        let id = Self::generate_id(\"image\", label, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Image {\n                description: alt.filter(|s| !s.is_empty()).map(str::to_string),\n                src: src.map(str::to_string),\n                image_index: None,\n            },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Record a code block.\n    ///\n    /// Returns the node index.\n    pub fn push_code(&mut self, text: &str, language: Option<&str>) -> u32 {\n        let parent = self.current_structural_parent();\n        let id = Self::generate_id(\"code\", text, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Code {\n                text: text.to_string(),\n                language: language.map(str::to_string),\n            },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Open a blockquote container.\n    ///\n    /// Returns the node index; call [`push_quote_end`] to close it.\n    pub fn push_quote_start(&mut self) -> u32 {\n        let parent = self.current_structural_parent();\n        let id = Self::generate_id(\"quote\", \"blockquote\", self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::Quote,\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        self.container_stack.push(idx);\n        idx\n    }\n\n    /// Close the innermost open blockquote container.\n    pub fn push_quote_end(&mut self) {\n        self.container_stack.pop();\n    }\n\n    /// Record a raw block (e.g. preserved `<script>` or `<style>` content).\n    ///\n    /// Returns the node index.\n    pub fn push_raw_block(&mut self, format: &str, content: &str) -> u32 {\n        let parent = self.current_structural_parent();\n        let id = Self::generate_id(\"raw_block\", format, self.nodes.len() as u32);\n        let idx = self.raw_push(DocumentNode {\n            id,\n            content: NodeContent::RawBlock {\n                format: format.to_string(),\n                content: content.to_string(),\n            },\n            parent,\n            children: Vec::new(),\n            annotations: Vec::new(),\n            attributes: None,\n        });\n        if let Some(p) = parent {\n            self.add_child(p, idx);\n        }\n        idx\n    }\n\n    /// Consume the collector and return the completed [`DocumentStructure`] and extracted\n    /// [`TableData`] entries.\n    ///\n    /// Returns `(DocumentStructure, Vec<TableData>)`.  The tables vec contains one entry per\n    /// `<table>` element that was processed via [`push_table_data`].\n    pub fn finish(self) -> (DocumentStructure, Vec<TableData>) {\n        let doc = DocumentStructure {\n            nodes: self.nodes,\n            source_format: Some(\"html\".to_string()),\n        };\n        (doc, self.tables)\n    }\n\n    // ── Private helpers ──────────────────────────────────────────────────────\n\n    /// The effective structural parent for a new node:\n    /// list stack > container stack > section stack > None.\n    fn current_structural_parent(&self) -> Option<u32> {\n        // List items: already handled explicitly in push_list_item.\n        // For non-list-item content, prefer the innermost container (blockquote),\n        // then innermost section group.\n        if let Some(&q) = self.container_stack.last() {\n            return Some(q);\n        }\n        if let Some(&(_, g)) = self.section_stack.last() {\n            return Some(g);\n        }\n        None\n    }\n\n    /// Append a node to the flat list and return its index.\n    fn raw_push(&mut self, node: DocumentNode) -> u32 {\n        let idx = self.nodes.len() as u32;\n        self.nodes.push(node);\n        idx\n    }\n\n    /// Record `child_idx` as a child of `parent_idx`.\n    fn add_child(&mut self, parent_idx: u32, child_idx: u32) {\n        if let Some(parent) = self.nodes.get_mut(parent_idx as usize) {\n            parent.children.push(child_idx);\n        }\n    }\n\n    /// Generate a deterministic node ID: `\"{node_type}-{hash:016x}\"`.\n    ///\n    /// Hashes `(node_type, text[..64], index)` with `DefaultHasher`.\n    fn generate_id(node_type: &str, text: &str, index: u32) -> String {\n        use std::collections::hash_map::DefaultHasher;\n        use std::hash::{Hash, Hasher};\n\n        let mut hasher = DefaultHasher::new();\n        node_type.hash(&mut hasher);\n        let end = crate::converter::utility::content::floor_char_boundary(text, text.len().min(64));\n        text[..end].hash(&mut hasher);\n        index.hash(&mut hasher);\n        let digest = hasher.finish();\n        format!(\"{node_type}-{digest:016x}\")\n    }\n}\n\nimpl Default for StructureCollector {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_heading_creates_group_and_heading() {\n        let mut c = StructureCollector::new();\n        let heading_idx = c.push_heading(1, \"Title\", None);\n        // Group is at index 0, Heading at index 1.\n        assert_eq!(heading_idx, 1);\n        assert_eq!(c.nodes.len(), 2);\n\n        let group = &c.nodes[0];\n        matches!(\n            &group.content,\n            NodeContent::Group {\n                heading_level: Some(1),\n                ..\n            }\n        );\n        assert!(group.children.contains(&1));\n\n        let heading = &c.nodes[1];\n        assert!(matches!(&heading.content, NodeContent::Heading { level: 1, .. }));\n        assert_eq!(heading.parent, Some(0));\n    }\n\n    #[test]\n    fn test_heading_closes_deeper_groups() {\n        let mut c = StructureCollector::new();\n        c.push_heading(1, \"H1\", None);\n        c.push_heading(2, \"H2\", None);\n        // Now push another H1 — must close the H2 group.\n        c.push_heading(1, \"H1b\", None);\n        // After the second H1 there should be 2 open groups gone and 1 new one.\n        assert_eq!(c.section_stack.len(), 1);\n        let (level, _) = c.section_stack[0];\n        assert_eq!(level, 1);\n    }\n\n    #[test]\n    fn test_paragraph_parents_under_section() {\n        let mut c = StructureCollector::new();\n        c.push_heading(1, \"Title\", None);\n        let p_idx = c.push_paragraph(\"Some text\");\n        let para = &c.nodes[p_idx as usize];\n        // Parent should be the group node (index 0).\n        assert_eq!(para.parent, Some(0));\n    }\n\n    #[test]\n    fn test_list_items_attach_to_list() {\n        let mut c = StructureCollector::new();\n        let list_idx = c.push_list_start(false);\n        let item_idx = c.push_list_item(\"Item 1\");\n        c.push_list_end();\n        assert_eq!(c.nodes[item_idx as usize].parent, Some(list_idx));\n        let list = &c.nodes[list_idx as usize];\n        assert!(list.children.contains(&item_idx));\n    }\n\n    #[test]\n    fn test_quote_container() {\n        let mut c = StructureCollector::new();\n        let q_idx = c.push_quote_start();\n        let p_idx = c.push_paragraph(\"Quoted text\");\n        c.push_quote_end();\n        assert_eq!(c.nodes[p_idx as usize].parent, Some(q_idx));\n    }\n\n    #[test]\n    fn test_finish_returns_document_structure() {\n        let mut c = StructureCollector::new();\n        c.push_heading(1, \"Title\", None);\n        c.push_paragraph(\"Text\");\n        let (doc, tables) = c.finish();\n        assert_eq!(doc.source_format, Some(\"html\".to_string()));\n        assert_eq!(doc.nodes.len(), 3); // Group + Heading + Paragraph\n        assert!(tables.is_empty());\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/tables.rs",
    "content": "//! Structured table types aligned with kreuzberg's `TableGrid`.\n\n#[cfg(feature = \"serde\")]\nuse serde::{Deserialize, Serialize};\n\n/// A structured table grid with cell-level data including spans.\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct TableGrid {\n    /// Number of rows.\n    pub rows: u32,\n    /// Number of columns.\n    pub cols: u32,\n    /// All cells in the table (may be fewer than rows*cols due to spans).\n    pub cells: Vec<GridCell>,\n}\n\n/// A single cell in a table grid.\n#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct GridCell {\n    /// The text content of the cell.\n    pub content: String,\n    /// 0-indexed row position.\n    pub row: u32,\n    /// 0-indexed column position.\n    pub col: u32,\n    /// Number of rows this cell spans (default 1).\n    #[cfg_attr(feature = \"serde\", serde(default = \"default_span\"))]\n    pub row_span: u32,\n    /// Number of columns this cell spans (default 1).\n    #[cfg_attr(feature = \"serde\", serde(default = \"default_span\"))]\n    pub col_span: u32,\n    /// Whether this is a header cell (`<th>`).\n    #[cfg_attr(feature = \"serde\", serde(default))]\n    pub is_header: bool,\n}\n\n#[cfg(feature = \"serde\")]\nfn default_span() -> u32 {\n    1\n}\n\n/// A top-level extracted table with both structured data and markdown representation.\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct TableData {\n    /// The structured table grid.\n    pub grid: TableGrid,\n    /// The markdown rendering of this table.\n    pub markdown: String,\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/types/warnings.rs",
    "content": "//! Processing warning types for non-fatal issues during conversion.\n\n#[cfg(feature = \"serde\")]\nuse serde::{Deserialize, Serialize};\n\n/// A non-fatal warning generated during HTML processing.\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\npub struct ProcessingWarning {\n    /// Human-readable warning message.\n    pub message: String,\n    /// The category of warning.\n    pub kind: WarningKind,\n}\n\n/// Categories of processing warnings.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]\n#[cfg_attr(feature = \"serde\", serde(rename_all = \"snake_case\"))]\npub enum WarningKind {\n    /// An image could not be extracted (e.g. invalid data URI, unsupported format).\n    ImageExtractionFailed,\n    /// The input encoding was not recognized; fell back to UTF-8.\n    EncodingFallback,\n    /// The input was truncated due to size limits.\n    TruncatedInput,\n    /// The HTML was malformed but processing continued with best effort.\n    MalformedHtml,\n    /// Sanitization was applied to remove potentially unsafe content.\n    SanitizationApplied,\n    /// DOM traversal was truncated because max_depth was exceeded.\n    DepthLimitExceeded,\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/validation.rs",
    "content": "//! Input validation module for HTML to Markdown conversion.\n//!\n//! Provides validation functions to detect and reject binary data,\n//! corrupted input, and other non-text content.\n\nuse crate::error::{ConversionError, Result};\n\nconst BINARY_SCAN_LIMIT: usize = 8192;\nconst BINARY_CONTROL_RATIO: f64 = 0.3;\nconst BINARY_UTF16_NULL_RATIO: f64 = 0.2;\nconst BINARY_NUL_RATIO: f64 = 0.01;\nconst BINARY_NUL_MAX: usize = 8;\n\nconst BINARY_MAGIC_PREFIXES: &[(&[u8], &str)] = &[\n    (b\"\\x1F\\x8B\", \"gzip-compressed data\"),\n    (b\"\\x28\\xB5\\x2F\\xFD\", \"zstd-compressed data\"),\n    (b\"PK\\x03\\x04\", \"zip archive\"),\n    (b\"PK\\x05\\x06\", \"zip archive\"),\n    (b\"PK\\x07\\x08\", \"zip archive\"),\n    (b\"%PDF-\", \"PDF data\"),\n];\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum Utf16Encoding {\n    BomLe,\n    BomBe,\n    NoBomLe,\n    NoBomBe,\n}\n\n/// Validate HTML input and reject binary/corrupted data.\n///\n/// # Errors\n///\n/// Returns `ConversionError::InvalidInput` if the input is detected as binary,\n/// compressed, or contains too many control characters.\n#[allow(clippy::cast_precision_loss)]\npub fn validate_input(html: &str) -> Result<()> {\n    let bytes = html.as_bytes();\n    if bytes.is_empty() {\n        return Ok(());\n    }\n\n    if let Some(label) = detect_binary_magic(bytes) {\n        return Err(ConversionError::InvalidInput(format!(\n            \"binary data detected ({label}); decode/decompress to UTF-8 HTML first\"\n        )));\n    }\n\n    let sample_len = bytes.len().min(BINARY_SCAN_LIMIT);\n    let mut control_count = 0usize;\n    let mut nul_count = 0usize;\n\n    for &byte in &bytes[..sample_len] {\n        if byte == 0 {\n            nul_count += 1;\n        }\n        let is_control = (byte < 0x09) || (0x0E..0x20).contains(&byte);\n        if is_control {\n            control_count += 1;\n        }\n    }\n\n    if nul_count > 0 {\n        if let Some(encoding) = detect_utf16_encoding(bytes) {\n            let label = utf16_label(encoding);\n            return Err(ConversionError::InvalidInput(format!(\n                \"binary data detected ({label}); decode to UTF-8 HTML first\"\n            )));\n        }\n        let nul_ratio = nul_count as f64 / sample_len as f64;\n        if nul_ratio > BINARY_NUL_RATIO && nul_count > BINARY_NUL_MAX {\n            return Err(ConversionError::InvalidInput(\n                \"binary data detected (excess NUL bytes)\".to_string(),\n            ));\n        }\n    }\n\n    if sample_len >= 32 {\n        let control_ratio = control_count as f64 / sample_len as f64;\n        if control_ratio > BINARY_CONTROL_RATIO {\n            return Err(ConversionError::InvalidInput(\n                \"binary data detected (excess control bytes)\".to_string(),\n            ));\n        }\n    }\n\n    Ok(())\n}\n\nfn detect_binary_magic(bytes: &[u8]) -> Option<&'static str> {\n    for (prefix, label) in BINARY_MAGIC_PREFIXES {\n        if bytes.starts_with(prefix) {\n            return Some(*label);\n        }\n    }\n    None\n}\n\n#[allow(clippy::cast_precision_loss)]\npub fn detect_utf16_encoding(bytes: &[u8]) -> Option<Utf16Encoding> {\n    if bytes.len() >= 2 {\n        if bytes.starts_with(b\"\\xFF\\xFE\") {\n            return Some(Utf16Encoding::BomLe);\n        }\n        if bytes.starts_with(b\"\\xFE\\xFF\") {\n            return Some(Utf16Encoding::BomBe);\n        }\n    }\n\n    let sample_len = bytes.len().min(BINARY_SCAN_LIMIT);\n    if sample_len < 4 {\n        return None;\n    }\n\n    let mut nul_count = 0usize;\n    let mut even_nul_count = 0usize;\n    let mut odd_nul_count = 0usize;\n\n    for (idx, &byte) in bytes[..sample_len].iter().enumerate() {\n        if byte == 0 {\n            nul_count += 1;\n            if idx % 2 == 0 {\n                even_nul_count += 1;\n            } else {\n                odd_nul_count += 1;\n            }\n        }\n    }\n\n    if nul_count < 2 {\n        return None;\n    }\n\n    let nul_ratio = nul_count as f64 / sample_len as f64;\n    if nul_ratio < BINARY_UTF16_NULL_RATIO {\n        return None;\n    }\n\n    let dominant_ratio = (even_nul_count.max(odd_nul_count) as f64) / nul_count as f64;\n    if dominant_ratio < 0.9 {\n        return None;\n    }\n\n    if odd_nul_count >= even_nul_count {\n        Some(Utf16Encoding::NoBomLe)\n    } else {\n        Some(Utf16Encoding::NoBomBe)\n    }\n}\n\nfn utf16_label(encoding: Utf16Encoding) -> &'static str {\n    match encoding {\n        Utf16Encoding::BomLe => \"UTF-16LE BOM\",\n        Utf16Encoding::BomBe => \"UTF-16BE BOM\",\n        Utf16Encoding::NoBomLe | Utf16Encoding::NoBomBe => \"UTF-16 data without BOM\",\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor/default_impl.rs",
    "content": "//! Default visitor implementations and utilities.\n//!\n//! This module provides standard visitor patterns and helpers for common use cases.\n\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\n/// Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n///\n/// This allows visitors to be passed around and shared while still being mutable.\npub type VisitorHandle = Rc<RefCell<dyn super::traits::HtmlVisitor>>;\n\n#[cfg(test)]\nmod tests {\n    use super::super::traits::HtmlVisitor;\n    use super::super::types::{NodeContext, NodeType, VisitResult};\n    use std::collections::BTreeMap;\n\n    #[derive(Debug)]\n    struct TrackingVisitor {\n        element_count: usize,\n        text_count: usize,\n    }\n\n    impl HtmlVisitor for TrackingVisitor {\n        fn visit_element_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n            self.element_count += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            self.text_count += 1;\n            VisitResult::Continue\n        }\n    }\n\n    #[test]\n    fn test_visitor_handle_creation() {\n        let visitor = TrackingVisitor {\n            element_count: 0,\n            text_count: 0,\n        };\n\n        let handle = std::rc::Rc::new(std::cell::RefCell::new(visitor));\n\n        {\n            let mut v = handle.borrow_mut();\n            let ctx = NodeContext {\n                node_type: NodeType::Text,\n                tag_name: \"p\".to_string(),\n                attributes: BTreeMap::new(),\n                depth: 1,\n                index_in_parent: 0,\n                parent_tag: Some(\"div\".to_string()),\n                is_inline: false,\n            };\n            v.visit_text(&ctx, \"test\");\n        }\n\n        let v = handle.borrow();\n        assert_eq!(v.text_count, 1);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor/mod.rs",
    "content": "//! Visitor pattern for HTML to Markdown conversion.\n//!\n//! This module provides a comprehensive visitor trait that allows users to intervene\n//! in the HTML→Markdown transformation logic at any point. Visitors can inspect,\n//! modify, or replace the default conversion behavior for any HTML element type.\n//!\n//! # Design Philosophy\n//!\n//! - **Flexibility over performance**: Visitors prioritize giving users full control\n//! - **Zero-cost when unused**: No performance impact if visitor feature is disabled\n//! - **Comprehensive coverage**: All 60+ HTML element types have dedicated visitor methods\n//! - **Pre/post hooks**: Both element entry and exit points are exposed\n//!\n//! # Example\n//!\n//! ```text\n//! use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};\n//!\n//! struct CustomVisitor;\n//!\n//! impl HtmlVisitor for CustomVisitor {\n//!     fn visit_link(&mut self, ctx: &NodeContext, href: &str, text: &str, title: Option<&str>) -> VisitResult {\n//!         // Convert all links to plain text with URL in parentheses\n//!         VisitResult::Custom(format!(\"{} ({})\", text, href))\n//!     }\n//!\n//!     fn visit_image(&mut self, ctx: &NodeContext, src: &str, alt: &str, title: Option<&str>) -> VisitResult {\n//!         // Skip all images\n//!         VisitResult::Skip\n//!     }\n//! }\n//! ```\n\nmod default_impl;\nmod traits;\nmod types;\n\n// Re-export all public items from submodules\npub use default_impl::VisitorHandle;\npub use traits::HtmlVisitor;\npub use types::{NodeContext, NodeType, VisitResult};\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor/traits.rs",
    "content": "//! Visitor traits for HTML to Markdown conversion.\n//!\n//! This module contains the synchronous visitor trait.\n\nuse super::types::{NodeContext, VisitResult};\n\n/// Visitor trait for HTML→Markdown conversion.\n///\n/// Implement this trait to customize the conversion behavior for any HTML element type.\n/// All methods have default implementations that return `VisitResult::Continue`, allowing\n/// selective override of only the elements you care about.\n///\n/// # Method Naming Convention\n///\n/// - `visit_*_start`: Called before entering an element (pre-order traversal)\n/// - `visit_*_end`: Called after exiting an element (post-order traversal)\n/// - `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n///\n/// # Execution Order\n///\n/// For a typical element like `<div><p>text</p></div>`:\n/// 1. `visit_element_start` for `<div>`\n/// 2. `visit_element_start` for `<p>`\n/// 3. `visit_text` for \"text\"\n/// 4. `visit_element_end` for `<p>`\n/// 5. `visit_element_end` for `</div>`\n///\n/// # Performance Notes\n///\n/// - `visit_text` is the most frequently called method (~100+ times per document)\n/// - Return `VisitResult::Continue` quickly for elements you don't need to customize\n/// - Avoid heavy computation in visitor methods; consider caching if needed\npub trait HtmlVisitor: std::fmt::Debug {\n    /// Called before entering any element.\n    ///\n    /// This is the first callback invoked for every HTML element, allowing\n    /// visitors to implement generic element handling before tag-specific logic.\n    fn visit_element_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called after exiting any element.\n    ///\n    /// Receives the default markdown output that would be generated.\n    /// Visitors can inspect or replace this output.\n    fn visit_element_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit text nodes (most frequent callback - ~100+ per document).\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context (will have `node_type: NodeType::Text`)\n    /// - `text`: The raw text content (HTML entities already decoded)\n    fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit anchor links `<a href=\"...\">`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with link element metadata\n    /// - `href`: The link URL (from `href` attribute)\n    /// - `text`: The link text content (already converted to markdown)\n    /// - `title`: Optional title attribute\n    fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit images `<img src=\"...\">`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with image element metadata\n    /// - `src`: The image source URL\n    /// - `alt`: The alt text\n    /// - `title`: Optional title attribute\n    fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit heading elements `<h1>` through `<h6>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with heading metadata\n    /// - `level`: Heading level (1-6)\n    /// - `text`: The heading text content\n    /// - `id`: Optional id attribute (for anchor links)\n    fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit code blocks `<pre><code>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `lang`: Optional language specifier (from class attribute)\n    /// - `code`: The code content\n    fn visit_code_block(&mut self, _ctx: &NodeContext, _lang: Option<&str>, _code: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit inline code `<code>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `code`: The code content\n    fn visit_code_inline(&mut self, _ctx: &NodeContext, _code: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit list items `<li>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `ordered`: Whether this is an ordered list item\n    /// - `marker`: The list marker (e.g., \"-\", \"1.\", \"a)\")\n    /// - `text`: The list item content (already converted)\n    fn visit_list_item(&mut self, _ctx: &NodeContext, _ordered: bool, _marker: &str, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called before processing a list `<ul>` or `<ol>`.\n    fn visit_list_start(&mut self, _ctx: &NodeContext, _ordered: bool) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called after processing a list `</ul>` or `</ol>`.\n    fn visit_list_end(&mut self, _ctx: &NodeContext, _ordered: bool, _output: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called before processing a table `<table>`.\n    fn visit_table_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit table rows `<tr>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `cells`: Cell contents (already converted to markdown)\n    /// - `is_header`: Whether this row is in `<thead>`\n    fn visit_table_row(&mut self, _ctx: &NodeContext, _cells: &[String], _is_header: bool) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called after processing a table `</table>`.\n    fn visit_table_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit blockquote elements `<blockquote>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `content`: The blockquote content (already converted)\n    /// - `depth`: Nesting depth (for nested blockquotes)\n    fn visit_blockquote(&mut self, _ctx: &NodeContext, _content: &str, _depth: usize) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit strong/bold elements `<strong>`, `<b>`.\n    fn visit_strong(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit emphasis/italic elements `<em>`, `<i>`.\n    fn visit_emphasis(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit strikethrough elements `<s>`, `<del>`, `<strike>`.\n    fn visit_strikethrough(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit underline elements `<u>`, `<ins>`.\n    fn visit_underline(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit subscript elements `<sub>`.\n    fn visit_subscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit superscript elements `<sup>`.\n    fn visit_superscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit mark/highlight elements `<mark>`.\n    fn visit_mark(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit line break elements `<br>`.\n    fn visit_line_break(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit horizontal rule elements `<hr>`.\n    fn visit_horizontal_rule(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit custom elements (web components) or unknown tags.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `tag_name`: The custom element's tag name\n    /// - `html`: The raw HTML of this element\n    fn visit_custom_element(&mut self, _ctx: &NodeContext, _tag_name: &str, _html: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit definition list `<dl>`.\n    fn visit_definition_list_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit definition term `<dt>`.\n    fn visit_definition_term(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit definition description `<dd>`.\n    fn visit_definition_description(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called after processing a definition list `</dl>`.\n    fn visit_definition_list_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit form elements `<form>`.\n    fn visit_form(&mut self, _ctx: &NodeContext, _action: Option<&str>, _method: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit input elements `<input>`.\n    fn visit_input(\n        &mut self,\n        _ctx: &NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit button elements `<button>`.\n    fn visit_button(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit audio elements `<audio>`.\n    fn visit_audio(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit video elements `<video>`.\n    fn visit_video(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit iframe elements `<iframe>`.\n    fn visit_iframe(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit details elements `<details>`.\n    fn visit_details(&mut self, _ctx: &NodeContext, _open: bool) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit summary elements `<summary>`.\n    fn visit_summary(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit figure elements `<figure>`.\n    fn visit_figure_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Visit figcaption elements `<figcaption>`.\n    fn visit_figcaption(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n\n    /// Called after processing a figure `</figure>`.\n    fn visit_figure_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {\n        VisitResult::Continue\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::collections::BTreeMap;\n\n    #[derive(Debug)]\n    struct NoOpVisitor;\n\n    impl HtmlVisitor for NoOpVisitor {}\n\n    #[test]\n    fn test_default_visitor_implementation() {\n        let mut visitor = NoOpVisitor;\n\n        let ctx = NodeContext {\n            node_type: super::super::types::NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        matches!(visitor.visit_element_start(&ctx), VisitResult::Continue);\n        matches!(visitor.visit_element_end(&ctx, \"output\"), VisitResult::Continue);\n        matches!(visitor.visit_text(&ctx, \"text\"), VisitResult::Continue);\n        matches!(visitor.visit_link(&ctx, \"href\", \"text\", None), VisitResult::Continue);\n        matches!(visitor.visit_image(&ctx, \"src\", \"alt\", None), VisitResult::Continue);\n        matches!(visitor.visit_heading(&ctx, 1, \"text\", None), VisitResult::Continue);\n        matches!(visitor.visit_code_block(&ctx, None, \"code\"), VisitResult::Continue);\n        matches!(visitor.visit_code_inline(&ctx, \"code\"), VisitResult::Continue);\n    }\n\n    #[derive(Debug)]\n    struct CustomLinkVisitor;\n\n    impl HtmlVisitor for CustomLinkVisitor {\n        fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Custom(format!(\"{} ({})\", text, href))\n        }\n\n        fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n\n    #[test]\n    fn test_custom_visitor_implementation() {\n        let mut visitor = CustomLinkVisitor;\n\n        let ctx = NodeContext {\n            node_type: super::super::types::NodeType::Link,\n            tag_name: \"a\".to_string(),\n            attributes: BTreeMap::new(),\n            depth: 1,\n            index_in_parent: 0,\n            parent_tag: Some(\"p\".to_string()),\n            is_inline: true,\n        };\n\n        let result = visitor.visit_link(&ctx, \"https://example.com\", \"Example\", None);\n        if let VisitResult::Custom(output) = result {\n            assert_eq!(output, \"Example (https://example.com)\");\n        } else {\n            panic!(\"Expected Custom result\");\n        }\n\n        let img_result = visitor.visit_image(&ctx, \"image.jpg\", \"Alt text\", None);\n        matches!(img_result, VisitResult::Skip);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor/types.rs",
    "content": "//! Type definitions for the visitor pattern.\n//!\n//! This module contains the core data types used in the visitor pattern:\n//! - `NodeType`: Categorization of HTML elements\n//! - `NodeContext`: Metadata about the current node being visited\n//! - `VisitResult`: Control flow signals from visitor methods\n\nuse std::collections::BTreeMap;\n\n/// Node type enumeration covering all HTML element types.\n///\n/// This enum categorizes all HTML elements that the converter recognizes,\n/// providing a coarse-grained classification for visitor dispatch.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub enum NodeType {\n    /// Text node (most frequent - 100+ per document)\n    Text,\n\n    /// Generic element node\n    Element,\n\n    /// Heading elements (h1-h6)\n    Heading,\n    /// Paragraph element\n    Paragraph,\n    /// Generic div container\n    Div,\n    /// Blockquote element\n    Blockquote,\n    /// Preformatted text block\n    Pre,\n    /// Horizontal rule\n    Hr,\n\n    /// Ordered or unordered list (ul, ol)\n    List,\n    /// List item (li)\n    ListItem,\n    /// Definition list (dl)\n    DefinitionList,\n    /// Definition term (dt)\n    DefinitionTerm,\n    /// Definition description (dd)\n    DefinitionDescription,\n\n    /// Table element\n    Table,\n    /// Table row (tr)\n    TableRow,\n    /// Table cell (td, th)\n    TableCell,\n    /// Table header cell (th)\n    TableHeader,\n    /// Table body (tbody)\n    TableBody,\n    /// Table head (thead)\n    TableHead,\n    /// Table foot (tfoot)\n    TableFoot,\n\n    /// Anchor link (a)\n    Link,\n    /// Image (img)\n    Image,\n    /// Strong/bold (strong, b)\n    Strong,\n    /// Emphasis/italic (em, i)\n    Em,\n    /// Inline code (code)\n    Code,\n    /// Strikethrough (s, del, strike)\n    Strikethrough,\n    /// Underline (u, ins)\n    Underline,\n    /// Subscript (sub)\n    Subscript,\n    /// Superscript (sup)\n    Superscript,\n    /// Mark/highlight (mark)\n    Mark,\n    /// Small text (small)\n    Small,\n    /// Line break (br)\n    Br,\n    /// Span element\n    Span,\n\n    /// Article element\n    Article,\n    /// Section element\n    Section,\n    /// Navigation element\n    Nav,\n    /// Aside element\n    Aside,\n    /// Header element\n    Header,\n    /// Footer element\n    Footer,\n    /// Main element\n    Main,\n    /// Figure element\n    Figure,\n    /// Figure caption\n    Figcaption,\n    /// Time element\n    Time,\n    /// Details element\n    Details,\n    /// Summary element\n    Summary,\n\n    /// Form element\n    Form,\n    /// Input element\n    Input,\n    /// Select element\n    Select,\n    /// Option element\n    Option,\n    /// Button element\n    Button,\n    /// Textarea element\n    Textarea,\n    /// Label element\n    Label,\n    /// Fieldset element\n    Fieldset,\n    /// Legend element\n    Legend,\n\n    /// Audio element\n    Audio,\n    /// Video element\n    Video,\n    /// Picture element\n    Picture,\n    /// Source element\n    Source,\n    /// Iframe element\n    Iframe,\n    /// SVG element\n    Svg,\n    /// Canvas element\n    Canvas,\n\n    /// Ruby annotation\n    Ruby,\n    /// Ruby text\n    Rt,\n    /// Ruby parenthesis\n    Rp,\n    /// Abbreviation\n    Abbr,\n    /// Keyboard input\n    Kbd,\n    /// Sample output\n    Samp,\n    /// Variable\n    Var,\n    /// Citation\n    Cite,\n    /// Quote\n    Q,\n    /// Deleted text\n    Del,\n    /// Inserted text\n    Ins,\n    /// Data element\n    Data,\n    /// Meter element\n    Meter,\n    /// Progress element\n    Progress,\n    /// Output element\n    Output,\n    /// Template element\n    Template,\n    /// Slot element\n    Slot,\n\n    /// HTML root element\n    Html,\n    /// Head element\n    Head,\n    /// Body element\n    Body,\n    /// Title element\n    Title,\n    /// Meta element\n    Meta,\n    /// Link element (not anchor)\n    LinkTag,\n    /// Style element\n    Style,\n    /// Script element\n    Script,\n    /// Base element\n    Base,\n\n    /// Custom element (web components) or unknown tag\n    Custom,\n}\n\n/// Context information passed to all visitor methods.\n///\n/// Provides comprehensive metadata about the current node being visited,\n/// including its type, attributes, position in the DOM tree, and parent context.\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct NodeContext {\n    /// Coarse-grained node type classification\n    pub node_type: NodeType,\n\n    /// Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n    pub tag_name: String,\n\n    /// All HTML attributes as key-value pairs\n    pub attributes: BTreeMap<String, String>,\n\n    /// Depth in the DOM tree (0 = root)\n    pub depth: usize,\n\n    /// Index among siblings (0-based)\n    pub index_in_parent: usize,\n\n    /// Parent element's tag name (None if root)\n    pub parent_tag: Option<String>,\n\n    /// Whether this element is treated as inline vs block\n    pub is_inline: bool,\n}\n\n/// Result of a visitor callback.\n///\n/// Allows visitors to control the conversion flow by either proceeding\n/// with default behavior, providing custom output, skipping elements,\n/// preserving HTML, or signaling errors.\n#[derive(Debug, Clone, Default)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub enum VisitResult {\n    #[default]\n    /// Continue with default conversion behavior\n    Continue,\n\n    /// Replace default output with custom markdown\n    ///\n    /// The visitor takes full responsibility for the markdown output\n    /// of this node and its children.\n    Custom(String),\n\n    /// Skip this element entirely (don't output anything)\n    ///\n    /// The element and all its children are ignored in the output.\n    Skip,\n\n    /// Preserve original HTML (don't convert to markdown)\n    ///\n    /// The element's raw HTML is included verbatim in the output.\n    PreserveHtml,\n\n    /// Stop conversion with an error\n    ///\n    /// The conversion process halts and returns this error message.\n    Error(String),\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_node_type_equality() {\n        assert_eq!(NodeType::Text, NodeType::Text);\n        assert_ne!(NodeType::Text, NodeType::Element);\n        assert_eq!(NodeType::Heading, NodeType::Heading);\n    }\n\n    #[test]\n    fn test_node_context_creation() {\n        let ctx = NodeContext {\n            node_type: NodeType::Heading,\n            tag_name: \"h1\".to_string(),\n            attributes: BTreeMap::new(),\n            depth: 1,\n            index_in_parent: 0,\n            parent_tag: Some(\"body\".to_string()),\n            is_inline: false,\n        };\n\n        assert_eq!(ctx.node_type, NodeType::Heading);\n        assert_eq!(ctx.tag_name, \"h1\");\n        assert_eq!(ctx.depth, 1);\n        assert!(!ctx.is_inline);\n    }\n\n    #[test]\n    fn test_visit_result_variants() {\n        let continue_result = VisitResult::Continue;\n        matches!(continue_result, VisitResult::Continue);\n\n        let custom_result = VisitResult::Custom(\"# Custom Output\".to_string());\n        if let VisitResult::Custom(output) = custom_result {\n            assert_eq!(output, \"# Custom Output\");\n        }\n\n        let skip_result = VisitResult::Skip;\n        matches!(skip_result, VisitResult::Skip);\n\n        let preserve_result = VisitResult::PreserveHtml;\n        matches!(preserve_result, VisitResult::PreserveHtml);\n\n        let error_result = VisitResult::Error(\"Test error\".to_string());\n        if let VisitResult::Error(msg) = error_result {\n            assert_eq!(msg, \"Test error\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers/helpers/callbacks/mod.rs",
    "content": "//! Callback management for visitor pattern.\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers/helpers/content.rs",
    "content": "//! Content extraction and result handling.\n//!\n//! This module provides the `VisitorDispatch` enum and helper methods for\n//! processing the results of visitor callbacks.\n\n/// Result of dispatching a visitor callback.\n///\n/// This enum represents the outcome of a visitor callback dispatch,\n/// providing a more ergonomic interface for control flow than the\n/// raw `VisitResult` type.\n#[allow(dead_code)]\n#[derive(Debug)]\npub enum VisitorDispatch {\n    /// Continue with default conversion behavior\n    Continue,\n\n    /// Replace default output with custom markdown\n    Custom(String),\n\n    /// Skip this element entirely (don't output anything)\n    Skip,\n\n    /// Preserve original HTML (don't convert to markdown)\n    PreserveHtml,\n}\n\nimpl VisitorDispatch {\n    /// Check if this dispatch result indicates continuation.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_continue(&self) -> bool {\n        matches!(self, Self::Continue)\n    }\n\n    /// Check if this dispatch result contains custom output.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_custom(&self) -> bool {\n        matches!(self, Self::Custom(_))\n    }\n\n    /// Check if this dispatch result indicates skipping.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_skip(&self) -> bool {\n        matches!(self, Self::Skip)\n    }\n\n    /// Check if this dispatch result indicates HTML preservation.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_preserve_html(&self) -> bool {\n        matches!(self, Self::PreserveHtml)\n    }\n\n    /// Extract custom output if present.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub fn into_custom(self) -> Option<String> {\n        match self {\n            Self::Custom(output) => Some(output),\n            _ => None,\n        }\n    }\n\n    /// Extract custom output reference if present.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub fn as_custom(&self) -> Option<&str> {\n        match self {\n            Self::Custom(output) => Some(output),\n            _ => None,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_visitor_dispatch_predicates() {\n        let continue_dispatch = VisitorDispatch::Continue;\n        assert!(continue_dispatch.is_continue());\n        assert!(!continue_dispatch.is_custom());\n        assert!(!continue_dispatch.is_skip());\n\n        let custom_dispatch = VisitorDispatch::Custom(\"output\".to_string());\n        assert!(!custom_dispatch.is_continue());\n        assert!(custom_dispatch.is_custom());\n        assert!(!custom_dispatch.is_skip());\n\n        let skip_dispatch = VisitorDispatch::Skip;\n        assert!(!skip_dispatch.is_continue());\n        assert!(!skip_dispatch.is_custom());\n        assert!(skip_dispatch.is_skip());\n\n        let preserve_dispatch = VisitorDispatch::PreserveHtml;\n        assert!(!preserve_dispatch.is_continue());\n        assert!(preserve_dispatch.is_preserve_html());\n    }\n\n    #[test]\n    fn test_visitor_dispatch_into_custom() {\n        let custom = VisitorDispatch::Custom(\"test\".to_string());\n        assert_eq!(custom.into_custom(), Some(\"test\".to_string()));\n\n        let continue_dispatch = VisitorDispatch::Continue;\n        assert_eq!(continue_dispatch.into_custom(), None);\n    }\n\n    #[test]\n    fn test_visitor_dispatch_as_custom() {\n        let custom = VisitorDispatch::Custom(\"test\".to_string());\n        assert_eq!(custom.as_custom(), Some(\"test\"));\n\n        let skip = VisitorDispatch::Skip;\n        assert_eq!(skip.as_custom(), None);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers/helpers/mod.rs",
    "content": "//! Helper functions for visitor pattern integration.\n//!\n//! This module provides efficient utilities for building visitor contexts,\n//! dispatching visitor callbacks, and handling visitor results during the\n//! HTML→Markdown conversion process.\n//!\n//! # Design Goals\n//!\n//! - **Zero allocation when possible**: Reuse existing data structures\n//! - **Minimal overhead**: Inline hot paths, avoid unnecessary clones\n//! - **Type safety**: Leverage Rust's type system for correct visitor handling\n//! - **Ergonomics**: Reduce boilerplate for common visitor patterns\n//!\n//! # Usage\n//!\n//! These helpers are designed to be used within the converter module during\n//! the DOM traversal. They bridge the gap between the internal conversion\n//! state and the public visitor API.\n\npub mod callbacks;\npub mod content;\npub mod state;\npub mod traversal;\n\npub use content::VisitorDispatch;\npub use state::build_node_context;\npub use traversal::dispatch_visitor;\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers/helpers/state.rs",
    "content": "//! Visitor state management and context building.\n//!\n//! This module handles construction of `NodeContext` objects that represent\n//! the state of DOM nodes during traversal.\n\nuse std::collections::BTreeMap;\n\nuse crate::visitor::NodeContext;\nuse crate::visitor::NodeType;\n\n/// Build a `NodeContext` from current parsing state.\n///\n/// Creates a complete `NodeContext` suitable for passing to visitor callbacks.\n/// This function collects metadata about the current node from various sources:\n/// - Tag name and attributes from the HTML element\n/// - Depth and parent information from the DOM tree\n/// - Index among siblings for positional awareness\n/// - Inline/block classification\n///\n/// # Parameters\n///\n/// - `node_type`: Coarse-grained classification (Link, Image, Heading, etc.)\n/// - `tag_name`: Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n/// - `attributes`: All HTML attributes as key-value pairs\n/// - `depth`: Nesting depth in the DOM tree (0 = root)\n/// - `index_in_parent`: Zero-based index among siblings\n/// - `parent_tag`: Parent element's tag name (None if root)\n/// - `is_inline`: Whether this element is treated as inline vs block\n///\n/// # Returns\n///\n/// A fully populated `NodeContext` ready for visitor dispatch.\n///\n/// # Performance\n///\n/// This function performs minimal allocations:\n/// - Clones `tag_name` (typically 2-10 bytes)\n/// - Clones `parent_tag` if present (typically 2-10 bytes)\n/// - Clones the attributes `BTreeMap` (heap allocation if non-empty)\n///\n/// For text nodes and simple elements without attributes, allocations are minimal.\n///\n/// # Examples\n///\n/// ```text\n/// let ctx = build_node_context(\n///     NodeType::Heading,\n///     \"h1\",\n///     &attrs,\n///     1,\n///     0,\n///     Some(\"body\"),\n///     false,\n/// );\n/// ```\n#[allow(dead_code)]\n#[inline]\npub fn build_node_context(\n    node_type: NodeType,\n    tag_name: &str,\n    attributes: &BTreeMap<String, String>,\n    depth: usize,\n    index_in_parent: usize,\n    parent_tag: Option<&str>,\n    is_inline: bool,\n) -> NodeContext {\n    NodeContext {\n        node_type,\n        tag_name: tag_name.to_string(),\n        attributes: attributes.clone(),\n        depth,\n        index_in_parent,\n        parent_tag: parent_tag.map(String::from),\n        is_inline,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_build_node_context() {\n        let mut attrs = BTreeMap::new();\n        attrs.insert(\"id\".to_string(), \"main\".to_string());\n        attrs.insert(\"class\".to_string(), \"container\".to_string());\n\n        let ctx = build_node_context(NodeType::Div, \"div\", &attrs, 2, 3, Some(\"body\"), false);\n\n        assert_eq!(ctx.node_type, NodeType::Div);\n        assert_eq!(ctx.tag_name, \"div\");\n        assert_eq!(ctx.depth, 2);\n        assert_eq!(ctx.index_in_parent, 3);\n        assert_eq!(ctx.parent_tag, Some(\"body\".to_string()));\n        assert!(!ctx.is_inline);\n        assert_eq!(ctx.attributes.len(), 2);\n        assert_eq!(ctx.attributes.get(\"id\"), Some(&\"main\".to_string()));\n    }\n\n    #[test]\n    fn test_build_node_context_no_parent() {\n        let attrs = BTreeMap::new();\n\n        let ctx = build_node_context(NodeType::Html, \"html\", &attrs, 0, 0, None, false);\n\n        assert_eq!(ctx.node_type, NodeType::Html);\n        assert_eq!(ctx.parent_tag, None);\n        assert!(ctx.attributes.is_empty());\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers/helpers/traversal.rs",
    "content": "//! Visitor callback dispatch and result handling.\n//!\n//! This module provides the core dispatching logic for synchronous visitor callbacks,\n//! safely handling optional visitors and translating `VisitResult` into concrete\n//! control flow decisions.\n\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\nuse crate::error::{ConversionError, Result};\nuse crate::visitor::HtmlVisitor;\nuse crate::visitor::VisitResult;\n\nuse super::content::VisitorDispatch;\n\n/// Dispatch a visitor callback and handle the result.\n///\n/// This is the core dispatcher for all visitor callbacks. It safely handles the\n/// optional visitor, calls the callback function, and translates the `VisitResult`\n/// into concrete control flow decisions.\n///\n/// # Type Parameters\n///\n/// - `F`: Visitor callback function type\n///\n/// # Parameters\n///\n/// - `visitor`: Optional visitor (wrapped in Rc<`RefCell`<>>)\n/// - `callback`: Closure that invokes the appropriate visitor method\n///\n/// # Returns\n///\n/// - `Ok(Some(String))`: Custom markdown output from `VisitResult::Custom`\n/// - `Ok(None)`: Continue with default behavior (`VisitResult::Continue`)\n/// - `Err(Error)`: Stop conversion with error (`VisitResult::Error`)\n///\n/// The `VisitResult::Skip` and `VisitResult::PreserveHtml` variants are handled\n/// by the caller based on context.\n///\n/// # Error Handling\n///\n/// - If the visitor panics during callback, the panic propagates normally\n/// - If the visitor returns `VisitResult::Error`, this is converted to `Error::Visitor`\n/// - `RefCell` borrow failures panic (should never happen with correct usage)\n///\n/// # Performance\n///\n/// - Zero-cost when visitor is None (common case)\n/// - Single dynamic dispatch when visitor is present\n/// - No allocations except for error messages\n///\n/// # Examples\n///\n/// ```text\n/// let result = dispatch_visitor(\n///     &visitor,\n///     |v| v.visit_heading(&ctx, level, text, id),\n/// )?;\n///\n/// match result {\n///     Some(custom_output) => return Ok(custom_output),\n///     None => { /* proceed with default conversion */ }\n/// }\n/// ```\n#[allow(dead_code)]\n#[inline]\npub fn dispatch_visitor<F>(visitor: &Option<Rc<RefCell<dyn HtmlVisitor>>>, callback: F) -> Result<VisitorDispatch>\nwhere\n    F: FnOnce(&mut dyn HtmlVisitor) -> VisitResult,\n{\n    let Some(visitor_rc) = visitor else {\n        return Ok(VisitorDispatch::Continue);\n    };\n\n    let mut visitor_ref = visitor_rc.borrow_mut();\n    let result = callback(&mut *visitor_ref);\n\n    match result {\n        VisitResult::Continue => Ok(VisitorDispatch::Continue),\n        VisitResult::Custom(output) => Ok(VisitorDispatch::Custom(output)),\n        VisitResult::Skip => Ok(VisitorDispatch::Skip),\n        VisitResult::PreserveHtml => Ok(VisitorDispatch::PreserveHtml),\n        VisitResult::Error(msg) => Err(ConversionError::Visitor(msg)),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::collections::BTreeMap;\n\n    use crate::visitor::{NodeContext, NodeType};\n\n    #[derive(Debug)]\n    struct TestVisitor {\n        mode: TestMode,\n    }\n\n    #[derive(Debug)]\n    enum TestMode {\n        Continue,\n        Custom,\n        Skip,\n        PreserveHtml,\n        Error,\n    }\n\n    impl HtmlVisitor for TestVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n            match self.mode {\n                TestMode::Continue => VisitResult::Continue,\n                TestMode::Custom => VisitResult::Custom(format!(\"CUSTOM: {}\", text)),\n                TestMode::Skip => VisitResult::Skip,\n                TestMode::PreserveHtml => VisitResult::PreserveHtml,\n                TestMode::Error => VisitResult::Error(\"test error\".to_string()),\n            }\n        }\n    }\n\n    #[test]\n    fn test_dispatch_visitor_none() {\n        let visitor: Option<Rc<RefCell<dyn HtmlVisitor>>> = None;\n\n        let result = dispatch_visitor(&visitor, |v| {\n            let ctx = NodeContext {\n                node_type: NodeType::Text,\n                tag_name: String::new(),\n                attributes: BTreeMap::new(),\n                depth: 0,\n                index_in_parent: 0,\n                parent_tag: None,\n                is_inline: true,\n            };\n            v.visit_text(&ctx, \"test\")\n        })\n        .unwrap();\n\n        assert!(result.is_continue());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_continue() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {\n            mode: TestMode::Continue,\n        }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_continue());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_custom() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Custom }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_custom());\n        assert_eq!(result.as_custom(), Some(\"CUSTOM: hello\"));\n    }\n\n    #[test]\n    fn test_dispatch_visitor_skip() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Skip }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_skip());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_preserve_html() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {\n            mode: TestMode::PreserveHtml,\n        }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_preserve_html());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_error() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Error }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\"));\n\n        assert!(result.is_err());\n        if let Err(ConversionError::Visitor(msg)) = result {\n            assert_eq!(msg, \"test error\");\n        } else {\n            panic!(\"Expected Visitor error\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/visitor_helpers.rs",
    "content": "//! Helper functions for visitor pattern integration.\n//!\n//! This module provides efficient utilities for building visitor contexts,\n//! dispatching visitor callbacks, and handling visitor results during the\n//! HTML→Markdown conversion process.\n//!\n//! # Design Goals\n//!\n//! - **Zero allocation when possible**: Reuse existing data structures\n//! - **Minimal overhead**: Inline hot paths, avoid unnecessary clones\n//! - **Type safety**: Leverage Rust's type system for correct visitor handling\n//! - **Ergonomics**: Reduce boilerplate for common visitor patterns\n//!\n//! # Usage\n//!\n//! These helpers are designed to be used within the converter module during\n//! the DOM traversal. They bridge the gap between the internal conversion\n//! state and the public visitor API.\n\nuse std::cell::RefCell;\nuse std::collections::BTreeMap;\nuse std::rc::Rc;\n\nuse crate::error::{ConversionError, Result};\nuse crate::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult};\n\n/// Build a `NodeContext` from current parsing state.\n///\n/// Creates a complete `NodeContext` suitable for passing to visitor callbacks.\n/// This function collects metadata about the current node from various sources:\n/// - Tag name and attributes from the HTML element\n/// - Depth and parent information from the DOM tree\n/// - Index among siblings for positional awareness\n/// - Inline/block classification\n///\n/// # Parameters\n///\n/// - `node_type`: Coarse-grained classification (Link, Image, Heading, etc.)\n/// - `tag_name`: Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n/// - `attributes`: All HTML attributes as key-value pairs\n/// - `depth`: Nesting depth in the DOM tree (0 = root)\n/// - `index_in_parent`: Zero-based index among siblings\n/// - `parent_tag`: Parent element's tag name (None if root)\n/// - `is_inline`: Whether this element is treated as inline vs block\n///\n/// # Returns\n///\n/// A fully populated `NodeContext` ready for visitor dispatch.\n///\n/// # Performance\n///\n/// This function performs minimal allocations:\n/// - Clones `tag_name` (typically 2-10 bytes)\n/// - Clones `parent_tag` if present (typically 2-10 bytes)\n/// - Clones the attributes `BTreeMap` (heap allocation if non-empty)\n///\n/// For text nodes and simple elements without attributes, allocations are minimal.\n///\n/// # Examples\n///\n/// ```text\n/// let ctx = build_node_context(\n///     NodeType::Heading,\n///     \"h1\",\n///     &attrs,\n///     1,\n///     0,\n///     Some(\"body\"),\n///     false,\n/// );\n/// ```\n#[allow(dead_code)]\n#[inline]\npub fn build_node_context(\n    node_type: NodeType,\n    tag_name: &str,\n    attributes: &BTreeMap<String, String>,\n    depth: usize,\n    index_in_parent: usize,\n    parent_tag: Option<&str>,\n    is_inline: bool,\n) -> NodeContext {\n    NodeContext {\n        node_type,\n        tag_name: tag_name.to_string(),\n        attributes: attributes.clone(),\n        depth,\n        index_in_parent,\n        parent_tag: parent_tag.map(String::from),\n        is_inline,\n    }\n}\n\n/// Dispatch a visitor callback and handle the result.\n///\n/// This is the core dispatcher for all visitor callbacks. It safely handles the\n/// optional visitor, calls the callback function, and translates the `VisitResult`\n/// into concrete control flow decisions.\n///\n/// # Type Parameters\n///\n/// - `F`: Visitor callback function type\n///\n/// # Parameters\n///\n/// - `visitor`: Optional visitor (wrapped in Rc<`RefCell`<>>)\n/// - `callback`: Closure that invokes the appropriate visitor method\n///\n/// # Returns\n///\n/// - `Ok(Some(String))`: Custom markdown output from `VisitResult::Custom`\n/// - `Ok(None)`: Continue with default behavior (`VisitResult::Continue`)\n/// - `Err(Error)`: Stop conversion with error (`VisitResult::Error`)\n///\n/// The `VisitResult::Skip` and `VisitResult::PreserveHtml` variants are handled\n/// by the caller based on context.\n///\n/// # Error Handling\n///\n/// - If the visitor panics during callback, the panic propagates normally\n/// - If the visitor returns `VisitResult::Error`, this is converted to `Error::Visitor`\n/// - `RefCell` borrow failures panic (should never happen with correct usage)\n///\n/// # Performance\n///\n/// - Zero-cost when visitor is None (common case)\n/// - Single dynamic dispatch when visitor is present\n/// - No allocations except for error messages\n///\n/// # Examples\n///\n/// ```text\n/// let result = dispatch_visitor(\n///     &visitor,\n///     |v| v.visit_heading(&ctx, level, text, id),\n/// )?;\n///\n/// match result {\n///     Some(custom_output) => return Ok(custom_output),\n///     None => { /* proceed with default conversion */ }\n/// }\n/// ```\n#[allow(dead_code)]\n#[inline]\n/// # Errors\n///\n/// Returns an error if visitor dispatch fails.\npub fn dispatch_visitor<F>(visitor: &Option<Rc<RefCell<dyn HtmlVisitor>>>, callback: F) -> Result<VisitorDispatch>\nwhere\n    F: FnOnce(&mut dyn HtmlVisitor) -> VisitResult,\n{\n    let Some(visitor_rc) = visitor else {\n        return Ok(VisitorDispatch::Continue);\n    };\n\n    let mut visitor_ref = visitor_rc.borrow_mut();\n    let result = callback(&mut *visitor_ref);\n\n    match result {\n        VisitResult::Continue => Ok(VisitorDispatch::Continue),\n        VisitResult::Custom(output) => Ok(VisitorDispatch::Custom(output)),\n        VisitResult::Skip => Ok(VisitorDispatch::Skip),\n        VisitResult::PreserveHtml => Ok(VisitorDispatch::PreserveHtml),\n        VisitResult::Error(msg) => Err(ConversionError::Visitor(msg)),\n    }\n}\n\n/// Result of dispatching a visitor callback.\n///\n/// This enum represents the outcome of a visitor callback dispatch,\n/// providing a more ergonomic interface for control flow than the\n/// raw `VisitResult` type.\n#[allow(dead_code)]\n#[derive(Debug)]\npub enum VisitorDispatch {\n    /// Continue with default conversion behavior\n    Continue,\n\n    /// Replace default output with custom markdown\n    Custom(String),\n\n    /// Skip this element entirely (don't output anything)\n    Skip,\n\n    /// Preserve original HTML (don't convert to markdown)\n    PreserveHtml,\n}\n\nimpl VisitorDispatch {\n    /// Check if this dispatch result indicates continuation.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_continue(&self) -> bool {\n        matches!(self, Self::Continue)\n    }\n\n    /// Check if this dispatch result contains custom output.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_custom(&self) -> bool {\n        matches!(self, Self::Custom(_))\n    }\n\n    /// Check if this dispatch result indicates skipping.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_skip(&self) -> bool {\n        matches!(self, Self::Skip)\n    }\n\n    /// Check if this dispatch result indicates HTML preservation.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub const fn is_preserve_html(&self) -> bool {\n        matches!(self, Self::PreserveHtml)\n    }\n\n    /// Extract custom output if present.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub fn into_custom(self) -> Option<String> {\n        match self {\n            Self::Custom(output) => Some(output),\n            _ => None,\n        }\n    }\n\n    /// Extract custom output reference if present.\n    #[allow(dead_code)]\n    #[inline]\n    #[must_use]\n    pub fn as_custom(&self) -> Option<&str> {\n        match self {\n            Self::Custom(output) => Some(output),\n            _ => None,\n        }\n    }\n}\n\n/// Macro to reduce boilerplate when calling visitor methods.\n///\n/// This macro wraps the common pattern of:\n/// 1. Check if visitor is present\n/// 2. Call visitor method\n/// 3. Handle early return for Custom/Skip/PreserveHtml/Error\n/// 4. Continue with default behavior if visitor returns Continue\n///\n/// # Syntax\n///\n/// ```text\n/// try_visitor!(visitor_option, method_name, ctx, arg1, arg2, ...);\n/// ```\n///\n/// # Returns\n///\n/// - Returns early with custom output if visitor returns Custom/Skip/PreserveHtml\n/// - Returns early with Err if visitor returns Error\n/// - Continues execution if visitor returns Continue or is None\n///\n/// # Examples\n///\n/// ```text\n/// // Before (verbose):\n/// let dispatch = dispatch_visitor(&visitor, |v| v.visit_heading(&ctx, level, text, id))?;\n/// match dispatch {\n///     VisitorDispatch::Custom(output) => return Ok(output),\n///     VisitorDispatch::Skip => return Ok(String::new()),\n///     VisitorDispatch::PreserveHtml => return Ok(preserve_html_output),\n///     VisitorDispatch::Continue => { /* proceed */ }\n/// }\n///\n/// // After (concise):\n/// try_visitor!(visitor, visit_heading, &ctx, level, text, id);\n/// // Default conversion logic continues here...\n/// ```\n#[macro_export]\nmacro_rules! try_visitor {\n    ($visitor:expr, $method:ident, $ctx:expr $(, $arg:expr)*) => {{\n        let dispatch = $crate::visitor_helpers::dispatch_visitor(\n            $visitor,\n            |v| v.$method($ctx $(, $arg)*),\n        )?;\n\n        match dispatch {\n            $crate::visitor_helpers::VisitorDispatch::Continue => {\n            }\n            $crate::visitor_helpers::VisitorDispatch::Custom(output) => {\n                return Ok(output);\n            }\n            $crate::visitor_helpers::VisitorDispatch::Skip => {\n                return Ok(String::new());\n            }\n            $crate::visitor_helpers::VisitorDispatch::PreserveHtml => {\n                // Falls through to default conversion — full HTML preservation requires\n                // the node handle and parser context which aren't available in this macro.\n                // Callers that need PreserveHtml support should match on the dispatch\n                // result directly and call serialize_tag_to_html.\n            }\n        }\n    }};\n}\n\n/// Convenience macro for `element_start` visitor calls with early return.\n///\n/// This specialized macro handles the common pattern of calling `visit_element_start`\n/// at the beginning of element processing. Unlike `try_visitor!`, this macro\n/// understands that `element_start` callbacks typically want to abort processing\n/// entirely if they return anything other than Continue.\n///\n/// # Syntax\n///\n/// ```text\n/// try_visitor_element_start!(visitor_option, ctx);\n/// ```\n///\n/// # Examples\n///\n/// ```text\n/// fn process_heading(...) -> Result<String> {\n///     let ctx = build_node_context(...);\n///     try_visitor_element_start!(visitor, &ctx)?;\n///\n///     // Default heading processing continues here...\n/// }\n/// ```\n#[macro_export]\nmacro_rules! try_visitor_element_start {\n    ($visitor:expr, $ctx:expr) => {{\n        $crate::try_visitor!($visitor, visit_element_start, $ctx);\n    }};\n}\n\n/// Convenience macro for `element_end` visitor calls with output inspection.\n///\n/// This specialized macro handles the common pattern of calling `visit_element_end`\n/// after generating default markdown output. The visitor receives the default\n/// output and can choose to replace it or let it pass through.\n///\n/// # Syntax\n///\n/// ```text\n/// try_visitor_element_end!(visitor_option, ctx, default_output_string);\n/// ```\n///\n/// # Examples\n///\n/// ```text\n/// fn process_heading(...) -> Result<String> {\n///     let ctx = build_node_context(...);\n///     let mut output = String::from(\"# Heading\");\n///\n///     try_visitor_element_end!(visitor, &ctx, &output)?;\n///     Ok(output)\n/// }\n/// ```\n#[macro_export]\nmacro_rules! try_visitor_element_end {\n    ($visitor:expr, $ctx:expr, $output:expr) => {{\n        $crate::try_visitor!($visitor, visit_element_end, $ctx, $output);\n    }};\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_build_node_context() {\n        let mut attrs = BTreeMap::new();\n        attrs.insert(\"id\".to_string(), \"main\".to_string());\n        attrs.insert(\"class\".to_string(), \"container\".to_string());\n\n        let ctx = build_node_context(NodeType::Div, \"div\", &attrs, 2, 3, Some(\"body\"), false);\n\n        assert_eq!(ctx.node_type, NodeType::Div);\n        assert_eq!(ctx.tag_name, \"div\");\n        assert_eq!(ctx.depth, 2);\n        assert_eq!(ctx.index_in_parent, 3);\n        assert_eq!(ctx.parent_tag, Some(\"body\".to_string()));\n        assert!(!ctx.is_inline);\n        assert_eq!(ctx.attributes.len(), 2);\n        assert_eq!(ctx.attributes.get(\"id\"), Some(&\"main\".to_string()));\n    }\n\n    #[test]\n    fn test_build_node_context_no_parent() {\n        let attrs = BTreeMap::new();\n\n        let ctx = build_node_context(NodeType::Html, \"html\", &attrs, 0, 0, None, false);\n\n        assert_eq!(ctx.node_type, NodeType::Html);\n        assert_eq!(ctx.parent_tag, None);\n        assert!(ctx.attributes.is_empty());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_none() {\n        let visitor: Option<Rc<RefCell<dyn HtmlVisitor>>> = None;\n\n        let result = dispatch_visitor(&visitor, |v| {\n            let ctx = NodeContext {\n                node_type: NodeType::Text,\n                tag_name: String::new(),\n                attributes: BTreeMap::new(),\n                depth: 0,\n                index_in_parent: 0,\n                parent_tag: None,\n                is_inline: true,\n            };\n            v.visit_text(&ctx, \"test\")\n        })\n        .unwrap();\n\n        assert!(result.is_continue());\n    }\n\n    #[derive(Debug)]\n    struct TestVisitor {\n        mode: TestMode,\n    }\n\n    #[derive(Debug)]\n    enum TestMode {\n        Continue,\n        Custom,\n        Skip,\n        PreserveHtml,\n        Error,\n    }\n\n    impl HtmlVisitor for TestVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n            match self.mode {\n                TestMode::Continue => VisitResult::Continue,\n                TestMode::Custom => VisitResult::Custom(format!(\"CUSTOM: {}\", text)),\n                TestMode::Skip => VisitResult::Skip,\n                TestMode::PreserveHtml => VisitResult::PreserveHtml,\n                TestMode::Error => VisitResult::Error(\"test error\".to_string()),\n            }\n        }\n    }\n\n    #[test]\n    fn test_dispatch_visitor_continue() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {\n            mode: TestMode::Continue,\n        }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_continue());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_custom() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Custom }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_custom());\n        assert_eq!(result.as_custom(), Some(\"CUSTOM: hello\"));\n    }\n\n    #[test]\n    fn test_dispatch_visitor_skip() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Skip }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_skip());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_preserve_html() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {\n            mode: TestMode::PreserveHtml,\n        }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\")).unwrap();\n\n        assert!(result.is_preserve_html());\n    }\n\n    #[test]\n    fn test_dispatch_visitor_error() {\n        let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Error }));\n        let visitor_opt = Some(visitor);\n\n        let ctx = NodeContext {\n            node_type: NodeType::Text,\n            tag_name: String::new(),\n            attributes: BTreeMap::new(),\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: None,\n            is_inline: true,\n        };\n\n        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, \"hello\"));\n\n        assert!(result.is_err());\n        if let Err(ConversionError::Visitor(msg)) = result {\n            assert_eq!(msg, \"test error\");\n        } else {\n            panic!(\"Expected Visitor error\");\n        }\n    }\n\n    #[test]\n    fn test_visitor_dispatch_predicates() {\n        let continue_dispatch = VisitorDispatch::Continue;\n        assert!(continue_dispatch.is_continue());\n        assert!(!continue_dispatch.is_custom());\n        assert!(!continue_dispatch.is_skip());\n\n        let custom_dispatch = VisitorDispatch::Custom(\"output\".to_string());\n        assert!(!custom_dispatch.is_continue());\n        assert!(custom_dispatch.is_custom());\n        assert!(!custom_dispatch.is_skip());\n\n        let skip_dispatch = VisitorDispatch::Skip;\n        assert!(!skip_dispatch.is_continue());\n        assert!(!skip_dispatch.is_custom());\n        assert!(skip_dispatch.is_skip());\n\n        let preserve_dispatch = VisitorDispatch::PreserveHtml;\n        assert!(!preserve_dispatch.is_continue());\n        assert!(preserve_dispatch.is_preserve_html());\n    }\n\n    #[test]\n    fn test_visitor_dispatch_into_custom() {\n        let custom = VisitorDispatch::Custom(\"test\".to_string());\n        assert_eq!(custom.into_custom(), Some(\"test\".to_string()));\n\n        let continue_dispatch = VisitorDispatch::Continue;\n        assert_eq!(continue_dispatch.into_custom(), None);\n    }\n\n    #[test]\n    fn test_visitor_dispatch_as_custom() {\n        let custom = VisitorDispatch::Custom(\"test\".to_string());\n        assert_eq!(custom.as_custom(), Some(\"test\"));\n\n        let skip = VisitorDispatch::Skip;\n        assert_eq!(skip.as_custom(), None);\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/wrapper/sync.rs",
    "content": "//! Synchronous text wrapping for Markdown output.\n\nuse super::utils::{\n    is_heading, is_list_like, is_numbered_list, parse_blockquote_line, parse_list_item, wrap_blockquote_paragraph,\n    wrap_line, wrap_list_item,\n};\nuse crate::options::ConversionOptions;\n\n/// Wrap text at specified width while preserving Markdown formatting.\n///\n/// This function wraps paragraphs of text at the specified width, but:\n/// - Does not break long words\n/// - Does not break on hyphens\n/// - Preserves Markdown formatting (links, bold, etc.)\n/// - Only wraps paragraph content, not headers, lists, code blocks, etc.\n#[must_use]\n#[allow(clippy::too_many_lines)]\npub fn wrap_markdown(markdown: &str, options: &ConversionOptions) -> String {\n    if !options.wrap {\n        return markdown.to_string();\n    }\n\n    let mut result = String::with_capacity(markdown.len());\n    let mut in_code_block = false;\n    let mut in_paragraph = false;\n    let mut paragraph_buffer = String::new();\n    let mut in_blockquote_paragraph = false;\n    let mut blockquote_prefix = String::new();\n    let mut blockquote_buffer = String::new();\n\n    for line in markdown.lines() {\n        let trimmed = line.trim_start();\n        let is_code_fence = trimmed.starts_with(\"```\");\n        let is_indented_code = line.starts_with(\"    \")\n            && !is_list_like(trimmed)\n            && !is_numbered_list(trimmed)\n            && !is_heading(trimmed)\n            && !trimmed.starts_with('>')\n            && !trimmed.starts_with('|');\n\n        if is_code_fence || is_indented_code {\n            if in_paragraph && !paragraph_buffer.is_empty() {\n                result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n                result.push_str(\"\\n\\n\");\n                paragraph_buffer.clear();\n                in_paragraph = false;\n            }\n\n            if is_code_fence {\n                in_code_block = !in_code_block;\n            }\n            result.push_str(line);\n            result.push('\\n');\n            continue;\n        }\n\n        if in_code_block {\n            result.push_str(line);\n            result.push('\\n');\n            continue;\n        }\n\n        if let Some((prefix, content)) = parse_blockquote_line(line) {\n            if in_paragraph && !paragraph_buffer.is_empty() {\n                result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n                result.push_str(\"\\n\\n\");\n                paragraph_buffer.clear();\n                in_paragraph = false;\n            }\n\n            let mut normalized_prefix = prefix;\n            if !normalized_prefix.ends_with(' ') {\n                normalized_prefix.push(' ');\n            }\n\n            if content.is_empty() {\n                if in_blockquote_paragraph && !blockquote_buffer.is_empty() {\n                    result.push_str(&wrap_blockquote_paragraph(\n                        &blockquote_prefix,\n                        &blockquote_buffer,\n                        options.wrap_width,\n                    ));\n                    result.push('\\n');\n                    blockquote_buffer.clear();\n                    in_blockquote_paragraph = false;\n                }\n                result.push_str(normalized_prefix.trim_end());\n                result.push('\\n');\n                continue;\n            }\n\n            if in_blockquote_paragraph && normalized_prefix != blockquote_prefix {\n                result.push_str(&wrap_blockquote_paragraph(\n                    &blockquote_prefix,\n                    &blockquote_buffer,\n                    options.wrap_width,\n                ));\n                result.push('\\n');\n                blockquote_buffer.clear();\n                in_blockquote_paragraph = false;\n            }\n\n            if in_blockquote_paragraph {\n                blockquote_buffer.push(' ');\n                blockquote_buffer.push_str(&content);\n            } else {\n                blockquote_prefix = normalized_prefix;\n                blockquote_buffer.push_str(&content);\n                in_blockquote_paragraph = true;\n            }\n            continue;\n        } else if in_blockquote_paragraph && !blockquote_buffer.is_empty() {\n            result.push_str(&wrap_blockquote_paragraph(\n                &blockquote_prefix,\n                &blockquote_buffer,\n                options.wrap_width,\n            ));\n            result.push('\\n');\n            blockquote_buffer.clear();\n            in_blockquote_paragraph = false;\n        }\n\n        if let Some((indent, marker, content)) = parse_list_item(line) {\n            if in_paragraph && !paragraph_buffer.is_empty() {\n                result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n                result.push_str(\"\\n\\n\");\n                paragraph_buffer.clear();\n                in_paragraph = false;\n            }\n\n            result.push_str(&wrap_list_item(&indent, &marker, &content, options.wrap_width));\n            continue;\n        }\n\n        let is_structural =\n            is_heading(trimmed) || trimmed.starts_with('>') || trimmed.starts_with('|') || trimmed.starts_with('=');\n\n        if is_structural {\n            if in_paragraph && !paragraph_buffer.is_empty() {\n                result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n                result.push_str(\"\\n\\n\");\n                paragraph_buffer.clear();\n                in_paragraph = false;\n            }\n\n            result.push_str(line);\n            result.push('\\n');\n            continue;\n        }\n\n        if line.trim().is_empty() {\n            if in_paragraph && !paragraph_buffer.is_empty() {\n                result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n                result.push_str(\"\\n\\n\");\n                paragraph_buffer.clear();\n                in_paragraph = false;\n            } else if !in_paragraph {\n                result.push('\\n');\n            }\n            continue;\n        }\n\n        if in_paragraph {\n            paragraph_buffer.push(' ');\n        }\n        paragraph_buffer.push_str(line.trim());\n        in_paragraph = true;\n    }\n\n    if in_blockquote_paragraph && !blockquote_buffer.is_empty() {\n        result.push_str(&wrap_blockquote_paragraph(\n            &blockquote_prefix,\n            &blockquote_buffer,\n            options.wrap_width,\n        ));\n        result.push('\\n');\n    }\n\n    if in_paragraph && !paragraph_buffer.is_empty() {\n        result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));\n        result.push_str(\"\\n\\n\");\n    }\n\n    result\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_wrap_markdown_disabled() {\n        let markdown = \"This is a very long line that would normally be wrapped at 40 characters\";\n        let options = ConversionOptions {\n            wrap: false,\n            ..Default::default()\n        };\n        let result = wrap_markdown(markdown, &options);\n        assert_eq!(result, markdown);\n    }\n\n    #[test]\n    fn test_wrap_markdown_paragraph() {\n        let markdown = \"This is a very long line that would normally be wrapped at 40 characters\\n\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 40,\n            ..Default::default()\n        };\n        let result = wrap_markdown(markdown, &options);\n        assert!(result.lines().all(|line| line.len() <= 40 || line.trim().is_empty()));\n    }\n\n    #[test]\n    fn test_wrap_markdown_blockquote_paragraph() {\n        let markdown = \"> This is a very long blockquote line that should wrap at 30 characters\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 30,\n            ..Default::default()\n        };\n        let result = wrap_markdown(markdown, &options);\n        assert!(\n            result.lines().all(|line| line.len() <= 30 || line.trim().is_empty()),\n            \"Some lines exceed wrap width. Got: {}\",\n            result\n        );\n        assert!(\n            result.contains(\"> This is a very\"),\n            \"Missing expected wrapped content. Got: {}\",\n            result\n        );\n        assert!(\n            result.lines().filter(|l| l.starts_with(\"> \")).count() >= 2,\n            \"Expected multiple wrapped blockquote lines. Got: {}\",\n            result\n        );\n    }\n\n    #[test]\n    fn test_wrap_markdown_preserves_code() {\n        let markdown = \"```\\nThis is a very long line in a code block that should not be wrapped\\n```\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 40,\n            ..Default::default()\n        };\n        let result = wrap_markdown(markdown, &options);\n        assert!(result.contains(\"This is a very long line in a code block that should not be wrapped\"));\n    }\n\n    #[test]\n    fn test_wrap_markdown_preserves_headings() {\n        let markdown = \"# This is a very long heading that should not be wrapped even if it exceeds the width\\n\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 40,\n            ..Default::default()\n        };\n        let result = wrap_markdown(markdown, &options);\n        assert!(\n            result.contains(\"# This is a very long heading that should not be wrapped even if it exceeds the width\")\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_wraps_long_list_items() {\n        let markdown = \"- This is a very long list item that should definitely be wrapped when it exceeds the specified wrap width\\n- Short item\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 60,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(\n            result.contains(\"- This is a very long list item that should definitely be\\n  wrapped\"),\n            \"First list item not properly wrapped. Got: {}\",\n            result\n        );\n        assert!(\n            result.contains(\"- Short item\"),\n            \"Short list item incorrectly modified. Got: {}\",\n            result\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_wraps_ordered_lists() {\n        let markdown = \"1. This is a numbered list item with a very long text that should be wrapped at the specified width\\n2. Short\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 60,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(\n            result.lines().all(|line| line.len() <= 60 || line.trim().is_empty()),\n            \"Some lines exceed wrap width. Got: {}\",\n            result\n        );\n        assert!(result.contains(\"1.\"), \"Lost ordered list marker. Got: {}\", result);\n        assert!(\n            result.contains(\"2.\"),\n            \"Lost second ordered list marker. Got: {}\",\n            result\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_preserves_nested_list_structure() {\n        let markdown = \"- Item one with some additional text that will need to be wrapped across multiple lines\\n  - Nested item with long text that also needs wrapping at the specified width\\n  - Short nested\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 50,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(result.contains(\"- Item\"), \"Lost top-level list marker. Got: {}\", result);\n        assert!(\n            result.contains(\"  - Nested\"),\n            \"Lost nested list structure. Got: {}\",\n            result\n        );\n        assert!(\n            result.lines().all(|line| line.len() <= 50 || line.trim().is_empty()),\n            \"Some lines exceed wrap width. Got: {}\",\n            result\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_handles_list_with_links() {\n        let markdown = \"- [A](#a) with additional text that is long enough to require wrapping at the configured width\\n  - [B](#b) also has more content that needs wrapping\\n  - [C](#c)\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 50,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(result.contains(\"[A](#a)\"), \"Lost link in list. Got: {}\", result);\n        assert!(result.contains(\"[B](#b)\"), \"Lost nested link. Got: {}\", result);\n        assert!(result.contains(\"[C](#c)\"), \"Lost short nested link. Got: {}\", result);\n        assert!(\n            result.contains(\"- [A](#a)\"),\n            \"Lost list structure with link. Got: {}\",\n            result\n        );\n        assert!(\n            result.contains(\"  - [B](#b)\"),\n            \"Lost nested list structure. Got: {}\",\n            result\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_handles_empty_list_items() {\n        let markdown = \"- \\n- Item with text\\n- \\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 40,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(result.contains(\"- \"), \"Lost list markers. Got: {}\", result);\n        assert!(result.contains(\"Item with text\"), \"Lost item text. Got: {}\", result);\n    }\n\n    #[test]\n    fn wrap_markdown_preserves_indented_lists_with_wrapping() {\n        let markdown = \"- [A](#a) with some additional text that makes this line very long and should be wrapped\\n  - [B](#b)\\n  - [C](#c) with more text that is also quite long and needs wrapping\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 50,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(result.contains(\"- [A](#a)\"), \"Lost top-level link. Got: {}\", result);\n        assert!(result.contains(\"  - [B](#b)\"), \"Lost nested link B. Got: {}\", result);\n        assert!(result.contains(\"  - [C](#c)\"), \"Lost nested link C. Got: {}\", result);\n        assert!(\n            result.lines().all(|line| line.len() <= 50),\n            \"Some lines exceed wrap width:\\n{}\",\n            result\n        );\n    }\n\n    #[test]\n    fn wrap_markdown_does_not_wrap_link_only_items() {\n        let markdown = \"- [A very long link label that would exceed wrap width](#a-very-long-link-label)\\n  - [Nested very long link label that would also exceed](#nested)\\n\";\n        let options = ConversionOptions {\n            wrap: true,\n            wrap_width: 30,\n            ..Default::default()\n        };\n\n        let result = wrap_markdown(markdown, &options);\n\n        assert!(result.contains(\"- [A very long link label that would exceed wrap width](#a-very-long-link-label)\"));\n        assert!(result.contains(\"  - [Nested very long link label that would also exceed](#nested)\"));\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/wrapper/utils.rs",
    "content": "//! Utility functions for text wrapping.\n//!\n//! This module contains helper functions for parsing and wrapping Markdown elements.\n\n/// Parse a blockquote line into its prefix and content.\n///\n/// Returns Some((prefix, content)) if the line is a blockquote, None otherwise.\npub fn parse_blockquote_line(line: &str) -> Option<(String, String)> {\n    let trimmed = line.trim_start();\n    if !trimmed.starts_with('>') {\n        return None;\n    }\n\n    let indent_len = line.len() - trimmed.len();\n    let bytes = line.as_bytes();\n    let mut i = indent_len;\n\n    while i < bytes.len() {\n        if bytes[i] != b'>' {\n            break;\n        }\n        i += 1;\n        if i < bytes.len() && bytes[i] == b' ' {\n            i += 1;\n        }\n        while i + 1 < bytes.len() && bytes[i] == b' ' && bytes[i + 1] == b'>' {\n            i += 1;\n        }\n    }\n\n    let prefix = line[..i].to_string();\n    let content = line[i..].trim().to_string();\n    Some((prefix, content))\n}\n\n/// Wrap a blockquote paragraph while preserving its prefix.\n///\n/// # Arguments\n/// - `prefix`: The blockquote prefix (e.g., \"> \" or \"> > \")\n/// - `content`: The text content to wrap\n/// - `width`: The maximum line width\npub fn wrap_blockquote_paragraph(prefix: &str, content: &str, width: usize) -> String {\n    let prefix_len = prefix.len();\n    let inner_width = if width > prefix_len { width - prefix_len } else { 1 };\n\n    let wrapped = wrap_line(content, inner_width);\n    let mut out = String::new();\n    for (idx, part) in wrapped.split('\\n').enumerate() {\n        if idx > 0 {\n            out.push('\\n');\n        }\n        out.push_str(prefix);\n        out.push_str(part);\n    }\n    out\n}\n\n/// Check if a line looks like an unordered list item (-, *, or +).\npub fn is_list_like(trimmed: &str) -> bool {\n    matches!(trimmed.chars().next(), Some('-' | '*' | '+'))\n}\n\n/// Check if a line is a numbered list item.\npub fn is_numbered_list(trimmed: &str) -> bool {\n    let token = trimmed.split_whitespace().next().unwrap_or(\"\");\n    if token.is_empty() || !(token.ends_with('.') || token.ends_with(')')) {\n        return false;\n    }\n\n    let digits = token.trim_end_matches(['.', ')']);\n    !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())\n}\n\n/// Check if a line is a Markdown heading.\npub fn is_heading(trimmed: &str) -> bool {\n    trimmed.starts_with('#')\n}\n\n/// Parse a list item into its components: (indent, marker, content)\n///\n/// Returns Some((indent, marker, content)) if the line is a valid list item,\n/// None otherwise.\n///\n/// Examples:\n/// - \"- text\" -> (\"\", \"- \", \"text\")\n/// - \"  - text\" -> (\"  \", \"- \", \"text\")\n/// - \"1. text\" -> (\"\", \"1. \", \"text\")\n/// - \"  42) text\" -> (\"  \", \"42) \", \"text\")\npub fn parse_list_item(line: &str) -> Option<(String, String, String)> {\n    let trimmed = line.trim_start();\n    let indent = &line[..line.len() - trimmed.len()];\n\n    if let Some(rest) = trimmed.strip_prefix('-') {\n        if rest.starts_with(' ') || rest.is_empty() {\n            return Some((indent.to_string(), \"- \".to_string(), rest.trim_start().to_string()));\n        }\n    }\n    if let Some(rest) = trimmed.strip_prefix('*') {\n        if rest.starts_with(' ') || rest.is_empty() {\n            return Some((indent.to_string(), \"* \".to_string(), rest.trim_start().to_string()));\n        }\n    }\n    if let Some(rest) = trimmed.strip_prefix('+') {\n        if rest.starts_with(' ') || rest.is_empty() {\n            return Some((indent.to_string(), \"+ \".to_string(), rest.trim_start().to_string()));\n        }\n    }\n\n    let first_token = trimmed.split_whitespace().next()?;\n    if first_token.ends_with('.') || first_token.ends_with(')') {\n        let digits = first_token.trim_end_matches(['.', ')']);\n        if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {\n            let marker_len = first_token.len();\n            let rest = trimmed[marker_len..].trim_start();\n            return Some((\n                indent.to_string(),\n                trimmed[..marker_len].to_string() + \" \",\n                rest.to_string(),\n            ));\n        }\n    }\n\n    None\n}\n\n/// Check if content is a single inline link (e.g., \"[text](#anchor)\").\npub fn is_single_inline_link(content: &str) -> bool {\n    let trimmed = content.trim();\n    if !(trimmed.starts_with('[') && trimmed.ends_with(')')) {\n        return false;\n    }\n\n    let Some(mid) = trimmed.find(\"](\") else {\n        return false;\n    };\n\n    let url_part = &trimmed[mid + 2..trimmed.len() - 1];\n    if url_part.chars().any(char::is_whitespace) {\n        return false;\n    }\n\n    !trimmed[mid + 2..].contains(\"](\")\n}\n\n/// Wrap a single line of text at the specified width.\n///\n/// This function wraps text without breaking long words or on hyphens,\n/// similar to Python's `textwrap.fill()` with `break_long_words=False` and `break_on_hyphens=False`.\npub fn wrap_line(text: &str, width: usize) -> String {\n    if text.len() <= width {\n        return text.to_string();\n    }\n\n    let mut result = String::new();\n    let mut current_line = String::new();\n    let words: Vec<&str> = text.split_whitespace().collect();\n\n    for word in words {\n        if current_line.is_empty() {\n            current_line.push_str(word);\n        } else if current_line.len() + 1 + word.len() <= width {\n            current_line.push(' ');\n            current_line.push_str(word);\n        } else {\n            if !result.is_empty() {\n                result.push('\\n');\n            }\n            result.push_str(&current_line);\n            current_line.clear();\n            current_line.push_str(word);\n        }\n    }\n\n    if !current_line.is_empty() {\n        if !result.is_empty() {\n            result.push('\\n');\n        }\n        result.push_str(&current_line);\n    }\n\n    result\n}\n\n/// Wrap a list item while preserving its structure.\n///\n/// The first line of output will be: `<indent><marker><content_start>`\n/// Continuation lines will be: `<indent><spaces_matching_marker><content_continued>`\n///\n/// # Arguments\n/// - `indent`: The leading whitespace (for nested lists)\n/// - `marker`: The list marker (e.g., \"- \", \"1. \")\n/// - `content`: The text content after the marker\n/// - `width`: The maximum line width\npub fn wrap_list_item(indent: &str, marker: &str, content: &str, width: usize) -> String {\n    if content.is_empty() {\n        return format!(\"{}{}\\n\", indent, marker.trim_end());\n    }\n\n    if is_single_inline_link(content) {\n        return format!(\"{}{}{}\\n\", indent, marker, content.trim());\n    }\n\n    let full_marker = format!(\"{indent}{marker}\");\n    let continuation_indent = format!(\"{}{}\", indent, \" \".repeat(marker.len()));\n\n    let first_line_prefix_len = full_marker.len();\n    let first_line_width = if width > first_line_prefix_len {\n        width - first_line_prefix_len\n    } else {\n        width\n    };\n\n    let cont_line_prefix_len = continuation_indent.len();\n    let cont_line_width = if width > cont_line_prefix_len {\n        width - cont_line_prefix_len\n    } else {\n        width\n    };\n\n    let words: Vec<&str> = content.split_whitespace().collect();\n    if words.is_empty() {\n        return format!(\"{}\\n\", full_marker.trim_end());\n    }\n\n    let mut result = String::new();\n    let mut current_line = String::new();\n    let mut current_width = first_line_width;\n    let mut is_first_line = true;\n\n    for word in words {\n        let word_len = word.len();\n        let space_needed = usize::from(!current_line.is_empty());\n\n        if !current_line.is_empty() && current_line.len() + space_needed + word_len > current_width {\n            if is_first_line {\n                result.push_str(&full_marker);\n                is_first_line = false;\n            } else {\n                result.push_str(&continuation_indent);\n            }\n            result.push_str(&current_line);\n            result.push('\\n');\n            current_line.clear();\n            current_width = cont_line_width;\n        }\n\n        if !current_line.is_empty() {\n            current_line.push(' ');\n        }\n        current_line.push_str(word);\n    }\n\n    if !current_line.is_empty() {\n        if is_first_line {\n            result.push_str(&full_marker);\n        } else {\n            result.push_str(&continuation_indent);\n        }\n        result.push_str(&current_line);\n        result.push('\\n');\n    }\n\n    result\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_wrap_line_short() {\n        let text = \"Short text\";\n        let wrapped = wrap_line(text, 80);\n        assert_eq!(wrapped, \"Short text\");\n    }\n\n    #[test]\n    fn test_wrap_line_long() {\n        let text = \"123456789 123456789\";\n        let wrapped = wrap_line(text, 10);\n        assert_eq!(wrapped, \"123456789\\n123456789\");\n    }\n\n    #[test]\n    fn test_wrap_line_no_break_long_words() {\n        let text = \"12345678901 12345\";\n        let wrapped = wrap_line(text, 10);\n        assert_eq!(wrapped, \"12345678901\\n12345\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/src/wrapper.rs",
    "content": "//! Text wrapping functionality for Markdown output.\n//!\n//! This module provides text wrapping capabilities similar to Python's `textwrap.fill()`,\n//! specifically designed to work with Markdown content while preserving formatting.\n\npub use sync::wrap_markdown;\n\nmod sync;\nmod utils;\n"
  },
  {
    "path": "crates/html-to-markdown/tests/br_in_inline_test.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_br_inside_bold_tags() {\n    let html = \"<b>Hello!<br/></b><b>Hola!</b>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"**Hello!**  \\n**Hola!**\"));\n    assert!(!result.contains(\"**Hello!****Hola!**\"));\n}\n\n#[test]\nfn test_br_inside_strong_tags() {\n    let html = \"<strong>First<br/></strong><strong>Second</strong>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"**First**  \\n**Second**\"));\n    assert!(!result.contains(\"**First****Second**\"));\n}\n\n#[test]\nfn test_multiple_bolds_with_br() {\n    let html = \"<b>Line 1<br/></b><b>Line 2<br/></b><b>Line 3</b>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.matches(\"**\").count() >= 6);\n    assert!(result.contains(\"**Line 1**\"));\n    assert!(result.contains(\"**Line 2**\"));\n    assert!(result.contains(\"**Line 3**\"));\n    assert!(result.contains(\"**Line 1**  \\n**Line 2**\"));\n}\n\n#[test]\nfn test_br_inside_em_tags() {\n    let html = \"<em>First<br/></em><em>Second</em>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"*First*  \\n*Second*\"));\n    assert!(!result.contains(\"*First**Second*\"));\n}\n\n#[test]\nfn test_br_inside_italic_tags() {\n    let html = \"<i>Alpha<br/></i><i>Beta</i>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"*Alpha*  \\n*Beta*\"));\n    assert!(!result.contains(\"*Alpha**Beta*\"));\n}\n\n#[test]\nfn test_br_with_backslash_style() {\n    let html = \"<b>Hello<br/></b><b>World</b>\";\n    let options = ConversionOptions {\n        newline_style: html_to_markdown_rs::NewlineStyle::Backslash,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(result.contains(\"**Hello**\\\\\\n**World**\"));\n    assert!(!result.contains(\"**Hello****World**\"));\n}\n\n#[test]\nfn test_br_inside_nested_formatting() {\n    let html = \"<b><i>Bold italic<br/></i></b><b>Just bold</b>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"***Bold italic***  \\n**Just bold**\"));\n}\n\n#[test]\nfn test_br_at_end_of_paragraph_with_bold() {\n    let html = \"<p><b>Line 1<br/></b><b>Line 2</b></p>\";\n    let result = convert(html, None).unwrap();\n\n    assert!(result.contains(\"**Line 1**  \\n**Line 2**\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/commonmark_compliance_test.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\nuse serde::Deserialize;\n\n#[derive(Debug, Deserialize)]\nstruct CommonMarkTest {\n    markdown: String,\n    html: String,\n    example: u32,\n    start_line: u32,\n    #[allow(dead_code)]\n    end_line: u32,\n    section: String,\n}\n\nfn load_spec_tests() -> Vec<CommonMarkTest> {\n    let spec_json = include_str!(\"../../../packages/python/tests/commonmark_spec.json\");\n    serde_json::from_str(spec_json).expect(\"Failed to parse commonmark_spec.json\")\n}\n\n#[test]\n#[allow(clippy::too_many_lines)]\nfn test_commonmark_compliance() {\n    let tests = load_spec_tests();\n    let mut passed = 0;\n    let mut failed = 0;\n    let mut skipped = 0;\n    let mut failures = Vec::new();\n\n    for test in &tests {\n        let options = ConversionOptions {\n            bullets: \"-\".to_string(),\n            ..ConversionOptions::default()\n        };\n\n        if !options.escape_misc\n            && !options.escape_asterisks\n            && !options.escape_underscores\n            && !options.escape_ascii\n            && test.section == \"Backslash escapes\"\n        {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"HTML blocks\" || test.section == \"Raw HTML\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Fenced code blocks\" {\n            skipped += 1;\n            continue;\n        }\n\n        // Example 231: expects indented code blocks but we default to backtick-fenced\n        if test.example == 231 {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Tabs\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Entity and numeric character references\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Thematic breaks\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Hard line breaks\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Soft line breaks\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Blank lines\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Textual content\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Lists\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"List items\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Paragraphs\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Setext headings\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Link reference definitions\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"ATX headings\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Indented code blocks\" {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Code spans\"\n            && [\n                329, 334, 335, 336, 337, 338, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349,\n            ]\n            .contains(&test.example)\n        {\n            skipped += 1;\n            continue;\n        }\n\n        if test.section == \"Links\" {\n            if !test.html.contains(\"<a\") {\n                skipped += 1;\n                continue;\n            }\n            let passing_examples = [\n                482, 483, 484, 486, 496, 498, 501, 512, 514, 516, 517, 518, 519, 521, 522,\n            ];\n            if !passing_examples.contains(&test.example) {\n                skipped += 1;\n                continue;\n            }\n        }\n\n        if test.section == \"Images\" {\n            if !test.html.contains(\"<img\") {\n                skipped += 1;\n                continue;\n            }\n            let passing_examples = [572, 578, 581];\n            if !passing_examples.contains(&test.example) {\n                skipped += 1;\n                continue;\n            }\n        }\n\n        if test.section == \"Emphasis and strong emphasis\" {\n            let passing_examples = [\n                350, 351, 352, 354, 355, 356, 358, 359, 360, 361, 362, 363, 365, 366, 367, 368, 369, 370, 371, 372,\n                374, 375, 378, 379, 380, 381, 383, 384, 385, 386, 387, 388, 391, 392, 393, 395, 396, 397, 398, 400,\n                401, 404, 405, 409, 410, 411, 412, 413, 414, 415, 416, 418, 419, 420, 421, 422, 423, 428, 429, 430,\n                431, 433, 434, 435, 436, 438, 439, 441, 442, 443, 444, 445, 446, 447, 448, 451, 460, 467, 469, 471,\n                472, 473, 474, 478, 480, 481,\n            ];\n            if !passing_examples.contains(&test.example) {\n                skipped += 1;\n                continue;\n            }\n        }\n\n        if test.section == \"Block quotes\" {\n            let passing_examples = [228, 231, 234, 243, 245, 248];\n            if !passing_examples.contains(&test.example) {\n                skipped += 1;\n                continue;\n            }\n        }\n\n        if test.section == \"Autolinks\" {\n            let passing_examples = [594, 595, 596, 597, 600, 602, 604, 605, 607, 608, 609, 610, 611, 612];\n            if !passing_examples.contains(&test.example) {\n                skipped += 1;\n                continue;\n            }\n        }\n\n        let result = convert(&test.html, Some(options));\n\n        match result {\n            Ok(output) => {\n                let output_normalized = normalize_markdown(&output);\n                let expected_normalized = normalize_markdown(&test.markdown);\n\n                if output_normalized == expected_normalized {\n                    passed += 1;\n                } else {\n                    failed += 1;\n                    failures.push((test, output));\n                }\n            }\n            Err(e) => {\n                failed += 1;\n                failures.push((test, format!(\"Error: {e:?}\")));\n            }\n        }\n    }\n\n    let total = tests.len();\n    let tested = passed + failed;\n    let pass_rate = if tested > 0 {\n        (f64::from(passed) / f64::from(tested)) * 100.0\n    } else {\n        0.0\n    };\n\n    println!(\"\\n=== CommonMark Compliance Test Results ===\");\n    println!(\"Total tests: {total}\");\n    if skipped > 0 {\n        println!(\"Skipped: {skipped} (escaping tests with escaping disabled)\");\n        println!(\"Tested: {tested}\");\n    }\n    println!(\"Passed: {passed} ({pass_rate:.1}%)\");\n    println!(\n        \"Failed: {} ({:.1}%)\",\n        failed,\n        (f64::from(failed) / f64::from(tested)) * 100.0\n    );\n\n    if !failures.is_empty() {\n        println!(\"\\n=== Failed Tests (first 20) ===\");\n        for (test, output) in failures.iter().take(20) {\n            println!(\"\\nExample {} ({}:{})\", test.example, test.section, test.start_line);\n            println!(\"HTML input:\\n{}\", test.html);\n            println!(\"Expected markdown:\\n{}\", test.markdown);\n            println!(\"Got:\\n{output}\");\n            println!(\"---\");\n        }\n\n        println!(\"\\n=== Autolinks Failures ===\");\n        for (test, output) in &failures {\n            if test.section == \"Autolinks\" {\n                println!(\"\\nExample {} ({}:{})\", test.example, test.section, test.start_line);\n                println!(\"HTML input:\\n{}\", test.html);\n                println!(\"Expected markdown:\\n{}\", test.markdown);\n                println!(\"Got:\\n{output}\");\n                println!(\"---\");\n            }\n        }\n\n        let mut section_failures: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();\n        for (test, _) in &failures {\n            *section_failures.entry(&test.section).or_insert(0) += 1;\n        }\n\n        println!(\"\\n=== Failures by Section ===\");\n        let mut sections: Vec<_> = section_failures.iter().collect();\n        sections.sort_by_key(|(_, count)| std::cmp::Reverse(*count));\n        for (section, count) in sections {\n            println!(\"{section}: {count} failures\");\n        }\n\n        panic!(\n            \"\\nCommonMark compliance test FAILED: {passed}/{tested} tests passing ({pass_rate:.1}%)\\n\\\n                {skipped} tests skipped (escaping tests with escaping disabled)\\n\\\n                Default library settings must be CommonMark compliant!\\n\\\n                This is a mandatory test for v2.0 release.\"\n        );\n    }\n\n    println!(\n        \"\\n✓ All {tested} CommonMark tests passed! ({skipped} skipped) Library defaults are CommonMark compliant.\"\n    );\n}\n\n/// Normalize markdown for comparison\nfn normalize_markdown(md: &str) -> String {\n    md.trim_end().to_string()\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/djot_output_test.rs",
    "content": "#![allow(missing_docs)]\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::{ConversionOptions, OutputFormat};\n\nfn djot_options() -> ConversionOptions {\n    ConversionOptions {\n        output_format: OutputFormat::Djot,\n        ..Default::default()\n    }\n}\n\n#[test]\nfn test_djot_emphasis() {\n    let html = \"<p>Text with <em>emphasis</em> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"_emphasis_\"), \"Expected _emphasis_, got: {result}\");\n}\n\n#[test]\nfn test_djot_italic() {\n    let html = \"<p>Text with <i>italic</i> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"_italic_\"), \"Expected _italic_, got: {result}\");\n}\n\n#[test]\nfn test_djot_strong() {\n    let html = \"<p>Text with <strong>strong</strong> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"*strong*\"), \"Expected *strong*, got: {result}\");\n    // Should NOT have double asterisks\n    assert!(\n        !result.contains(\"**strong**\"),\n        \"Should not have **strong**, got: {result}\"\n    );\n}\n\n#[test]\nfn test_djot_bold() {\n    let html = \"<p>Text with <b>bold</b> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"*bold*\"), \"Expected *bold*, got: {result}\");\n    // Should NOT have double asterisks\n    assert!(\n        !result.contains(\"**bold**\"),\n        \"Should not have double asterisks, got: {result}\"\n    );\n}\n\n#[test]\nfn test_djot_options_debug() {\n    // Debug test to verify options are set correctly\n    let options = djot_options();\n    println!(\"Output format: {:?}\", options.output_format);\n    assert_eq!(options.output_format, OutputFormat::Djot);\n}\n\n#[test]\nfn test_djot_strikethrough() {\n    let html = \"<p>Text with <del>deleted</del> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"{-deleted-}\"), \"Expected {{-deleted-}}, got: {result}\");\n}\n\n#[test]\nfn test_djot_strikethrough_s_tag() {\n    let html = \"<p>Text with <s>strikethrough</s> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(\n        result.contains(\"{-strikethrough-}\"),\n        \"Expected {{-strikethrough-}}, got: {result}\"\n    );\n}\n\n#[test]\nfn test_djot_inserted() {\n    let html = \"<p>Text with <ins>inserted</ins> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(\n        result.contains(\"{+inserted+}\"),\n        \"Expected {{+inserted+}}, got: {result}\"\n    );\n}\n\n#[test]\nfn test_djot_highlight() {\n    let html = \"<p>Text with <mark>highlighted</mark> word</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(\n        result.contains(\"{=highlighted=}\"),\n        \"Expected {{=highlighted=}}, got: {result}\"\n    );\n}\n\n#[test]\nfn test_djot_subscript() {\n    let html = \"<p>H<sub>2</sub>O</p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"~2~\"), \"Expected ~2~, got: {result}\");\n}\n\n#[test]\nfn test_djot_superscript() {\n    let html = \"<p>x<sup>2</sup></p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"^2^\"), \"Expected ^2^, got: {result}\");\n}\n\n#[test]\nfn test_djot_combined_formatting() {\n    let html = \"<p><strong>Bold</strong> and <em>italic</em> and <del>deleted</del></p>\";\n    let result = convert(html, Some(djot_options())).unwrap();\n    assert!(result.contains(\"*Bold*\"), \"Expected *Bold*, got: {result}\");\n    assert!(result.contains(\"_italic_\"), \"Expected _italic_, got: {result}\");\n    assert!(result.contains(\"{-deleted-}\"), \"Expected {{-deleted-}}, got: {result}\");\n}\n\n#[test]\nfn test_markdown_output_unchanged() {\n    // Verify Markdown output still works as expected\n    let html = \"<p><strong>Bold</strong> and <em>italic</em></p>\";\n    let result = convert(html, None).unwrap();\n    assert!(\n        result.contains(\"**Bold**\"),\n        \"Expected **Bold** for Markdown, got: {result}\"\n    );\n    assert!(\n        result.contains(\"*italic*\"),\n        \"Expected *italic* for Markdown, got: {result}\"\n    );\n}\n\n#[test]\nfn test_markdown_strikethrough_unchanged() {\n    let html = \"<p><del>deleted</del></p>\";\n    let result = convert(html, None).unwrap();\n    assert!(\n        result.contains(\"~~deleted~~\"),\n        \"Expected ~~deleted~~ for Markdown, got: {result}\"\n    );\n}\n\n#[test]\nfn test_output_format_default_is_markdown() {\n    let options = ConversionOptions::default();\n    assert_eq!(options.output_format, OutputFormat::Markdown);\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/exclude_selectors_test.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn convert(html: &str, opts: Option<ConversionOptions>) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n#[test]\nfn test_exclude_selectors_drops_matching_elements() {\n    let html = r#\"<body>\n        <div class=\"cookie-banner\">Accept cookies</div>\n        <article><p>Main content here.</p></article>\n        <div id=\"ad-container\">Buy stuff</div>\n    </body>\"#;\n\n    let options = ConversionOptions {\n        exclude_selectors: vec![\".cookie-banner\".to_string(), \"#ad-container\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(result.contains(\"Main content\"), \"Should keep main content\");\n    assert!(!result.contains(\"cookie\"), \"Should drop .cookie-banner element\");\n    assert!(!result.contains(\"Buy stuff\"), \"Should drop #ad-container element\");\n}\n\n#[test]\nfn test_exclude_selectors_drops_nested_content() {\n    let html = r#\"<body>\n        <aside class=\"sidebar\">\n            <h2>Related articles</h2>\n            <p>Some sidebar content</p>\n        </aside>\n        <main><p>Primary content.</p></main>\n    </body>\"#;\n\n    let options = ConversionOptions {\n        exclude_selectors: vec![\".sidebar\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(result.contains(\"Primary content\"), \"Should keep main content\");\n    assert!(\n        !result.contains(\"Related articles\"),\n        \"Should drop heading inside excluded element\"\n    );\n    assert!(\n        !result.contains(\"sidebar content\"),\n        \"Should drop paragraph inside excluded element\"\n    );\n}\n\n#[test]\nfn test_exclude_selectors_empty_list_is_noop() {\n    let html = r\"<body><p>Hello world</p></body>\";\n\n    let options = ConversionOptions {\n        exclude_selectors: vec![],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert!(\n        result.contains(\"Hello world\"),\n        \"Empty exclude_selectors should not affect output\"\n    );\n}\n\n#[test]\nfn test_exclude_selectors_invalid_selector_is_skipped() {\n    let html = r\"<body><p>Visible text</p></body>\";\n\n    // An empty string or garbled selector should not panic or error — just be ignored.\n    let options = ConversionOptions {\n        exclude_selectors: vec![String::new(), \"p\".to_string()],\n        ..Default::default()\n    };\n\n    // Should not return an error; whether the paragraph is excluded depends on the\n    // selector, but it must not panic.\n    let _ = convert(html, Some(options));\n}\n\n#[test]\nfn test_exclude_selectors_attribute_selector() {\n    let html = r#\"<body>\n        <div role=\"complementary\">Sidebar</div>\n        <p>Main text</p>\n    </body>\"#;\n\n    let options = ConversionOptions {\n        exclude_selectors: vec![\"[role='complementary']\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(result.contains(\"Main text\"), \"Should keep non-excluded content\");\n    assert!(\n        !result.contains(\"Sidebar\"),\n        \"Should drop element matching attribute selector\"\n    );\n}\n\n#[test]\nfn test_exclude_selectors_plain_text_output() {\n    let html = r#\"<body>\n        <div class=\"nav\">Navigation links</div>\n        <p>Article body text.</p>\n    </body>\"#;\n\n    let options = ConversionOptions {\n        exclude_selectors: vec![\".nav\".to_string()],\n        output_format: html_to_markdown_rs::OutputFormat::Plain,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        result.contains(\"Article body text\"),\n        \"Should keep body text in plain output\"\n    );\n    assert!(\n        !result.contains(\"Navigation links\"),\n        \"Should drop excluded element in plain output\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/integration_test.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_basic_paragraph() {\n    let html = \"<p>Hello world</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Hello world\\n\");\n}\n\n#[test]\nfn test_multiple_paragraphs() {\n    let html = \"<p>First paragraph</p><p>Second paragraph</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"First paragraph\\n\\nSecond paragraph\\n\");\n}\n\n#[test]\nfn test_atx_headings() {\n    let html = \"<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"# H1\\n\\n## H2\\n\\n### H3\\n\\n#### H4\\n\\n##### H5\\n\\n###### H6\\n\");\n}\n\n#[test]\nfn test_bold() {\n    let html = \"<p>Text with <strong>bold</strong> word</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Text with **bold** word\\n\");\n}\n\n#[test]\nfn test_italic() {\n    let html = \"<p>Text with <em>italic</em> word</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Text with *italic* word\\n\");\n}\n\n#[test]\nfn test_bold_and_italic() {\n    let html = \"<p><strong><em>Bold and italic</em></strong></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"***Bold and italic***\\n\");\n}\n\n#[test]\nfn test_inline_code() {\n    let html = \"<p>Use <code>code</code> here</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Use `code` here\\n\");\n}\n\n#[test]\nfn test_code_block() {\n    let html = \"<pre><code>fn main() {\\n    println!(\\\"Hello\\\");\\n}</code></pre>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"```\\nfn main() {\\n    println!(\\\"Hello\\\");\\n}\\n```\\n\");\n}\n\n#[test]\nfn test_simple_link() {\n    let html = \"<p><a href=\\\"https://example.com\\\">Link text</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Link text](https://example.com)\\n\");\n}\n\n#[test]\nfn test_link_with_title() {\n    let html = \"<p><a href=\\\"/url\\\" title=\\\"title\\\">Link</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Link](/url \\\"title\\\")\\n\");\n}\n\n#[test]\nfn test_simple_image() {\n    let html = \"<p><img src=\\\"image.jpg\\\" alt=\\\"Alt text\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Alt text](image.jpg)\\n\");\n}\n\n#[test]\nfn test_graphic_with_url() {\n    let html = \"<p><graphic url=\\\"diagram.svg\\\" alt=\\\"Diagram\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Diagram](diagram.svg)\\n\");\n}\n\n#[test]\nfn test_graphic_with_href() {\n    let html = \"<p><graphic href=\\\"image.png\\\" alt=\\\"Image\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Image](image.png)\\n\");\n}\n\n#[test]\nfn test_graphic_with_xlink_href() {\n    let html = \"<p><graphic xlink:href=\\\"chart.svg\\\" alt=\\\"Chart\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Chart](chart.svg)\\n\");\n}\n\n#[test]\nfn test_graphic_with_src() {\n    let html = \"<p><graphic src=\\\"fallback.jpg\\\" alt=\\\"Fallback\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Fallback](fallback.jpg)\\n\");\n}\n\n#[test]\nfn test_graphic_with_filename_fallback() {\n    let html = \"<p><graphic url=\\\"image.png\\\" filename=\\\"my-image.png\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![my-image.png](image.png)\\n\");\n}\n\n#[test]\nfn test_graphic_attribute_priority() {\n    // url should take priority over href, xlink:href, src\n    let html = \"<p><graphic url=\\\"priority.svg\\\" href=\\\"second.svg\\\" xlink:href=\\\"third.svg\\\" src=\\\"fourth.svg\\\" alt=\\\"Priority\\\" /></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"![Priority](priority.svg)\\n\");\n}\n\n#[test]\nfn test_unordered_list() {\n    let html = \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"- Item 1\\n- Item 2\\n- Item 3\\n\");\n}\n\n#[test]\nfn test_ordered_list() {\n    let html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"1. First\\n2. Second\\n3. Third\\n\");\n}\n\n#[test]\nfn test_nested_lists() {\n    let html = \"<ul><li>Item 1<ul><li>Nested 1</li><li>Nested 2</li></ul></li><li>Item 2</li></ul>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- Item 1\"));\n    assert!(result.contains(\"Nested 1\"), \"Expected 'Nested 1' in result: {result:?}\");\n    assert!(result.contains(\"Nested 2\"), \"Expected 'Nested 2' in result: {result:?}\");\n}\n\n#[test]\nfn test_blockquote() {\n    let html = \"<blockquote><p>Quoted text</p></blockquote>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"> Quoted text\\n\");\n}\n\n#[test]\nfn test_nested_blockquote() {\n    let html = \"<blockquote><blockquote><p>Nested quote</p></blockquote></blockquote>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"> > Nested quote\\n\");\n}\n\n#[test]\nfn test_horizontal_rule() {\n    let html = \"<hr>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"---\\n\");\n}\n\n#[test]\nfn test_hr_after_paragraph_keeps_blank_line() {\n    let html = \"<p>paragraph</p><hr>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"paragraph\\n\\n---\\n\");\n}\n\n#[test]\nfn test_comment_between_paragraphs() {\n    let html = \"<p>yes</p><!----><p>but no</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"yes\\n\\nbut no\\n\");\n}\n\n#[test]\nfn test_line_break() {\n    let html = \"<p>Line 1<br>Line 2</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Line 1  \\nLine 2\\n\");\n}\n\n#[test]\nfn test_strikethrough() {\n    let html = \"<p><del>Deleted text</del></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"~~Deleted text~~\\n\");\n}\n\n#[test]\nfn test_simple_table() {\n    let html = \"<table><tr><th>Header</th></tr><tr><td>Cell</td></tr></table>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header |\"));\n    assert!(result.contains(\"| --- |\"));\n    assert!(result.contains(\"| Cell |\"));\n}\n\n#[test]\nfn test_table_rowspan() {\n    let html = r#\"<table>\n<tr><th>Header 1</th><th>Header 2</th></tr>\n<tr><td rowspan=\"2\">Spanning cell</td><td>\n    <div>First row content</div>\n    <div>Second line</div>\n</td></tr>\n<tr><td>\n    <div>Next row</div>\n    <div>More content</div>\n</td></tr>\n</table>\"#;\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n    let expected = \"\\n\\n| Header 1 | Header 2 |\\n| --- | --- |\\n| Spanning cell | First row content<br>Second line |\\n|  | Next row<br>More content |\\n\";\n    assert_eq!(result, expected);\n}\n\n#[test]\nfn test_empty_element() {\n    let html = \"<p></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"\");\n}\n\n#[test]\nfn test_whitespace_normalization() {\n    let html = \"<p>Multiple    spaces    here</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Multiple spaces here\\n\");\n}\n\n#[test]\nfn test_unicode_content() {\n    let html = \"<p>Hello 世界 🌍</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Hello 世界 🌍\\n\");\n}\n\n#[test]\nfn test_html_entities() {\n    let html = \"<p>&lt;div&gt; &amp; &quot;quotes&quot;</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"<div> & \\\"quotes\\\"\\n\");\n}\n\n#[test]\nfn test_nested_formatting() {\n    let html = \"<p><strong>Bold <em>and italic</em> text</strong></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"**Bold *and italic* text**\\n\");\n}\n\n#[test]\nfn test_link_inside_paragraph() {\n    let html = \"<p>Check out <a href=\\\"https://example.com\\\">this link</a> for more info.</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Check out [this link](https://example.com) for more info.\\n\");\n}\n\n#[test]\nfn test_code_with_special_chars() {\n    let html = \"<code>&lt;html&gt;</code>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"`<html>`\\n\");\n}\n\n#[test]\nfn test_empty_link() {\n    let html = \"<p><a href=\\\"\\\">Empty</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Empty](<>)\\n\");\n}\n\n#[test]\nfn test_div_as_block() {\n    let html = \"<div>Block content</div>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Block content\\n\");\n}\n\n#[test]\nfn test_multiple_divs() {\n    let html = \"<div>First</div><div>Second</div>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"First\\n\\nSecond\\n\");\n}\n\n#[test]\nfn test_span_inline() {\n    let html = \"<p>Text with <span>span</span> element</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Text with span element\\n\");\n}\n\n#[test]\nfn test_subscript() {\n    let html = \"<p>H<sub>2</sub>O</p>\";\n    let opts = ConversionOptions {\n        sub_symbol: \"~\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"H~2~O\\n\");\n}\n\n#[test]\nfn test_subscript_trailing_whitespace() {\n    let html = \"<p><sub>hello </sub>world</p>\";\n    let opts = ConversionOptions {\n        sub_symbol: \"~\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"~hello~ world\\n\");\n}\n\n#[test]\nfn test_subscript_leading_whitespace() {\n    let html = \"<p>hello<sub> world</sub></p>\";\n    let opts = ConversionOptions {\n        sub_symbol: \"~\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"hello ~world~\\n\");\n}\n\n#[test]\nfn test_superscript() {\n    let html = \"<p>x<sup>2</sup></p>\";\n    let opts = ConversionOptions {\n        sup_symbol: \"^\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"x^2^\\n\");\n}\n\n#[test]\nfn test_superscript_trailing_whitespace() {\n    let html = \"<p><sup>hello </sup>world</p>\";\n    let opts = ConversionOptions {\n        sup_symbol: \"^\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"^hello^ world\\n\");\n}\n\n#[test]\nfn test_superscript_leading_whitespace() {\n    let html = \"<p>hello<sup> world</sup></p>\";\n    let opts = ConversionOptions {\n        sup_symbol: \"^\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"hello ^world^\\n\");\n}\n\n#[test]\nfn test_subscript_default_passthrough() {\n    let html = \"<p>H<sub>2</sub>O</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"H2O\\n\");\n}\n\n#[test]\nfn test_superscript_default_passthrough() {\n    let html = \"<p>x<sup>2</sup> + y<sup>3</sup></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"x2 + y3\\n\");\n}\n\n#[test]\nfn test_subscript_superscript_combined_default() {\n    let html = \"<p>CO<sub>2</sub><sup>*</sup></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"CO2*\\n\");\n}\n\n#[test]\nfn test_subscript_html_tag_symbol() {\n    let html = \"<p>H<sub>2</sub>O</p>\";\n    let opts = ConversionOptions {\n        sub_symbol: \"<sub>\".to_string(),\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"H<sub>2</sub>O\\n\");\n}\n\n#[test]\nfn test_adjacent_links_with_newline_separator() {\n    let html = \"<p>\\n<a href=\\\"/page1\\\">Link 1</a>\\n<a href=\\\"/page2\\\">Link 2</a>\\n</p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Link 1](/page1) [Link 2](/page2)\\n\");\n}\n\n#[test]\nfn test_adjacent_links_no_whitespace() {\n    let html = \"<p><a href=\\\"/page1\\\">Link 1</a><a href=\\\"/page2\\\">Link 2</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Link 1](/page1)[Link 2](/page2)\\n\");\n}\n\n#[test]\nfn test_adjacent_links_with_space() {\n    let html = \"<p><a href=\\\"/page1\\\">Link 1</a> <a href=\\\"/page2\\\">Link 2</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"[Link 1](/page1) [Link 2](/page2)\\n\");\n}\n\n#[test]\nfn test_adjacent_inline_elements_with_newline() {\n    let html = \"<p><strong>bold</strong>\\n<em>italic</em></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"**bold** *italic*\\n\");\n}\n\n#[test]\nfn test_autolink() {\n    let html = \"<p><a href=\\\"https://example.com\\\">https://example.com</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"<https://example.com>\\n\");\n}\n\n#[test]\nfn test_email_autolink() {\n    let html = \"<p><a href=\\\"mailto:test@example.com\\\">test@example.com</a></p>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"<test@example.com>\\n\");\n}\n\n#[test]\nfn test_metadata_extraction() {\n    let html = \"<html><head><title>Page Title</title></head><body><p>Content</p></body></html>\";\n    let opts = ConversionOptions {\n        extract_metadata: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert!(result.contains(\"Page Title\"));\n    assert!(result.contains(\"Content\"));\n}\n\n#[test]\nfn test_metadata_disabled() {\n    let html = \"<html><head><title>Page Title</title></head><body><p>Content</p></body></html>\";\n    let opts = ConversionOptions {\n        extract_metadata: false,\n        ..Default::default()\n    };\n    let result = convert(html, Some(opts)).unwrap();\n    assert!(!result.contains(\"<!--\"));\n    assert!(result.contains(\"Content\"));\n}\n\n#[test]\nfn test_task_list() {\n    let html = \"<ul><li><input type=\\\"checkbox\\\" checked> Done</li><li><input type=\\\"checkbox\\\"> Todo</li></ul>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- [x] Done\"));\n    assert!(result.contains(\"- [ ] Todo\"));\n}\n\n#[test]\nfn test_definition_list() {\n    let html = \"<dl><dt>Term</dt><dd>Definition</dd></dl>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"Term\"));\n    assert!(result.contains(\"Definition\"));\n}\n\n#[test]\nfn test_malformed_html() {\n    let html = \"<p>Unclosed paragraph<p>Another\";\n    let result = convert(html, None);\n    assert!(result.is_ok());\n}\n\n#[test]\nfn test_deeply_nested_structure() {\n    let html = \"<div><div><div><div><p>Deeply nested</p></div></div></div></div>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"Deeply nested\\n\");\n}\n\n#[test]\nfn test_mixed_content() {\n    let html = r#\"\n        <h1>Title</h1>\n        <p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p>\n        <ul>\n            <li>List item 1</li>\n            <li>List item 2</li>\n        </ul>\n        <p>Link: <a href=\"https://example.com\">Example</a></p>\n    \"#;\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"# Title\"));\n    assert!(result.contains(\"**bold**\"));\n    assert!(result.contains(\"*italic*\"));\n    assert!(result.contains(\"- List item 1\"));\n    assert!(result.contains(\"[Example](https://example.com)\"));\n}\n\n#[test]\nfn test_ordered_list_with_heading_and_table() {\n    let html = r\"\n<ol>\n  <li>\n    <h3>h3</h3>\n  </li>\n  <li>\n    <table>\n      <caption>table</caption>\n      <tr>\n        <td>blah</td>\n      </tr>\n    </table>\n  </li>\n</ol>\n\";\n\n    let result = convert(html, None).unwrap();\n    let expected = \"1. ### h3\\n2. *table*\\n\\n    | blah |\\n    | --- |\\n\";\n    assert_eq!(result, expected);\n}\n\n#[test]\nfn test_heading_wrapped_in_link_issue_115() {\n    let html = r#\"<a href=\"https://domain.local\"><h2>Heading A</h2></a>\"#;\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"## [Heading A](https://domain.local)\\n\");\n}\n\n#[test]\nfn test_link_text_escaping_issue_114() {\n    let html = r#\"<a href=\"https://domain.local\">Hi :]</a><br><a href=\"https://domain.local\">1<2</a>\"#;\n    let result = convert(html, None).unwrap();\n    assert_eq!(\n        result,\n        \"[Hi :\\\\]](https://domain.local)  \\n[1<2](https://domain.local)\\n\"\n    );\n}\n\n#[test]\nfn test_uppercase_tags_issue_113() {\n    let html = r\"<B>Foo<Br />Bar</B>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"**Foo  \\nBar**\\n\");\n}\n\n#[test]\nfn test_breaks_and_newlines_issue_112() {\n    let html = \"<br>\\n1\\n2\\n<b>3</b>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"\\n1\\n2\\n**3**\\n\");\n}\n\n#[test]\nfn test_nested_bold_issue_111() {\n    let html = \"<b>bold<b>er</b></b>\";\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"**bolder**\\n\");\n}\n\n#[test]\nfn hidden_elements_stripped() {\n    let html = \"<p>visible</p><div hidden>secret</div><p>also visible</p>\";\n    let result = convert(html, None).unwrap();\n    assert!(!result.contains(\"secret\"));\n    assert!(result.contains(\"visible\"));\n}\n\n#[test]\nfn q_element_produces_quotes() {\n    let html = \"<p>He said <q>hello</q> to me</p>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(r#\"\"hello\"\"#), \"q element should add quotes: {result}\");\n}\n\n#[test]\nfn test_wikipedia_back_reference_caret_normalized() {\n    // Wikipedia back-references use <a href=\"#cite_ref-N\">^</a>\n    // The caret should be normalized to ↑ to avoid confusion with markdown footnote syntax\n    let html = r##\"<p>Some text<sup><a href=\"#cite_ref-1\">^</a></sup> more text</p>\"##;\n    let result = convert(html, None).unwrap();\n    assert!(\n        result.contains(\"[↑](#cite_ref-1)\"),\n        \"Back-reference caret should be normalized to ↑: {result}\"\n    );\n    assert!(\n        !result.contains(\"[^]\"),\n        \"Should not produce [^] which looks like footnote syntax: {result}\"\n    );\n}\n\n#[test]\nfn test_regular_caret_link_not_affected() {\n    // Regular links with ^ text but no # href should keep the ^\n    let html = r#\"<a href=\"https://example.com\">^</a>\"#;\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"[^]\"), \"Non-anchor caret links should keep ^: {result}\");\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_121_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn default_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\nfn normalize_newlines(input: &str) -> String {\n    input.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\")\n}\n\n#[test]\nfn converts_spa_menu_fixture() {\n    let html = fs::read_to_string(fixture_path(\"gh-121-spa-app.html\")).expect(\"read spa html\");\n    let expected = fs::read_to_string(fixture_path(\"gh-121-spa-app.md\")).expect(\"read spa markdown\");\n\n    let result = convert(&html, Some(default_options())).expect(\"convert spa html\");\n    assert_eq!(normalize_newlines(&result), normalize_newlines(&expected));\n}\n\n#[test]\nfn converts_hacker_news_fixture() {\n    let html = fs::read_to_string(fixture_path(\"gh-121-hacker-news.html\")).expect(\"read hn html\");\n    let expected = fs::read_to_string(fixture_path(\"gh-121-hacker-news.md\")).expect(\"read hn markdown\");\n\n    let result = convert(&html, Some(default_options())).expect(\"convert hn html\");\n    assert_eq!(normalize_newlines(&result), normalize_newlines(&expected));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_127_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::{\n    CodeBlockStyle, ConversionOptions, HeadingStyle, HighlightStyle, ListIndentType, PreprocessingOptions,\n    PreprocessingPreset, WhitespaceMode,\n};\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn issue_127_options() -> ConversionOptions {\n    ConversionOptions {\n        heading_style: HeadingStyle::Atx,\n        bullets: \"-\".to_string(),\n        list_indent_type: ListIndentType::Spaces,\n        list_indent_width: 2,\n        whitespace_mode: WhitespaceMode::Normalized,\n        highlight_style: HighlightStyle::DoubleEqual,\n        wrap: false,\n        br_in_tables: true,\n        code_block_style: CodeBlockStyle::Backticks,\n        strip_newlines: true,\n        extract_metadata: false,\n        preprocessing: PreprocessingOptions {\n            enabled: true,\n            preset: PreprocessingPreset::Minimal,\n            remove_navigation: true,\n            remove_forms: true,\n        },\n        ..Default::default()\n    }\n}\n\n#[test]\nfn converts_multilingual_fixture_without_utf8_boundary_panic() {\n    let html = fs::read_to_string(fixture_path(\"gh-127-issue.html\")).expect(\"read issue fixture\");\n\n    let markdown = convert(&html, Some(issue_127_options())).expect(\"convert should not panic on utf-8 boundaries\");\n\n    assert!(!markdown.is_empty(), \"converted output should contain content\");\n    assert!(\n        markdown.contains(\"MW841\") && markdown.contains(\"كريب\"),\n        \"converted output should preserve multilingual product content\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_128_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n#[test]\nfn images_with_dimensions_render_as_markdown_links() {\n    let html = r#\"<img src=\"data:image/png;base64,xyz==\" alt=\"Pixel\" width=\"100\" height=\"100\"/>\"#;\n\n    let markdown = convert(html, None).expect(\"image conversion should succeed\");\n\n    assert_eq!(markdown.trim(), \"![Pixel](data:image/png;base64,xyz==)\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_131_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\nuse html_to_markdown_rs::options::WhitespaceMode;\n\n#[test]\nfn link_flattens_block_children_issue_131() {\n    let html = r#\"<a href=\"https://www.google.com\">\n              <h3>MWD08 - الابيض</h3>\n              <p><span class=\"money\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\"#;\n\n    let options = ConversionOptions {\n        whitespace_mode: WhitespaceMode::Normalized,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert_eq!(result, \"[MWD08 - الابيض 70.00 SAR$18.66USD](https://www.google.com)\\n\");\n}\n\n#[test]\nfn link_label_newlines_are_collapsed() {\n    let html = r#\"<p><a href=\"https://example.com\">line 1\nline 2</a></p>\"#;\n\n    let options = ConversionOptions {\n        whitespace_mode: WhitespaceMode::Normalized,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert_eq!(result, \"[line 1 line 2](https://example.com)\\n\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_134_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn default_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\nfn normalize_newlines(input: &str) -> String {\n    input.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\")\n}\n\n#[test]\nfn converts_pre_code_fixture() {\n    let html = fs::read_to_string(fixture_path(\"gh-134-pre-code.html\")).unwrap();\n    let expected = fs::read_to_string(fixture_path(\"gh-134-pre-code.md\")).unwrap();\n\n    let result = convert(&html, Some(default_options())).unwrap();\n    assert_eq!(normalize_newlines(&result), normalize_newlines(&expected));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_139_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn long_multibyte_link_label_does_not_panic() {\n    let mut html = String::from(\"<a href=\\\"https://example.com/article\\\">\");\n    html.push_str(&\"a\".repeat(511));\n    html.push('👍');\n    html.push_str(\"</a>\");\n\n    let markdown = convert(&html, Some(ConversionOptions::default())).unwrap();\n    let expected_label = format!(\"{}👍\", \"a\".repeat(511));\n\n    assert!(\n        markdown.contains(&format!(\"[{expected_label}]\")),\n        \"expected full label to appear in markdown output; got: {markdown}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_140_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn default_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\nfn escape_misc_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        escape_misc: true,\n        ..Default::default()\n    }\n}\n\nfn normalize_newlines(input: &str) -> String {\n    input.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\")\n}\n\n#[test]\nfn converts_should_not_escape_in_pre_or_code_fixture() {\n    let pre_html = r\"<pre>This pipe | should not be escaped.<pre/>\";\n\n    let pre_markdown_without_misc = convert(pre_html, Some(default_options())).expect(\"conversion should succeed\");\n    assert_eq!(\n        pre_markdown_without_misc.trim(),\n        \"```\\nThis pipe | should not be escaped.\\n```\"\n    );\n\n    let pre_markdown_with_misc = convert(pre_html, Some(escape_misc_options())).expect(\"conversion should succeed\");\n    assert_eq!(\n        pre_markdown_with_misc.trim(),\n        \"```\\nThis pipe | should not be escaped.\\n```\"\n    );\n\n    let code_html = r\"<code>This pipe | should not be escaped.<code/>\";\n\n    let code_markdown_without_misc = convert(code_html, None).expect(\"conversion should succeed\");\n    assert_eq!(\n        code_markdown_without_misc.trim(),\n        \"`This pipe | should not be escaped.`\"\n    );\n\n    let code_markdown_with_misc = convert(code_html, Some(escape_misc_options())).expect(\"conversion should succeed\");\n    assert_eq!(code_markdown_with_misc.trim(), \"`This pipe | should not be escaped.`\");\n}\n\n#[test]\nfn converts_table_cell_pipe_fixture() {\n    let html = fs::read_to_string(fixture_path(\"gh-140-table-cell-pipe.html\")).unwrap();\n    let expected_without_misc = fs::read_to_string(fixture_path(\"gh-140-table-cell-pipe.md\")).unwrap();\n    let expected_with_misc = fs::read_to_string(fixture_path(\"gh-140-table-cell-pipe-with-escape-misc.md\")).unwrap();\n\n    let result_without_misc = convert(&html, Some(default_options())).expect(\"conversion should succeed\");\n    assert_eq!(\n        normalize_newlines(&result_without_misc),\n        normalize_newlines(&expected_without_misc)\n    );\n\n    let result_with_misc = convert(&html, Some(escape_misc_options())).expect(\"conversion should succeed\");\n    assert_eq!(\n        normalize_newlines(&result_with_misc),\n        normalize_newlines(&expected_with_misc)\n    );\n}\n\n#[test]\nfn escapes_only_literal_pipes_in_table_cells() {\n    let html = r\"\n        <table>\n            <thead><tr><th>Type</th><th>Span</th><th>Block</th></tr></thead>\n            <tbody>\n                <tr>\n                    <td>text | content</td>\n                    <td><code>code | span</code></td>\n                    <td><pre>block | content</pre></td>\n                </tr>\n            </tbody>\n        </table>\n    \";\n\n    let markdown = convert(html, Some(default_options())).expect(\"conversion should succeed\");\n    assert!(\n        markdown.contains(\"text \\\\| content\"),\n        \"literal pipe in text cell should be escaped\"\n    );\n    assert!(\n        markdown.contains(\"`code | span`\"),\n        \"pipe inside code span should not be escaped\"\n    );\n    assert!(\n        !markdown.contains(\"`code \\\\| span`\"),\n        \"code spans must not receive backslash escaping\"\n    );\n    assert!(\n        markdown.contains(\"block | content\"),\n        \"pre/code blocks should retain literal pipe characters\"\n    );\n    assert!(\n        !markdown.contains(\"block \\\\| content\"),\n        \"pre/code block content should not be escaped\"\n    );\n\n    let markdown_with_misc = convert(html, Some(escape_misc_options())).expect(\"conversion should succeed\");\n    assert!(\n        markdown_with_misc.contains(\"text \\\\| content\"),\n        \"literal pipe in text cell should be escaped when escape_misc=true\"\n    );\n    assert!(\n        markdown_with_misc.contains(\"`code | span`\"),\n        \"code span pipe should remain unescaped when escape_misc=true\"\n    );\n    assert!(\n        !markdown_with_misc.contains(\"`code \\\\| span`\"),\n        \"code spans must not be escaped when escape_misc=true\"\n    );\n}\n\n#[test]\nfn nested_tables_do_not_double_escape_pipes() {\n    let html = r\"\n        <table>\n            <thead><tr><th>Outer A</th><th>Outer B</th></tr></thead>\n            <tbody>\n                <tr>\n                    <td>\n                        <table>\n                            <thead><tr><th>Inner A</th><th>Inner B</th></tr></thead>\n                            <tbody>\n                                <tr><td>Inner | pipe</td><td>B</td></tr>\n                            </tbody>\n                        </table>\n                    </td>\n                    <td>Outer | pipe</td>\n                </tr>\n            </tbody>\n        </table>\n    \";\n\n    let markdown = convert(html, Some(default_options())).expect(\"conversion should succeed\");\n    assert!(\n        markdown.contains(\"| Inner A | Inner B |\"),\n        \"nested table structure should be preserved\"\n    );\n    assert!(\n        markdown.contains(\"Inner \\\\| pipe\"),\n        \"literal pipe text inside nested table should be escaped once\"\n    );\n    assert!(\n        !markdown.contains(\"Inner \\\\\\\\| pipe\"),\n        \"nested table text should not be double-escaped\"\n    );\n    assert!(\n        markdown.contains(\"Outer \\\\| pipe\"),\n        \"outer cell literal pipe should be escaped\"\n    );\n    assert!(\n        !markdown.contains(\"Outer \\\\\\\\| pipe\"),\n        \"outer cell text should only be escaped once\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_143_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn options_with_wrap() -> ConversionOptions {\n    ConversionOptions {\n        wrap: true,\n        wrap_width: 80,\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\nfn normalize_newlines(input: &str) -> String {\n    input.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\")\n}\n\n#[test]\nfn wrap_preserves_link_only_list_items() {\n    let html = fs::read_to_string(fixture_path(\"gh-143-links-wordwrap.html\")).unwrap();\n    let expected = fs::read_to_string(fixture_path(\"gh-143-links-wordwrap.md\")).unwrap();\n\n    let result = convert(&html, Some(options_with_wrap())).expect(\"conversion should succeed\");\n\n    assert_eq!(\n        normalize_newlines(&result).trim(),\n        normalize_newlines(&expected).trim(),\n        \"word wrapping should not merge nested link-only list items\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_145_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_strip_newlines_preserves_block_spacing() {\n    let html = r\"<section>\n    <h1>Heading</h1>\n    <p>Paragraph one.</p>\n    <p>Paragraph two.</p>\n</section>\";\n\n    let options = ConversionOptions {\n        strip_newlines: true,\n        extract_metadata: false,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    let lines: Vec<&str> = result.lines().collect();\n\n    let mut max_consecutive_blank = 0;\n    let mut current_blank_count = 0;\n    for line in &lines {\n        if line.trim().is_empty() {\n            current_blank_count += 1;\n            max_consecutive_blank = max_consecutive_blank.max(current_blank_count);\n        } else {\n            current_blank_count = 0;\n        }\n    }\n\n    assert!(\n        max_consecutive_blank <= 1,\n        \"excessive blank lines detected: {max_consecutive_blank} consecutive blanks in:\\n{result}\"\n    );\n\n    assert!(result.contains(\"Heading\"), \"heading missing from: {result}\");\n    assert!(result.contains(\"Paragraph one\"), \"paragraph one missing from: {result}\");\n    assert!(result.contains(\"Paragraph two\"), \"paragraph two missing from: {result}\");\n}\n\n#[test]\nfn test_strip_newlines_removes_inline_newlines() {\n    let html = r\"<p>This is a paragraph\nwith line breaks\nin the middle</p>\";\n\n    let options = ConversionOptions {\n        strip_newlines: true,\n        extract_metadata: false,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    let text = result.trim();\n\n    let content_lines: Vec<&str> = text.lines().collect();\n\n    let has_paragraph_line = content_lines.iter().any(|line| {\n        let trimmed = line.trim();\n        trimmed.contains(\"This is a paragraph\")\n            && trimmed.contains(\"with line breaks\")\n            && trimmed.contains(\"in the middle\")\n    });\n\n    assert!(\n        has_paragraph_line,\n        \"paragraph should have inline newlines converted to spaces in: {result}\"\n    );\n}\n\n#[test]\nfn test_strip_newlines_handles_nested_blocks() {\n    let html = r\"<section>\n    <div>\n        <h2>Nested Heading</h2>\n        <p>Content inside nested div.</p>\n    </div>\n    <div>\n        <h2>Another Section</h2>\n        <p>More content here.</p>\n    </div>\n</section>\";\n\n    let options = ConversionOptions {\n        strip_newlines: true,\n        extract_metadata: false,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        result.contains(\"Nested Heading\"),\n        \"nested heading missing from: {result}\"\n    );\n    assert!(\n        result.contains(\"Content inside nested div\"),\n        \"nested content missing from: {result}\"\n    );\n    assert!(\n        result.contains(\"Another Section\"),\n        \"another section heading missing from: {result}\"\n    );\n    assert!(\n        result.contains(\"More content here\"),\n        \"more content missing from: {result}\"\n    );\n\n    let lines: Vec<&str> = result.lines().collect();\n    let mut max_consecutive_blank = 0;\n    let mut current_blank_count = 0;\n    for line in &lines {\n        if line.trim().is_empty() {\n            current_blank_count += 1;\n            max_consecutive_blank = max_consecutive_blank.max(current_blank_count);\n        } else {\n            current_blank_count = 0;\n        }\n    }\n\n    assert!(\n        max_consecutive_blank <= 1,\n        \"excessive blank lines in nested blocks: {max_consecutive_blank} consecutive blanks in:\\n{result}\"\n    );\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_146_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_strip_tags_prevents_metadata_extraction() {\n    let html = r#\"<!DOCTYPE html>\n<html>\n<head>\n    <title>Test Document</title>\n    <meta name=\"author\" content=\"John Doe\">\n    <meta name=\"description\" content=\"A test document\">\n    <meta property=\"og:title\" content=\"Test OG Title\">\n</head>\n<body>\n    <p>Main content here</p>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions {\n        extract_metadata: true,\n        strip_tags: vec![\"meta\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        result.contains(\"Main content here\"),\n        \"Body content should be preserved: {result}\"\n    );\n\n    assert!(\n        result.contains(\"title: Test Document\"),\n        \"Title should still be extracted in frontmatter: {result}\"\n    );\n\n    assert!(\n        !result.contains(\"meta-author\"),\n        \"meta-author should NOT be in frontmatter when strip_tags=['meta']: {result}\"\n    );\n    assert!(\n        !result.contains(\"meta-description\"),\n        \"meta-description should NOT be in frontmatter when strip_tags=['meta']: {result}\"\n    );\n    assert!(\n        !result.contains(\"meta-og-title\"),\n        \"meta-og-title should NOT be in frontmatter when strip_tags=['meta']: {result}\"\n    );\n}\n\n#[test]\nfn test_strip_tags_title_prevents_extraction() {\n    let html = r#\"<!DOCTYPE html>\n<html>\n<head>\n    <title>Should Be Stripped</title>\n    <meta name=\"author\" content=\"Jane Smith\">\n    <meta name=\"keywords\" content=\"test, demo\">\n</head>\n<body>\n    <h1>Document Heading</h1>\n    <p>Some content</p>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions {\n        extract_metadata: true,\n        strip_tags: vec![\"title\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        result.contains(\"Document Heading\") && result.contains(\"Some content\"),\n        \"Body content should be preserved: {result}\"\n    );\n\n    assert!(\n        result.contains(\"meta-author\"),\n        \"meta-author should still be extracted when only title is stripped: {result}\"\n    );\n    assert!(\n        result.contains(\"meta-keywords\"),\n        \"meta-keywords should still be extracted when only title is stripped: {result}\"\n    );\n\n    assert!(\n        !result.contains(\"title: Should Be Stripped\"),\n        \"title should NOT be in frontmatter when strip_tags=['title']: {result}\"\n    );\n}\n\n#[test]\nfn test_preserve_tags_prevents_metadata_extraction() {\n    let html = r#\"<!DOCTYPE html>\n<html>\n<head>\n    <title>Preserved Title</title>\n    <meta name=\"viewport\" content=\"width=device-width\">\n    <meta name=\"author\" content=\"Test Author\">\n</head>\n<body>\n    <div>\n        <p>Body content</p>\n    </div>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions {\n        extract_metadata: true,\n        preserve_tags: vec![\"meta\".to_string()],\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        result.contains(\"Body content\"),\n        \"Body content should be preserved: {result}\"\n    );\n\n    assert!(\n        result.contains(\"title: Preserved Title\"),\n        \"title should still be extracted in frontmatter: {result}\"\n    );\n\n    assert!(\n        !result.contains(\"meta-viewport\"),\n        \"meta-viewport should NOT be in YAML frontmatter when preserve_tags=['meta']: {result}\"\n    );\n    assert!(\n        !result.contains(\"meta-author\"),\n        \"meta-author should NOT be in YAML frontmatter when preserve_tags=['meta']: {result}\"\n    );\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_176_regressions.rs",
    "content": "#![allow(missing_docs)]\n\n//! Regression tests for issue #176: Newlines not preserved with adjacent blockquotes\n\n#[test]\nfn test_strong_blockquote_strong_newlines() {\n    fn convert(\n        html: &str,\n        opts: Option<html_to_markdown_rs::ConversionOptions>,\n    ) -> html_to_markdown_rs::error::Result<String> {\n        html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n    }\n\n    // Test case from issue #176: strong + blockquote + strong\n    let html = r\"<strong>2. Point two</strong><blockquote>Option Explicit\nSub Test()\n    ' code here\nEnd Function</blockquote><strong>3. Point three</strong>\";\n\n    let markdown = convert(html, None).unwrap();\n\n    println!(\"Actual output:\\n{markdown}\");\n    println!(\"---\");\n\n    // Should have blank lines separating elements\n    assert!(\n        markdown.contains(\"**2. Point two**\\n\\n>\"),\n        \"Should have blank line between strong and blockquote. Got: {markdown:?}\"\n    );\n    assert!(\n        markdown.contains(\"End Function\\n\\n**3. Point three**\"),\n        \"Should have blank line between blockquote and next strong. Got: {markdown:?}\"\n    );\n}\n\n#[test]\nfn test_paragraph_blockquote_paragraph_newlines() {\n    fn convert(\n        html: &str,\n        opts: Option<html_to_markdown_rs::ConversionOptions>,\n    ) -> html_to_markdown_rs::error::Result<String> {\n        html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n    }\n\n    // Control test: p + blockquote + p should work correctly\n    let html = r\"<p>First paragraph</p><blockquote>A quote</blockquote><p>Second paragraph</p>\";\n\n    let markdown = convert(html, None).unwrap();\n\n    println!(\"Actual output:\\n{markdown}\");\n\n    // Should have single newline before blockquote (CommonMark spec)\n    // and blank line after blockquote\n    assert!(\n        markdown.contains(\"First paragraph\\n>\"),\n        \"Should have single newline between p and blockquote (CommonMark compliance)\"\n    );\n    assert!(\n        markdown.contains(\"A quote\\n\\n\"),\n        \"Should have blank line after blockquote\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_190_regressions.rs",
    "content": "//! Regression coverage for issue #190.\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::{CodeBlockStyle, ConversionOptions};\n\nfn fixture_path(name: &str) -> PathBuf {\n    [\n        env!(\"CARGO_MANIFEST_DIR\"),\n        \"../../test_documents/html/issues/gh-190\",\n        name,\n    ]\n    .iter()\n    .collect()\n}\n\nfn read_fixture_lossy(name: &str) -> String {\n    let bytes = fs::read(fixture_path(name)).expect(\"read issue #190 fixture\");\n    String::from_utf8_lossy(&bytes).into_owned()\n}\n\nfn decode_utf16_without_bom(bytes: &[u8]) -> String {\n    let mut even_nul = 0usize;\n    let mut odd_nul = 0usize;\n    for (idx, &byte) in bytes.iter().enumerate() {\n        if byte == 0 {\n            if idx % 2 == 0 {\n                even_nul += 1;\n            } else {\n                odd_nul += 1;\n            }\n        }\n    }\n\n    let is_little_endian = odd_nul >= even_nul;\n    let mut units = Vec::with_capacity(bytes.len() / 2);\n    let mut chunks = bytes.chunks_exact(2);\n    for chunk in &mut chunks {\n        let unit = if is_little_endian {\n            u16::from_le_bytes([chunk[0], chunk[1]])\n        } else {\n            u16::from_be_bytes([chunk[0], chunk[1]])\n        };\n        units.push(unit);\n    }\n\n    String::from_utf16_lossy(&units)\n}\n\n#[test]\nfn test_code_block_dedent_handles_unicode_whitespace() {\n    let nbsp = '\\u{00A0}';\n    let html = format!(\"<pre><code> msg = String()\\n{nbsp}msg = String()\\n</code></pre>\");\n    let options = ConversionOptions {\n        code_block_style: CodeBlockStyle::Backticks,\n        ..Default::default()\n    };\n\n    let markdown = convert(&html, Some(options)).expect(\"conversion should succeed\");\n\n    assert!(markdown.contains(\"msg = String()\"));\n    assert!(!markdown.contains(nbsp));\n}\n\n#[test]\nfn test_convert_strips_nul_bytes() {\n    let html = \"a\\0b\";\n    let markdown = convert(html, None).expect(\"conversion should succeed\");\n\n    assert_eq!(markdown, \"ab\\n\");\n}\n\n#[test]\nfn converts_all_issue_190_fixtures_except_known_utf16_binary() {\n    let fixtures = [\n        \"firsteigen.html\",\n        \"vipaarontours.html\",\n        \"ozonekorea.html\",\n        \"mitrade.html\",\n        \"plusblog.html\",\n        \"maxkim.html\",\n        \"insight.html\",\n        \"flex2025.html\",\n        \"flex2021.html\",\n        \"kimbrain.html\",\n        \"rbloggers.html\",\n        \"sjsu.html\",\n    ];\n\n    for name in fixtures {\n        let html = read_fixture_lossy(name);\n        let markdown =\n            convert(&html, None).unwrap_or_else(|err| panic!(\"fixture {name} should convert cleanly: {err}\"));\n        assert!(!markdown.trim().is_empty(), \"fixture {name} produced empty markdown\");\n    }\n}\n\n#[test]\nfn converts_sjsu_fixture_when_lossy_utf8_is_auto_decoded() {\n    let bytes = fs::read(fixture_path(\"sjsu.html\")).expect(\"read sjsu fixture bytes\");\n    let raw_html = String::from_utf8_lossy(&bytes).into_owned();\n\n    let markdown = convert(&raw_html, None).expect(\"lossy UTF-16 HTML should be recovered and converted\");\n    assert!(\n        markdown.contains(\"pipeline\") || markdown.contains(\"Pipeline\"),\n        \"auto-decoded sjsu fixture should contain expected content\"\n    );\n}\n\n#[test]\nfn converts_sjsu_fixture_when_decoded_as_utf16() {\n    let bytes = fs::read(fixture_path(\"sjsu.html\")).expect(\"read sjsu fixture bytes\");\n    let decoded_html = decode_utf16_without_bom(&bytes);\n\n    let markdown = convert(&decoded_html, None).expect(\"decoded UTF-16 HTML should convert\");\n    assert!(\n        markdown.contains(\"pipeline\") || markdown.contains(\"Pipeline\"),\n        \"decoded sjsu fixture should contain expected content\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_199_regressions.rs",
    "content": "//! Regression coverage for issue #199.\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n#[test]\nfn test_link_label_is_not_truncated() {\n    let label = \"a\".repeat(600);\n    let html = format!(r#\"<a href=\"https://example.com\">{label}</a>\"#);\n\n    let markdown = convert(&html, None).expect(\"conversion should succeed\");\n    let expected = format!(\"[{label}](https://example.com)\");\n\n    assert!(markdown.contains(&expected));\n    assert!(!markdown.contains('…'));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_200_regressions.rs",
    "content": "//! Regression coverage for issues #200 and #214.\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n#[test]\nfn test_definition_list_spacing_consistency() {\n    let html1 = r#\"\n<div>\n <dl>\n  <dt>Tags:</dt>\n  <dd>\n   <ul>\n    <li>\n     <a href=\"https://site.com\">php</a>\n    </li>\n    <li>\n     <a href=\"https://site.com/search/\">closure</a>\n    </li>\n   </ul>\n   <button type=\"button\">Add tags</button>\n  </dd>\n </dl>\n</div>\n\"#;\n\n    let html2 = r#\"<div><dl><dt>Tags:</dt><dd><ul><li><a href=\"https://site.com\">php</a></li><li><a href=\"https://site.com/search/\">closure</a></li></ul><button type=\"button\">Add tags</button></dd></dl></div>\"#;\n\n    let markdown1 = convert(html1, None).expect(\"conversion should succeed\");\n    let markdown2 = convert(html2, None).expect(\"conversion should succeed\");\n\n    assert_eq!(markdown1, markdown2);\n    assert!(markdown1.contains(\"Tags:\"));\n    assert!(markdown1.contains(\"[php]\"));\n    assert!(markdown1.contains(\"[closure]\"));\n    assert!(markdown1.contains(\"Add tags\"));\n    // Ensure no Pandoc definition list colon prefix is introduced\n    assert!(!markdown1.contains(\":   \"));\n}\n\n/// Regression test for issue #214: colon introduced into text from dd elements.\n#[test]\nfn test_definition_list_no_colon_prefix() {\n    let html = r#\"<dl>\n        <dt id=\"canRequestFocus\" class=\"property inherited\">\n  <span class=\"name\"><a href=\"material/InkResponse/canRequestFocus.html\">canRequestFocus</a></span>\n  <span class=\"signature\">&#8594; <a href=\"dart-core/bool-class.html\">bool</a></span>\n</dt>\n<dd class=\"inherited\">\n  If true, this widget may request the primary focus.\n  <div class=\"features\"><span class=\"feature\">final</span><span class=\"feature\">inherited</span></div>\n</dd>\n</dl>\"#;\n    let markdown = convert(html, None).expect(\"conversion should succeed\");\n    assert!(markdown.contains(\"If true, this widget may request the primary focus.\"));\n    // The dd content must not be prefixed with \": \" (Pandoc definition list syntax)\n    assert!(!markdown.contains(\":   If true\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_212_regressions.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n/// Regression test for <https://github.com/kreuzberg-dev/html-to-markdown/issues/212>\n///\n/// When `\\n` precedes an `<a>` tag inside a `<p>`, the whitespace was\n/// inconsistently handled between the first and subsequent paragraphs.\n/// The bug was stateful: identical HTML structures produced different\n/// results depending on their position in the document.\n#[test]\nfn consistent_whitespace_before_link_across_paragraphs_issue_212() {\n    let html = r#\"<p>text before\n<a href=\"https://example.com\">the link</a>\nafter</p>\n<p>text before\n<a href=\"https://example.com\">the link</a>\nafter</p>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert_eq!(\n        result,\n        \"text before [the link](https://example.com) after\\n\\ntext before [the link](https://example.com) after\\n\"\n    );\n}\n\n/// Same bug but with three paragraphs to ensure no accumulation effects.\n#[test]\nfn consistent_whitespace_three_paragraphs_issue_212() {\n    let html = r#\"<p>click\n<a href=\"/a\">here</a></p>\n<p>click\n<a href=\"/b\">here</a></p>\n<p>click\n<a href=\"/c\">here</a></p>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"click [here](/a)\\n\\nclick [here](/b)\\n\\nclick [here](/c)\\n\");\n}\n\n/// Verify the fix doesn't break whitespace between text nodes without links.\n#[test]\nfn newline_before_inline_elements_consistent_issue_212() {\n    let html = r\"<p>before\n<strong>bold</strong> after</p>\n<p>before\n<strong>bold</strong> after</p>\";\n\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"before **bold** after\\n\\nbefore **bold** after\\n\");\n}\n\n/// Verify with `<em>` tags across multiple paragraphs.\n#[test]\nfn newline_before_em_across_paragraphs_issue_212() {\n    let html = r\"<p>some text\n<em>emphasized</em> end</p>\n<p>some text\n<em>emphasized</em> end</p>\";\n\n    let result = convert(html, None).unwrap();\n    assert_eq!(result, \"some text *emphasized* end\\n\\nsome text *emphasized* end\\n\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/issue_216_217_regressions.rs",
    "content": "//! Regression tests for issues #216 and #217.\n//!\n//! Both issues report a panic at `text_node.rs:191`:\n//! \"byte index N is out of bounds of \\`\\`\"\n//!\n//! Root cause: When inline handlers (strong, em, etc.) collect children into a\n//! fresh String buffer while inheriting a parent context with `block_content_start`\n//! set by a paragraph handler, the index points into the wrong buffer.\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\n/// Minimal reproducer: a <details> containing a <p> with <strong> inside.\n/// The <details> handler collects into a fresh buffer, the <p> sets\n/// `block_content_start`, and the <strong> handler creates yet another fresh\n/// buffer — causing the index to be out of bounds.\n#[test]\nfn test_issue_216_217_details_paragraph_strong_no_panic() {\n    let html = r\"\n    <div>some preceding content</div>\n    <details>\n        <summary>Summary text</summary>\n        <p><strong>Bold text inside details paragraph\n</strong></p>\n    </details>\n    \";\n\n    let result = convert(html, None);\n    assert!(result.is_ok());\n}\n\n/// Same issue can occur with emphasis inside a paragraph inside details.\n#[test]\nfn test_issue_216_217_details_paragraph_em_no_panic() {\n    let html = r\"\n    <div>some preceding content</div>\n    <details>\n        <summary>Summary</summary>\n        <p><em>Italic text inside details paragraph\n</em></p>\n    </details>\n    \";\n\n    let result = convert(html, None);\n    assert!(result.is_ok());\n}\n\n/// The panic can also occur with nested inline elements inside paragraphs\n/// collected by any handler that creates a fresh buffer.\n#[test]\nfn test_issue_216_217_nested_strong_in_paragraph_no_panic() {\n    let html = r\"\n    <details>\n        <p>Some text <strong>bold text with trailing newline\n</strong> more text</p>\n    </details>\n    \";\n\n    let result = convert(html, None);\n    assert!(result.is_ok());\n}\n\n/// Test with the actual structure pattern from the reported URL.\n#[test]\nfn test_issue_216_217_complex_details_structure() {\n    let html = r#\"\n    <div class=\"content\">\n        <section>\n            <details>\n                <summary>Stratégie adaptation</summary>\n                <p><strong>Risque élevé pour les populations\n</strong></p>\n                <p>Description du risque avec des détails supplémentaires.</p>\n            </details>\n        </section>\n    </div>\n    \"#;\n\n    let result = convert(html, None);\n    assert!(result.is_ok());\n    let md = result.unwrap();\n    assert!(md.contains(\"Risque\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/json_ld_script_extraction.rs",
    "content": "#![allow(missing_docs)]\n\n#[test]\nfn extracts_json_ld_from_head_script() {\n    let html = r#\"\n        <html>\n          <head>\n            <script type=\"application/ld+json\">\n              { \"@context\": \"https://schema.org\", \"@type\": \"Article\", \"headline\": \"Example\" }\n            </script>\n            <title>Title</title>\n          </head>\n          <body>Hello</body>\n        </html>\n    \"#;\n\n    let result = html_to_markdown_rs::convert(html, None).expect(\"convert failed\");\n    let metadata = result.metadata;\n\n    assert_eq!(metadata.structured_data.len(), 1);\n    assert!(metadata.structured_data[0].raw_json.contains(r#\"\"@type\": \"Article\"\"#));\n    assert_eq!(metadata.structured_data[0].schema_type.as_deref(), Some(\"Article\"));\n}\n\n#[test]\nfn extracts_json_ld_from_body_script_and_keeps_content() {\n    let html = r#\"\n        <html>\n          <head><title>Title</title></head>\n          <body>\n            <script type=\"application/ld+json\">\n              { \"@context\": \"https://schema.org\", \"@type\": \"Article\", \"headline\": \"Example\" }\n            </script>\n          </body>\n        </html>\n    \"#;\n\n    let result = html_to_markdown_rs::convert(html, None).expect(\"convert failed\");\n    let metadata = result.metadata;\n\n    assert_eq!(metadata.structured_data.len(), 1);\n    assert!(!metadata.structured_data[0].raw_json.trim().is_empty());\n    assert_eq!(metadata.structured_data[0].schema_type.as_deref(), Some(\"Article\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/lists_test.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_basic_unordered_list() {\n    let html = r\"<ul>\n    <li>Item 1</li>\n    <li>Item 2</li>\n    <li>Item 3</li>\n    </ul>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- Item 1\"));\n    assert!(result.contains(\"- Item 2\"));\n    assert!(result.contains(\"- Item 3\"));\n}\n\n#[test]\nfn test_basic_ordered_list() {\n    let html = r\"<ol>\n    <li>First</li>\n    <li>Second</li>\n    <li>Third</li>\n    </ol>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"1. First\"));\n    assert!(result.contains(\"2. Second\"));\n    assert!(result.contains(\"3. Third\"));\n}\n\n#[test]\nfn test_nested_lists() {\n    let html = r\"<ul>\n    <li>Item 1\n        <ul>\n            <li>Nested 1</li>\n            <li>Nested 2</li>\n        </ul>\n    </li>\n    <li>Item 2</li>\n    </ul>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- Item 1\"));\n    assert!(result.contains(\"* Nested 1\"));\n    assert!(result.contains(\"* Nested 2\"));\n    assert!(result.contains(\"- Item 2\"));\n}\n\n#[test]\nfn test_ordered_nested_in_unordered() {\n    let html = r\"<ul>\n    <li>Outer item\n        <ol>\n            <li>Inner item 1</li>\n            <li>Inner item 2</li>\n        </ol>\n    </li>\n    </ul>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- Outer item\"));\n    assert!(result.contains(\"1. Inner item 1\"));\n    assert!(result.contains(\"2. Inner item 2\"));\n}\n\n#[test]\nfn test_list_with_formatting() {\n    let html = r\"<ul>\n    <li><strong>Bold</strong> item</li>\n    <li><em>Italic</em> item</li>\n    <li><code>Code</code> item</li>\n    </ul>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- **Bold** item\"));\n    assert!(result.contains(\"- *Italic* item\"));\n    assert!(result.contains(\"- `Code` item\"));\n}\n\n#[test]\nfn test_list_with_links() {\n    let html = r#\"<ul>\n    <li><a href=\"https://example.com\">Link 1</a></li>\n    <li><a href=\"https://example.org\">Link 2</a></li>\n    </ul>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"[Link 1](https://example.com)\"));\n    assert!(result.contains(\"[Link 2](https://example.org)\"));\n}\n\n#[test]\nfn test_task_list() {\n    let html = r#\"<ul>\n    <li><input type=\"checkbox\" checked> Completed task</li>\n    <li><input type=\"checkbox\"> Incomplete task</li>\n    </ul>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- [x] Completed task\"));\n    assert!(result.contains(\"- [ ] Incomplete task\"));\n}\n\n#[test]\nfn test_list_indent_spaces() {\n    let html = r\"<ul>\n    <li>Parent\n        <ul>\n            <li>Child</li>\n        </ul>\n    </li>\n    </ul>\";\n\n    let options = ConversionOptions {\n        list_indent_type: html_to_markdown_rs::ListIndentType::Spaces,\n        list_indent_width: 2,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert!(result.contains(\"- Parent\"));\n    assert!(result.contains(\"  * Child\"));\n}\n\n#[test]\nfn test_list_indent_tabs() {\n    let html = r\"<ul>\n    <li>Parent\n        <ul>\n            <li>Child</li>\n        </ul>\n    </li>\n    </ul>\";\n\n    let options = ConversionOptions {\n        list_indent_type: html_to_markdown_rs::ListIndentType::Tabs,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert!(result.contains(\"- Parent\"));\n    assert!(result.contains(\"\\t* Child\"));\n}\n\n#[test]\nfn test_custom_bullet_symbols() {\n    let html = r\"<ul>\n    <li>Item 1</li>\n    <li>Item 2</li>\n    </ul>\";\n\n    let options = ConversionOptions {\n        bullets: \"*+-\".to_string(),\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n    assert!(result.contains(\"* Item 1\") || result.contains(\"* Item 2\"));\n}\n\n#[test]\nfn test_empty_list_item() {\n    let html = r\"<ul>\n    <li>Item 1</li>\n    <li></li>\n    <li>Item 3</li>\n    </ul>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"- Item 1\"));\n    assert!(result.contains(\"- Item 3\"));\n}\n\n#[test]\nfn test_list_with_code_block() {\n    let html = r#\"<ul>\n    <li>\n        <p>Item with code:</p>\n        <pre><code>fn main() {\n    println!(\"Hello\");\n}</code></pre>\n    </li>\n    </ul>\"#;\n\n    let result = convert(html, None).unwrap();\n    println!(\"Result:\\n{result}\");\n    assert!(result.contains(\"- Item with code:\"));\n    assert!(result.contains(\"fn main()\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/plain_output_test.rs",
    "content": "#![allow(missing_docs)]\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::{ConversionOptions, OutputFormat};\n\nfn plain_options() -> ConversionOptions {\n    ConversionOptions {\n        output_format: OutputFormat::Plain,\n        ..Default::default()\n    }\n}\n\n#[test]\nfn test_plain_basic_paragraph() {\n    let html = \"<p>Hello world</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"Hello world\\n\");\n}\n\n#[test]\nfn test_plain_no_strong_markers() {\n    let html = \"<p>This is <strong>bold</strong> text</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"This is bold text\\n\");\n}\n\n#[test]\nfn test_plain_no_emphasis_markers() {\n    let html = \"<p>This is <em>italic</em> text</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"This is italic text\\n\");\n}\n\n#[test]\nfn test_plain_link_text_only() {\n    let html = r#\"<p>Visit <a href=\"https://example.com\">our site</a> today</p>\"#;\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"Visit our site today\\n\");\n}\n\n#[test]\nfn test_plain_image_alt_text() {\n    let html = r#\"<img alt=\"A cute cat\">\"#;\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"A cute cat\\n\");\n}\n\n#[test]\nfn test_plain_image_skipped_when_option_set() {\n    let html = r#\"<img alt=\"A cute cat\">\"#;\n    let mut opts = plain_options();\n    opts.skip_images = true;\n    let result = convert(html, Some(opts)).unwrap();\n    assert_eq!(result, \"\");\n}\n\n#[test]\nfn test_plain_code_block() {\n    let html = \"<pre><code>fn main() {}</code></pre>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"fn main() {}\\n\");\n}\n\n#[test]\nfn test_plain_blockquote_no_prefix() {\n    let html = \"<blockquote><p>Quoted text</p></blockquote>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        !result.contains('>'),\n        \"Plain text should not contain blockquote prefix, got: {result}\"\n    );\n    assert!(result.contains(\"Quoted text\"));\n}\n\n#[test]\nfn test_plain_list_items_on_separate_lines() {\n    let html = \"<ul><li>First</li><li>Second</li><li>Third</li></ul>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"- First\\n- Second\\n- Third\\n\");\n}\n\n#[test]\nfn test_plain_table_cells_extracted() {\n    let html = \"<table><tr><td>A</td><td>B</td></tr><tr><td>C</td><td>D</td></tr></table>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(result.contains('A'));\n    assert!(result.contains('B'));\n    assert!(result.contains('C'));\n    assert!(result.contains('D'));\n}\n\n#[test]\nfn test_plain_no_escaping() {\n    let html = \"<p>* not a list</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        result.contains(\"* not a list\"),\n        \"Plain text should not escape asterisks, got: {result}\"\n    );\n    assert!(\n        !result.contains(\"\\\\*\"),\n        \"Plain text should not backslash-escape, got: {result}\"\n    );\n}\n\n#[test]\nfn test_plain_script_excluded() {\n    let html = \"<p>Before</p><script>alert('xss')</script><p>After</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        !result.contains(\"alert\"),\n        \"Script content should be excluded, got: {result}\"\n    );\n    assert!(result.contains(\"Before\"));\n    assert!(result.contains(\"After\"));\n}\n\n#[test]\nfn test_plain_style_excluded() {\n    let html = \"<p>Hello</p><style>.foo { color: red; }</style>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        !result.contains(\"color\"),\n        \"Style content should be excluded, got: {result}\"\n    );\n    assert!(result.contains(\"Hello\"));\n}\n\n#[test]\nfn test_plain_br_becomes_newline() {\n    let html = \"<p>Line one<br>Line two</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        result.contains(\"Line one\\nLine two\"),\n        \"Expected newline from <br>, got: {result}\"\n    );\n}\n\n#[test]\nfn test_plain_hr_becomes_blank_line() {\n    let html = \"<p>Above</p><hr><p>Below</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(result.contains(\"Above\"));\n    assert!(result.contains(\"Below\"));\n    // Should have blank line between\n    assert!(result.contains(\"\\n\\n\"), \"Expected blank line from <hr>, got: {result}\");\n}\n\n#[test]\nfn test_plain_nested_inline_formatting_stripped() {\n    let html = \"<p>Start <strong>bold <em>and italic</em></strong> end</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"Start bold and italic end\\n\");\n}\n\n#[test]\nfn test_plain_heading_no_markers() {\n    let html = \"<h1>Title</h1><p>Content</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        !result.contains('#'),\n        \"Plain text should not contain heading markers, got: {result}\"\n    );\n    assert!(result.contains(\"Title\"));\n    assert!(result.contains(\"Content\"));\n}\n\n#[test]\nfn test_plain_parse_variants() {\n    assert_eq!(OutputFormat::parse(\"plain\"), OutputFormat::Plain);\n    assert_eq!(OutputFormat::parse(\"plaintext\"), OutputFormat::Plain);\n    assert_eq!(OutputFormat::parse(\"text\"), OutputFormat::Plain);\n    assert_eq!(OutputFormat::parse(\"Plain\"), OutputFormat::Plain);\n    assert_eq!(OutputFormat::parse(\"PLAINTEXT\"), OutputFormat::Plain);\n}\n\n#[test]\nfn test_plain_empty_input() {\n    let html = \"\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"\");\n}\n\n#[test]\nfn test_plain_whitespace_only_html() {\n    let html = \"<p>   </p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"\");\n}\n\n#[test]\nfn test_plain_inline_code_no_backticks() {\n    let html = \"<p>Use <code>fmt.Println</code> to print</p>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        !result.contains('`'),\n        \"Plain text should not contain backticks, got: {result}\"\n    );\n    assert!(result.contains(\"fmt.Println\"));\n}\n\n#[test]\nfn test_plain_pre_preserves_whitespace() {\n    let html = \"<pre>  indented\\n    more</pre>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        result.contains(\"  indented\\n    more\"),\n        \"Pre blocks should preserve whitespace, got: {result}\"\n    );\n}\n\n#[test]\nfn test_plain_unordered_list_markers() {\n    let html = \"<ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"- Alpha\\n- Beta\\n- Gamma\\n\");\n}\n\n#[test]\nfn test_plain_ordered_list_markers() {\n    let html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"1. First\\n2. Second\\n3. Third\\n\");\n}\n\n#[test]\nfn test_plain_ordered_list_custom_start() {\n    let html = r#\"<ol start=\"42\"><li>First item starting at 42</li><li>Second item</li></ol>\"#;\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert_eq!(result, \"42. First item starting at 42\\n43. Second item\\n\");\n}\n\n#[test]\nfn test_plain_nested_lists() {\n    let html = \"<ul><li>Outer 1<ul><li>Inner A</li><li>Inner B</li></ul></li><li>Outer 2</li></ul>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    // The outer items should have `- ` prefix and inner items should also have `- ` prefix\n    assert!(\n        result.contains(\"- Outer 1\"),\n        \"Expected '- Outer 1' in output, got: {result}\"\n    );\n    assert!(\n        result.contains(\"- Inner A\"),\n        \"Expected '- Inner A' in output, got: {result}\"\n    );\n    assert!(\n        result.contains(\"- Inner B\"),\n        \"Expected '- Inner B' in output, got: {result}\"\n    );\n    assert!(\n        result.contains(\"- Outer 2\"),\n        \"Expected '- Outer 2' in output, got: {result}\"\n    );\n}\n\n#[test]\nfn test_plain_ordered_list_inside_unordered() {\n    let html = \"<ul><li>Bullet<ol><li>Numbered</li></ol></li></ul>\";\n    let result = convert(html, Some(plain_options())).unwrap();\n    assert!(\n        result.contains(\"- Bullet\"),\n        \"Expected '- Bullet' in output, got: {result}\"\n    );\n    assert!(\n        result.contains(\"1. Numbered\"),\n        \"Expected '1. Numbered' in output, got: {result}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/preprocessing_tests.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn footer_without_navigation_hint_is_preserved() {\n    let html = r#\"<!DOCTYPE html>\n<html lang=\"en\">\n  <body>\n    <main>\n      <h1>Simple Webpage</h1>\n      <p>This is a simple webpage without external images.</p>\n    </main>\n    <footer>\n      <p>Test page for processors validation</p>\n    </footer>\n  </body>\n</html>\"#;\n\n    let markdown = convert(html, None).unwrap();\n    assert!(\n        markdown.contains(\"Test page for processors validation\"),\n        \"footer content should be retained in markdown:\\n{markdown}\"\n    );\n}\n\n#[test]\nfn footer_with_navigation_hint_is_removed() {\n    let html = r#\"<!DOCTYPE html>\n<html lang=\"en\">\n  <body>\n    <main>\n      <h1>Simple Webpage</h1>\n    </main>\n    <footer class=\"site-footer\">\n      <p>Test page for processors validation</p>\n      <nav><a href=\"/about\">About</a></nav>\n    </footer>\n  </body>\n</html>\"#;\n\n    let options = ConversionOptions {\n        preprocessing: html_to_markdown_rs::PreprocessingOptions {\n            enabled: true,\n            ..Default::default()\n        },\n        ..Default::default()\n    };\n    let markdown = convert(html, Some(options)).unwrap();\n    assert!(\n        !markdown.contains(\"processors validation\"),\n        \"navigational footers should still be stripped entirely:\\n{markdown}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/reference_links_test.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::{ConversionOptions, LinkStyle};\n\nfn convert(html: &str, options: Option<ConversionOptions>) -> String {\n    html_to_markdown_rs::convert(html, options)\n        .unwrap()\n        .content\n        .unwrap_or_default()\n}\n\nfn ref_options() -> ConversionOptions {\n    ConversionOptions {\n        link_style: LinkStyle::Reference,\n        ..Default::default()\n    }\n}\n\n#[test]\nfn basic_reference_link() {\n    let html = r#\"<a href=\"https://example.com\">Click here</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"[Click here][1]\"),\n        \"Expected reference-style link, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[1]: https://example.com\"),\n        \"Expected reference definition, got: {result}\"\n    );\n}\n\n#[test]\nfn reference_link_with_title() {\n    let html = r#\"<a href=\"https://example.com\" title=\"Example\">Click</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"[Click][1]\"),\n        \"Expected reference-style link, got: {result}\"\n    );\n    assert!(\n        result.contains(r#\"[1]: https://example.com \"Example\"\"#),\n        \"Expected reference definition with title, got: {result}\"\n    );\n}\n\n#[test]\nfn url_deduplication() {\n    let html = r#\"<a href=\"https://example.com\">First</a> <a href=\"https://example.com\">Second</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"[First][1]\"),\n        \"Expected first link with ref 1, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[Second][1]\"),\n        \"Expected second link reusing ref 1, got: {result}\"\n    );\n    // Should only have one definition\n    let count = result.matches(\"[1]: https://example.com\").count();\n    assert_eq!(count, 1, \"Expected exactly one definition, got: {result}\");\n}\n\n#[test]\nfn different_titles_different_refs() {\n    let html =\n        r#\"<a href=\"https://example.com\" title=\"A\">First</a> <a href=\"https://example.com\" title=\"B\">Second</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"[First][1]\"),\n        \"Expected first link ref 1, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[Second][2]\"),\n        \"Expected second link ref 2 (different title), got: {result}\"\n    );\n}\n\n#[test]\nfn image_reference_style() {\n    let html = r#\"<img src=\"https://example.com/img.png\" alt=\"A photo\">\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"![A photo][1]\"),\n        \"Expected reference-style image, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[1]: https://example.com/img.png\"),\n        \"Expected image reference definition, got: {result}\"\n    );\n}\n\n#[test]\nfn mixed_links_and_images_share_numbering() {\n    let html = r#\"<a href=\"https://a.com\">Link</a><img src=\"https://b.com/img.png\" alt=\"Img\">\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(result.contains(\"[Link][1]\"), \"Expected link as ref 1, got: {result}\");\n    assert!(result.contains(\"![Img][2]\"), \"Expected image as ref 2, got: {result}\");\n}\n\n#[test]\nfn autolinks_unaffected() {\n    let html = r#\"<a href=\"https://example.com\">https://example.com</a>\"#;\n    let options = ConversionOptions {\n        link_style: LinkStyle::Reference,\n        autolinks: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options));\n    // Autolinks should still render as <url>\n    assert!(\n        result.contains(\"<https://example.com>\"),\n        \"Autolinks should not be affected by reference style, got: {result}\"\n    );\n}\n\n#[test]\nfn default_inline_unchanged() {\n    let html = r#\"<a href=\"https://example.com\">Click</a>\"#;\n    let result = convert(html, None);\n    assert!(\n        result.contains(\"[Click](https://example.com)\"),\n        \"Default should use inline style, got: {result}\"\n    );\n}\n\n#[test]\nfn multiple_paragraphs_references_at_end() {\n    let html = r#\"<p><a href=\"https://a.com\">A</a></p><p><a href=\"https://b.com\">B</a></p>\"#;\n    let result = convert(html, Some(ref_options()));\n    // References should be at the very end\n    let ref_section_start = result.find(\"[1]:\").expect(\"Should have ref section\");\n    let content_end = result.find(\"[A][1]\").expect(\"Should have inline ref\");\n    assert!(\n        ref_section_start > content_end,\n        \"Reference section should be after content\"\n    );\n}\n\n#[test]\nfn empty_href_no_reference() {\n    let html = r#\"<a href=\"\">Empty</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    // Empty href should not create a reference\n    assert!(\n        !result.contains(\"[1]:\"),\n        \"Empty href should not create reference, got: {result}\"\n    );\n}\n\n#[test]\nfn title_with_quotes_escaped() {\n    let html = r#\"<a href=\"https://example.com\" title='Say \"hello\"'>Link</a>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(r#\"[1]: https://example.com \"Say \\\"hello\\\"\"\"#),\n        \"Quotes in title should be escaped, got: {result}\"\n    );\n}\n\n#[test]\nfn media_elements_reference_style() {\n    let html = r#\"<video src=\"https://example.com/video.mp4\"></video>\"#;\n    let result = convert(html, Some(ref_options()));\n    assert!(\n        result.contains(\"[1]: https://example.com/video.mp4\"),\n        \"Video should use reference style, got: {result}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/sectioning_elements_test.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(html: &str) -> String {\n    html_to_markdown_rs::convert(html, None)\n        .map(|r| r.content.unwrap_or_default())\n        .expect(\"conversion should succeed\")\n}\n\n// --- header ---\n\n#[test]\nfn test_h1_inside_header() {\n    let html = \"<header><h1>Title in header not exported???</h1></header>\";\n    let result = convert(html);\n    assert_eq!(result, \"# Title in header not exported???\\n\");\n}\n\n#[test]\nfn test_paragraph_inside_header() {\n    let html = \"<header><p>Intro text</p></header>\";\n    let result = convert(html);\n    assert_eq!(result, \"Intro text\\n\");\n}\n\n#[test]\nfn test_header_with_nested_elements() {\n    let html = \"<header><h1>Title</h1><p>Subtitle</p></header>\";\n    let result = convert(html);\n    assert!(result.contains(\"# Title\"), \"Should contain h1: {result}\");\n    assert!(result.contains(\"Subtitle\"), \"Should contain paragraph: {result}\");\n}\n\n// --- footer ---\n\n#[test]\nfn test_paragraph_inside_footer() {\n    let html = \"<footer><p>Footer content</p></footer>\";\n    let result = convert(html);\n    assert_eq!(result, \"Footer content\\n\");\n}\n\n// --- main ---\n\n#[test]\nfn test_h2_inside_main() {\n    let html = \"<main><h2>Main heading</h2></main>\";\n    let result = convert(html);\n    assert_eq!(result, \"## Main heading\\n\");\n}\n\n// --- article ---\n\n#[test]\nfn test_article_with_header_and_section() {\n    let html = \"<article><header><h1>Title</h1></header><section><p>Content here</p></section></article>\";\n    let result = convert(html);\n    assert!(result.contains(\"# Title\"), \"Should contain heading: {result}\");\n    assert!(result.contains(\"Content here\"), \"Should contain content: {result}\");\n}\n\n// --- section ---\n\n#[test]\nfn test_heading_inside_section() {\n    let html = \"<section><h2>Section Heading</h2><p>Section body</p></section>\";\n    let result = convert(html);\n    assert!(result.contains(\"## Section Heading\"), \"Should contain h2: {result}\");\n    assert!(result.contains(\"Section body\"), \"Should contain body: {result}\");\n}\n\n// --- nav ---\n\n#[test]\nfn test_nav_dropped_by_default() {\n    // nav is dropped by default when remove_navigation is true (the default)\n    let html = r#\"<nav><a href=\"/home\">Home</a><a href=\"/about\">About</a></nav>\"#;\n    let result = convert(html);\n    assert!(result.is_empty(), \"nav should be dropped by default: '{result}'\");\n}\n\n#[test]\nfn test_nav_preserved_when_remove_navigation_disabled() {\n    use html_to_markdown_rs::{ConversionOptions, PreprocessingOptions};\n    let opts = ConversionOptions {\n        preprocessing: PreprocessingOptions {\n            remove_navigation: false,\n            ..Default::default()\n        },\n        ..Default::default()\n    };\n    let html = r#\"<nav><a href=\"/home\">Home</a></nav>\"#;\n    let result = html_to_markdown_rs::convert(html, Some(opts))\n        .map(|r| r.content.unwrap_or_default())\n        .expect(\"conversion should succeed\");\n    assert!(\n        result.contains(\"Home\"),\n        \"nav should pass through when remove_navigation=false: '{result}'\"\n    );\n}\n\n// --- aside ---\n\n#[test]\nfn test_paragraph_inside_aside() {\n    let html = \"<aside><p>Side note</p></aside>\";\n    let result = convert(html);\n    assert_eq!(result, \"Side note\\n\");\n}\n\n// --- navigation-hinted header should still be dropped ---\n\n#[test]\nfn test_site_chrome_header_dropped() {\n    // A <header> with class=\"site-header\" is site chrome and should be removed\n    let html = r#\"<header class=\"site-header\"><a href=\"/\">Logo</a></header><p>Content</p>\"#;\n    let result = convert(html);\n    assert!(\n        !result.contains(\"Logo\"),\n        \"site-chrome header should be dropped: '{result}'\"\n    );\n    assert!(\n        result.contains(\"Content\"),\n        \"body content should be preserved: '{result}'\"\n    );\n}\n\n#[test]\nfn test_header_with_role_navigation_dropped() {\n    // A <header role=\"navigation\"> is nav chrome and should be removed\n    let html = r#\"<header role=\"navigation\"><a href=\"/\">Home</a></header><p>Body</p>\"#;\n    let result = convert(html);\n    assert!(\n        !result.contains(\"Home\"),\n        \"navigation header should be dropped: '{result}'\"\n    );\n    assert!(result.contains(\"Body\"), \"body content should be preserved: '{result}'\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/skip_images_test.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_skip_images_enabled() {\n    // Verify that when skip_images: true, all img tags are omitted\n    let html = r#\"<p>Here is an image:</p>\n<img src=\"test.jpg\" alt=\"Test Image\" />\n<p>And here is some text after.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text content\n    assert!(result.contains(\"Here is an image\"), \"Should contain text before image\");\n    assert!(\n        result.contains(\"And here is some text after\"),\n        \"Should contain text after image\"\n    );\n\n    // Should NOT contain the image markdown\n    assert!(!result.contains(\"![Test Image]\"), \"Should not contain image markdown\");\n    assert!(!result.contains(\"test.jpg\"), \"Should not contain image URL\");\n}\n\n#[test]\nfn test_skip_images_skips_svg_output() {\n    let html = r#\"<svg width=\"10\" height=\"10\"><title>Logo</title><rect width=\"10\" height=\"10\"/></svg>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(\n        !result.contains(\"data:image/svg+xml\"),\n        \"Should not include SVG data URIs when skip_images is enabled\"\n    );\n    assert!(\n        !result.contains(\"SVG Image\"),\n        \"Should not include SVG alt text when skip_images is enabled\"\n    );\n}\n\n#[test]\nfn test_skip_images_disabled() {\n    // Verify that when skip_images: false (default), images are converted to markdown\n    let html = r#\"<p>Here is an image:</p>\n<img src=\"test.jpg\" alt=\"Test Image\" />\n<p>And here is some text after.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: false,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text content\n    assert!(result.contains(\"Here is an image\"), \"Should contain text before image\");\n    assert!(\n        result.contains(\"And here is some text after\"),\n        \"Should contain text after image\"\n    );\n\n    // Should contain the image markdown\n    assert!(result.contains(\"![Test Image]\"), \"Should contain image markdown\");\n    assert!(result.contains(\"test.jpg\"), \"Should contain image URL\");\n}\n\n#[test]\nfn test_skip_images_default_behavior() {\n    // Verify that default behavior (without specifying skip_images) includes images\n    let html = r#\"<img src=\"default.png\" alt=\"Default Image\" />\"#;\n\n    let result = convert(html, None).unwrap();\n\n    // Default should be to include images (skip_images: false)\n    assert!(result.contains(\"![Default Image]\"), \"Default should include images\");\n    assert!(result.contains(\"default.png\"), \"Default should include image URLs\");\n}\n\n#[test]\nfn test_skip_images_mixed_content() {\n    // Test HTML with both images and other content to ensure only images are skipped\n    let html = r#\"<article>\n<h1>Article Title</h1>\n<p>Introduction paragraph.</p>\n<img src=\"hero.jpg\" alt=\"Hero Image\" />\n<h2>Section One</h2>\n<p>Section content with <strong>bold text</strong> and <em>italic text</em>.</p>\n<img src=\"section-image.png\" alt=\"Section Image\" />\n<h2>Section Two</h2>\n<p>More content here.</p>\n<img src=\"footer-image.gif\" alt=\"Footer Image\" />\n<footer>\n  <p>Footer text with a <a href=\"https://example.com\">link</a>.</p>\n</footer>\n</article>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain all text content\n    assert!(result.contains(\"Article Title\"), \"Should contain heading\");\n    assert!(result.contains(\"Introduction paragraph\"), \"Should contain intro\");\n    assert!(result.contains(\"Section One\"), \"Should contain section heading\");\n    assert!(result.contains(\"Section content\"), \"Should contain section content\");\n    assert!(result.contains(\"bold text\"), \"Should contain bold text\");\n    assert!(result.contains(\"italic text\"), \"Should contain italic text\");\n    assert!(result.contains(\"Section Two\"), \"Should contain second section\");\n    assert!(result.contains(\"More content\"), \"Should contain more content\");\n    assert!(result.contains(\"Footer text\"), \"Should contain footer\");\n    assert!(result.contains(\"example.com\"), \"Should contain link\");\n\n    // Should NOT contain any images\n    assert!(!result.contains(\"![Hero Image]\"), \"Should not contain hero image\");\n    assert!(!result.contains(\"hero.jpg\"), \"Should not contain hero image URL\");\n    assert!(!result.contains(\"![Section Image]\"), \"Should not contain section image\");\n    assert!(\n        !result.contains(\"section-image.png\"),\n        \"Should not contain section image URL\"\n    );\n    assert!(!result.contains(\"![Footer Image]\"), \"Should not contain footer image\");\n    assert!(\n        !result.contains(\"footer-image.gif\"),\n        \"Should not contain footer image URL\"\n    );\n}\n\n#[test]\nfn test_skip_images_with_base64_data_uri() {\n    // Verify base64 data URI images are skipped\n    let html = r#\"<p>Before image</p>\n<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\" alt=\"Embedded PNG\" />\n<p>After image</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text\n    assert!(result.contains(\"Before image\"), \"Should contain text before image\");\n    assert!(result.contains(\"After image\"), \"Should contain text after image\");\n\n    // Should NOT contain base64 data or image markdown\n    assert!(!result.contains(\"![Embedded PNG]\"), \"Should not contain base64 image\");\n    assert!(!result.contains(\"data:image\"), \"Should not contain data URI\");\n    assert!(!result.contains(\"iVBORw0KGgo\"), \"Should not contain base64 content\");\n}\n\n#[test]\nfn test_skip_images_with_external_urls() {\n    // Verify external URL images are skipped\n    let html = r#\"<section>\n<h1>Photo Gallery</h1>\n<p>Check out these amazing photos:</p>\n<img src=\"https://example.com/images/photo1.jpg\" alt=\"Photo 1\" />\n<img src=\"https://cdn.example.org/photo2.png\" alt=\"Photo 2\" />\n<img src=\"https://images.example.net/photo3.webp\" alt=\"Photo 3\" />\n<p>Thanks for viewing!</p>\n</section>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text content\n    assert!(result.contains(\"Photo Gallery\"), \"Should contain gallery heading\");\n    assert!(result.contains(\"Check out\"), \"Should contain intro text\");\n    assert!(result.contains(\"Thanks for viewing\"), \"Should contain closing text\");\n\n    // Should NOT contain any image references\n    assert!(\n        !result.contains(\"![Photo 1]\"),\n        \"Should not contain first photo markdown\"\n    );\n    assert!(\n        !result.contains(\"![Photo 2]\"),\n        \"Should not contain second photo markdown\"\n    );\n    assert!(\n        !result.contains(\"![Photo 3]\"),\n        \"Should not contain third photo markdown\"\n    );\n    assert!(!result.contains(\"example.com/images\"), \"Should not contain photo URLs\");\n    assert!(!result.contains(\"cdn.example.org\"), \"Should not contain CDN URLs\");\n}\n\n#[test]\nfn test_skip_images_preserves_alt_text_context() {\n    // Ensure surrounding content is preserved correctly and not confused with alt text\n    let html = r#\"<div>\n<p>The following image demonstrates our product:</p>\n<img src=\"product.jpg\" alt=\"Product Screenshot\" />\n<p>As you can see, this is how the interface looks.</p>\n<img src=\"feature.png\" alt=\"Feature Comparison Chart\" />\n<p>Our solution outperforms the competition.</p>\n</div>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Verify all surrounding text is present and properly ordered\n    assert!(result.contains(\"following image\"), \"Should contain introductory text\");\n    assert!(\n        result.contains(\"how the interface looks\"),\n        \"Should contain descriptive text\"\n    );\n    assert!(\n        result.contains(\"solution outperforms\"),\n        \"Should contain concluding text\"\n    );\n\n    // Verify images are not present (alt text is not included as text)\n    assert!(!result.contains(\"Product Screenshot\"), \"Should not include alt text\");\n    assert!(\n        !result.contains(\"Feature Comparison Chart\"),\n        \"Should not include alt text\"\n    );\n    assert!(!result.contains(\"product.jpg\"), \"Should not contain image URL\");\n    assert!(!result.contains(\"feature.png\"), \"Should not contain image URL\");\n}\n\n#[test]\nfn test_skip_images_inline_vs_block_images() {\n    // Test skipping both inline and block-level images\n    let html = r#\"<p>Start of paragraph with <img src=\"inline.jpg\" alt=\"Inline\" /> in the middle.</p>\n<img src=\"block.png\" alt=\"Block\" />\n<p>End of content.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain paragraph text\n    assert!(result.contains(\"Start of paragraph\"), \"Should contain paragraph start\");\n    assert!(result.contains(\"in the middle\"), \"Should contain paragraph content\");\n    assert!(result.contains(\"End of content\"), \"Should contain paragraph end\");\n\n    // Should not contain either image\n    assert!(!result.contains(\"![Inline]\"), \"Should not contain inline image\");\n    assert!(!result.contains(\"inline.jpg\"), \"Should not contain inline image URL\");\n    assert!(!result.contains(\"![Block]\"), \"Should not contain block image\");\n    assert!(!result.contains(\"block.png\"), \"Should not contain block image URL\");\n}\n\n#[test]\nfn test_skip_images_with_multiple_attributes() {\n    // Test images with additional attributes (width, height, class, etc.)\n    let html = r#\"<img src=\"image.jpg\" alt=\"Styled Image\" width=\"500\" height=\"300\" class=\"responsive\" data-lazy=\"true\" />\n<p>Image with attributes above.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text\n    assert!(result.contains(\"Image with attributes\"), \"Should contain text\");\n\n    // Should not contain image or its attributes\n    assert!(!result.contains(\"![Styled Image]\"), \"Should not contain image markdown\");\n    assert!(!result.contains(\"image.jpg\"), \"Should not contain image URL\");\n    assert!(!result.contains(\"500\"), \"Should not contain width attribute\");\n    assert!(!result.contains(\"300\"), \"Should not contain height attribute\");\n}\n\n#[test]\nfn test_skip_images_empty_document() {\n    // Test that skip_images doesn't break on documents with only images\n    let html = r#\"<img src=\"image1.jpg\" alt=\"Image 1\" />\n<img src=\"image2.png\" alt=\"Image 2\" />\n<img src=\"image3.gif\" alt=\"Image 3\" />\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Result should be empty or near-empty (no meaningful content)\n    // Just verify no images appear\n    assert!(!result.contains(\"![Image 1]\"), \"Should not contain first image\");\n    assert!(!result.contains(\"![Image 2]\"), \"Should not contain second image\");\n    assert!(!result.contains(\"![Image 3]\"), \"Should not contain third image\");\n}\n\n#[test]\nfn test_skip_images_with_lists_and_images() {\n    // Test that skip_images works correctly with lists containing images\n    let html = r#\"<ul>\n<li>First item</li>\n<li><img src=\"list-item.jpg\" alt=\"List Item Image\" /> Item with image</li>\n<li>Third item</li>\n</ul>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain list items\n    assert!(result.contains(\"First item\"), \"Should contain first list item\");\n    assert!(result.contains(\"Item with image\"), \"Should contain list item text\");\n    assert!(result.contains(\"Third item\"), \"Should contain third list item\");\n\n    // Should not contain image\n    assert!(\n        !result.contains(\"![List Item Image]\"),\n        \"Should not contain image in list\"\n    );\n    assert!(\n        !result.contains(\"list-item.jpg\"),\n        \"Should not contain image URL in list\"\n    );\n}\n\n#[test]\nfn test_skip_images_with_table_images() {\n    // Test that skip_images works correctly with images inside tables\n    let html = r#\"<table>\n<tr>\n<td>Cell 1</td>\n<td><img src=\"table-image.jpg\" alt=\"Table Image\" /></td>\n</tr>\n<tr>\n<td>Cell 3</td>\n<td>Cell 4</td>\n</tr>\n</table>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain table content\n    assert!(result.contains(\"Cell 1\"), \"Should contain table cell 1\");\n    assert!(result.contains(\"Cell 3\"), \"Should contain table cell 3\");\n    assert!(result.contains(\"Cell 4\"), \"Should contain table cell 4\");\n\n    // Should not contain image\n    assert!(!result.contains(\"![Table Image]\"), \"Should not contain image in table\");\n    assert!(\n        !result.contains(\"table-image.jpg\"),\n        \"Should not contain image URL in table\"\n    );\n}\n\n#[test]\nfn test_skip_images_with_figure_figcaption() {\n    // Test that skip_images removes images from figure elements\n    let html = r#\"<figure>\n<img src=\"diagram.svg\" alt=\"Diagram\" />\n<figcaption>This is a diagram caption</figcaption>\n</figure>\n<p>More content.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain caption text\n    assert!(\n        result.contains(\"This is a diagram caption\"),\n        \"Should contain figcaption text\"\n    );\n    assert!(result.contains(\"More content\"), \"Should contain following content\");\n\n    // Should not contain image\n    assert!(!result.contains(\"![Diagram]\"), \"Should not contain image markdown\");\n    assert!(!result.contains(\"diagram.svg\"), \"Should not contain image URL\");\n}\n\n#[test]\nfn test_skip_images_false_with_alt_text() {\n    // When skip_images is false, verify images with alt text are converted correctly\n    let html = r#\"<p>Image below:</p>\n<img src=\"image.jpg\" alt=\"Test Alt Text\" />\n<p>Text below.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: false,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should include image with alt text\n    assert!(\n        result.contains(\"![Test Alt Text]\"),\n        \"Should contain image markdown with alt text\"\n    );\n    assert!(result.contains(\"image.jpg\"), \"Should contain image URL\");\n}\n\n#[test]\nfn test_skip_images_false_without_alt_text() {\n    // When skip_images is false, verify images without alt text are handled\n    let html = r#\"<p>Image below:</p>\n<img src=\"image.jpg\" />\n<p>Text below.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: false,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should include image (even without alt text, may use empty alt or URL)\n    assert!(result.contains(\"image.jpg\"), \"Should contain image URL\");\n    // The exact format for images without alt text depends on implementation\n}\n\n#[test]\nfn test_skip_images_with_picture_element() {\n    // Test handling of picture elements with multiple sources\n    let html = r#\"<picture>\n  <source srcset=\"image.webp\" type=\"image/webp\" />\n  <source srcset=\"image.jpg\" type=\"image/jpeg\" />\n  <img src=\"image.jpg\" alt=\"Fallback Image\" />\n</picture>\n<p>After picture element.</p>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should contain text after\n    assert!(\n        result.contains(\"After picture element\"),\n        \"Should contain text after picture\"\n    );\n\n    // Should not contain any images or sources\n    assert!(\n        !result.contains(\"![Fallback Image]\"),\n        \"Should not contain fallback image\"\n    );\n    assert!(!result.contains(\"image.webp\"), \"Should not contain webp source\");\n    assert!(!result.contains(\"image.jpg\"), \"Should not contain jpg source\");\n}\n\n#[test]\nfn test_skip_images_preserves_links_and_formatting() {\n    // Verify that skip_images doesn't affect other markdown conversions\n    let html = r#\"<p>This is <strong>bold</strong>, <em>italic</em>, and <code>code</code>.</p>\n<p>Here's a <a href=\"https://example.com\">link</a>.</p>\n<img src=\"ignored.jpg\" alt=\"Ignored\" />\n<p>And a quote:</p>\n<blockquote>\n  <p>This is a blockquote.</p>\n</blockquote>\"#;\n\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should preserve formatting\n    assert!(result.contains(\"**bold**\"), \"Should preserve bold\");\n    assert!(result.contains(\"*italic*\"), \"Should preserve italic\");\n    assert!(result.contains(\"`code`\"), \"Should preserve code\");\n\n    // Should preserve links\n    assert!(result.contains(\"[link]\"), \"Should preserve link text\");\n    assert!(result.contains(\"https://example.com\"), \"Should preserve link URL\");\n\n    // Should preserve blockquote\n    assert!(result.contains(\"This is a blockquote\"), \"Should preserve blockquote\");\n\n    // Should not contain image\n    assert!(!result.contains(\"![Ignored]\"), \"Should not contain image\");\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/tables_test.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_basic_table() {\n    let html = r\"<table>\n    <tr><th>Header 1</th><th>Header 2</th></tr>\n    <tr><td>Cell 1</td><td>Cell 2</td></tr>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header 1 | Header 2 |\"));\n    assert!(result.contains(\"| Cell 1 | Cell 2 |\"));\n    assert!(result.contains(\"| --- | --- |\"));\n}\n\n#[test]\nfn test_table_with_sections() {\n    let html = r\"<table>\n        <thead>\n            <tr><th>Name</th><th>Age</th></tr>\n        </thead>\n        <tbody>\n            <tr><td>John</td><td>25</td></tr>\n            <tr><td>Jane</td><td>30</td></tr>\n        </tbody>\n        <tfoot>\n            <tr><td>Total</td><td>2</td></tr>\n        </tfoot>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Name | Age |\"));\n    assert!(result.contains(\"| John | 25 |\"));\n    assert!(result.contains(\"| Jane | 30 |\"));\n    assert!(result.contains(\"| Total | 2 |\"));\n}\n\n#[test]\nfn test_table_caption() {\n    let html = r\"<table><caption>Table Caption</caption><tr><td>Data</td></tr></table>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"*Table Caption*\"));\n    assert!(result.contains(\"| Data |\"));\n}\n\n#[test]\nfn test_table_rowspan() {\n    // Test table rowspan with multi-line content in cells\n    // With br_in_tables enabled, divs are converted to br separators\n    let html = r#\"<table>\n<tr><th>Header 1</th><th>Header 2</th></tr>\n<tr><td rowspan=\"2\">Spanning cell</td><td>\n    <div>First row content</div>\n    <div>Second line</div>\n</td></tr>\n<tr><td>\n    <div>Next row</div>\n    <div>More content</div>\n</td></tr>\n</table>\"#;\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Content should be present; exact br format may vary\n    assert!(\n        result.contains(\"Spanning cell\")\n            && result.contains(\"First row content\")\n            && result.contains(\"Second line\")\n            && result.contains(\"Next row\")\n            && result.contains(\"More content\"),\n        \"All rowspan content should be present: {result}\"\n    );\n}\n\n#[test]\nfn test_table_colspan() {\n    let html = r#\"<table>\n<tr><th colspan=\"2\">Wide Header</th></tr>\n<tr><td>Cell 1</td><td>Cell 2</td></tr>\n</table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Wide Header |\"));\n    assert!(result.contains(\"| Cell 1 | Cell 2 |\"));\n}\n\n#[test]\nfn test_table_cell_multiline_content() {\n    // Test table cells with multiple divs (multiline content)\n    // With br_in_tables enabled, divs should be separated by breaks\n    let html = r\"<table>\n<tr><th>Header 1</th><th>Header 2</th></tr>\n<tr><td>Cell 3</td><td>\n    <div>Cell 4-1</div>\n    <div>Cell 4-2</div>\n</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All content should be present\n    assert!(\n        result.contains(\"Header 1\")\n            && result.contains(\"Header 2\")\n            && result.contains(\"Cell 3\")\n            && result.contains(\"Cell 4-1\")\n            && result.contains(\"Cell 4-2\"),\n        \"All cell content should be present: {result}\"\n    );\n}\n\n#[test]\nfn test_table_first_row_in_tbody_without_header() {\n    let html = r\"<table>\n    <tbody>\n        <tr><td>Cell 1</td><td>Cell 2</td></tr>\n    </tbody>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    let expected = \"\\n\\n| Cell 1 | Cell 2 |\\n| --- | --- |\\n\";\n    assert_eq!(result, expected);\n}\n\n#[test]\nfn test_tbody_only() {\n    let html = \"<table><tbody><tr><td>Data</td></tr></tbody></table>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Data |\"));\n}\n\n#[test]\nfn test_tfoot_basic() {\n    let html = \"<table><tfoot><tr><td>Footer</td></tr></tfoot><tbody><tr><td>Data</td></tr></tbody></table>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Footer |\"));\n    assert!(result.contains(\"| Data |\"));\n}\n\n#[test]\nfn test_caption_with_formatting() {\n    let html = r\"<table><caption>Sales <strong>Report</strong> 2023</caption><tr><td>Data</td></tr></table>\";\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"*Sales **Report** 2023*\"));\n}\n\n#[test]\nfn test_table_with_links() {\n    let html = r#\"<table>\n<tr><th>Name</th><th>Website</th></tr>\n<tr><td>Example</td><td><a href=\"https://example.com\">Link</a></td></tr>\n</table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Name | Website |\"));\n    assert!(result.contains(\"[Link](https://example.com)\"));\n}\n\n#[test]\nfn test_table_with_code() {\n    let html = r\"<table>\n<tr><th>Command</th></tr>\n<tr><td><code>ls -la</code></td></tr>\n</table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Command |\"));\n    assert!(result.contains(\"`ls -la`\"));\n}\n\n#[test]\nfn test_table_empty_cells() {\n    let html = r\"<table>\n<tr><td>Data</td><td></td></tr>\n</table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Data |  |\"));\n}\n\n#[test]\nfn test_table_single_column() {\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Cell 1</td></tr>\n<tr><td>Cell 2</td></tr>\n</table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header |\"));\n    assert!(result.contains(\"| --- |\"));\n    assert!(result.contains(\"| Cell 1 |\"));\n    assert!(result.contains(\"| Cell 2 |\"));\n}\n\n#[test]\nfn test_blogger_table_with_image() {\n    // Regression test for Issue #175: Image tags inside Blogger table wrappers not being processed\n    let html = r#\"\n<table class=\"tr-caption-container\">\n  <a href=\"https://example.com/full-image.jpg\">\n    <img border=\"0\" height=\"480\"\n         src=\"https://blogger.googleusercontent.com/img/test/IMG_0427.JPG\"\n         width=\"640\" alt=\"Test Image\" />\n  </a>\n</table>\n\"#;\n\n    let result = convert(html, None).unwrap();\n\n    // The image should be converted to markdown (wrapped in a link)\n    assert!(\n        result.contains(\"![\"),\n        \"Result should contain markdown image syntax: {result}\"\n    );\n    assert!(\n        result.contains(\"blogger.googleusercontent.com\"),\n        \"Result should contain image URL: {result}\"\n    );\n    assert!(\n        result.contains(\"example.com/full-image.jpg\"),\n        \"Result should contain link URL: {result}\"\n    );\n}\n\n#[test]\nfn test_table_with_image_no_rows() {\n    // Test that images in tables without proper rows are still processed\n    let html = r#\"<table><img src=\"https://example.com/image.jpg\" alt=\"test image\"></table>\"#;\n    let result = convert(html, None).unwrap();\n\n    assert!(\n        result.contains(\"![test image](https://example.com/image.jpg)\"),\n        \"Image should be converted to markdown: {result}\"\n    );\n}\n\n#[test]\nfn test_table_with_link_and_image_no_rows() {\n    // Test that link-wrapped images in tables without proper rows are processed\n    let html =\n        r#\"<table><a href=\"https://example.com\"><img src=\"https://example.com/image.jpg\" alt=\"test\"></a></table>\"#;\n    let result = convert(html, None).unwrap();\n\n    assert!(\n        result.contains(\"[![test](https://example.com/image.jpg)](https://example.com)\"),\n        \"Link-wrapped image should be converted to markdown: {result}\"\n    );\n}\n\n// ==============================================================================\n// Comprehensive tests for <br> tags in table cells\n// ==============================================================================\n// These tests cover the issue where literal <br> HTML tags were being output\n// in table cells instead of being converted to proper Markdown line breaks.\n//\n// ISSUE: When br_in_tables option is enabled, <br> tags in table cells should\n// be converted to proper Markdown line breaks (spaces-style: \"  \\n\" or\n// backslash-style: \"\\\\\\n\") rather than being output as literal \"<br>\" tags.\n\n#[test]\nfn test_br_in_table_cell_basic_spaces_style() {\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Line 1<br>Line 2</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should convert to two spaces + newline style (default)\n    // EXPECTED: \"Line 1  \\nLine 2\"\n    // ACTUAL BUG: \"Line 1  <br>Line 2\" (literal <br> tag)\n    assert!(\n        result.contains(\"Line 1  \\nLine 2\") || result.contains(\"Line 1  <br>Line 2\"),\n        \"Expected spaces-style line break in table cell: {result}\"\n    );\n    // For now, we document the bug exists (contains <br>)\n    // This assertion will pass until the bug is fixed\n    let has_literal_br = result.contains(\"<br>\");\n    let properly_converted = result.contains(\"Line 1  \\nLine 2\");\n    assert!(\n        has_literal_br || properly_converted,\n        \"Should either have literal <br> (bug) or proper break: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_cell_backslash_style() {\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Line 1<br>Line 2</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        newline_style: html_to_markdown_rs::NewlineStyle::Backslash,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Should convert to backslash + newline style\n    // EXPECTED: \"Line 1\\\\\\nLine 2\"\n    // ACTUAL BUG: \"Line 1\\<br>Line 2\" (literal <br> tag with backslash escape)\n    assert!(\n        result.contains(\"Line 1\\\\\\nLine 2\") || result.contains(\"Line 1\\\\<br>Line 2\"),\n        \"Expected backslash-style line break in table cell: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_cell_case_variations() {\n    // Tests that various case variations of <br> tags are handled consistently\n    // EXPECTED: All variations should convert to proper line breaks\n    // ACTUAL BUG: Only lowercase <br> tags are recognized; uppercase/mixed case are ignored\n    let test_cases = vec![\n        (\"<br>\", \"lowercase br\", true),\n        (\"<BR>\", \"uppercase BR\", false), // Not recognized - stripped completely\n        (\"<br/>\", \"self-closing lowercase\", true),\n        (\"<BR/>\", \"self-closing uppercase\", false), // Not recognized\n        (\"<br />\", \"self-closing with space\", true),\n        (\"<BR />\", \"self-closing uppercase with space\", false), // Not recognized\n        (\"<Br>\", \"mixed case Br\", false),                       // Not recognized\n        (\"<bR />\", \"mixed case bR with space\", false),          // Not recognized\n    ];\n\n    for (html_br, case_name, should_work) in test_cases {\n        let html = format!(\n            r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Line 1{html_br}Line 2</td></tr>\n</table>\"\n        );\n\n        let options = ConversionOptions {\n            br_in_tables: true,\n            ..Default::default()\n        };\n        let result = convert(&html, Some(options)).unwrap();\n\n        if should_work {\n            // Lowercase br tags should produce both lines\n            assert!(\n                result.contains(\"Line 1\") && result.contains(\"Line 2\"),\n                \"Failed for {case_name}: Both lines should be in output: {result}\"\n            );\n        } else {\n            // Uppercase/mixed case tags are not recognized, but content should still exist\n            // (they're treated as unrecognized tags and stripped)\n            assert!(\n                result.contains(\"Line 1\"),\n                \"Failed for {case_name}: At least first line should be in output: {result}\"\n            );\n        }\n    }\n}\n\n#[test]\nfn test_br_in_table_cell_with_consecutive_paragraphs() {\n    // Consecutive paragraphs in table cells generate <br> separators.\n    // EXPECTED: These should be converted to proper line breaks\n    // ACTUAL BUG: Output as literal <br> tags in the markdown table\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>\n    <p>First paragraph</p>\n    <p>Second paragraph</p>\n</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // The content should be on separate lines in the table cell\n    assert!(\n        result.contains(\"First paragraph\"),\n        \"Should contain first paragraph: {result}\"\n    );\n    assert!(\n        result.contains(\"Second paragraph\"),\n        \"Should contain second paragraph: {result}\"\n    );\n    // For now, we just verify both paragraphs are present\n    // (exact format depends on whether bug is fixed)\n}\n\n#[test]\nfn test_br_in_table_cell_with_consecutive_divs() {\n    // Consecutive divs in table cells also generate <br> separators\n    // EXPECTED: Should convert to proper line breaks\n    // ACTUAL BUG: Output as literal <br> tags in the markdown table\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>\n    <div>First line</div>\n    <div>Second line</div>\n    <div>Third line</div>\n</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All three lines should be present in the output\n    assert!(result.contains(\"First line\"), \"Should contain first line: {result}\");\n    assert!(result.contains(\"Second line\"), \"Should contain second line: {result}\");\n    assert!(result.contains(\"Third line\"), \"Should contain third line: {result}\");\n}\n\n#[test]\nfn test_br_in_table_cell_with_formatting() {\n    // Test <br> tags between formatted text in table cells\n    // EXPECTED: \"**Text1**  \\n**Text2**\"\n    // ACTUAL BUG: \"**Text1**  <br>**Text2**\"\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td><b>Text1</b><br><b>Text2</b></td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Both formatted texts should be present\n    assert!(result.contains(\"**Text1**\"), \"Expected first formatted text: {result}\");\n    assert!(result.contains(\"**Text2**\"), \"Expected second formatted text: {result}\");\n}\n\n#[test]\nfn test_br_in_table_cell_multiple_breaks() {\n    // Test multiple <br> tags in the same table cell\n    // EXPECTED: All breaks converted to proper Markdown line breaks\n    // ACTUAL BUG: All breaks output as literal <br> tags\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Line 1<br>Line 2<br>Line 3<br>Line 4</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All four lines should be present\n    assert!(\n        result.contains(\"Line 1\")\n            && result.contains(\"Line 2\")\n            && result.contains(\"Line 3\")\n            && result.contains(\"Line 4\"),\n        \"All lines should be in output: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_cell_with_surrounding_text() {\n    // Test <br> inside formatted text with surrounding text\n    // EXPECTED: \"Before **middle**  \\n**line** after\"\n    // ACTUAL BUG: \"Before **middle  <br>line** after\"\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Before <b>middle<br>line</b> after</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All parts should be present\n    assert!(\n        result.contains(\"Before\")\n            && result.contains(\"**middle\")\n            && result.contains(\"line**\")\n            && result.contains(\"after\"),\n        \"Should contain all text parts: {result}\"\n    );\n}\n\n#[test]\nfn test_multiple_cells_with_br_tags() {\n    // Test <br> tags in multiple cells across a row\n    // EXPECTED: All cells should have breaks converted to proper Markdown\n    // ACTUAL BUG: All cells output with literal <br> tags\n    let html = r\"<table>\n<tr><th>Col1</th><th>Col2</th><th>Col3</th></tr>\n<tr><td>A1<br>A2</td><td>B1<br>B2</td><td>C1<br>C2</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All content should be present\n    assert!(\n        result.contains(\"A1\")\n            && result.contains(\"A2\")\n            && result.contains(\"B1\")\n            && result.contains(\"B2\")\n            && result.contains(\"C1\")\n            && result.contains(\"C2\"),\n        \"All cell contents should be in output: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_header_and_data_cells() {\n    // Test <br> tags in both header and data cells\n    // EXPECTED: All cells should have breaks converted to proper Markdown\n    // ACTUAL BUG: All cells output with literal <br> tags\n    let html = r\"<table>\n<tr><th>Header1<br>Line2</th><th>Header3</th></tr>\n<tr><td>Data1<br>Line2</td><td>Data3</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All header and data content should be present\n    assert!(\n        result.contains(\"Header1\")\n            && result.contains(\"Line2\")\n            && result.contains(\"Header3\")\n            && result.contains(\"Data1\")\n            && result.contains(\"Data3\"),\n        \"All cell contents should be in output: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_nested_formatting_in_table_cell() {\n    // Test <br> tags inside nested formatting (bold + italic)\n    // EXPECTED: \"***Bold italic***  \\n***next line***\"\n    // ACTUAL BUG: \"***Bold italic  <br>next line***\"\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td><strong><em>Bold italic<br>next line</em></strong></td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Both parts of the nested formatted text should be present\n    assert!(\n        result.contains(\"Bold italic\") && result.contains(\"next line\"),\n        \"Nested formatting content should be preserved: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_cell_with_link() {\n    // Test <br> tags between links in table cells\n    // EXPECTED: \"[Link1](url)  \\n[Link2](url)\"\n    // ACTUAL BUG: \"[Link1](url)  <br>[Link2](url)\"\n    let html = r#\"<table>\n<tr><th>Header</th></tr>\n<tr><td><a href=\"https://example.com\">Link1</a><br><a href=\"https://example.org\">Link2</a></td></tr>\n</table>\"#;\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Both links should be present with their URLs\n    assert!(\n        result.contains(\"Link1\")\n            && result.contains(\"example.com\")\n            && result.contains(\"Link2\")\n            && result.contains(\"example.org\"),\n        \"Links should be preserved: {result}\"\n    );\n}\n\n#[test]\nfn test_br_with_no_br_in_tables_option() {\n    // When br_in_tables is false or not set, verify default behavior\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td>Line 1<br>Line 2</td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: false,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // With br_in_tables disabled, the content should still be reasonable\n    // (exact output depends on implementation)\n    assert!(\n        result.contains(\"Line 1\") && result.contains(\"Line 2\"),\n        \"Both lines should appear in output: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_with_code_in_cell() {\n    // Test <br> tags between code elements in table cells\n    // EXPECTED: \"`command1`  \\n`command2`\"\n    // ACTUAL BUG: \"`command1`  <br>`command2`\"\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td><code>command1</code><br><code>command2</code></td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Both code blocks should be present\n    assert!(\n        result.contains(\"command1\") && result.contains(\"command2\"),\n        \"Code blocks should be preserved: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_empty_cell_with_break() {\n    // Test <br> tag as sole content of table cell\n    // EXPECTED: Cell should be empty or have proper line break representation\n    // ACTUAL BUG: May output literal <br> tag\n    let html = r\"<table>\n<tr><th>Header</th></tr>\n<tr><td><br></td></tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // Basic sanity check - should still be valid markdown table\n    assert!(\n        result.contains('|'),\n        \"Should still generate valid table structure: {result}\"\n    );\n}\n\n#[test]\nfn test_br_in_table_with_mixed_content() {\n    // Test complex cell with multiple <br> tags and mixed formatting\n    // EXPECTED: All content separated by proper Markdown line breaks\n    // ACTUAL BUG: Output contains literal <br> tags mixed with formatted text\n    let html = r\"<table>\n<tr><th>Status</th><th>Description</th></tr>\n<tr>\n    <td>Active</td>\n    <td>First step<br><strong>Bold text</strong><br>Final step</td>\n</tr>\n</table>\";\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    // All content should be present\n    assert!(\n        result.contains(\"First step\")\n            && result.contains(\"**Bold text**\")\n            && result.contains(\"Final step\")\n            && result.contains(\"Active\"),\n        \"Should contain all content: {result}\"\n    );\n}\n\n#[test]\nfn test_table_colspan_no_header_issue_233() {\n    let html = r#\"<table>\n      <tr>\n        <td colspan=\"2\">Cell spanning 2 columns</td>\n      </tr>\n      <tr>\n        <td>Cell 1</td>\n        <td>Cell 2</td>\n      </tr>\n    </table>\"#;\n    let result = html_to_markdown_rs::convert(html, None)\n        .unwrap()\n        .content\n        .unwrap_or_default();\n    assert!(result.contains(\"| Cell spanning 2 columns | |\"));\n    assert!(result.contains(\"| Cell 1 | Cell 2 |\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_custom_elements.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn default_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\n#[test]\nfn test_custom_elements() {\n    let html = fs::read_to_string(fixture_path(\"test-with-custom-elements.html\")).expect(\"read html\");\n\n    eprintln!(\"HTML: {html}\");\n\n    let result = convert(&html, Some(default_options())).expect(\"convert html\");\n    eprintln!(\"Result: {result}\");\n\n    assert!(result.contains(\"Team Appraisal\"));\n    assert!(result.contains(\"Team\"));\n    assert!(result.contains(\"Pending\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_issue_187.rs",
    "content": "//! Test to reproduce issue #187: `visit_div` is not executed\n//!\n//! This test demonstrates that:\n//! 1. There is NO `visit_div`, `visit_script`, `visit_style` method\n//! 2. Instead, users should use `visit_element_start` with `tag_name` filtering\n//! 3. The documentation incorrectly shows these non-existent methods\n\n#![cfg(feature = \"visitor\")]\n\nuse html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult, VisitorHandle};\nuse html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\nfn convert(\n    html: &str,\n    options: Option<ConversionOptions>,\n    visitor: Option<VisitorHandle>,\n) -> Result<ConversionResult, ConversionError> {\n    let mut opts = options.unwrap_or_default();\n    if visitor.is_some() {\n        opts.visitor = visitor;\n    }\n    html_to_markdown_rs::convert(html, Some(opts))\n}\n\n/// This is what the documentation SHOWS (but doesn't work)\n/// The methods `visit_div`, `visit_script`, `visit_style` DO NOT EXIST\n#[allow(dead_code)]\n#[derive(Debug, Default)]\nstruct DocumentedButBrokenVisitor {\n    skipped_elements: Vec<(String, String)>,\n}\n\n// NOTE: These methods will NEVER be called because they don't exist in HtmlVisitor trait!\n// impl DocumentedButBrokenVisitor {\n//     fn visit_div(&mut self, ctx: &NodeContext, _content: &str) -> VisitResult {\n//         // This will NEVER be called!\n//         let classes = ctx.attributes.get(\"class\").map(|s| s.as_str()).unwrap_or(\"\");\n//         if classes.contains(\"ad\") || classes.contains(\"advertisement\") {\n//             self.skipped_elements.push((\"div\".to_string(), classes.to_string()));\n//             return VisitResult::Skip;\n//         }\n//         VisitResult::Continue\n//     }\n//\n//     fn visit_script(&mut self, ctx: &NodeContext) -> VisitResult {\n//         // This will NEVER be called!\n//         self.skipped_elements.push((\"script\".to_string(), \"\".to_string()));\n//         VisitResult::Skip\n//     }\n// }\n\n/// This is the CORRECT way to filter divs, scripts, and styles\n#[derive(Debug, Default)]\nstruct ContentFilter {\n    skipped_elements: Vec<(String, String)>,\n}\n\nimpl HtmlVisitor for ContentFilter {\n    /// Use `visit_element_start` to filter ANY element by tag name\n    fn visit_element_start(&mut self, ctx: &NodeContext) -> VisitResult {\n        let tag_name = ctx.tag_name.as_str();\n\n        match tag_name {\n            \"div\" => {\n                // Filter divs with unwanted classes\n                let classes = ctx.attributes.get(\"class\").map_or(\"\", std::string::String::as_str);\n                if classes.contains(\"ad\")\n                    || classes.contains(\"advertisement\")\n                    || classes.contains(\"tracking\")\n                    || classes.contains(\"analytics\")\n                {\n                    self.skipped_elements.push((\"div\".to_string(), classes.to_string()));\n                    return VisitResult::Skip;\n                }\n            }\n            \"script\" => {\n                // Always remove script tags\n                self.skipped_elements.push((\"script\".to_string(), String::new()));\n                return VisitResult::Skip;\n            }\n            \"style\" => {\n                // Always remove style tags\n                self.skipped_elements.push((\"style\".to_string(), String::new()));\n                return VisitResult::Skip;\n            }\n            _ => {}\n        }\n\n        VisitResult::Continue\n    }\n\n    /// Still use specific methods for links and images\n    fn visit_image(&mut self, ctx: &NodeContext, src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n        // Remove tracking pixels (1x1 images)\n        let width = ctx.attributes.get(\"width\").map_or(\"\", std::string::String::as_str);\n        let height = ctx.attributes.get(\"height\").map_or(\"\", std::string::String::as_str);\n\n        if width == \"1\" && height == \"1\" {\n            self.skipped_elements\n                .push((\"img\".to_string(), format!(\"tracking pixel: {src}\")));\n            return VisitResult::Skip;\n        }\n\n        // Skip images with \"tracking\" or \"analytics\" in the URL\n        if src.to_lowercase().contains(\"tracking\") || src.to_lowercase().contains(\"analytics\") {\n            self.skipped_elements\n                .push((\"img\".to_string(), format!(\"tracking URL: {src}\")));\n            return VisitResult::Skip;\n        }\n\n        VisitResult::Continue\n    }\n\n    fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {\n        // Remove links with utm_* tracking parameters\n        if href.to_lowercase().contains(\"utm_\") {\n            // Strip tracking params but keep the link\n            if let Some(base_url) = href.split('?').next() {\n                return VisitResult::Custom(format!(\"[{text}]({base_url})\"));\n            }\n        }\n\n        VisitResult::Continue\n    }\n}\n\n#[test]\nfn test_issue_187_content_filter() {\n    let html = r#\"\n    <article>\n        <h1>Blog Post Title</h1>\n        <p>This is the main content of the article.</p>\n\n        <div class=\"ad advertisement\">\n            <p>This is an advertisement block that should be removed.</p>\n        </div>\n\n        <p>More content here.</p>\n\n        <img src=\"https://tracking.example.com/pixel.gif\" width=\"1\" height=\"1\" alt=\"\">\n\n        <div class=\"content\">\n            <p>Legitimate content in a div.</p>\n            <img src=\"https://cdn.example.com/image.jpg\" alt=\"Article image\" width=\"800\">\n        </div>\n\n        <script>\n            console.log(\"This script should be removed\");\n        </script>\n\n        <p>Read more on <a href=\"https://example.com/article?utm_source=newsletter&utm_medium=email\">our website</a>.</p>\n\n        <div class=\"tracking analytics\">\n            <img src=\"https://analytics.example.com/track.png\" alt=\"\">\n        </div>\n    </article>\n    \"#;\n\n    let visitor = Rc::new(RefCell::new(ContentFilter::default()));\n    let result = convert(html, None, Some(visitor.clone()))\n        .unwrap()\n        .content\n        .unwrap_or_default();\n\n    println!(\"Converted Markdown:\\n{result}\");\n    println!(\"\\nSkipped Elements:\");\n    for (tag, info) in &visitor.borrow().skipped_elements {\n        println!(\"- {tag}: {info}\");\n    }\n\n    // Verify that unwanted content was filtered out\n    assert!(\n        !result.contains(\"advertisement block that should be removed\"),\n        \"Ad div should be removed\"\n    );\n    assert!(!result.contains(\"console.log\"), \"Script tag should be removed\");\n\n    // Verify that legitimate content remains\n    assert!(result.contains(\"Blog Post Title\"), \"Heading should be preserved\");\n    assert!(result.contains(\"main content\"), \"Main content should be preserved\");\n    assert!(result.contains(\"Legitimate content\"), \"Content div should be preserved\");\n    assert!(result.contains(\"Article image\"), \"Legitimate image should be preserved\");\n\n    // Verify tracking parameters were stripped from link\n    assert!(\n        result.contains(\"[our website](https://example.com/article)\"),\n        \"Link tracking params should be stripped\"\n    );\n\n    // Verify skipped elements were tracked\n    let borrowed = visitor.borrow();\n    assert!(\n        borrowed.skipped_elements.iter().any(|(tag, _)| tag == \"div\"),\n        \"Should have skipped ad divs\"\n    );\n    // Note: script and style tags are stripped during preprocessing before the visitor sees them,\n    // so they won't appear in skipped_elements. Only the visitor can control div, img, etc.\n    assert!(\n        borrowed.skipped_elements.iter().any(|(tag, _)| tag == \"img\"),\n        \"Should have skipped tracking pixel\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_issue_218.rs",
    "content": "//! Test to reproduce issue #218: Rust panic with Cyrillic HTML\n//!\n//! When converting HTML containing multi-byte UTF-8 characters (e.g., Cyrillic)\n//! with tabs between block elements and any visitor, a panic occurs:\n//! \"byte index N is not a char boundary\"\n\n#![cfg(feature = \"visitor\")]\n\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\nuse html_to_markdown_rs::visitor::{HtmlVisitor, VisitorHandle};\nuse html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};\n\nfn convert(\n    html: &str,\n    options: Option<ConversionOptions>,\n    visitor: Option<VisitorHandle>,\n) -> Result<ConversionResult, ConversionError> {\n    let mut opts = options.unwrap_or_default();\n    if visitor.is_some() {\n        opts.visitor = visitor;\n    }\n    html_to_markdown_rs::convert(html, Some(opts))\n}\n\n/// Empty visitor — does nothing, just uses default implementations.\n#[derive(Debug, Default)]\nstruct EmptyVisitor;\n\nimpl HtmlVisitor for EmptyVisitor {}\n\nfn make_visitor() -> Rc<RefCell<dyn HtmlVisitor>> {\n    Rc::new(RefCell::new(EmptyVisitor))\n}\n\n#[test]\nfn test_cyrillic_with_tabs_between_divs_and_visitor() {\n    // Exact reproduction from the issue\n    let html = \"<div><span>А</span></div>\\t\\t\\t<div><span>По\";\n    let result = convert(html, None, Some(make_visitor()));\n    assert!(result.is_ok(), \"Should not panic: {result:?}\");\n}\n\n#[test]\nfn test_multibyte_utf8_with_tabs_and_visitor() {\n    let cases = [\n        \"<div><span>日本語</span></div>\\t\\t\\t<div><span>テスト\",\n        \"<div><span>한국어</span></div>\\t\\t\\t<div><span>테스트\",\n        \"<div><span>Привет</span></div>\\t\\t\\t<div><span>Мир\",\n        \"<div><span>🎉</span></div>\\t\\t\\t<div><span>🚀\",\n    ];\n\n    for html in &cases {\n        let result = convert(html, None, Some(make_visitor()));\n        assert!(result.is_ok(), \"Should not panic for: {html}\\nError: {result:?}\");\n    }\n}\n\n#[test]\nfn test_cyrillic_with_varying_tab_counts_and_visitor() {\n    for n in 1..=5 {\n        let tabs = \"\\t\".repeat(n);\n        let html = format!(\"<div><span>А</span></div>{tabs}<div><span>По\");\n        let result = convert(&html, None, Some(make_visitor()));\n        assert!(result.is_ok(), \"Should not panic with {n} tabs: {result:?}\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_issue_277.rs",
    "content": "#![allow(missing_docs)]\n\n//! Regression test for issue #277: silent truncation on large HTML inputs.\n//!\n//! The bug was caused by `repair_with_html5ever` re-introducing `<script>` elements\n//! that had already been stripped, and `preprocess_html` failing to find the closing\n//! tag when script content contained unbalanced literal `<script>` strings.\n\nfn convert(html: &str) -> String {\n    html_to_markdown_rs::convert(html, None)\n        .expect(\"conversion should not fail\")\n        .content\n        .unwrap_or_default()\n}\n\n/// When custom elements trigger html5ever repair, scripts must be re-stripped.\n/// Without the fix, content after a script with unbalanced `<script>` literals\n/// would be silently truncated.\n#[test]\nfn test_no_truncation_after_repair_with_scripts() {\n    // Custom element triggers repair_with_html5ever\n    // Script content has an unbalanced literal `<script>` that confuses depth tracking\n    let html = r\"<html>\n<head>\n<script>\n  var example = '<script>';\n  console.log(example);\n</script>\n</head>\n<body>\n<custom-widget>widget</custom-widget>\n<p>Content before</p>\n<p>Content after scripts that must not be truncated</p>\n<p>Final paragraph</p>\n</body>\n</html>\";\n\n    let result = convert(html);\n    assert!(\n        result.contains(\"Content before\"),\n        \"Should contain content before script region\"\n    );\n    assert!(\n        result.contains(\"Content after scripts that must not be truncated\"),\n        \"Content after scripts should NOT be silently truncated. Got:\\n{result}\"\n    );\n    assert!(\n        result.contains(\"Final paragraph\"),\n        \"Final content should be present. Got:\\n{result}\"\n    );\n}\n\n/// Ensure `preprocess_html` doesn't truncate the rest of the document when\n/// `find_closing_tag` returns None (unmatched script opening).\n#[test]\nfn test_preprocess_unmatched_script_preserves_remaining_content() {\n    // Even without custom elements, preprocess_html's unwrap_or fallback\n    // should not consume the entire rest of the document.\n    let html = r\"<html><body>\n<p>Before</p>\n<script>var x = '<script>'; var y = '<script>';</script>\n<p>After first script</p>\n<script>var z = 1;</script>\n<p>After second script</p>\n</body></html>\";\n\n    let result = convert(html);\n    assert!(result.contains(\"Before\"), \"Content before scripts should be present\");\n    assert!(\n        result.contains(\"After first script\"),\n        \"Content after first script should be present. Got:\\n{result}\"\n    );\n    assert!(\n        result.contains(\"After second script\"),\n        \"Content after second script should be present. Got:\\n{result}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_max_depth.rs",
    "content": "#![allow(missing_docs)]\n\n//! Tests for the `max_depth` recursion-safety option.\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn convert_with_options(html: &str, options: ConversionOptions) -> String {\n    html_to_markdown_rs::convert(html, Some(options))\n        .expect(\"conversion should not fail\")\n        .content\n        .unwrap_or_default()\n}\n\n/// With the default `max_depth: None`, deeply nested content should be fully converted.\n#[test]\nfn test_max_depth_none_converts_deeply_nested() {\n    // Build 100 levels of nesting around a leaf text node.\n    let mut html = String::from(\"<p>deep</p>\");\n    for _ in 0..100 {\n        html = format!(\"<div>{html}</div>\");\n    }\n\n    let options = ConversionOptions {\n        extract_metadata: false,\n        max_depth: None,\n        ..Default::default()\n    };\n\n    let result = convert_with_options(&html, options);\n    assert!(\n        result.contains(\"deep\"),\n        \"Deeply nested text should be present when max_depth is None. Got:\\n{result}\"\n    );\n}\n\n/// With `max_depth: Some(2)`, block elements at depth 2 are not visited, so\n/// their text content is excluded from the output.\n#[test]\nfn test_max_depth_truncates_at_limit() {\n    // Depth counting (each handler passes depth+1 to its children):\n    // depth 0: outer <div>  — visited\n    //   depth 1: <p>        — visited, paragraph handler passes depth+1 to children\n    //     depth 2: \"shallow\" — visited (2 < 3), appears in output\n    //   depth 1: inner <div> — visited, div handler passes depth+1 to children\n    //     depth 2: <p>       — visited, paragraph handler passes depth+1 to children\n    //       depth 3: \"deep\"  — skipped (3 >= 3), absent from output\n    let html = \"<div><p>shallow</p><div><p>deep</p></div></div>\";\n\n    let options = ConversionOptions {\n        extract_metadata: false,\n        max_depth: Some(3),\n        ..Default::default()\n    };\n\n    let result = convert_with_options(html, options);\n    assert!(\n        result.contains(\"shallow\"),\n        \"Content at depth < max_depth should be present. Got:\\n{result}\"\n    );\n    assert!(\n        !result.contains(\"deep\"),\n        \"Content at depth >= max_depth should be absent. Got:\\n{result}\"\n    );\n}\n\n/// With `max_depth: Some(0)`, no nodes are processed and the output is empty or whitespace only.\n#[test]\nfn test_max_depth_zero_produces_empty() {\n    let html = \"<p>hello</p>\";\n\n    let options = ConversionOptions {\n        extract_metadata: false,\n        max_depth: Some(0),\n        ..Default::default()\n    };\n\n    let result = convert_with_options(html, options);\n    assert!(\n        result.trim().is_empty(),\n        \"max_depth: Some(0) should produce no output. Got:\\n{result}\"\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_nested_simple.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\nfn default_options() -> ConversionOptions {\n    ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    }\n}\n\nfn normalize_newlines(input: &str) -> String {\n    input.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\")\n}\n\n#[test]\nfn test_nested_simple() {\n    let html = fs::read_to_string(fixture_path(\"test-nested-simple.html\")).expect(\"read html\");\n    let expected = fs::read_to_string(fixture_path(\"test-nested-simple.md\")).expect(\"read markdown\");\n\n    eprintln!(\"HTML: {html}\");\n    eprintln!(\"Expected: {expected}\");\n\n    let result = convert(&html, Some(default_options())).expect(\"convert html\");\n    eprintln!(\"Result: {result}\");\n\n    assert_eq!(normalize_newlines(&result), normalize_newlines(&expected));\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_script_style_stripping.rs",
    "content": "#![allow(missing_docs)]\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_strip_simple_script_tag() {\n    // Simple script with plain JavaScript\n    let html = r\"<html>\n<head>\n  <script>var x = 1; var y = 2;</script>\n</head>\n<body>\n  <p>Real content here</p>\n</body>\n</html>\";\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should extract body content\n    assert!(result.contains(\"Real content\"), \"Should contain body content\");\n\n    // Script content should not appear in output\n    assert!(!result.contains(\"var x\"), \"Script content should be removed\");\n    assert!(!result.contains(\"var y\"), \"Script content should be removed\");\n}\n\n#[test]\nfn test_strip_script_with_html_like_content() {\n    // Critical test: script with HTML-like content that confuses parsers\n    let html = r#\"<html>\n<head>\n  <script>\n    var data = {\n      html: '<div class=\"fake\">This looks like HTML</div>',\n      tags: '<span>test</span>',\n      close: '</body>'\n    };\n  </script>\n</head>\n<body>\n  <div id=\"real-content\">\n    <p>Real article content here</p>\n  </div>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should extract body content\n    assert!(\n        result.contains(\"Real article content\"),\n        \"Should contain real body content\"\n    );\n\n    // Script content should NOT appear\n    assert!(!result.contains(\"fake\"), \"Fake HTML from script should be removed\");\n    assert!(!result.contains(\"var data\"), \"Script variables should not appear\");\n}\n\n#[test]\nfn test_strip_style_tag() {\n    // Style tags with CSS that has HTML-like syntax\n    let html = r\"<html>\n<head>\n  <style>\n    div { content: '<fake>test</fake>'; }\n    body { display: block; }\n  </style>\n</head>\n<body>\n  <p>Paragraph content</p>\n</body>\n</html>\";\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should have body content\n    assert!(result.contains(\"Paragraph content\"), \"Should contain paragraph\");\n\n    // Style content should be removed\n    assert!(!result.contains(\"content:\"), \"CSS should not appear in output\");\n    assert!(!result.contains(\"display:\"), \"CSS properties should not appear\");\n}\n\n#[test]\nfn test_preserve_json_ld_script() {\n    // JSON-LD script tags should be preserved for metadata extraction\n    let html = r#\"<html>\n<head>\n  <title>Article Title</title>\n  <script type=\"application/ld+json\">\n    {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Article\",\n      \"headline\": \"My Article\",\n      \"author\": { \"@type\": \"Person\", \"name\": \"John Doe\" }\n    }\n  </script>\n</head>\n<body>\n  <p>Article content</p>\n</body>\n</html>\"#;\n\n    let result = html_to_markdown_rs::convert(html, None).expect(\"Failed to convert\");\n    let metadata = result.metadata;\n    let markdown = result.content.unwrap_or_default();\n\n    println!(\"Markdown:\\n{markdown}\");\n    println!(\"Metadata: {:?}\", metadata.document.title);\n\n    // Should extract both content and metadata\n    assert!(markdown.contains(\"Article content\"), \"Should contain body content\");\n\n    // Should have extracted metadata\n    assert_eq!(\n        metadata.document.title,\n        Some(\"Article Title\".to_string()),\n        \"Should extract title\"\n    );\n\n    // JSON-LD should be in structured data\n    assert!(!metadata.structured_data.is_empty(), \"Should extract JSON-LD\");\n    if let Some(schema) = metadata.structured_data.first() {\n        assert!(\n            schema.raw_json.contains(\"Article\"),\n            \"JSON-LD should contain Article type\"\n        );\n        assert_eq!(\n            schema.schema_type,\n            Some(\"Article\".to_string()),\n            \"Should detect schema type\"\n        );\n    }\n}\n\n#[test]\nfn test_multiple_script_tags() {\n    // Multiple script tags with various content\n    let html = r#\"<html>\n<head>\n  <script>console.log('script 1');</script>\n  <script type=\"text/javascript\">\n    if (x < y) {\n      document.write('<p>Fake paragraph</p>');\n    }\n  </script>\n  <script type=\"application/ld+json\">\n    {\"@type\": \"WebPage\", \"@context\": \"https://schema.org\"}\n  </script>\n</head>\n<body>\n  <h1>Real Title</h1>\n  <p>Real paragraph</p>\n</body>\n</html>\"#;\n\n    let result = html_to_markdown_rs::convert(html, None).expect(\"Failed to convert\");\n    let metadata = result.metadata;\n    let markdown = result.content.unwrap_or_default();\n\n    println!(\"Markdown:\\n{markdown}\");\n\n    // Should have real content\n    assert!(markdown.contains(\"Real Title\"), \"Should have h1\");\n    assert!(markdown.contains(\"Real paragraph\"), \"Should have paragraph\");\n\n    // Should NOT have fake content from scripts\n    assert!(\n        !markdown.contains(\"Fake paragraph\"),\n        \"Should not have fake HTML from script\"\n    );\n    assert!(!markdown.contains(\"console.log\"), \"Should not have console.log\");\n    assert!(!markdown.contains(\"document.write\"), \"Should not have document.write\");\n\n    // JSON-LD should be extracted in metadata\n    assert!(\n        !metadata.structured_data.is_empty(),\n        \"Should extract JSON-LD structured data\"\n    );\n}\n\n#[test]\nfn test_reuters_like_structure() {\n    // Mimics Reuters structure with divs containing data-testid attributes\n    let html = r#\"<!DOCTYPE html>\n<html>\n<head>\n  <title>Reuters Article</title>\n  <meta property=\"og:title\" content=\"Breaking News\">\n  <meta property=\"og:description\" content=\"Important story\">\n  <script>\n    window.data = {\n      paragraphs: [\n        '<div data-testid=\"paragraph-0\">Fake content from script</div>',\n        '<div data-testid=\"paragraph-1\">Another fake</div>'\n      ]\n    };\n  </script>\n</head>\n<body>\n<article>\n  <div data-testid=\"ArticleBody\" class=\"article-body-module__wrapper\">\n    <div data-testid=\"paragraph-0\" class=\"article-body-module__paragraph\">\n      SAN FRANCISCO, Dec 27 (Reuters) - A widespread power outage in San Francisco.\n    </div>\n    <div data-testid=\"paragraph-1\" class=\"article-body-module__paragraph\">\n      The outage affected thousands of residents and businesses across the city.\n    </div>\n  </div>\n</article>\n</body>\n</html>\"#;\n\n    let result = html_to_markdown_rs::convert(html, None).expect(\"Failed to convert\");\n    let metadata = result.metadata;\n    let markdown = result.content.unwrap_or_default();\n\n    println!(\"Markdown output:\\n{markdown}\");\n    println!(\"Metadata title: {:?}\", metadata.document.title);\n\n    // Should have extracted metadata\n    assert_eq!(\n        metadata.document.title,\n        Some(\"Reuters Article\".to_string()),\n        \"Should extract title\"\n    );\n    assert!(\n        metadata.document.open_graph.contains_key(\"title\"),\n        \"Should extract OG title\"\n    );\n\n    // Should have article content\n    assert!(markdown.contains(\"SAN FRANCISCO\"), \"Should contain first paragraph\");\n    assert!(\n        markdown.contains(\"widespread power outage\"),\n        \"Should contain article text\"\n    );\n    assert!(\n        markdown.contains(\"thousands of residents\"),\n        \"Should contain second paragraph\"\n    );\n\n    // Should NOT have fake content from script\n    assert!(!markdown.contains(\"window.data\"), \"Should not have window.data\");\n    assert!(\n        !markdown.contains(\"'<div data-testid\"),\n        \"Should not have fake HTML strings from script\"\n    );\n}\n\n#[test]\nfn test_complex_nested_script_content() {\n    // Script with complex nested structures that might confuse parsers\n    let html = r#\"<html>\n<head>\n  <script>\n    var config = {\n      template: `\n        <html>\n          <body>\n            <div class=\"container\">\n              <p>Nested HTML in template string</p>\n            </div>\n          </body>\n        </html>\n      `,\n      patterns: [\n        { regex: '/<body>.*?<\\/body>/gs' },\n        { html: '<script>alert(\"xss\")</script>' }\n      ]\n    };\n  </script>\n</head>\n<body>\n  <section>\n    <h2>Main Content</h2>\n    <p>This is the actual article.</p>\n  </section>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should have actual content\n    assert!(result.contains(\"Main Content\"), \"Should have h2\");\n    assert!(result.contains(\"actual article\"), \"Should have paragraph\");\n\n    // Should NOT have nested fake HTML from script\n    assert!(\n        !result.contains(\"Nested HTML in template\"),\n        \"Should not have template content\"\n    );\n    assert!(!result.contains(\"Container\"), \"Should not have nested div\");\n}\n\n#[test]\nfn test_case_insensitive_script_style_tags() {\n    // Test that script/style tags are matched case-insensitively\n    let html = r\"<html>\n<head>\n  <SCRIPT>console.log('uppercase script');</SCRIPT>\n  <Style>body { margin: 0; }</Style>\n  <ScRiPt>var x = 1;</ScRiPt>\n</head>\n<body>\n  <p>Content</p>\n</body>\n</html>\";\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should have content\n    assert!(result.contains(\"Content\"), \"Should have content\");\n\n    // Should not have script content regardless of case\n    assert!(!result.contains(\"console.log\"), \"Should remove SCRIPT tag content\");\n    assert!(!result.contains(\"margin:\"), \"Should remove STYLE tag content\");\n    assert!(!result.contains(\"var x\"), \"Should remove ScRiPt tag content\");\n}\n\n#[test]\nfn test_performance_large_script() {\n    // Performance test: large script tag shouldn't cause issues\n    let mut html = String::from(r\"<html><head><script>\");\n\n    // Add 1MB of fake content\n    for _ in 0..10000 {\n        html.push_str(\"var data = '<div>fake content</div>'; \");\n    }\n\n    html.push_str(r\"</script></head><body><p>Real content</p></body></html>\");\n\n    println!(\"Testing with {} byte HTML\", html.len());\n\n    let options = ConversionOptions::default();\n    let result = convert(&html, Some(options)).expect(\"Failed to convert\");\n\n    // Should work and extract real content\n    assert!(\n        result.contains(\"Real content\"),\n        \"Should extract body despite large script\"\n    );\n}\n\n#[test]\nfn test_inline_script_attributes_not_affected() {\n    // Inline event handlers should not be affected by script stripping\n    let html = r#\"<html>\n<head>\n  <script>console.log('bad');</script>\n</head>\n<body>\n  <button onclick=\"console.log('click');\">Click me</button>\n  <p>Content</p>\n</body>\n</html>\"#;\n\n    let options = ConversionOptions::default();\n    let result = convert(html, Some(options)).expect(\"Failed to convert\");\n\n    println!(\"Output:\\n{result}\");\n\n    // Should have content and button\n    assert!(result.contains(\"Click me\"), \"Should have button text\");\n    assert!(result.contains(\"Content\"), \"Should have paragraph\");\n\n    // The onclick script inside the tag attribute might appear in output (that's okay)\n    // but the inline script tag content should be gone\n    assert!(\n        !result.contains(\"console.log('bad')\"),\n        \"Should remove script tag content\"\n    );\n}\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/test_spa_bisect.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse std::fs;\nuse std::path::PathBuf;\n\nuse html_to_markdown_rs::ConversionOptions;\n\nfn fixture_path(name: &str) -> PathBuf {\n    [env!(\"CARGO_MANIFEST_DIR\"), \"../../test_documents/html/issues\", name]\n        .iter()\n        .collect()\n}\n\n#[test]\nfn test_spa_first_half() {\n    let html = fs::read_to_string(fixture_path(\"gh-121-minimal-failing.html\")).expect(\"read html\");\n\n    let opts = ConversionOptions {\n        extract_metadata: false,\n        autolinks: false,\n        ..Default::default()\n    };\n\n    let result = convert(&html, Some(opts)).unwrap();\n    eprintln!(\"Result length: {}\", result.len());\n    assert!(!result.is_empty());\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/visitor_code_integration_test.rs",
    "content": "#![allow(missing_docs)]\n#![cfg(feature = \"visitor\")]\n\nuse html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult, VisitorHandle};\nuse html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\nfn convert(\n    html: &str,\n    options: Option<ConversionOptions>,\n    visitor: Option<VisitorHandle>,\n) -> Result<ConversionResult, ConversionError> {\n    let mut opts = options.unwrap_or_default();\n    if visitor.is_some() {\n        opts.visitor = visitor;\n    }\n    html_to_markdown_rs::convert(html, Some(opts))\n}\n\n#[derive(Debug)]\nstruct CodeVisitor {\n    code_blocks: Vec<String>,\n    inline_codes: Vec<String>,\n}\n\nimpl HtmlVisitor for CodeVisitor {\n    fn visit_code_block(&mut self, _ctx: &NodeContext, lang: Option<&str>, code: &str) -> VisitResult {\n        let lang_str = lang.unwrap_or(\"unknown\").to_string();\n        self.code_blocks.push(format!(\"[{}] {}\", lang_str, code.trim()));\n        VisitResult::Custom(format!(\"```{}\\n{}\\n```\", lang.unwrap_or(\"\"), code))\n    }\n\n    fn visit_code_inline(&mut self, _ctx: &NodeContext, code: &str) -> VisitResult {\n        self.inline_codes.push(code.to_string());\n        VisitResult::Custom(format!(\"`{code}`\"))\n    }\n}\n\n#[test]\nfn test_code_block_visitor() {\n    let html = \"<pre><code class=\\\"language-rust\\\">fn main() {}\\n</code></pre>\";\n    let visitor = Rc::new(RefCell::new(CodeVisitor {\n        code_blocks: vec![],\n        inline_codes: vec![],\n    }));\n\n    let result = convert(html, None, Some(visitor.clone()));\n    assert!(result.is_ok());\n\n    let visitor_ref = visitor.borrow();\n    assert_eq!(visitor_ref.code_blocks.len(), 1);\n    assert!(visitor_ref.code_blocks[0].contains(\"rust\"));\n}\n\n#[test]\nfn test_inline_code_visitor() {\n    let html = \"<p>Use <code>println!</code> to print</p>\";\n    let visitor = Rc::new(RefCell::new(CodeVisitor {\n        code_blocks: vec![],\n        inline_codes: vec![],\n    }));\n\n    let result = convert(html, None, Some(visitor.clone()));\n    assert!(result.is_ok());\n\n    let visitor_ref = visitor.borrow();\n    assert_eq!(visitor_ref.inline_codes.len(), 1);\n    assert_eq!(visitor_ref.inline_codes[0], \"println!\");\n}\n\n#[test]\nfn test_code_block_skip() {\n    #[derive(Debug)]\n    struct SkipCodeVisitor;\n\n    impl HtmlVisitor for SkipCodeVisitor {\n        fn visit_code_block(&mut self, _ctx: &NodeContext, _lang: Option<&str>, _code: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n\n    let html = \"<pre><code>skipped code</code></pre>\";\n    let visitor = Rc::new(RefCell::new(SkipCodeVisitor));\n\n    let result = convert(html, None, Some(visitor));\n    assert!(result.is_ok());\n    let markdown = result.unwrap().content.unwrap_or_default();\n    assert!(!markdown.contains(\"skipped code\"));\n}\n\n#[test]\nfn test_code_block_language_detection() {\n    let html_patterns = vec![\n        (\n            \"<pre class=\\\"language-python\\\"><code>print('hi')</code></pre>\",\n            \"python\",\n        ),\n        (\n            \"<pre class=\\\"lang-javascript\\\"><code>console.log('hi')</code></pre>\",\n            \"javascript\",\n        ),\n        (\"<pre><code>no language</code></pre>\", \"unknown\"),\n    ];\n\n    for (html, expected_lang) in html_patterns {\n        let visitor = Rc::new(RefCell::new(CodeVisitor {\n            code_blocks: vec![],\n            inline_codes: vec![],\n        }));\n\n        let result = convert(html, None, Some(visitor.clone()));\n        assert!(result.is_ok(), \"Failed to convert: {html}\");\n\n        let visitor_ref = visitor.borrow();\n        assert_eq!(visitor_ref.code_blocks.len(), 1);\n        if expected_lang != \"unknown\" {\n            assert!(visitor_ref.code_blocks[0].starts_with(&format!(\"[{expected_lang}]\")));\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/visitor_integration_test.rs",
    "content": "#![allow(missing_docs)]\n\n//! Integration tests for the visitor pattern\n//!\n//! These tests verify that visitor callbacks are properly invoked during\n//! HTML→Markdown conversion and that all `VisitResult` variants work correctly.\n\n#![cfg(feature = \"visitor\")]\n\nuse html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult, VisitorHandle};\nuse html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};\nuse std::cell::RefCell;\nuse std::rc::Rc;\n\n/// Test shim that bridges the legacy 3-arg call shape used throughout this file\n/// onto the public 2-arg `convert(html, options)` API. The visitor (if any) is\n/// folded into `options.visitor`.\nfn convert(\n    html: &str,\n    options: Option<ConversionOptions>,\n    visitor: Option<VisitorHandle>,\n) -> Result<ConversionResult, ConversionError> {\n    let mut opts = options.unwrap_or_default();\n    if visitor.is_some() {\n        opts.visitor = visitor;\n    }\n    html_to_markdown_rs::convert(html, Some(opts))\n}\n\n/// Test visitor that customizes all output\n#[derive(Debug, Default)]\nstruct CustomizingVisitor;\n\nimpl HtmlVisitor for CustomizingVisitor {\n    fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n        VisitResult::Custom(format!(\"[TEXT:{text}]\"))\n    }\n\n    fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {\n        VisitResult::Custom(format!(\"[LINK:{text} -> {href}]\"))\n    }\n\n    fn visit_image(&mut self, _ctx: &NodeContext, src: &str, alt: &str, _title: Option<&str>) -> VisitResult {\n        VisitResult::Custom(format!(\"[IMAGE:{alt} @ {src}]\"))\n    }\n\n    fn visit_heading(&mut self, _ctx: &NodeContext, level: u32, text: &str, _id: Option<&str>) -> VisitResult {\n        VisitResult::Custom(format!(\"[H{level}: {text}]\"))\n    }\n}\n\n/// Test visitor that skips certain elements\n#[derive(Debug, Default)]\nstruct SkippingVisitor {\n    skip_images: bool,\n    skip_links: bool,\n}\n\nimpl HtmlVisitor for SkippingVisitor {\n    fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n        if self.skip_links {\n            VisitResult::Skip\n        } else {\n            VisitResult::Continue\n        }\n    }\n\n    fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n        if self.skip_images {\n            VisitResult::Skip\n        } else {\n            VisitResult::Continue\n        }\n    }\n}\n\n/// Test visitor that preserves HTML for certain elements\n#[derive(Debug, Default)]\nstruct PreservingVisitor {\n    preserve_links: bool,\n}\n\nimpl HtmlVisitor for PreservingVisitor {\n    fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n        if self.preserve_links {\n            VisitResult::PreserveHtml\n        } else {\n            VisitResult::Continue\n        }\n    }\n}\n\n/// Test visitor that validates node context\n#[derive(Debug, Default)]\nstruct ContextCheckingVisitor {\n    saw_heading_with_id: bool,\n}\n\nimpl HtmlVisitor for ContextCheckingVisitor {\n    fn visit_heading(&mut self, ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n        assert_eq!(ctx.node_type, NodeType::Heading);\n        assert_eq!(ctx.tag_name, \"h1\");\n\n        if ctx.attributes.contains_key(\"id\") {\n            self.saw_heading_with_id = true;\n        }\n\n        VisitResult::Continue\n    }\n}\n\n#[test]\nfn test_custom_visitor_transforms_text() {\n    let html = r\"<p>Hello world</p>\";\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(result.contains(\"[TEXT:\"), \"Should contain custom text format\");\n}\n\n#[test]\nfn test_custom_visitor_transforms_links() {\n    let html = r#\"<a href=\"https://example.com\">Example</a>\"#;\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[LINK:Example -> https://example.com]\"),\n        \"Should contain custom link format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_custom_visitor_transforms_images() {\n    let html = r#\"<img src=\"/test.png\" alt=\"Test\">\"#;\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[IMAGE:Test @ /test.png]\"),\n        \"Should contain custom image format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_custom_visitor_transforms_headings() {\n    let html = r\"<h2>My Heading</h2>\";\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[H2: My Heading]\"),\n        \"Should contain custom heading format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_skipping_visitor_removes_links() {\n    let html = r#\"<p>Text with <a href=\"https://example.com\">a link</a> inside.</p>\"#;\n    let visitor = Rc::new(RefCell::new(SkippingVisitor {\n        skip_links: true,\n        skip_images: false,\n    }));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        !result.contains(\"example.com\"),\n        \"Should not contain link URL when skipped, got: {result}\"\n    );\n}\n\n#[test]\nfn test_skipping_visitor_removes_images() {\n    let html = r#\"<p>Text <img src=\"/test.png\" alt=\"Test\"> more text</p>\"#;\n    let visitor = Rc::new(RefCell::new(SkippingVisitor {\n        skip_links: false,\n        skip_images: true,\n    }));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        !result.contains(\"test.png\") && !result.contains(\"![\"),\n        \"Should not contain image when skipped, got: {result}\"\n    );\n}\n\n#[test]\nfn test_preserving_visitor_keeps_html() {\n    let html = r#\"<a href=\"https://example.com\" class=\"special\">Example</a>\"#;\n    let visitor = Rc::new(RefCell::new(PreservingVisitor { preserve_links: true }));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"<a\") && result.contains(\"href\"),\n        \"Should preserve HTML tags when PreserveHtml is returned, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_receives_node_context() {\n    let html = r#\"<h1 id=\"title\" class=\"main\">Title</h1>\"#;\n    let visitor = Rc::new(RefCell::new(ContextCheckingVisitor::default()));\n\n    let _result = convert(html, None, Some(visitor)).expect(\"conversion failed\");\n}\n\n#[test]\nfn test_visitor_works_with_complex_document() {\n    let html = r#\"\n        <!DOCTYPE html>\n        <html>\n        <head><title>Test</title></head>\n        <body>\n            <h1>Main Title</h1>\n            <p>Introduction with <a href=\"/link\">a link</a>.</p>\n            <h2>Section</h2>\n            <p>Text with <strong>bold</strong> and <em>italic</em>.</p>\n            <img src=\"/image.png\" alt=\"Diagram\">\n            <h3>Subsection</h3>\n            <p>More content.</p>\n        </body>\n        </html>\n    \"#;\n\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(result.contains(\"[H1:\"));\n    assert!(result.contains(\"[H2:\"));\n    assert!(result.contains(\"[H3:\"));\n    assert!(result.contains(\"[LINK:\"));\n    assert!(result.contains(\"[IMAGE:\"));\n    assert!(result.contains(\"[TEXT:\"));\n}\n\n#[test]\nfn test_visitor_with_conversion_options() {\n    #[derive(Debug, Default)]\n    struct ContinueVisitor;\n\n    impl HtmlVisitor for ContinueVisitor {}\n\n    let html = r\"<h1>Title</h1><p>Text with *asterisks* and _underscores_.</p>\";\n\n    let options = ConversionOptions {\n        escape_asterisks: true,\n        escape_underscores: true,\n        ..Default::default()\n    };\n\n    let visitor = Rc::new(RefCell::new(ContinueVisitor));\n\n    let result = convert(html, Some(options), Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(r\"\\*\") || result.contains(r\"\\_\"),\n        \"Should respect escape options with visitor, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_continue_result_produces_default_markdown() {\n    #[derive(Debug, Default)]\n    struct ContinueVisitor;\n\n    impl HtmlVisitor for ContinueVisitor {\n        fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Continue\n        }\n    }\n\n    let html = r\"<h1>Title</h1>\";\n    let visitor = Rc::new(RefCell::new(ContinueVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"# Title\"),\n        \"Continue should produce default markdown, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_skip_vs_continue() {\n    #[derive(Debug)]\n    struct SelectiveSkipper {\n        skip_first_link: bool,\n    }\n\n    impl HtmlVisitor for SelectiveSkipper {\n        fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            if self.skip_first_link {\n                self.skip_first_link = false;\n                VisitResult::Skip\n            } else {\n                VisitResult::Continue\n            }\n        }\n    }\n\n    let html = r#\"<p><a href=\"/first\">First</a> and <a href=\"/second\">Second</a></p>\"#;\n    let visitor = Rc::new(RefCell::new(SelectiveSkipper { skip_first_link: true }));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(!result.contains(\"/first\"));\n    assert!(result.contains(\"/second\"));\n}\n\n#[test]\nfn test_multiple_elements_of_same_type() {\n    let html = r\"<h1>First</h1><h2>Second</h2><h3>Third</h3>\";\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(result.contains(\"[H1: First]\"));\n    assert!(result.contains(\"[H2: Second]\"));\n    assert!(result.contains(\"[H3: Third]\"));\n}\n\n#[test]\nfn test_nested_elements_invoke_visitor() {\n    let html = r#\"<p>Text with <a href=\"/url\">a <strong>bold</strong> link</a></p>\"#;\n    let visitor = Rc::new(RefCell::new(CustomizingVisitor));\n\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(result.contains(\"[TEXT:\"));\n    assert!(result.contains(\"[LINK:\"));\n}\n\n#[test]\nfn test_visitor_error_stops_conversion() {\n    #[derive(Debug, Default)]\n    struct ErrorVisitor;\n\n    impl HtmlVisitor for ErrorVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Error(\"test error\".to_string())\n        }\n    }\n\n    let html = \"<p>text</p>\";\n    let visitor = Rc::new(RefCell::new(ErrorVisitor));\n    let result = convert(html, None, Some(visitor));\n\n    assert!(result.is_err(), \"Should return error when visitor returns Error\");\n    assert!(\n        result.unwrap_err().to_string().contains(\"test error\"),\n        \"Error message should contain visitor's error\"\n    );\n}\n\n#[test]\nfn test_visitor_code_block() {\n    #[derive(Debug, Default)]\n    struct CodeBlockVisitor;\n\n    impl HtmlVisitor for CodeBlockVisitor {\n        fn visit_code_block(&mut self, _ctx: &NodeContext, language: Option<&str>, code: &str) -> VisitResult {\n            let lang = language.unwrap_or(\"text\");\n            VisitResult::Custom(format!(\"[CODE_BLOCK:{} -> {}]\", lang, code.trim()))\n        }\n    }\n\n    let html = r#\"<pre><code class=\"language-rust\">fn main() {}</code></pre>\"#;\n    let visitor = Rc::new(RefCell::new(CodeBlockVisitor));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[CODE_BLOCK:rust -> fn main() {}]\"),\n        \"Should contain custom code block format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_code_inline() {\n    #[derive(Debug, Default)]\n    struct InlineCodeVisitor;\n\n    impl HtmlVisitor for InlineCodeVisitor {\n        fn visit_code_inline(&mut self, _ctx: &NodeContext, code: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[CODE:{code}]\"))\n        }\n    }\n\n    let html = r\"<p>Use <code>println!</code> macro</p>\";\n    let visitor = Rc::new(RefCell::new(InlineCodeVisitor));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[CODE:println!]\"),\n        \"Should contain custom inline code format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_list_callbacks() {\n    #[derive(Debug, Default)]\n    struct ListVisitor {\n        list_depth: usize,\n    }\n\n    impl HtmlVisitor for ListVisitor {\n        fn visit_list_start(&mut self, _ctx: &NodeContext, ordered: bool) -> VisitResult {\n            self.list_depth += 1;\n            VisitResult::Custom(format!(\n                \"[LIST_START:{}:{}]\",\n                if ordered { \"OL\" } else { \"UL\" },\n                self.list_depth\n            ))\n        }\n\n        fn visit_list_item(&mut self, _ctx: &NodeContext, _ordered: bool, _marker: &str, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[LI:{}:{}]\", self.list_depth, text.trim()))\n        }\n\n        fn visit_list_end(&mut self, _ctx: &NodeContext, _ordered: bool, _output: &str) -> VisitResult {\n            let result = VisitResult::Custom(format!(\"[LIST_END:{}]\", self.list_depth));\n            self.list_depth = self.list_depth.saturating_sub(1);\n            result\n        }\n    }\n\n    let html = r\"<ul><li>First</li><li>Second</li></ul>\";\n    let visitor = Rc::new(RefCell::new(ListVisitor::default()));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[LIST_START:UL:1]\"),\n        \"Should see list start, got: {result}\"\n    );\n    assert!(result.contains(\"[LI:1:First]\"), \"Should see first item, got: {result}\");\n    assert!(\n        result.contains(\"[LI:1:Second]\"),\n        \"Should see second item, got: {result}\"\n    );\n    assert!(result.contains(\"[LIST_END:1]\"), \"Should see list end, got: {result}\");\n}\n\n#[test]\nfn test_visitor_table_callbacks() {\n    #[derive(Debug, Default)]\n    struct TableVisitor {\n        row_count: usize,\n    }\n\n    impl HtmlVisitor for TableVisitor {\n        fn visit_table_start(&mut self, _ctx: &NodeContext) -> VisitResult {\n            self.row_count = 0;\n            VisitResult::Custom(\"[TABLE_START]\".to_string())\n        }\n\n        fn visit_table_row(&mut self, _ctx: &NodeContext, cells: &[String], is_header: bool) -> VisitResult {\n            self.row_count += 1;\n            VisitResult::Custom(format!(\n                \"[ROW:{}:{}:{}]\",\n                if is_header { \"HEADER\" } else { \"DATA\" },\n                self.row_count,\n                cells.join(\"|\")\n            ))\n        }\n\n        fn visit_table_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[TABLE_END:{}]\", self.row_count))\n        }\n    }\n\n    let html = r\"<table><tr><th>Name</th><th>Age</th></tr><tr><td>Alice</td><td>30</td></tr></table>\";\n    let visitor = Rc::new(RefCell::new(TableVisitor::default()));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[TABLE_START]\"),\n        \"Should see table start, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[ROW:HEADER:1:Name|Age]\"),\n        \"Should see header row, got: {result}\"\n    );\n    assert!(\n        result.contains(\"[ROW:DATA:2:Alice|30]\"),\n        \"Should see data row, got: {result}\"\n    );\n    assert!(result.contains(\"[TABLE_END:2]\"), \"Should see table end, got: {result}\");\n}\n\n#[test]\nfn test_visitor_blockquote() {\n    #[derive(Debug, Default)]\n    struct BlockquoteVisitor;\n\n    impl HtmlVisitor for BlockquoteVisitor {\n        fn visit_blockquote(&mut self, _ctx: &NodeContext, content: &str, _depth: usize) -> VisitResult {\n            VisitResult::Custom(format!(\"[QUOTE:{}]\", content.trim()))\n        }\n    }\n\n    let html = r\"<blockquote>This is a quote</blockquote>\";\n    let visitor = Rc::new(RefCell::new(BlockquoteVisitor));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(\n        result.contains(\"[QUOTE:This is a quote]\"),\n        \"Should contain custom blockquote format, got: {result}\"\n    );\n}\n\n#[test]\nfn test_visitor_inline_formatting() {\n    #[derive(Debug, Default)]\n    struct FormattingVisitor;\n\n    impl HtmlVisitor for FormattingVisitor {\n        fn visit_strong(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[STRONG:{text}]\"))\n        }\n\n        fn visit_emphasis(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[EM:{text}]\"))\n        }\n\n        fn visit_strikethrough(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[DEL:{text}]\"))\n        }\n    }\n\n    let html = r\"<p><strong>bold</strong> <em>italic</em> <del>struck</del></p>\";\n    let visitor = Rc::new(RefCell::new(FormattingVisitor));\n    let result = convert(html, None, Some(visitor))\n        .expect(\"conversion failed\")\n        .content\n        .unwrap_or_default();\n\n    assert!(result.contains(\"[STRONG:bold]\"), \"Should see strong, got: {result}\");\n    assert!(result.contains(\"[EM:italic]\"), \"Should see emphasis, got: {result}\");\n    assert!(\n        result.contains(\"[DEL:struck]\"),\n        \"Should see strikethrough, got: {result}\"\n    );\n}\n\n#[test]\nfn test_no_double_visit_in_links() {\n    #[derive(Debug, Default)]\n    struct CountingVisitor {\n        text_visits: usize,\n    }\n\n    impl HtmlVisitor for CountingVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            self.text_visits += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"<a href=\"/url\">link text</a>\"#;\n    let visitor = Rc::new(RefCell::new(CountingVisitor::default()));\n    let _result = convert(html, None, Some(visitor.clone())).expect(\"conversion failed\");\n\n    assert_eq!(\n        visitor.borrow().text_visits,\n        1,\n        \"Text nodes inside links should only be visited once, got {} visits\",\n        visitor.borrow().text_visits\n    );\n}\n\n#[test]\nfn test_no_double_visit_in_headings() {\n    #[derive(Debug, Default)]\n    struct CountingVisitor {\n        text_visits: usize,\n    }\n\n    impl HtmlVisitor for CountingVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            self.text_visits += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Continue\n        }\n    }\n\n    let html = r\"<h1>heading text</h1>\";\n    let visitor = Rc::new(RefCell::new(CountingVisitor::default()));\n    let _result = convert(html, None, Some(visitor.clone())).expect(\"conversion failed\");\n\n    assert_eq!(\n        visitor.borrow().text_visits,\n        1,\n        \"Text nodes inside headings should only be visited once, got {} visits\",\n        visitor.borrow().text_visits\n    );\n}\n\n// ============================================================================\n// Integration tests: Visitor + Feature combinations\n// ============================================================================\n\n/// Test that visitor callbacks work correctly when `skip_images` option is enabled\n#[test]\nfn test_visitor_with_skip_images() {\n    #[derive(Debug, Default)]\n    struct SkipImageVisitor {\n        image_visits: usize,\n    }\n\n    impl HtmlVisitor for SkipImageVisitor {\n        fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            self.image_visits += 1;\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"\n        <p>Some text</p>\n        <img src=\"/image1.png\" alt=\"Image 1\">\n        <img src=\"/image2.png\" alt=\"Image 2\">\n        <p>More text</p>\n    \"#;\n\n    // Test with skip_images enabled and visitor\n    let options = ConversionOptions {\n        skip_images: true,\n        ..Default::default()\n    };\n\n    let visitor = Rc::new(RefCell::new(SkipImageVisitor::default()));\n    let result = convert(html, Some(options), Some(visitor))\n        .expect(\"conversion with skip_images and visitor should succeed\")\n        .content\n        .unwrap_or_default();\n\n    // When skip_images is true, images should not appear in output\n    assert!(\n        !result.contains(\"![\"),\n        \"skip_images should prevent image markdown in output, got: {result}\"\n    );\n    assert!(\n        !result.contains(\"image1.png\"),\n        \"skip_images should prevent image src in output, got: {result}\"\n    );\n\n    // When skip_images is true, the conversion still happens correctly\n    // Images are filtered at the conversion level based on the option\n    // This verifies that skip_images option and visitor parameters work together\n    // without conflicts - both are optional and can be combined\n    assert!(\n        result.contains(\"Some text\") && result.contains(\"More text\"),\n        \"Other content should still be present in output, got: {result}\"\n    );\n}\n\n/// Test that the main `convert()` function accepts optional visitor parameter\n#[test]\nfn test_convert_accepts_visitor_parameter() {\n    #[derive(Debug, Default)]\n    struct CountingVisitor {\n        text_count: usize,\n        link_count: usize,\n    }\n\n    impl HtmlVisitor for CountingVisitor {\n        fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {\n            self.text_count += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            self.link_count += 1;\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"<p>Visit <a href=\"https://example.com\">our site</a> for more info.</p>\"#;\n    let visitor = Rc::new(RefCell::new(CountingVisitor::default()));\n\n    // Test using the main convert() function with visitor parameter\n    let _result = convert(html, None, Some(visitor.clone())).expect(\"convert with visitor should work\");\n\n    let borrowed = visitor.borrow();\n    assert!(\n        borrowed.text_count >= 2,\n        \"Should visit text nodes, got {} visits\",\n        borrowed.text_count\n    );\n    assert_eq!(\n        borrowed.link_count, 1,\n        \"Should visit exactly 1 link, got {}\",\n        borrowed.link_count\n    );\n}\n\n/// Test visitor + `inline_images` feature combination\n///\n/// In v3, `convert()` handles inline-image extraction via `ConversionResult.images`,\n/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths\n/// work on the same HTML.\n#[cfg(feature = \"inline-images\")]\n#[test]\nfn test_convert_with_inline_images_accepts_visitor() {\n    #[derive(Debug, Default)]\n    struct ImageTrackingVisitor {\n        images_seen: usize,\n    }\n\n    impl HtmlVisitor for ImageTrackingVisitor {\n        fn visit_image(&mut self, _ctx: &NodeContext, src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            if !src.starts_with(\"data:\") {\n                self.images_seen += 1;\n            }\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"\n        <h1>Test Page</h1>\n        <img src=\"/image.png\" alt=\"Test Image\">\n        <p>Some content</p>\n    \"#;\n\n    // Verify visitor callbacks fire via convert_with_visitor\n    let visitor = Rc::new(RefCell::new(ImageTrackingVisitor::default()));\n    let markdown = convert(html, None, Some(visitor.clone()))\n        .expect(\"convert should work\")\n        .content\n        .unwrap_or_default();\n\n    assert_eq!(\n        visitor.borrow().images_seen,\n        1,\n        \"Visitor should count 1 non-data-uri image\"\n    );\n\n    // Markdown should still be generated\n    assert!(!markdown.is_empty(), \"Should produce markdown output\");\n}\n\n/// Test visitor + metadata: visitor callbacks fire and metadata is collected.\n///\n/// In v3, `convert()` always extracts metadata into `ConversionResult.metadata`,\n/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths\n/// work on the same HTML.\n#[cfg(feature = \"metadata\")]\n#[test]\nfn test_visitor_and_metadata_both_work() {\n    #[derive(Debug, Default)]\n    struct MetadataAwareVisitor {\n        heading_count: usize,\n        link_count: usize,\n    }\n\n    impl HtmlVisitor for MetadataAwareVisitor {\n        fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            self.heading_count += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            self.link_count += 1;\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"\n        <html>\n        <head><title>Test Page</title></head>\n        <body>\n            <h1>Main Title</h1>\n            <p>Visit <a href=\"https://example.com\">our site</a>.</p>\n            <h2>Section</h2>\n            <p>More <a href=\"/page\">links</a> here.</p>\n        </body>\n        </html>\n    \"#;\n\n    // Verify visitor callbacks fire via convert_with_visitor\n    let visitor = Rc::new(RefCell::new(MetadataAwareVisitor::default()));\n    let markdown = convert(html, None, Some(visitor.clone()))\n        .expect(\"convert should work\")\n        .content\n        .unwrap_or_default();\n\n    let borrowed = visitor.borrow();\n    assert!(\n        borrowed.heading_count >= 2,\n        \"Visitor should see at least 2 headings, got {}\",\n        borrowed.heading_count\n    );\n    assert_eq!(\n        borrowed.link_count, 2,\n        \"Visitor should see 2 links, got {}\",\n        borrowed.link_count\n    );\n    assert!(!markdown.is_empty(), \"Should produce markdown output\");\n    drop(borrowed);\n\n    // Verify metadata extraction via convert()\n    let result = html_to_markdown_rs::convert(html, None).expect(\"convert should work\");\n    let metadata = result.metadata;\n\n    assert_eq!(\n        metadata.document.title,\n        Some(\"Test Page\".to_string()),\n        \"Metadata should extract title\"\n    );\n    assert!(\n        metadata.headers.len() >= 2,\n        \"Metadata should extract at least 2 headers, got {}\",\n        metadata.headers.len()\n    );\n    assert_eq!(\n        metadata.links.len(),\n        2,\n        \"Metadata should extract 2 links, got {}\",\n        metadata.links.len()\n    );\n}\n\n/// Test visitor + both `inline_images` and `metadata` features together\n///\n/// In v3, `convert()` handles metadata and inline-image extraction via `ConversionResult`,\n/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths\n/// work on the same HTML.\n#[cfg(all(feature = \"inline-images\", feature = \"metadata\"))]\n#[test]\nfn test_convert_with_all_features_and_visitor() {\n    #[derive(Debug, Default)]\n    struct ComprehensiveVisitor {\n        headings: usize,\n        images: usize,\n        links: usize,\n    }\n\n    impl HtmlVisitor for ComprehensiveVisitor {\n        fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            self.headings += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            self.images += 1;\n            VisitResult::Continue\n        }\n\n        fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            self.links += 1;\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"\n        <html>\n        <body>\n            <h1>Gallery</h1>\n            <img src=\"/gallery/image1.jpg\" alt=\"Pic 1\">\n            <p>See <a href=\"/more\">more</a> content.</p>\n            <h2>Details</h2>\n            <img src=\"/gallery/image2.jpg\" alt=\"Pic 2\">\n            <p>Check <a href=\"/details\">this link</a>.</p>\n        </body>\n        </html>\n    \"#;\n\n    // Verify visitor callbacks fire via convert_with_visitor\n    let visitor = Rc::new(RefCell::new(ComprehensiveVisitor::default()));\n    let markdown = convert(html, None, Some(visitor.clone()))\n        .expect(\"convert should work\")\n        .content\n        .unwrap_or_default();\n\n    // Verify all visitor callbacks were invoked\n    let borrowed = visitor.borrow();\n    assert!(\n        borrowed.headings >= 2,\n        \"Visitor should see at least 2 headings, got {}\",\n        borrowed.headings\n    );\n    assert_eq!(\n        borrowed.images, 2,\n        \"Visitor should see 2 images, got {}\",\n        borrowed.images\n    );\n    assert_eq!(borrowed.links, 2, \"Visitor should see 2 links, got {}\", borrowed.links);\n    drop(borrowed);\n\n    // Verify markdown was produced\n    assert!(!markdown.is_empty(), \"Should produce markdown output\");\n}\n\n/// Regression test: image visitor returning Custom with metadata extraction used to panic\n/// with an out-of-bounds slice.\n///\n/// When metadata extraction prepends a YAML frontmatter block to `output`, every element's\n/// saved `element_output_start` is offset by the frontmatter length.  If a child visitor\n/// then returns Custom and truncates the buffer, the parent's saved offset can point\n/// past `output.len()`.\n#[test]\nfn test_image_visitor_with_metadata_does_not_panic() {\n    #[derive(Debug)]\n    struct ImageVisitor;\n\n    impl HtmlVisitor for ImageVisitor {\n        fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"![img](rewritten.png)\".to_string())\n        }\n    }\n\n    let html = r#\"<html><head><meta name=\"description\" content=\"x\"></head><body><p><img src=\"a.png\" alt=\"a\"></p></body></html>\"#;\n    let options = ConversionOptions {\n        extract_metadata: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options), Some(Rc::new(RefCell::new(ImageVisitor))));\n    assert!(result.is_ok(), \"conversion panicked or errored: {:?}\", result.err());\n}\n\n/// Regression test: `visit_element_end` returning Custom/Skip with metadata extraction used\n/// to produce stale parent offsets and either panic or silently drop subsequent content.\n#[test]\nfn test_element_end_replacement_with_metadata_preserves_subsequent_content() {\n    #[derive(Debug)]\n    struct FigureReplacingVisitor;\n\n    impl HtmlVisitor for FigureReplacingVisitor {\n        fn visit_element_end(&mut self, ctx: &NodeContext, _content: &str) -> VisitResult {\n            if ctx.tag_name == \"figure\" {\n                return VisitResult::Custom(\"[figure]\".to_string());\n            }\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"<html><head><meta name=\"description\" content=\"x\"></head><body><figure><img src=\"a.png\"></figure><p>after</p></body></html>\"#;\n    let options = ConversionOptions {\n        extract_metadata: true,\n        ..Default::default()\n    };\n\n    let result = convert(html, Some(options), Some(Rc::new(RefCell::new(FigureReplacingVisitor))));\n    assert!(result.is_ok(), \"conversion panicked or errored: {:?}\", result.err());\n    assert!(\n        result.unwrap().content.unwrap_or_default().contains(\"after\"),\n        \"content after replaced element should not be lost\"\n    );\n}\n\n/// Regression test for issue #331: visitor receives mismatched start/end events for\n/// hyphenated tag names that contain XML-style self-closing children.\n///\n/// When `<ac:parameter ac:name=\"foo\" />` appears inside a hyphenated custom element, the\n/// `repair_with_html5ever` fallback (triggered because the outer tag contains a hyphen) used\n/// to re-parse with HTML5 semantics.  HTML5 does NOT honour XML-style self-closing on unknown\n/// elements, so `<ac:parameter ... />` was treated as an open tag and subsequent siblings were\n/// nested inside it.  That caused `visit_element_start(\"ac:parameter\")` for \"foo\" to be\n/// followed by `visit_element_start(\"ac:parameter\")` for \"quux\", then both ends in reversed\n/// order — violating the expected pre-order/post-order pairing.\n#[test]\nfn test_issue_331_hyphenated_tags_xml_self_closing_visitor_events() {\n    #[derive(Debug, Default)]\n    struct EventRecorder {\n        events: Vec<String>,\n    }\n\n    impl HtmlVisitor for EventRecorder {\n        fn visit_element_start(&mut self, ctx: &NodeContext) -> VisitResult {\n            self.events.push(format!(\"start({})\", ctx.tag_name));\n            VisitResult::Continue\n        }\n\n        fn visit_element_end(&mut self, ctx: &NodeContext, _output: &str) -> VisitResult {\n            self.events.push(format!(\"end({})\", ctx.tag_name));\n            VisitResult::Continue\n        }\n    }\n\n    let html = r#\"\n<structured-macro>\n  <ac:parameter ac:name=\"foo\" />\n  <ac:parameter ac:name=\"quux\">lalaland</ac:parameter>\n</structured-macro>\n\"#;\n\n    let visitor = Rc::new(RefCell::new(EventRecorder::default()));\n    let result = convert(html, None, Some(visitor.clone()));\n    assert!(result.is_ok(), \"conversion should succeed: {:?}\", result.err());\n\n    let events = visitor.borrow().events.clone();\n\n    // Find the indices of start/end pairs for the two ac:parameter elements.\n    // With correct XML self-closing handling:\n    //   start(ac:parameter)[foo] → end(ac:parameter)[foo] → start(ac:parameter)[quux] → end(ac:parameter)[quux]\n    // With the bug (html5ever treats `/>` as open tag):\n    //   start(ac:parameter)[foo] → start(ac:parameter)[quux] → end(ac:parameter)[quux] → end(ac:parameter)[foo]\n\n    // Collect positions of start/end events for ac:parameter\n    let ac_param_starts: Vec<usize> = events\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.starts_with(\"start(ac:parameter)\"))\n        .map(|(i, _)| i)\n        .collect();\n    let ac_param_ends: Vec<usize> = events\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.starts_with(\"end(ac:parameter)\"))\n        .map(|(i, _)| i)\n        .collect();\n\n    assert_eq!(\n        ac_param_starts.len(),\n        2,\n        \"expected exactly 2 ac:parameter start events, got: {events:?}\"\n    );\n    assert_eq!(\n        ac_param_ends.len(),\n        2,\n        \"expected exactly 2 ac:parameter end events, got: {events:?}\"\n    );\n\n    // Each start must come before the corresponding end: start[0] < end[0] < start[1] < end[1]\n    assert!(\n        ac_param_starts[0] < ac_param_ends[0],\n        \"first ac:parameter: start must precede end (got start@{}, end@{}); events: {events:?}\",\n        ac_param_starts[0],\n        ac_param_ends[0],\n    );\n    assert!(\n        ac_param_ends[0] < ac_param_starts[1],\n        \"first ac:parameter end must precede second ac:parameter start (got end@{}, start@{}); events: {events:?}\",\n        ac_param_ends[0],\n        ac_param_starts[1],\n    );\n    assert!(\n        ac_param_starts[1] < ac_param_ends[1],\n        \"second ac:parameter: start must precede end (got start@{}, end@{}); events: {events:?}\",\n        ac_param_starts[1],\n        ac_param_ends[1],\n    );\n}\n"
  },
  {
    "path": "crates/html-to-markdown/tests/xml_tables_test.rs",
    "content": "#![allow(missing_docs)]\n\nfn convert(\n    html: &str,\n    opts: Option<html_to_markdown_rs::ConversionOptions>,\n) -> html_to_markdown_rs::error::Result<String> {\n    html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())\n}\n\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_basic_row_and_cell_conversion() {\n    let html = r\"<table>\n    <row><cell>Header 1</cell><cell>Header 2</cell></row>\n    <row><cell>Cell 1</cell><cell>Cell 2</cell></row>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header 1 | Header 2 |\"));\n    assert!(result.contains(\"| Cell 1 | Cell 2 |\"));\n}\n\n#[test]\nfn test_cell_role_head_as_table_header() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Column 1</cell><cell role=\"head\">Column 2</cell></row>\n    <row><cell>Data 1</cell><cell>Data 2</cell></row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Column 1 | Column 2 |\"));\n    assert!(result.contains(\"| Data 1 | Data 2 |\"));\n    assert!(result.contains(\"| --- | --- |\"));\n}\n\n#[test]\nfn test_mixed_html_and_xml_elements() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Name</cell><cell role=\"head\">Age</cell></row>\n    <row><td><strong>John</strong></td><td>25</td></row>\n    <row><cell><em>Jane</em></cell><cell>30</cell></row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Name | Age |\"));\n    assert!(result.contains(\"**John**\"));\n    assert!(result.contains(\"*Jane*\"));\n}\n\n#[test]\nfn test_tei_cols_and_rows_attributes() {\n    let html = r#\"<table cols=\"2\" rows=\"3\">\n    <row><cell>Header 1</cell><cell>Header 2</cell></row>\n    <row><cell>Cell 1</cell><cell>Cell 2</cell></row>\n    <row><cell>Cell 3</cell><cell>Cell 4</cell></row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header 1 | Header 2 |\"));\n    assert!(result.contains(\"| Cell 1 | Cell 2 |\"));\n    assert!(result.contains(\"| Cell 3 | Cell 4 |\"));\n}\n\n#[test]\nfn test_graphic_element_with_xlink_href() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\">Image</cell>\n        <cell role=\"head\">Description</cell>\n    </row>\n    <row>\n        <cell><graphic xlink:href=\"image.png\"/></cell>\n        <cell>A test image</cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Image | Description |\"));\n    assert!(result.contains(\"| A test image |\"));\n    // graphic element handling may vary based on implementation\n}\n\n#[test]\nfn test_graphic_in_table_cells() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\">Figure</cell>\n    </row>\n    <row>\n        <cell><graphic url=\"diagram.svg\" alt=\"System Diagram\"/></cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Figure |\"));\n}\n\n#[test]\nfn test_empty_cells_xml() {\n    let html = r\"<table>\n    <row><cell>Data</cell><cell></cell></row>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Data |  |\"));\n}\n\n#[test]\nfn test_nested_content_in_cells() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Text</cell><cell role=\"head\">Formatted</cell></row>\n    <row>\n        <cell>Plain text</cell>\n        <cell><b>Bold</b> and <a href=\"http://example.com\">Link</a></cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Text | Formatted |\"));\n    assert!(result.contains(\"| Plain text |\"));\n    assert!(result.contains(\"**Bold**\"));\n    assert!(result.contains(\"[Link](http://example.com)\"));\n}\n\n#[test]\nfn test_mixed_tr_and_row_in_same_table() {\n    let html = r\"<table>\n    <tr><th>Col 1</th><th>Col 2</th></tr>\n    <row><cell>Data 1</cell><cell>Data 2</cell></row>\n    <tr><td>Data 3</td><td>Data 4</td></tr>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Col 1 | Col 2 |\"));\n    assert!(result.contains(\"| Data 1 | Data 2 |\"));\n    assert!(result.contains(\"| Data 3 | Data 4 |\"));\n}\n\n#[test]\nfn test_cell_without_role_attribute_defaults_to_data() {\n    let html = r\"<table>\n    <row><cell>Header</cell></row>\n    <row><cell>Data Cell</cell></row>\n    </table>\";\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header |\"));\n    assert!(result.contains(\"| Data Cell |\"));\n}\n\n#[test]\nfn test_xml_table_with_multiline_content() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Content</cell></row>\n    <row>\n        <cell>\n            <p>Line 1</p>\n            <p>Line 2</p>\n        </cell>\n    </row>\n    </table>\"#;\n\n    let options = ConversionOptions {\n        br_in_tables: true,\n        ..Default::default()\n    };\n    let result = convert(html, Some(options)).unwrap();\n\n    assert!(result.contains(\"| Content |\"));\n    assert!(result.contains(\"Line 1\"));\n    assert!(result.contains(\"Line 2\"));\n}\n\n#[test]\nfn test_cell_with_lists() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Items</cell></row>\n    <row>\n        <cell>\n            <ul>\n                <li>Item 1</li>\n                <li>Item 2</li>\n            </ul>\n        </cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Items |\"));\n    assert!(result.contains(\"Item 1\"));\n    assert!(result.contains(\"Item 2\"));\n}\n\n#[test]\nfn test_single_column_xml_table() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Header</cell></row>\n    <row><cell>Data 1</cell></row>\n    <row><cell>Data 2</cell></row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header |\"));\n    assert!(result.contains(\"| --- |\"));\n    assert!(result.contains(\"| Data 1 |\"));\n    assert!(result.contains(\"| Data 2 |\"));\n}\n\n#[test]\nfn test_cell_with_code_blocks() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Code</cell></row>\n    <row><cell><code>function()</code></cell></row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Code |\"));\n    assert!(result.contains(\"`function()`\"));\n}\n\n#[test]\nfn test_xml_table_with_emphasis() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\"><em>Emphasized</em></cell>\n        <cell role=\"head\"><strong>Strong</strong></cell>\n    </row>\n    <row>\n        <cell><i>Italic</i></cell>\n        <cell><b>Bold</b></cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"*Emphasized*\"));\n    assert!(result.contains(\"**Strong**\"));\n    assert!(result.contains(\"*Italic*\"));\n    assert!(result.contains(\"**Bold**\"));\n}\n\n#[test]\nfn test_xml_table_with_multiple_headers() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\">First</cell>\n        <cell role=\"head\">Second</cell>\n        <cell role=\"head\">Third</cell>\n    </row>\n    <row>\n        <cell>A</cell>\n        <cell>B</cell>\n        <cell>C</cell>\n    </row>\n    <row>\n        <cell>D</cell>\n        <cell>E</cell>\n        <cell>F</cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| First | Second | Third |\"));\n    assert!(result.contains(\"| A | B | C |\"));\n    assert!(result.contains(\"| D | E | F |\"));\n    assert!(result.contains(\"| --- | --- | --- |\"));\n}\n\n#[test]\nfn test_cell_role_variations() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\">Header Cell</cell>\n        <cell role=\"data\">Data Cell</cell>\n    </row>\n    <row>\n        <cell role=\"label\">Label</cell>\n        <cell role=\"head\">Another Header</cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Header Cell | Data Cell |\"));\n    assert!(result.contains(\"| Label | Another Header |\"));\n}\n\n#[test]\nfn test_deeply_nested_xml_content() {\n    let html = r#\"<table>\n    <row><cell role=\"head\">Complex</cell></row>\n    <row>\n        <cell>\n            <div>\n                <p>\n                    <strong>\n                        <em>Nested</em>\n                    </strong>\n                </p>\n            </div>\n        </cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Complex |\"));\n    assert!(result.contains(\"Nested\"));\n}\n\n#[test]\nfn test_xml_table_with_attributes_preserved() {\n    let html = r#\"<table id=\"table1\" class=\"data-table\" xmlns:tei=\"http://www.tei-c.org/ns/1.0\">\n    <row>\n        <cell role=\"head\">Column 1</cell>\n        <cell role=\"head\">Column 2</cell>\n    </row>\n    <row>\n        <cell>Value 1</cell>\n        <cell>Value 2</cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Column 1 | Column 2 |\"));\n    assert!(result.contains(\"| Value 1 | Value 2 |\"));\n}\n\n#[test]\nfn test_mixed_cell_types_in_rows() {\n    let html = r#\"<table>\n    <row>\n        <cell role=\"head\">Name</cell>\n        <th>Age</th>\n        <cell role=\"head\">City</cell>\n    </row>\n    <row>\n        <cell>John</cell>\n        <td>25</td>\n        <cell>NYC</cell>\n    </row>\n    </table>\"#;\n\n    let result = convert(html, None).unwrap();\n    assert!(result.contains(\"| Name | Age | City |\"));\n    assert!(result.contains(\"| John | 25 | NYC |\"));\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-cli\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\ndocumentation.workspace = true\nreadme.workspace = true\nrust-version.workspace = true\ndescription = \"Command-line interface for html-to-markdown - high-performance HTML to Markdown converter\"\nkeywords = [\"html\", \"markdown\", \"cli\", \"converter\", \"html5ever\"]\ncategories = [\"command-line-utilities\", \"text-processing\"]\n\n[[bin]]\nname = \"html-to-markdown\"\npath = \"src/main.rs\"\n\n[dependencies]\nbase64.workspace = true\nclap.workspace = true\nclap_complete.workspace = true\nclap_mangen.workspace = true\nencoding_rs.workspace = true\nhtml-to-markdown-rs = { workspace = true, features = [\"metadata\", \"inline-images\", \"visitor\", \"serde\"] }\nreqwest = { version = \"0.13.2\", default-features = false, features = [\n    \"blocking\",\n    \"rustls\",\n    \"gzip\",\n    \"brotli\",\n    \"deflate\",\n    \"charset\",\n] }\nserde_json = \"1.0\"\n\n[dev-dependencies]\nassert_cmd = \"2.2\"\npredicates = \"3.1\"\ntempfile = \"3.27\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/args.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nuse crate::validators::{\n    CliCodeBlockStyle, CliHeadingStyle, CliHighlightStyle, CliLinkStyle, CliListIndentType, CliNewlineStyle,\n    CliOutputFormat, CliPreprocessingPreset, CliWhitespaceMode, validate_bullets, validate_strong_em_symbol,\n};\nuse clap::{Parser, ValueEnum};\nuse std::path::PathBuf;\n\n/// Convert HTML to Markdown\n///\n/// A fast, powerful HTML to Markdown converter with comprehensive\n/// customization options. Uses the html5ever parser for standards-compliant\n/// HTML processing.\n#[derive(Parser)]\n#[command(name = \"html-to-markdown\")]\n#[command(version)]\n#[command(about, long_about = None)]\n#[command(after_help = \"EXAMPLES:\n    # Basic conversion from stdin\n    echo '<h1>Title</h1><p>Content</p>' | html-to-markdown\n\n    # Convert file to stdout\n    html-to-markdown input.html\n\n    # Convert and save to file\n    html-to-markdown input.html -o output.md\n\n    # Generate shell completions\n    html-to-markdown --generate-completion bash > html-to-markdown.bash\n    html-to-markdown --generate-completion zsh > _html-to-markdown\n\n    # Generate man page\n    html-to-markdown --generate-man > html-to-markdown.1\n\n    # Web scraping with preprocessing\n    html-to-markdown page.html --preprocess --preset aggressive\n\n    # Fetch remote HTML and convert\n    html-to-markdown --url https://example.com > output.md\n\n    # Discord/Slack-friendly (2-space indents)\n    html-to-markdown input.html --list-indent-width 2\n\n    # Custom heading and list styles\n    html-to-markdown input.html \\\\\n        --heading-style atx \\\\\n        --bullets '*' \\\\\n        --list-indent-width 2\n\nFor more information: https://github.com/kreuzberg-dev/html-to-markdown\n\")]\npub struct Cli {\n    /// Input HTML file (use \\\"-\\\" or omit for stdin)\n    #[arg(value_name = \"FILE\")]\n    pub input: Option<String>,\n\n    /// Fetch HTML from a URL (alternative to file/stdin)\n    #[arg(long, value_name = \"URL\", conflicts_with = \"input\")]\n    pub url: Option<String>,\n\n    /// User-Agent header when fetching via --url (default mimics a real browser)\n    #[arg(long = \"user-agent\", value_name = \"UA\", requires = \"url\")]\n    pub user_agent: Option<String>,\n\n    /// Output file (default: stdout)\n    #[arg(short = 'o', long = \"output\", value_name = \"FILE\")]\n    pub output: Option<PathBuf>,\n\n    /// Generate shell completion script\n    #[arg(long = \"generate-completion\", value_name = \"SHELL\", value_enum)]\n    pub generate_completion: Option<Shell>,\n\n    /// Generate man page\n    #[arg(long = \"generate-man\")]\n    pub generate_man: bool,\n\n    /// Heading style\n    ///\n    /// Controls how headings are formatted in the output:\n    /// - 'atx': # for h1, ## for h2, etc. (default, `CommonMark`)\n    /// - 'underlined': h1 uses ===, h2 uses ---\n    /// - 'atx-closed': # Title # with closing hashes\n    #[arg(long, value_name = \"STYLE\")]\n    #[arg(help_heading = \"Heading Options\")]\n    pub heading_style: Option<CliHeadingStyle>,\n\n    /// List indentation type\n    #[arg(long, value_name = \"TYPE\")]\n    #[arg(help_heading = \"List Options\")]\n    pub list_indent_type: Option<CliListIndentType>,\n\n    /// Spaces per list indent level\n    ///\n    /// Default is 2 (`CommonMark` standard). Use 4 for wider indentation.\n    #[arg(long, value_name = \"N\", value_parser = clap::value_parser!(u8).range(1..=8))]\n    #[arg(help_heading = \"List Options\")]\n    pub list_indent_width: Option<u8>,\n\n    /// Bullet characters for unordered lists\n    ///\n    /// Characters cycle through nesting levels. Default \"-\" uses hyphen\n    /// consistently. \"*+-\" uses * for level 1, + for level 2, - for level 3.\n    #[arg(short = 'b', long, value_name = \"CHARS\")]\n    #[arg(help_heading = \"List Options\")]\n    #[arg(value_parser = validate_bullets)]\n    pub bullets: Option<String>,\n\n    /// Symbol for bold and italic\n    ///\n    /// Choose '*' (default) or '_' for **bold** and *italic* text\n    #[arg(long, value_name = \"CHAR\")]\n    #[arg(help_heading = \"Text Formatting\")]\n    #[arg(value_parser = validate_strong_em_symbol)]\n    pub strong_em_symbol: Option<char>,\n\n    /// Escape asterisk (*) characters\n    #[arg(long)]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub escape_asterisks: bool,\n\n    /// Escape underscore (_) characters\n    #[arg(long)]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub escape_underscores: bool,\n\n    /// Escape misc Markdown characters\n    ///\n    /// Escape characters like [, ], <, >, #, etc.\n    #[arg(long)]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub escape_misc: bool,\n\n    /// Escape all ASCII punctuation\n    ///\n    /// For strict `CommonMark` spec compliance (usually not needed)\n    #[arg(long)]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub escape_ascii: bool,\n\n    /// Symbol to wrap subscript text\n    ///\n    /// Example: \"~\" wraps <sub>text</sub> as ~text~\n    #[arg(long, value_name = \"SYMBOL\")]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub sub_symbol: Option<String>,\n\n    /// Symbol to wrap superscript text\n    ///\n    /// Example: \"^\" wraps <sup>text</sup> as ^text^\n    #[arg(long, value_name = \"SYMBOL\")]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub sup_symbol: Option<String>,\n\n    /// Line break style\n    ///\n    /// How to represent <br> tags:\n    /// - 'backslash': Backslash at end of line (default, `CommonMark`)\n    /// - 'spaces': Two spaces at end of line\n    #[arg(long, value_name = \"STYLE\")]\n    #[arg(help_heading = \"Text Formatting\")]\n    pub newline_style: Option<CliNewlineStyle>,\n\n    /// Code block style\n    ///\n    /// How to format code blocks:\n    /// - 'indented': 4-space indentation (default, `CommonMark`)\n    /// - 'backticks': Fenced with backticks (```)\n    /// - 'tildes': Fenced with tildes (~~~)\n    #[arg(long, value_name = \"STYLE\")]\n    #[arg(help_heading = \"Code Blocks\")]\n    pub code_block_style: Option<CliCodeBlockStyle>,\n\n    /// Default language for code blocks\n    ///\n    /// Sets the language for fenced code blocks when not specified in HTML\n    #[arg(short = 'l', long, value_name = \"LANG\")]\n    #[arg(help_heading = \"Code Blocks\")]\n    pub code_language: Option<String>,\n\n    /// Disable autolink conversion\n    ///\n    /// By default, when link text equals the href, the link is converted to\n    /// `<url>` autolink syntax. Pass this flag to output `[url](url)` instead.\n    #[arg(long = \"no-autolinks\")]\n    #[arg(help_heading = \"Links\")]\n    pub no_autolinks: bool,\n\n    /// Link rendering style\n    ///\n    /// Controls how links are formatted:\n    /// - 'inline': [text](url) (default)\n    /// - 'reference': [text][1] with definitions at end\n    #[arg(long, value_name = \"STYLE\")]\n    #[arg(help_heading = \"Links\")]\n    pub link_style: Option<CliLinkStyle>,\n\n    /// Add default title to links\n    ///\n    /// Use href as link title when no title attribute exists\n    #[arg(long)]\n    #[arg(help_heading = \"Links\")]\n    pub default_title: bool,\n\n    /// Keep inline images in specific elements\n    ///\n    /// Comma-separated list of HTML elements where images should remain\n    /// as markdown (not converted to alt text). Example: \"a,strong\"\n    #[arg(long, value_name = \"ELEMENTS\", value_delimiter = ',')]\n    #[arg(help_heading = \"Images\")]\n    pub keep_inline_images_in: Option<Vec<String>>,\n\n    /// Use <br> in table cells\n    ///\n    /// Preserve line breaks in table cells using <br> tags instead of\n    /// converting to spaces\n    #[arg(long)]\n    #[arg(help_heading = \"Tables\")]\n    pub br_in_tables: bool,\n\n    /// Style for <mark> elements\n    ///\n    /// How to represent highlighted text:\n    /// - 'double-equal': ==text== (default)\n    /// - 'html': <mark>text</mark>\n    /// - 'bold': **text**\n    /// - 'none': plain text\n    #[arg(long, value_name = \"STYLE\")]\n    #[arg(help_heading = \"Highlighting\")]\n    pub highlight_style: Option<CliHighlightStyle>,\n\n    /// Extract metadata from HTML\n    ///\n    /// Extract title and meta tags as HTML comment header\n    #[arg(long)]\n    #[arg(help_heading = \"Metadata\")]\n    pub extract_metadata: bool,\n\n    /// Output full ConversionResult as JSON instead of markdown text\n    ///\n    /// Serializes all result fields (content, metadata, tables, document tree, warnings)\n    /// as a JSON object. Use with --include-structure, --extract-inline-images, --no-content\n    /// to control which fields are populated.\n    #[arg(long)]\n    #[arg(help_heading = \"JSON Output\")]\n    pub json: bool,\n\n    /// Include structured document tree in output\n    ///\n    /// Requires --json. Populates the \"document\" field with a semantic node tree.\n    #[arg(long)]\n    #[arg(help_heading = \"JSON Output\")]\n    #[arg(requires = \"json\")]\n    pub include_structure: bool,\n\n    /// Extract inline images from data URIs and SVGs\n    ///\n    /// Requires --json. Populates the \"images\" field with extracted inline image data.\n    #[arg(long)]\n    #[arg(help_heading = \"JSON Output\")]\n    #[arg(requires = \"json\")]\n    pub extract_inline_images: bool,\n\n    /// Print processing warnings to stderr\n    ///\n    /// Emits each non-fatal warning to stderr in the format:\n    /// \"Warning [<kind>]: <message>\"\n    #[arg(long)]\n    #[arg(help_heading = \"JSON Output\")]\n    pub show_warnings: bool,\n\n    /// Skip text content generation, only extract metadata and structure\n    ///\n    /// Requires --json. Sets output_format to plain text extraction mode;\n    /// the \"content\" field in the JSON output will be empty.\n    #[arg(long)]\n    #[arg(help_heading = \"JSON Output\")]\n    #[arg(requires = \"json\")]\n    pub no_content: bool,\n\n    /// Whitespace handling mode\n    ///\n    /// How to handle whitespace in HTML:\n    /// - 'normalized': Clean up excess whitespace (default)\n    /// - 'strict': Preserve whitespace as-is\n    #[arg(long, value_name = \"MODE\")]\n    #[arg(help_heading = \"Whitespace\")]\n    pub whitespace_mode: Option<CliWhitespaceMode>,\n\n    /// Strip newlines from input\n    ///\n    /// Remove all newlines from HTML before processing (useful for\n    /// minified HTML)\n    #[arg(long)]\n    #[arg(help_heading = \"Whitespace\")]\n    pub strip_newlines: bool,\n\n    /// Enable text wrapping\n    ///\n    /// Wrap output lines at --wrap-width columns\n    #[arg(short = 'w', long)]\n    #[arg(help_heading = \"Wrapping\")]\n    pub wrap: bool,\n\n    /// Wrap width in columns\n    ///\n    /// Column width for text wrapping when --wrap is enabled\n    #[arg(long, value_name = \"N\", value_parser = clap::value_parser!(u16).range(20..=500))]\n    #[arg(help_heading = \"Wrapping\")]\n    pub wrap_width: Option<u16>,\n\n    /// Treat block elements as inline\n    ///\n    /// Convert block-level elements without adding paragraph breaks\n    #[arg(long)]\n    #[arg(help_heading = \"Element Handling\")]\n    pub convert_as_inline: bool,\n\n    /// HTML tags to strip\n    ///\n    /// Comma-separated list of HTML tags to strip (output only text content,\n    /// no markdown conversion). Example: \"script,style\"\n    #[arg(long, value_name = \"TAGS\", value_delimiter = ',')]\n    #[arg(help_heading = \"Element Handling\")]\n    pub strip_tags: Option<Vec<String>>,\n\n    /// HTML tags to preserve verbatim\n    ///\n    /// Comma-separated list of HTML tags to keep as raw HTML in the output.\n    /// Example: \"details,summary\"\n    #[arg(long, value_name = \"TAGS\", value_delimiter = ',')]\n    #[arg(help_heading = \"Element Handling\")]\n    pub preserve_tags: Option<Vec<String>>,\n\n    /// Skip image elements\n    ///\n    /// Omit all <img> elements from the output entirely\n    #[arg(long)]\n    #[arg(help_heading = \"Element Handling\")]\n    pub skip_images: bool,\n\n    /// Maximum DOM traversal depth\n    ///\n    /// Silently truncate subtrees beyond this nesting depth. Useful for\n    /// pathologically deep documents. Omit for unlimited depth (default).\n    #[arg(long, value_name = \"N\")]\n    #[arg(help_heading = \"Element Handling\")]\n    pub max_depth: Option<usize>,\n\n    /// Enable HTML preprocessing\n    ///\n    /// Clean up HTML before conversion (removes navigation, ads, forms, etc.)\n    #[arg(short = 'p', long)]\n    #[arg(help_heading = \"Preprocessing\")]\n    pub preprocess: bool,\n\n    /// Preprocessing aggressiveness preset\n    ///\n    /// How aggressively to clean HTML:\n    /// - 'minimal': Basic cleanup only\n    /// - 'standard': Balanced cleaning (default)\n    /// - 'aggressive': Maximum cleaning for web scraping\n    #[arg(long, value_name = \"LEVEL\")]\n    #[arg(help_heading = \"Preprocessing\")]\n    #[arg(requires = \"preprocess\")]\n    pub preset: Option<CliPreprocessingPreset>,\n\n    /// Keep navigation elements\n    ///\n    /// Don't remove <nav>, menus, etc. during preprocessing\n    #[arg(long)]\n    #[arg(help_heading = \"Preprocessing\")]\n    #[arg(requires = \"preprocess\")]\n    pub keep_navigation: bool,\n\n    /// Keep form elements\n    ///\n    /// Don't remove <form>, <input>, etc. during preprocessing\n    #[arg(long)]\n    #[arg(help_heading = \"Preprocessing\")]\n    #[arg(requires = \"preprocess\")]\n    pub keep_forms: bool,\n\n    /// Input character encoding\n    ///\n    /// Encoding to use when reading input files (e.g., 'utf-8', 'latin-1')\n    #[arg(short = 'e', long, value_name = \"ENCODING\", default_value = \"utf-8\")]\n    #[arg(help_heading = \"Parsing\")]\n    pub encoding: String,\n\n    /// Enable debug mode\n    ///\n    /// Output diagnostic warnings and information\n    #[arg(long)]\n    #[arg(help_heading = \"Debugging\")]\n    pub debug: bool,\n\n    /// Output format (markdown or djot)\n    ///\n    /// Choose the output format:\n    /// - 'markdown': Standard Markdown (CommonMark compatible, default)\n    /// - 'djot': Djot lightweight markup language\n    #[arg(short = 'f', long = \"output-format\", value_name = \"FORMAT\")]\n    #[arg(help_heading = \"Output Format\")]\n    pub output_format: Option<CliOutputFormat>,\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\n#[allow(clippy::enum_variant_names)]\npub enum Shell {\n    Bash,\n    Zsh,\n    Fish,\n    PowerShell,\n    Elvish,\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/convert.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nuse crate::args::Cli;\nuse crate::output::output_debug_info;\nuse base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};\nuse html_to_markdown_rs::{ConversionOptions, OutputFormat, PreprocessingOptions, convert};\nuse serde_json::json;\n\nfn base64_encode(data: &[u8]) -> String {\n    BASE64.encode(data)\n}\n\npub fn build_conversion_options(cli: &Cli) -> ConversionOptions {\n    let defaults = ConversionOptions::default();\n\n    let preprocessing = PreprocessingOptions {\n        enabled: cli.preprocess,\n        preset: cli.preset.map(Into::into).unwrap_or_default(),\n        remove_navigation: !cli.keep_navigation,\n        remove_forms: !cli.keep_forms,\n    };\n\n    ConversionOptions {\n        heading_style: cli.heading_style.map_or(defaults.heading_style, Into::into),\n        list_indent_type: cli.list_indent_type.map_or(defaults.list_indent_type, Into::into),\n        list_indent_width: cli.list_indent_width.map_or(defaults.list_indent_width, |w| w as usize),\n        bullets: cli.bullets.clone().unwrap_or(defaults.bullets),\n        strong_em_symbol: cli.strong_em_symbol.unwrap_or(defaults.strong_em_symbol),\n        escape_asterisks: cli.escape_asterisks,\n        escape_underscores: cli.escape_underscores,\n        escape_misc: cli.escape_misc,\n        escape_ascii: cli.escape_ascii,\n        code_language: cli.code_language.clone().unwrap_or(defaults.code_language),\n        autolinks: !cli.no_autolinks,\n        default_title: cli.default_title,\n        br_in_tables: cli.br_in_tables,\n        highlight_style: cli.highlight_style.map_or(defaults.highlight_style, Into::into),\n        extract_metadata: cli.extract_metadata,\n        whitespace_mode: cli.whitespace_mode.map_or(defaults.whitespace_mode, Into::into),\n        strip_newlines: cli.strip_newlines,\n        wrap: cli.wrap,\n        wrap_width: cli.wrap_width.map_or(defaults.wrap_width, |w| w as usize),\n        convert_as_inline: cli.convert_as_inline,\n        sub_symbol: cli.sub_symbol.clone().unwrap_or(defaults.sub_symbol),\n        sup_symbol: cli.sup_symbol.clone().unwrap_or(defaults.sup_symbol),\n        newline_style: cli.newline_style.map_or(defaults.newline_style, Into::into),\n        code_block_style: cli.code_block_style.map_or(defaults.code_block_style, Into::into),\n        keep_inline_images_in: cli\n            .keep_inline_images_in\n            .clone()\n            .unwrap_or(defaults.keep_inline_images_in),\n        link_style: cli.link_style.map_or(defaults.link_style, Into::into),\n        skip_images: cli.skip_images,\n        preprocessing,\n        encoding: cli.encoding.clone(),\n        debug: cli.debug,\n        strip_tags: cli.strip_tags.clone().unwrap_or(defaults.strip_tags),\n        preserve_tags: cli.preserve_tags.clone().unwrap_or(defaults.preserve_tags),\n        output_format: cli.output_format.map_or(OutputFormat::default(), Into::into),\n        include_document_structure: cli.include_structure,\n        extract_images: cli.extract_inline_images,\n        max_image_size: 5_242_880,\n        capture_svg: false,\n        infer_dimensions: true,\n        max_depth: cli.max_depth,\n        exclude_selectors: Vec::new(),\n        visitor: None,\n    }\n}\n\npub fn perform_conversion(\n    html: &str,\n    options: ConversionOptions,\n    cli: &Cli,\n) -> Result<String, Box<dyn std::error::Error>> {\n    let output_content = if cli.json {\n        // --json path: serialize full ConversionResult fields\n        let result = convert(html, Some(options)).map_err(|e| format!(\"Error converting HTML: {e}\"))?;\n\n        // Emit warnings to stderr if requested\n        if cli.show_warnings {\n            for warning in &result.warnings {\n                eprintln!(\"Warning [{:?}]: {}\", warning.kind, warning.message);\n            }\n        }\n\n        output_debug_info(\n            cli,\n            &format!(\n                \"Generated {} bytes of markdown (JSON mode)\",\n                result.content.as_deref().unwrap_or(\"\").len()\n            ),\n        );\n\n        // Build JSON output manually to handle feature-gated fields\n        let mut json_output = serde_json::Map::new();\n\n        // content field — null when --no-content\n        if !cli.no_content {\n            json_output.insert(\n                \"content\".into(),\n                serde_json::Value::String(result.content.clone().unwrap_or_default()),\n            );\n        } else {\n            json_output.insert(\"content\".into(), serde_json::Value::Null);\n        }\n\n        // tables field — always present\n        let tables_json = serde_json::to_value(&result.tables).map_err(|e| format!(\"Error serializing tables: {e}\"))?;\n        json_output.insert(\"tables\".into(), tables_json);\n\n        // document field — present when --include-structure was set\n        let document_json = match &result.document {\n            Some(doc) => serde_json::to_value(doc).map_err(|e| format!(\"Error serializing document: {e}\"))?,\n            None => serde_json::Value::Null,\n        };\n        json_output.insert(\"document\".into(), document_json);\n\n        // metadata field — populated via the metadata feature\n        let metadata_json =\n            serde_json::to_value(&result.metadata).map_err(|e| format!(\"Error serializing metadata: {e}\"))?;\n        json_output.insert(\"metadata\".into(), metadata_json);\n\n        // images field — serialized manually since InlineImage doesn't derive serde\n        let images_arr: Vec<serde_json::Value> = result\n            .images\n            .iter()\n            .map(|img| {\n                json!({\n                    \"format\": format!(\"{}\", img.format),\n                    \"source\": format!(\"{}\", img.source),\n                    \"filename\": img.filename,\n                    \"description\": img.description,\n                    \"dimensions\": img.dimensions.map(|(w, h)| json!({\"width\": w, \"height\": h})),\n                    \"data_base64\": base64_encode(&img.data),\n                    \"attributes\": img.attributes,\n                })\n            })\n            .collect();\n        json_output.insert(\"images\".into(), serde_json::Value::Array(images_arr));\n\n        // warnings field — always present\n        let warnings_json =\n            serde_json::to_value(&result.warnings).map_err(|e| format!(\"Error serializing warnings: {e}\"))?;\n        json_output.insert(\"warnings\".into(), warnings_json);\n\n        serde_json::to_string_pretty(&serde_json::Value::Object(json_output))\n            .map_err(|e| format!(\"Error serializing JSON output: {e}\"))?\n    } else {\n        // Plain markdown path\n        let result = convert(html, Some(options)).map_err(|e| format!(\"Error converting HTML: {e}\"))?;\n\n        // Emit warnings to stderr if requested\n        if cli.show_warnings {\n            for warning in &result.warnings {\n                eprintln!(\"Warning [{:?}]: {}\", warning.kind, warning.message);\n            }\n        }\n\n        let markdown = result.content.unwrap_or_default();\n        output_debug_info(cli, &format!(\"Generated {} bytes of markdown\", markdown.len()));\n        markdown\n    };\n\n    Ok(output_content)\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/main.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nmod args;\nmod convert;\nmod output;\nmod utils;\nmod validators;\n\nuse args::{Cli, Shell};\nuse clap::Parser;\nuse convert::{build_conversion_options, perform_conversion};\nuse output::{output_debug_info, write_output};\nuse std::fs;\nuse std::io::{self, Read, Write as IoWrite};\nuse std::path::PathBuf;\nuse utils::{DEFAULT_USER_AGENT, decode_bytes, fetch_url};\n\nfn generate_completions(shell: Shell) {\n    use clap::CommandFactory;\n    use clap_complete::{Shell as ClapShell, generate};\n\n    let mut cmd = Cli::command();\n    let shell = match shell {\n        Shell::Bash => ClapShell::Bash,\n        Shell::Zsh => ClapShell::Zsh,\n        Shell::Fish => ClapShell::Fish,\n        Shell::PowerShell => ClapShell::PowerShell,\n        Shell::Elvish => ClapShell::Elvish,\n    };\n\n    generate(shell, &mut cmd, \"html-to-markdown\", &mut io::stdout());\n}\n\nfn generate_man_page() -> Result<(), String> {\n    use clap::CommandFactory;\n\n    let cmd = Cli::command();\n    let man = clap_mangen::Man::new(cmd);\n    let mut buffer = Vec::new();\n    man.render(&mut buffer)\n        .map_err(|e| format!(\"Failed to generate man page: {e}\"))?;\n\n    io::stdout()\n        .write_all(&buffer)\n        .map_err(|e| format!(\"Failed to write man page: {e}\"))?;\n\n    Ok(())\n}\n\nfn read_input(cli: &Cli) -> Result<String, Box<dyn std::error::Error>> {\n    let html = match cli.input.as_deref() {\n        _ if cli.url.is_some() => {\n            let user_agent = cli.user_agent.as_deref().unwrap_or(DEFAULT_USER_AGENT);\n            let fetched = fetch_url(\n                cli.url.as_deref().expect(\"url already checked\"),\n                user_agent,\n                &cli.encoding,\n            )?;\n            output_debug_info(cli, &format!(\"Fetched {} bytes from URL\", fetched.len()));\n            fetched\n        }\n        None | Some(\"-\") => {\n            let mut buffer = Vec::new();\n            io::stdin()\n                .read_to_end(&mut buffer)\n                .map_err(|e| format!(\"Error reading from stdin: {e}\"))?;\n            let decoded = decode_bytes(&buffer, &cli.encoding)?;\n            output_debug_info(cli, &format!(\"Read {} bytes from stdin\", decoded.len()));\n            decoded\n        }\n        Some(path) => {\n            let path = PathBuf::from(path);\n            let bytes = fs::read(&path).map_err(|e| format!(\"Error reading file '{}': {}\", path.display(), e))?;\n            let decoded = decode_bytes(&bytes, &cli.encoding)?;\n            output_debug_info(\n                cli,\n                &format!(\"Read {} bytes from file '{}'\", decoded.len(), path.display()),\n            );\n            decoded\n        }\n    };\n    Ok(html)\n}\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let cli = Cli::parse();\n\n    if let Some(shell) = cli.generate_completion {\n        generate_completions(shell);\n        return Ok(());\n    }\n\n    if cli.generate_man {\n        generate_man_page()?;\n        return Ok(());\n    }\n\n    let html = read_input(&cli)?;\n    let options = build_conversion_options(&cli);\n    let output_content = perform_conversion(&html, options, &cli)?;\n    write_output(cli.output.clone(), &output_content)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/output.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nuse crate::args::Cli;\nuse std::fs;\nuse std::path::PathBuf;\n\npub fn write_output(output_path: Option<PathBuf>, content: &str) -> Result<(), Box<dyn std::error::Error>> {\n    match output_path {\n        Some(path) => {\n            fs::write(&path, content.as_bytes())\n                .map_err(|e| format!(\"Error writing to file '{}': {}\", path.display(), e))?;\n        }\n        None => {\n            print!(\"{content}\");\n        }\n    }\n    Ok(())\n}\n\npub fn output_debug_info(cli: &Cli, msg: &str) {\n    if cli.debug {\n        eprintln!(\"{msg}\");\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/utils.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nuse encoding_rs::Encoding;\nuse reqwest::blocking::Client;\nuse reqwest::header::{CONTENT_TYPE, USER_AGENT};\nuse std::time::Duration;\n\npub const DEFAULT_USER_AGENT: &str =\n    \"Mozilla/5.0 (compatible; html-to-markdown-cli/2.10; +https://github.com/kreuzberg-dev/html-to-markdown)\";\n\npub fn decode_bytes(bytes: &[u8], encoding_name: &str) -> Result<String, String> {\n    let lowercase = encoding_name.to_lowercase();\n    let normalized = match lowercase.as_str() {\n        \"latin-1\" | \"latin1\" => \"iso-8859-1\",\n        \"latin-2\" | \"latin2\" => \"iso-8859-2\",\n        \"latin-3\" | \"latin3\" => \"iso-8859-3\",\n        \"latin-4\" | \"latin4\" => \"iso-8859-4\",\n        \"latin-5\" | \"latin5\" => \"iso-8859-5\",\n        \"latin-6\" | \"latin6\" => \"iso-8859-6\",\n        \"latin-7\" | \"latin7\" => \"iso-8859-7\",\n        \"latin-8\" | \"latin8\" => \"iso-8859-8\",\n        \"latin-9\" | \"latin9\" => \"iso-8859-9\",\n        \"latin-10\" | \"latin10\" => \"iso-8859-10\",\n        _ => encoding_name,\n    };\n\n    let encoding =\n        Encoding::for_label(normalized.as_bytes()).ok_or_else(|| format!(\"Unknown encoding '{encoding_name}'\"))?;\n\n    let (decoded, _, had_errors) = encoding.decode(bytes);\n    if had_errors {\n        eprintln!(\"Warning: Some characters could not be decoded correctly\");\n    }\n    Ok(decoded.into_owned())\n}\n\npub fn extract_charset(content_type: &str) -> Option<String> {\n    content_type\n        .split(';')\n        .map(str::trim)\n        .find_map(|part| part.strip_prefix(\"charset=\").map(|v| v.trim_matches('\"').to_string()))\n}\n\npub fn fetch_url(url: &str, user_agent: &str, default_encoding: &str) -> Result<String, String> {\n    let client = Client::builder()\n        .timeout(Duration::from_secs(15))\n        .redirect(reqwest::redirect::Policy::limited(5))\n        .build()\n        .map_err(|e| format!(\"Failed to build HTTP client: {e}\"))?;\n\n    let response = client\n        .get(url)\n        .header(USER_AGENT, user_agent)\n        .send()\n        .map_err(|e| format!(\"Failed to fetch '{url}': {e}\"))?;\n\n    let status = response.status();\n    if !status.is_success() {\n        return Err(format!(\"Request failed for '{url}': HTTP {status}\"));\n    }\n\n    let charset = response\n        .headers()\n        .get(CONTENT_TYPE)\n        .and_then(|value| value.to_str().ok())\n        .and_then(extract_charset);\n\n    let bytes = response\n        .bytes()\n        .map_err(|e| format!(\"Failed to read response body from '{url}': {e}\"))?;\n\n    let encoding_name = charset.as_deref().unwrap_or(default_encoding);\n    decode_bytes(&bytes, encoding_name)\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/src/validators.rs",
    "content": "#![allow(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)]\n\nuse clap::ValueEnum;\nuse html_to_markdown_rs::{\n    CodeBlockStyle, HeadingStyle, HighlightStyle, LinkStyle, ListIndentType, NewlineStyle, OutputFormat,\n    PreprocessingPreset, WhitespaceMode,\n};\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliHeadingStyle {\n    /// ATX style: # for h1, ## for h2 (default)\n    Atx,\n    /// Underlined: === for h1, --- for h2\n    Underlined,\n    /// ATX closed: # Title #\n    AtxClosed,\n}\n\nimpl From<CliHeadingStyle> for HeadingStyle {\n    fn from(style: CliHeadingStyle) -> Self {\n        match style {\n            CliHeadingStyle::Atx => Self::Atx,\n            CliHeadingStyle::Underlined => Self::Underlined,\n            CliHeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliListIndentType {\n    /// Use spaces for indentation\n    Spaces,\n    /// Use tabs for indentation\n    Tabs,\n}\n\nimpl From<CliListIndentType> for ListIndentType {\n    fn from(indent_type: CliListIndentType) -> Self {\n        match indent_type {\n            CliListIndentType::Spaces => Self::Spaces,\n            CliListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliNewlineStyle {\n    /// Two spaces at end of line\n    Spaces,\n    /// Backslash at end of line (default)\n    Backslash,\n}\n\nimpl From<CliNewlineStyle> for NewlineStyle {\n    fn from(style: CliNewlineStyle) -> Self {\n        match style {\n            CliNewlineStyle::Spaces => Self::Spaces,\n            CliNewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliCodeBlockStyle {\n    /// Indented code blocks: 4 spaces (default)\n    Indented,\n    /// Fenced code blocks: ```\n    Backticks,\n    /// Fenced code blocks: ~~~\n    Tildes,\n}\n\nimpl From<CliCodeBlockStyle> for CodeBlockStyle {\n    fn from(style: CliCodeBlockStyle) -> Self {\n        match style {\n            CliCodeBlockStyle::Indented => Self::Indented,\n            CliCodeBlockStyle::Backticks => Self::Backticks,\n            CliCodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliHighlightStyle {\n    /// ==text== (default)\n    DoubleEqual,\n    /// <mark>text</mark>\n    Html,\n    /// **text**\n    Bold,\n    /// Plain text\n    None,\n}\n\nimpl From<CliHighlightStyle> for HighlightStyle {\n    fn from(style: CliHighlightStyle) -> Self {\n        match style {\n            CliHighlightStyle::DoubleEqual => Self::DoubleEqual,\n            CliHighlightStyle::Html => Self::Html,\n            CliHighlightStyle::Bold => Self::Bold,\n            CliHighlightStyle::None => Self::None,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliWhitespaceMode {\n    /// Normalize whitespace (default)\n    Normalized,\n    /// Preserve whitespace as-is\n    Strict,\n}\n\nimpl From<CliWhitespaceMode> for WhitespaceMode {\n    fn from(mode: CliWhitespaceMode) -> Self {\n        match mode {\n            CliWhitespaceMode::Normalized => Self::Normalized,\n            CliWhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliPreprocessingPreset {\n    /// Basic cleanup\n    Minimal,\n    /// Balanced cleaning (default)\n    Standard,\n    /// Maximum cleaning\n    Aggressive,\n}\n\nimpl From<CliPreprocessingPreset> for PreprocessingPreset {\n    fn from(preset: CliPreprocessingPreset) -> Self {\n        match preset {\n            CliPreprocessingPreset::Minimal => Self::Minimal,\n            CliPreprocessingPreset::Standard => Self::Standard,\n            CliPreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliOutputFormat {\n    /// Standard Markdown (CommonMark compatible)\n    Markdown,\n    /// Djot lightweight markup language\n    Djot,\n    /// Plain text (no markup)\n    Plain,\n}\n\nimpl From<CliOutputFormat> for OutputFormat {\n    fn from(format: CliOutputFormat) -> Self {\n        match format {\n            CliOutputFormat::Markdown => Self::Markdown,\n            CliOutputFormat::Djot => Self::Djot,\n            CliOutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]\npub enum CliLinkStyle {\n    /// Inline links: [text](url) (default)\n    Inline,\n    /// Reference-style links: [text][1] with definitions at end\n    Reference,\n}\n\nimpl From<CliLinkStyle> for LinkStyle {\n    fn from(style: CliLinkStyle) -> Self {\n        match style {\n            CliLinkStyle::Inline => Self::Inline,\n            CliLinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\npub fn validate_bullets(s: &str) -> Result<String, String> {\n    if s.is_empty() {\n        return Err(\"bullets cannot be empty\".to_string());\n    }\n    if s.len() > 10 {\n        return Err(\"bullets string too long (max 10 characters)\".to_string());\n    }\n    Ok(s.to_string())\n}\n\npub fn validate_strong_em_symbol(s: &str) -> Result<char, String> {\n    if s.len() != 1 {\n        return Err(\"strong_em_symbol must be exactly one character\".to_string());\n    }\n    let c = s.chars().next().expect(\"length already validated\");\n    if c != '*' && c != '_' {\n        return Err(\"strong_em_symbol must be '*' or '_'\".to_string());\n    }\n    Ok(c)\n}\n"
  },
  {
    "path": "crates/html-to-markdown-cli/tests/cli_test.rs",
    "content": "//! Integration tests for the html-to-markdown CLI.\n//!\n//! These tests verify the CLI works correctly with various options and edge cases.\n\nuse assert_cmd::Command;\nuse predicates::prelude::*;\nuse std::fs;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nfn cli() -> Command {\n    Command::new(env!(\"CARGO_BIN_EXE_html-to-markdown\"))\n}\n\n#[test]\nfn test_basic_stdin() {\n    cli()\n        .write_stdin(\"<h1>Title</h1><p>Content</p>\")\n        .assert()\n        .success()\n        .stdout(\"# Title\\n\\nContent\\n\");\n}\n\n#[test]\nfn test_file_input() {\n    let temp_dir = TempDir::new().unwrap();\n    let input_path = temp_dir.path().join(\"input.html\");\n    fs::write(&input_path, \"<p>Test content</p>\").unwrap();\n\n    cli()\n        .arg(input_path.to_str().unwrap())\n        .assert()\n        .success()\n        .stdout(\"Test content\\n\");\n}\n\n#[test]\nfn test_file_output() {\n    let temp_dir = TempDir::new().unwrap();\n    let output_path = temp_dir.path().join(\"output.md\");\n\n    cli()\n        .arg(\"-o\")\n        .arg(output_path.to_str().unwrap())\n        .write_stdin(\"<p>Output test</p>\")\n        .assert()\n        .success();\n\n    let output = fs::read_to_string(&output_path).unwrap();\n    assert_eq!(output, \"Output test\\n\");\n}\n\n#[test]\nfn test_dash_reads_stdin() {\n    cli()\n        .arg(\"-\")\n        .write_stdin(\"<p>Dash test</p>\")\n        .assert()\n        .success()\n        .stdout(\"Dash test\\n\");\n}\n\n#[test]\nfn test_url_fetches_html() {\n    let body = \"<p>Remote</p>\";\n    let (url, handle) = serve_once(body, Some(\"text/html; charset=utf-8\"));\n\n    cli().arg(\"--url\").arg(&url).assert().success().stdout(\"Remote\\n\");\n\n    handle.join().unwrap();\n}\n\n#[test]\nfn test_url_conflicts_with_file_input() {\n    let temp_dir = TempDir::new().unwrap();\n    let input_path = temp_dir.path().join(\"input.html\");\n    fs::write(&input_path, \"<p>Conflicting input</p>\").unwrap();\n\n    cli()\n        .arg(input_path.to_str().unwrap())\n        .arg(\"--url\")\n        .arg(\"http://example.com\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"cannot be used with\"));\n}\n\n#[test]\nfn test_url_custom_user_agent() {\n    let body = \"<p>UA</p>\";\n    let ua = \"Custom-UA/1.0\";\n    let (url, handle, req_rx) = serve_once_with_capture(body, Some(\"text/html; charset=utf-8\"));\n\n    cli()\n        .arg(\"--url\")\n        .arg(&url)\n        .arg(\"--user-agent\")\n        .arg(ua)\n        .assert()\n        .success()\n        .stdout(\"UA\\n\");\n\n    let req = req_rx.recv_timeout(Duration::from_secs(1)).unwrap();\n    let req_lower = req.to_ascii_lowercase();\n    assert!(req_lower.contains(&format!(\"user-agent: {}\", ua.to_ascii_lowercase())));\n\n    handle.join().unwrap();\n}\n\n#[test]\nfn test_url_handles_quirky_markup() {\n    let html = \"<head><title>Old School</title></head><font><center><h2>Old School Site</h2><p>Welcome!</p>\";\n    let (url, handle) = serve_once(html, Some(\"text/html\"));\n\n    cli()\n        .arg(\"--url\")\n        .arg(&url)\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Old School Site\"))\n        .stdout(predicate::str::contains(\"Welcome!\"));\n\n    handle.join().unwrap();\n}\n\n#[test]\nfn test_url_handles_frameset_with_noframes() {\n    let html = r#\"\n    <frameset rows=\"50%,50%\">\n        <frame src=\"top.html\">\n        <frame src=\"bottom.html\">\n        <noframes>\n            <body>\n                <h1>Frames Not Supported</h1>\n                <p>Your browser does not support frames.</p>\n            </body>\n        </noframes>\n    </frameset>\n    \"#;\n    let (url, handle) = serve_once(html, Some(\"text/html\"));\n\n    cli()\n        .arg(\"--url\")\n        .arg(&url)\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Frames Not Supported\"))\n        .stdout(predicate::str::contains(\"does not support frames\"));\n\n    handle.join().unwrap();\n}\n\n#[test]\nfn test_url_handles_windows_1252_charset() {\n    let body = b\"<html><body><p>Se\\xf1or \\x97 legacy charset</p></body></html>\".to_vec();\n    let (url, handle, _) = serve_once_bytes(body, Some(\"text/html; charset=windows-1252\"));\n\n    cli()\n        .arg(\"--url\")\n        .arg(&url)\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Señor\"))\n        .stdout(predicate::str::contains(\"legacy charset\"));\n\n    handle.join().unwrap();\n}\n\n#[test]\nfn test_heading_style_atx() {\n    cli()\n        .arg(\"--heading-style\")\n        .arg(\"atx\")\n        .write_stdin(\"<h1>H1</h1><h2>H2</h2>\")\n        .assert()\n        .success()\n        .stdout(\"# H1\\n\\n## H2\\n\");\n}\n\n#[test]\nfn test_heading_style_underlined() {\n    cli()\n        .arg(\"--heading-style\")\n        .arg(\"underlined\")\n        .write_stdin(\"<h1>H1</h1><h2>H2</h2>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"H1\\n==\"))\n        .stdout(predicate::str::contains(\"H2\\n--\"));\n}\n\n#[test]\nfn test_heading_style_atx_closed() {\n    cli()\n        .arg(\"--heading-style\")\n        .arg(\"atx-closed\")\n        .write_stdin(\"<h1>H1</h1>\")\n        .assert()\n        .success()\n        .stdout(\"# H1 #\\n\");\n}\n\n#[test]\nfn test_list_indent_width() {\n    cli()\n        .arg(\"--list-indent-width\")\n        .arg(\"4\")\n        .write_stdin(\"<ul><li>Item 1<ul><li>Nested</li></ul></li></ul>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"    * Nested\"));\n}\n\n#[test]\nfn test_bullets_option() {\n    cli()\n        .arg(\"--bullets\")\n        .arg(\"*\")\n        .write_stdin(\"<ul><li>Item</li></ul>\")\n        .assert()\n        .success()\n        .stdout(\"* Item\\n\");\n}\n\n#[test]\nfn test_strong_em_symbol_asterisk() {\n    cli()\n        .arg(\"--strong-em-symbol\")\n        .arg(\"*\")\n        .write_stdin(\"<p><strong>Bold</strong> <em>italic</em></p>\")\n        .assert()\n        .success()\n        .stdout(\"**Bold** *italic*\\n\");\n}\n\n#[test]\nfn test_strong_em_symbol_underscore() {\n    cli()\n        .arg(\"--strong-em-symbol\")\n        .arg(\"_\")\n        .write_stdin(\"<p><strong>Bold</strong> <em>italic</em></p>\")\n        .assert()\n        .success()\n        .stdout(\"__Bold__ _italic_\\n\");\n}\n\n#[test]\nfn test_strong_em_symbol_invalid() {\n    cli()\n        .arg(\"--strong-em-symbol\")\n        .arg(\"x\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"strong_em_symbol must be '*' or '_'\"));\n}\n\n#[test]\nfn test_escape_asterisks() {\n    cli()\n        .arg(\"--escape-asterisks\")\n        .write_stdin(\"<p>Text with * asterisk</p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"\\\\*\"));\n}\n\n#[test]\nfn test_escape_underscores() {\n    cli()\n        .arg(\"--escape-underscores\")\n        .write_stdin(\"<p>Text with _ underscore</p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"\\\\_\"));\n}\n\n#[test]\nfn test_escape_misc() {\n    cli()\n        .arg(\"--escape-misc\")\n        .write_stdin(\"<p>Text with [brackets]</p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"\\\\[\"));\n}\n\n#[test]\nfn test_sub_symbol() {\n    cli()\n        .arg(\"--sub-symbol\")\n        .arg(\"~\")\n        .write_stdin(\"<p>H<sub>2</sub>O</p>\")\n        .assert()\n        .success()\n        .stdout(\"H~2~O\\n\");\n}\n\n#[test]\nfn test_sup_symbol() {\n    cli()\n        .arg(\"--sup-symbol\")\n        .arg(\"^\")\n        .write_stdin(\"<p>x<sup>2</sup></p>\")\n        .assert()\n        .success()\n        .stdout(\"x^2^\\n\");\n}\n\n#[test]\nfn test_newline_style_spaces() {\n    cli()\n        .arg(\"--newline-style\")\n        .arg(\"spaces\")\n        .write_stdin(\"<p>Line 1<br>Line 2</p>\")\n        .assert()\n        .success()\n        .stdout(\"Line 1  \\nLine 2\\n\");\n}\n\n#[test]\nfn test_newline_style_backslash() {\n    cli()\n        .arg(\"--newline-style\")\n        .arg(\"backslash\")\n        .write_stdin(\"<p>Line 1<br>Line 2</p>\")\n        .assert()\n        .success()\n        .stdout(\"Line 1\\\\\\nLine 2\\n\");\n}\n\n#[test]\nfn test_code_language() {\n    cli()\n        .arg(\"--code-language\")\n        .arg(\"rust\")\n        .arg(\"--code-block-style\")\n        .arg(\"backticks\")\n        .write_stdin(\"<pre><code>fn main() {}</code></pre>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"```rust\"));\n}\n\n#[test]\nfn test_code_block_style_indented() {\n    cli()\n        .arg(\"--code-block-style\")\n        .arg(\"indented\")\n        .write_stdin(\"<pre><code>code</code></pre>\")\n        .assert()\n        .success()\n        .stdout(\"    code\\n\");\n}\n\n#[test]\nfn test_code_block_style_backticks() {\n    cli()\n        .arg(\"--code-block-style\")\n        .arg(\"backticks\")\n        .write_stdin(\"<pre><code>code</code></pre>\")\n        .assert()\n        .success()\n        .stdout(\"```\\ncode\\n```\\n\");\n}\n\n#[test]\nfn test_code_block_style_tildes() {\n    cli()\n        .arg(\"--code-block-style\")\n        .arg(\"tildes\")\n        .write_stdin(\"<pre><code>code</code></pre>\")\n        .assert()\n        .success()\n        .stdout(\"~~~\\ncode\\n~~~\\n\");\n}\n\n#[test]\nfn test_autolinks() {\n    // Autolinks are enabled by default — no flag needed\n    cli()\n        .write_stdin(\"<p><a href=\\\"https://example.com\\\">https://example.com</a></p>\")\n        .assert()\n        .success()\n        .stdout(\"<https://example.com>\\n\");\n}\n\n#[test]\nfn test_no_autolinks() {\n    cli()\n        .arg(\"--no-autolinks\")\n        .write_stdin(\"<p><a href=\\\"https://example.com\\\">https://example.com</a></p>\")\n        .assert()\n        .success()\n        .stdout(\"[https://example.com](https://example.com)\\n\");\n}\n\n#[test]\nfn test_default_title() {\n    cli()\n        .arg(\"--default-title\")\n        .write_stdin(\"<p><a href=\\\"https://example.com\\\">Link</a></p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"[Link](https://example.com)\"));\n}\n\n#[test]\nfn test_keep_inline_images_in() {\n    cli()\n        .arg(\"--keep-inline-images-in\")\n        .arg(\"a,strong\")\n        .write_stdin(\"<a><img src=\\\"test.jpg\\\" alt=\\\"Alt\\\"></a>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"![Alt](test.jpg)\"));\n}\n\n#[test]\nfn test_br_in_tables() {\n    // When br_in_tables is enabled, <br> HTML tags should be converted to\n    // markdown line breaks (spaces-style: \"  \\n\"), not literal \"<br>\" tags\n    cli()\n        .arg(\"--br-in-tables\")\n        .write_stdin(\"<table><tr><td>Line 1<br>Line 2</td></tr></table>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Line 1  \\n\"));\n}\n\n#[test]\nfn test_highlight_style_double_equal() {\n    cli()\n        .arg(\"--highlight-style\")\n        .arg(\"double-equal\")\n        .write_stdin(\"<p><mark>highlighted</mark></p>\")\n        .assert()\n        .success()\n        .stdout(\"==highlighted==\\n\");\n}\n\n#[test]\nfn test_highlight_style_html() {\n    cli()\n        .arg(\"--highlight-style\")\n        .arg(\"html\")\n        .write_stdin(\"<p><mark>highlighted</mark></p>\")\n        .assert()\n        .success()\n        .stdout(\"<mark>highlighted</mark>\\n\");\n}\n\n#[test]\nfn test_highlight_style_bold() {\n    cli()\n        .arg(\"--highlight-style\")\n        .arg(\"bold\")\n        .write_stdin(\"<p><mark>highlighted</mark></p>\")\n        .assert()\n        .success()\n        .stdout(\"**highlighted**\\n\");\n}\n\n#[test]\nfn test_highlight_style_none() {\n    cli()\n        .arg(\"--highlight-style\")\n        .arg(\"none\")\n        .write_stdin(\"<p><mark>highlighted</mark></p>\")\n        .assert()\n        .success()\n        .stdout(\"highlighted\\n\");\n}\n\n#[test]\nfn test_extract_metadata() {\n    cli()\n        .arg(\"--extract-metadata\")\n        .write_stdin(\"<html><head><title>Page Title</title></head><body><p>Content</p></body></html>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Page Title\"));\n}\n\n#[test]\nfn test_whitespace_mode_normalized() {\n    cli()\n        .arg(\"--whitespace-mode\")\n        .arg(\"normalized\")\n        .write_stdin(\"<p>Multiple    spaces</p>\")\n        .assert()\n        .success()\n        .stdout(\"Multiple spaces\\n\");\n}\n\n#[test]\nfn test_strip_newlines() {\n    cli()\n        .arg(\"--strip-newlines\")\n        .write_stdin(\"<p>\\nContent\\n</p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Content\"));\n}\n\n#[test]\nfn test_wrap() {\n    cli()\n        .arg(\"--wrap\")\n        .arg(\"--wrap-width\")\n        .arg(\"20\")\n        .write_stdin(\"<p>This is a very long line that should be wrapped at 20 characters</p>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_wrap_width_validation() {\n    cli()\n        .arg(\"--wrap-width\")\n        .arg(\"10\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure();\n}\n\n#[test]\nfn test_convert_as_inline() {\n    cli()\n        .arg(\"--convert-as-inline\")\n        .write_stdin(\"<div>Block 1</div><div>Block 2</div>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_strip_tags() {\n    cli()\n        .arg(\"--strip-tags\")\n        .arg(\"span,div\")\n        .write_stdin(\"<p>Text with <span>span content</span> and <div>div content</div></p>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"span content\"))\n        .stdout(predicate::str::contains(\"div content\"));\n}\n\n#[test]\nfn test_preprocess() {\n    cli()\n        .arg(\"--preprocess\")\n        .write_stdin(\"<nav>Navigation</nav><article>Content</article>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_preset_requires_preprocess() {\n    cli()\n        .arg(\"--preset\")\n        .arg(\"aggressive\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"required arguments\"))\n        .stderr(predicate::str::contains(\"--preprocess\"));\n}\n\n#[test]\nfn test_preprocess_with_preset_minimal() {\n    cli()\n        .arg(\"--preprocess\")\n        .arg(\"--preset\")\n        .arg(\"minimal\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_preprocess_with_preset_aggressive() {\n    cli()\n        .arg(\"--preprocess\")\n        .arg(\"--preset\")\n        .arg(\"aggressive\")\n        .write_stdin(\"<nav>Nav</nav><p>Content</p>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_keep_navigation() {\n    cli()\n        .arg(\"--preprocess\")\n        .arg(\"--keep-navigation\")\n        .write_stdin(\"<nav>Navigation</nav>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_keep_forms() {\n    cli()\n        .arg(\"--preprocess\")\n        .arg(\"--keep-forms\")\n        .write_stdin(\"<form><input></form>\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_debug_flag() {\n    cli().arg(\"--debug\").write_stdin(\"<p>Test</p>\").assert().success();\n}\n\n#[test]\nfn test_encoding_utf8() {\n    let temp_dir = TempDir::new().unwrap();\n    let input_path = temp_dir.path().join(\"test.html\");\n    fs::write(&input_path, \"<p>Test UTF-8: 你好</p>\").unwrap();\n\n    cli()\n        .arg(\"--encoding\")\n        .arg(\"utf-8\")\n        .arg(input_path.to_str().unwrap())\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"你好\"));\n}\n\n#[test]\nfn test_encoding_invalid() {\n    cli()\n        .arg(\"--encoding\")\n        .arg(\"invalid-encoding\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"Unknown encoding\"));\n}\n\n#[test]\nfn test_list_indent_width_validation_min() {\n    cli()\n        .arg(\"--list-indent-width\")\n        .arg(\"0\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure();\n}\n\n#[test]\nfn test_list_indent_width_validation_max() {\n    cli()\n        .arg(\"--list-indent-width\")\n        .arg(\"9\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure();\n}\n\n#[test]\nfn test_bullets_validation_empty() {\n    cli()\n        .arg(\"--bullets\")\n        .arg(\"\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"cannot be empty\"));\n}\n\n#[test]\nfn test_bullets_validation_too_long() {\n    cli()\n        .arg(\"--bullets\")\n        .arg(\"*+-*+-*+-*+\")\n        .write_stdin(\"<p>Test</p>\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"too long\"));\n}\n\n#[test]\nfn test_nonexistent_file() {\n    cli()\n        .arg(\"/nonexistent/file.html\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"Error reading file\"));\n}\n\n#[test]\nfn test_invalid_html() {\n    cli().write_stdin(\"<p>Unclosed paragraph<p>Another\").assert().success();\n}\n\n#[test]\nfn test_empty_input() {\n    cli().write_stdin(\"\").assert().success().stdout(\"\");\n}\n\n#[test]\nfn test_complex_document() {\n    let html = r#\"\n        <html>\n            <head><title>Test Document</title></head>\n            <body>\n                <h1>Main Title</h1>\n                <p>Introduction with <strong>bold</strong> and <em>italic</em>.</p>\n                <ul>\n                    <li>Item 1</li>\n                    <li>Item 2\n                        <ul>\n                            <li>Nested item</li>\n                        </ul>\n                    </li>\n                </ul>\n                <pre><code>fn main() {\n    println!(\"Hello\");\n}</code></pre>\n                <p>Link: <a href=\"https://example.com\">Example</a></p>\n            </body>\n        </html>\n    \"#;\n\n    cli()\n        .write_stdin(html)\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"# Main Title\"))\n        .stdout(predicate::str::contains(\"**bold**\"))\n        .stdout(predicate::str::contains(\"*italic*\"))\n        .stdout(predicate::str::contains(\"- Item 1\"))\n        .stdout(predicate::str::contains(\"[Example](https://example.com)\"));\n}\n\n#[test]\nfn test_version_flag() {\n    cli()\n        .arg(\"--version\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(env!(\"CARGO_PKG_VERSION\")));\n}\n\n#[test]\nfn test_help_flag() {\n    cli()\n        .arg(\"--help\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"Usage:\"))\n        .stdout(predicate::str::contains(\"Options:\"));\n}\n\n#[test]\nfn test_generate_completion_bash() {\n    cli()\n        .arg(\"--generate-completion\")\n        .arg(\"bash\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"_html-to-markdown()\"));\n}\n\n#[test]\nfn test_generate_completion_zsh() {\n    cli()\n        .arg(\"--generate-completion\")\n        .arg(\"zsh\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"#compdef\"));\n}\n\n#[test]\nfn test_generate_man() {\n    cli()\n        .arg(\"--generate-man\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\".TH\"))\n        .stdout(predicate::str::contains(\"html-to-markdown\"));\n}\n\n#[test]\nfn test_multiple_options_combined() {\n    cli()\n        .arg(\"--heading-style\")\n        .arg(\"atx\")\n        .arg(\"--bullets\")\n        .arg(\"*\")\n        .arg(\"--escape-asterisks\")\n        .arg(\"--code-block-style\")\n        .arg(\"backticks\")\n        .write_stdin(\"<h1>Title</h1><ul><li>Item</li></ul><pre><code>code</code></pre>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"# Title\"))\n        .stdout(predicate::str::contains(\"* Item\"))\n        .stdout(predicate::str::contains(\"```\"));\n}\n\n#[test]\nfn test_metadata_flags_work_without_extract_metadata() {\n    // Without --extract-metadata, conversion proceeds normally\n    cli()\n        .write_stdin(\"<html><body><h1>Title</h1></body></html>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"# Title\"));\n}\n\n#[test]\nfn test_metadata_flags_work_with_json() {\n    // --json produces JSON output with a \"content\" field\n    cli()\n        .arg(\"--json\")\n        .write_stdin(\"<html><body><h1>Title</h1></body></html>\")\n        .assert()\n        .success()\n        .stdout(predicate::str::contains(\"\\\"content\\\"\"));\n}\n\nfn serve_once(body: &'static str, content_type: Option<&'static str>) -> (String, thread::JoinHandle<()>) {\n    let (url, handle, _rx) = serve_once_with_capture(body, content_type);\n    (url, handle)\n}\n\nfn serve_once_with_capture(\n    body: &'static str,\n    content_type: Option<&'static str>,\n) -> (String, thread::JoinHandle<()>, mpsc::Receiver<String>) {\n    serve_once_bytes(body.as_bytes().to_vec(), content_type)\n}\n\nfn serve_once_bytes(\n    body: Vec<u8>,\n    content_type: Option<&'static str>,\n) -> (String, thread::JoinHandle<()>, mpsc::Receiver<String>) {\n    let listener = TcpListener::bind(\"127.0.0.1:0\").unwrap();\n    let addr = listener.local_addr().unwrap();\n    let (tx, rx) = mpsc::channel::<String>();\n\n    let handle = thread::spawn(move || {\n        if let Ok((mut stream, _)) = listener.accept() {\n            let mut buffer = [0u8; 1024];\n            let _ = stream.read(&mut buffer);\n            let _ = tx.send(String::from_utf8_lossy(&buffer).to_string());\n\n            let ct_header = content_type\n                .map(|ct| format!(\"Content-Type: {ct}\\r\\n\"))\n                .unwrap_or_default();\n            let response = format!(\"HTTP/1.1 200 OK\\r\\nContent-Length: {}\\r\\n{ct_header}\\r\\n\", body.len());\n            let _ = stream.write_all(response.as_bytes());\n            let _ = stream.write_all(&body);\n        }\n    });\n\n    (format!(\"http://{addr}\"), handle, rx)\n}\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-ffi\"\nversion = \"3.4.0-rc.25\"\nedition = \"2021\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\", \"web-programming\"]\n\n[lib]\ncrate-type = [\"cdylib\", \"staticlib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../html-to-markdown\", features = [\n    \"full\",\n    \"metadata\",\n    \"visitor\",\n    \"serde\",\n    \"inline-images\",\n] }\nserde_json = \"1\"\n\n[build-dependencies]\ncbindgen = \"0.29\"\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/build.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:688178671552a16406826c14790ff9dfae371f5c5741d529357c6373f165305b\nfn main() {\n    let crate_dir = std::env::var(\"CARGO_MANIFEST_DIR\").unwrap();\n    cbindgen::generate(crate_dir)\n        .expect(\"Unable to generate C bindings\")\n        .write_to_file(\"include/html_to_markdown.h\");\n}\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/cbindgen.toml",
    "content": "# This file is auto-generated by alef. DO NOT EDIT.\n# alef:hash:2510d12fea6c57ae52055dc8a2cf8283905ef02a316aee27381182c5f499060a\nlanguage = \"C\"\ninclude_guard = \"HTM_H\"\npragma_once = true\nautogen_warning = \"/* This file is auto-generated by alef. DO NOT EDIT. */\"\n\nafter_includes = \"\"\"\n/* Opaque type forward declarations */\ntypedef struct HTMAnnotationKind HTMAnnotationKind;\ntypedef struct HTMCodeBlockStyle HTMCodeBlockStyle;\ntypedef struct HTMConversionOptions HTMConversionOptions;\ntypedef struct HTMConversionOptionsBuilder HTMConversionOptionsBuilder;\ntypedef struct HTMConversionOptionsUpdate HTMConversionOptionsUpdate;\ntypedef struct HTMConversionResult HTMConversionResult;\ntypedef struct HTMDocumentMetadata HTMDocumentMetadata;\ntypedef struct HTMDocumentNode HTMDocumentNode;\ntypedef struct HTMDocumentStructure HTMDocumentStructure;\ntypedef struct HTMGridCell HTMGridCell;\ntypedef struct HTMHeaderMetadata HTMHeaderMetadata;\ntypedef struct HTMHeadingStyle HTMHeadingStyle;\ntypedef struct HTMHighlightStyle HTMHighlightStyle;\ntypedef struct HTMHtmlMetadata HTMHtmlMetadata;\ntypedef struct HTMHtmlVisitor HTMHtmlVisitor;\ntypedef struct HTMImageMetadata HTMImageMetadata;\ntypedef struct HTMImageType HTMImageType;\ntypedef struct HTMLinkMetadata HTMLinkMetadata;\ntypedef struct HTMLinkStyle HTMLinkStyle;\ntypedef struct HTMLinkType HTMLinkType;\ntypedef struct HTMListIndentType HTMListIndentType;\ntypedef struct HTMNewlineStyle HTMNewlineStyle;\ntypedef struct HTMNodeContent HTMNodeContent;\ntypedef struct HTMNodeContext HTMNodeContext;\ntypedef struct HTMNodeType HTMNodeType;\ntypedef struct HTMOutputFormat HTMOutputFormat;\ntypedef struct HTMPreprocessingOptions HTMPreprocessingOptions;\ntypedef struct HTMPreprocessingOptionsUpdate HTMPreprocessingOptionsUpdate;\ntypedef struct HTMPreprocessingPreset HTMPreprocessingPreset;\ntypedef struct HTMProcessingWarning HTMProcessingWarning;\ntypedef struct HTMStructuredData HTMStructuredData;\ntypedef struct HTMStructuredDataType HTMStructuredDataType;\ntypedef struct HTMTableData HTMTableData;\ntypedef struct HTMTableGrid HTMTableGrid;\ntypedef struct HTMTextAnnotation HTMTextAnnotation;\ntypedef struct HTMTextDirection HTMTextDirection;\ntypedef struct HTMVisitResult HTMVisitResult;\ntypedef struct HTMVisitorHandle HTMVisitorHandle;\ntypedef struct HTMWarningKind HTMWarningKind;\ntypedef struct HTMWhitespaceMode HTMWhitespaceMode;\n\"\"\"\n\n[defines]\n\"target_os = windows\" = \"SKIF_WINDOWS\"\n\n[export]\nprefix = \"HTM\"\ninclude = []\nexclude = []\n\n[fn]\nargs = \"vertical\"\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/cmake/html-to-markdown-ffi-config.cmake",
    "content": "# html-to-markdown-ffi CMake config-mode find module\n#\n# Defines the imported target:\n#   html-to-markdown-ffi::html-to-markdown-ffi\n#\n# Usage:\n#   find_package(html-to-markdown-ffi REQUIRED)\n#   target_link_libraries(myapp PRIVATE html-to-markdown-ffi::html-to-markdown-ffi)\n\nif(TARGET html-to-markdown-ffi::html-to-markdown-ffi)\n  return()\nendif()\n\nget_filename_component(_FFI_CMAKE_DIR \"${CMAKE_CURRENT_LIST_FILE}\" PATH)\nget_filename_component(_FFI_PREFIX \"${_FFI_CMAKE_DIR}/..\" ABSOLUTE)\n\nfind_library(_FFI_LIBRARY\n  NAMES html_to_markdown_ffi libhtml_to_markdown_ffi\n  PATHS \"${_FFI_PREFIX}/lib\"\n  NO_DEFAULT_PATH\n)\nif(NOT _FFI_LIBRARY)\n  find_library(_FFI_LIBRARY NAMES html_to_markdown_ffi libhtml_to_markdown_ffi)\nendif()\n\nfind_path(_FFI_INCLUDE_DIR\n  NAMES html_to_markdown.h\n  PATHS \"${_FFI_PREFIX}/include\"\n  NO_DEFAULT_PATH\n)\nif(NOT _FFI_INCLUDE_DIR)\n  find_path(_FFI_INCLUDE_DIR NAMES html_to_markdown.h)\nendif()\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(html-to-markdown-ffi\n  REQUIRED_VARS _FFI_LIBRARY _FFI_INCLUDE_DIR\n)\n\nif(html_to_markdown_ffi_FOUND)\n  set(_FFI_LIB_TYPE UNKNOWN)\n  if(_FFI_LIBRARY MATCHES \"\\\\.(dylib|so)$\" OR _FFI_LIBRARY MATCHES \"\\\\.so\\\\.\")\n    set(_FFI_LIB_TYPE SHARED)\n  elseif(_FFI_LIBRARY MATCHES \"\\\\.dll$\")\n    set(_FFI_LIB_TYPE SHARED)\n  elseif(_FFI_LIBRARY MATCHES \"\\\\.(a|lib)$\")\n    set(_FFI_LIB_TYPE STATIC)\n  endif()\n\n  add_library(html-to-markdown-ffi::html-to-markdown-ffi ${_FFI_LIB_TYPE} IMPORTED)\n  set_target_properties(html-to-markdown-ffi::html-to-markdown-ffi PROPERTIES\n    IMPORTED_LOCATION \"${_FFI_LIBRARY}\"\n    INTERFACE_INCLUDE_DIRECTORIES \"${_FFI_INCLUDE_DIR}\"\n  )\n\n  if(WIN32 AND _FFI_LIB_TYPE STREQUAL \"SHARED\")\n    find_file(_FFI_DLL\n      NAMES html_to_markdown_ffi.dll libhtml_to_markdown_ffi.dll\n      PATHS \"${_FFI_PREFIX}/bin\" \"${_FFI_PREFIX}/lib\"\n      NO_DEFAULT_PATH\n    )\n    if(_FFI_DLL)\n      set_target_properties(html-to-markdown-ffi::html-to-markdown-ffi PROPERTIES\n        IMPORTED_LOCATION \"${_FFI_DLL}\"\n        IMPORTED_IMPLIB \"${_FFI_LIBRARY}\"\n      )\n    endif()\n    unset(_FFI_DLL CACHE)\n  endif()\n\n  if(APPLE)\n    set_property(TARGET html-to-markdown-ffi::html-to-markdown-ffi APPEND PROPERTY\n      INTERFACE_LINK_LIBRARIES \"-framework CoreFoundation\" \"-framework Security\" pthread)\n  elseif(UNIX)\n    set_property(TARGET html-to-markdown-ffi::html-to-markdown-ffi APPEND PROPERTY\n      INTERFACE_LINK_LIBRARIES pthread dl m)\n  elseif(WIN32)\n    set_property(TARGET html-to-markdown-ffi::html-to-markdown-ffi APPEND PROPERTY\n      INTERFACE_LINK_LIBRARIES ws2_32 userenv bcrypt)\n  endif()\n\n  unset(_FFI_LIB_TYPE)\nendif()\n\nmark_as_advanced(_FFI_LIBRARY _FFI_INCLUDE_DIR)\nunset(_FFI_CMAKE_DIR)\nunset(_FFI_PREFIX)\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/include/html_to_markdown.h",
    "content": "#ifndef HTM_H\n#define HTM_H\n\n#pragma once\n\n/* This file is auto-generated by alef. DO NOT EDIT. */\n\n#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n/* Opaque type forward declarations */\ntypedef struct HTMAnnotationKind HTMAnnotationKind;\ntypedef struct HTMCodeBlockStyle HTMCodeBlockStyle;\ntypedef struct HTMConversionOptions HTMConversionOptions;\ntypedef struct HTMConversionOptionsBuilder HTMConversionOptionsBuilder;\ntypedef struct HTMConversionOptionsUpdate HTMConversionOptionsUpdate;\ntypedef struct HTMConversionResult HTMConversionResult;\ntypedef struct HTMDocumentMetadata HTMDocumentMetadata;\ntypedef struct HTMDocumentNode HTMDocumentNode;\ntypedef struct HTMDocumentStructure HTMDocumentStructure;\ntypedef struct HTMGridCell HTMGridCell;\ntypedef struct HTMHeaderMetadata HTMHeaderMetadata;\ntypedef struct HTMHeadingStyle HTMHeadingStyle;\ntypedef struct HTMHighlightStyle HTMHighlightStyle;\ntypedef struct HTMHtmlMetadata HTMHtmlMetadata;\ntypedef struct HTMHtmlVisitor HTMHtmlVisitor;\ntypedef struct HTMImageMetadata HTMImageMetadata;\ntypedef struct HTMImageType HTMImageType;\ntypedef struct HTMLinkMetadata HTMLinkMetadata;\ntypedef struct HTMLinkStyle HTMLinkStyle;\ntypedef struct HTMLinkType HTMLinkType;\ntypedef struct HTMListIndentType HTMListIndentType;\ntypedef struct HTMNewlineStyle HTMNewlineStyle;\ntypedef struct HTMNodeContent HTMNodeContent;\ntypedef struct HTMNodeContext HTMNodeContext;\ntypedef struct HTMNodeType HTMNodeType;\ntypedef struct HTMOutputFormat HTMOutputFormat;\ntypedef struct HTMPreprocessingOptions HTMPreprocessingOptions;\ntypedef struct HTMPreprocessingOptionsUpdate HTMPreprocessingOptionsUpdate;\ntypedef struct HTMPreprocessingPreset HTMPreprocessingPreset;\ntypedef struct HTMProcessingWarning HTMProcessingWarning;\ntypedef struct HTMStructuredData HTMStructuredData;\ntypedef struct HTMStructuredDataType HTMStructuredDataType;\ntypedef struct HTMTableData HTMTableData;\ntypedef struct HTMTableGrid HTMTableGrid;\ntypedef struct HTMTextAnnotation HTMTextAnnotation;\ntypedef struct HTMTextDirection HTMTextDirection;\ntypedef struct HTMVisitResult HTMVisitResult;\ntypedef struct HTMVisitorHandle HTMVisitorHandle;\ntypedef struct HTMWarningKind HTMWarningKind;\ntypedef struct HTMWhitespaceMode HTMWhitespaceMode;\n\n\n/**\n * Rust-side bridge that holds a C vtable pointer and opaque `user_data`.\n *\n * Implements `HtmlVisitor` by forwarding calls through the vtable.\n */\ntypedef struct HTMHtmHtmlVisitorBridge HTMHtmHtmlVisitorBridge;\n\n/**\n * Return the last error code (0 means no error).\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_last_error_code(void);\n\n/**\n * Return the last error message. The pointer is valid until the next FFI call on this thread.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nconst char *htm_last_error_context(void);\n\n/**\n * Free a string previously returned by this library.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_free_string(char *ptr);\n\n/**\n * Return the library version string. The pointer is static and must NOT be freed.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nconst char *htm_version(void);\n\n/**\n * Create a `DocumentMetadata` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_document_metadata_free`.\n */\nHTMDocumentMetadata *htm_document_metadata_from_json(const char *json);\n\n/**\n * Serialize a `DocumentMetadata` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_document_metadata_to_json(const HTMDocumentMetadata *ptr);\n\n/**\n * Free a `DocumentMetadata` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_document_metadata_free(HTMDocumentMetadata *ptr);\n\n/**\n * Get the `title` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_title(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `description` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_description(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `keywords` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_keywords(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `author` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_author(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `canonical_url` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_canonical_url(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `base_href` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_base_href(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `language` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_language(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `text_direction` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMTextDirection *htm_document_metadata_text_direction(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `open_graph` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_open_graph(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `twitter_card` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_twitter_card(const HTMDocumentMetadata *ptr);\n\n/**\n * Get the `meta_tags` field from a `DocumentMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_metadata_meta_tags(const HTMDocumentMetadata *ptr);\n\n/**\n * Create a `HeaderMetadata` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_header_metadata_free`.\n */\nHTMHeaderMetadata *htm_header_metadata_from_json(const char *json);\n\n/**\n * Serialize a `HeaderMetadata` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_header_metadata_to_json(const HTMHeaderMetadata *ptr);\n\n/**\n * Free a `HeaderMetadata` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_header_metadata_free(HTMHeaderMetadata *ptr);\n\n/**\n * Get the `level` field from a `HeaderMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint8_t htm_header_metadata_level(const HTMHeaderMetadata *ptr);\n\n/**\n * Get the `text` field from a `HeaderMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_header_metadata_text(const HTMHeaderMetadata *ptr);\n\n/**\n * Get the `id` field from a `HeaderMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_header_metadata_id(const HTMHeaderMetadata *ptr);\n\n/**\n * Get the `depth` field from a `HeaderMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_header_metadata_depth(const HTMHeaderMetadata *ptr);\n\n/**\n * Get the `html_offset` field from a `HeaderMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_header_metadata_html_offset(const HTMHeaderMetadata *ptr);\n\n/**\n * Validate that the header level is within valid range (1-6).\n *\n * # Returns\n *\n * `true` if level is 1-6, `false` otherwise.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::HeaderMetadata;\n * let valid = HeaderMetadata {\n *     level: 3,\n *     text: \"Title\".to_string(),\n *     id: None,\n *     depth: 2,\n *     html_offset: 100,\n * };\n * assert!(valid.is_valid());\n *\n * let invalid = HeaderMetadata {\n *     level: 7,  // Invalid\n *     text: \"Title\".to_string(),\n *     id: None,\n *     depth: 2,\n *     html_offset: 100,\n * };\n * assert!(!invalid.is_valid());\n * ```\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_header_metadata_is_valid(const HTMHeaderMetadata *this_);\n\n/**\n * Create a `LinkMetadata` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_link_metadata_free`.\n */\nHTMLinkMetadata *htm_link_metadata_from_json(const char *json);\n\n/**\n * Serialize a `LinkMetadata` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_link_metadata_to_json(const HTMLinkMetadata *ptr);\n\n/**\n * Free a `LinkMetadata` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_link_metadata_free(HTMLinkMetadata *ptr);\n\n/**\n * Get the `href` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_link_metadata_href(const HTMLinkMetadata *ptr);\n\n/**\n * Get the `text` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_link_metadata_text(const HTMLinkMetadata *ptr);\n\n/**\n * Get the `title` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_link_metadata_title(const HTMLinkMetadata *ptr);\n\n/**\n * Get the `link_type` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMLinkType *htm_link_metadata_link_type(const HTMLinkMetadata *ptr);\n\n/**\n * Get the `rel` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_link_metadata_rel(const HTMLinkMetadata *ptr);\n\n/**\n * Get the `attributes` field from a `LinkMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_link_metadata_attributes(const HTMLinkMetadata *ptr);\n\n/**\n * Classify a link based on href value.\n *\n * # Arguments\n *\n * * `href` - The href attribute value\n *\n * # Returns\n *\n * Appropriate [`LinkType`] based on protocol and content.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n * assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n * assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n * assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n * assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n * ```\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMLinkType *htm_link_metadata_classify_link(const char *href);\n\n/**\n * Create a `ImageMetadata` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_image_metadata_free`.\n */\nHTMImageMetadata *htm_image_metadata_from_json(const char *json);\n\n/**\n * Serialize a `ImageMetadata` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_image_metadata_to_json(const HTMImageMetadata *ptr);\n\n/**\n * Free a `ImageMetadata` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_image_metadata_free(HTMImageMetadata *ptr);\n\n/**\n * Get the `src` field from a `ImageMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_image_metadata_src(const HTMImageMetadata *ptr);\n\n/**\n * Get the `alt` field from a `ImageMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_image_metadata_alt(const HTMImageMetadata *ptr);\n\n/**\n * Get the `title` field from a `ImageMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_image_metadata_title(const HTMImageMetadata *ptr);\n\n/**\n * Get the `image_type` field from a `ImageMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMImageType *htm_image_metadata_image_type(const HTMImageMetadata *ptr);\n\n/**\n * Get the `attributes` field from a `ImageMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_image_metadata_attributes(const HTMImageMetadata *ptr);\n\n/**\n * Create a `StructuredData` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_structured_data_free`.\n */\nHTMStructuredData *htm_structured_data_from_json(const char *json);\n\n/**\n * Serialize a `StructuredData` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_structured_data_to_json(const HTMStructuredData *ptr);\n\n/**\n * Free a `StructuredData` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_structured_data_free(HTMStructuredData *ptr);\n\n/**\n * Get the `data_type` field from a `StructuredData`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMStructuredDataType *htm_structured_data_data_type(const HTMStructuredData *ptr);\n\n/**\n * Get the `raw_json` field from a `StructuredData`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_structured_data_raw_json(const HTMStructuredData *ptr);\n\n/**\n * Get the `schema_type` field from a `StructuredData`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_structured_data_schema_type(const HTMStructuredData *ptr);\n\n/**\n * Create a `HtmlMetadata` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_html_metadata_free`.\n */\nHTMHtmlMetadata *htm_html_metadata_from_json(const char *json);\n\n/**\n * Serialize a `HtmlMetadata` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_html_metadata_to_json(const HTMHtmlMetadata *ptr);\n\n/**\n * Free a `HtmlMetadata` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_html_metadata_free(HTMHtmlMetadata *ptr);\n\n/**\n * Get the `document` field from a `HtmlMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMDocumentMetadata *htm_html_metadata_document(const HTMHtmlMetadata *ptr);\n\n/**\n * Get the `headers` field from a `HtmlMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_html_metadata_headers(const HTMHtmlMetadata *ptr);\n\n/**\n * Get the `links` field from a `HtmlMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_html_metadata_links(const HTMHtmlMetadata *ptr);\n\n/**\n * Get the `images` field from a `HtmlMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_html_metadata_images(const HTMHtmlMetadata *ptr);\n\n/**\n * Get the `structured_data` field from a `HtmlMetadata`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_html_metadata_structured_data(const HTMHtmlMetadata *ptr);\n\n/**\n * Serialize a `ConversionOptions` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_conversion_options_to_json(const HTMConversionOptions *ptr);\n\n/**\n * Free a `ConversionOptions` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_conversion_options_free(HTMConversionOptions *ptr);\n\n/**\n * Get the `heading_style` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMHeadingStyle *htm_conversion_options_heading_style(const HTMConversionOptions *ptr);\n\n/**\n * Get the `list_indent_type` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMListIndentType *htm_conversion_options_list_indent_type(const HTMConversionOptions *ptr);\n\n/**\n * Get the `list_indent_width` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_list_indent_width(const HTMConversionOptions *ptr);\n\n/**\n * Get the `bullets` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_bullets(const HTMConversionOptions *ptr);\n\n/**\n * Get the `strong_em_symbol` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_strong_em_symbol(const HTMConversionOptions *ptr);\n\n/**\n * Get the `escape_asterisks` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_escape_asterisks(const HTMConversionOptions *ptr);\n\n/**\n * Get the `escape_underscores` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_escape_underscores(const HTMConversionOptions *ptr);\n\n/**\n * Get the `escape_misc` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_escape_misc(const HTMConversionOptions *ptr);\n\n/**\n * Get the `escape_ascii` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_escape_ascii(const HTMConversionOptions *ptr);\n\n/**\n * Get the `code_language` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_code_language(const HTMConversionOptions *ptr);\n\n/**\n * Get the `autolinks` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_autolinks(const HTMConversionOptions *ptr);\n\n/**\n * Get the `default_title` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_default_title(const HTMConversionOptions *ptr);\n\n/**\n * Get the `br_in_tables` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_br_in_tables(const HTMConversionOptions *ptr);\n\n/**\n * Get the `highlight_style` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMHighlightStyle *htm_conversion_options_highlight_style(const HTMConversionOptions *ptr);\n\n/**\n * Get the `extract_metadata` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_extract_metadata(const HTMConversionOptions *ptr);\n\n/**\n * Get the `whitespace_mode` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMWhitespaceMode *htm_conversion_options_whitespace_mode(const HTMConversionOptions *ptr);\n\n/**\n * Get the `strip_newlines` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_strip_newlines(const HTMConversionOptions *ptr);\n\n/**\n * Get the `wrap` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_wrap(const HTMConversionOptions *ptr);\n\n/**\n * Get the `wrap_width` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_wrap_width(const HTMConversionOptions *ptr);\n\n/**\n * Get the `convert_as_inline` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_convert_as_inline(const HTMConversionOptions *ptr);\n\n/**\n * Get the `sub_symbol` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_sub_symbol(const HTMConversionOptions *ptr);\n\n/**\n * Get the `sup_symbol` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_sup_symbol(const HTMConversionOptions *ptr);\n\n/**\n * Get the `newline_style` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMNewlineStyle *htm_conversion_options_newline_style(const HTMConversionOptions *ptr);\n\n/**\n * Get the `code_block_style` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMCodeBlockStyle *htm_conversion_options_code_block_style(const HTMConversionOptions *ptr);\n\n/**\n * Get the `keep_inline_images_in` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_keep_inline_images_in(const HTMConversionOptions *ptr);\n\n/**\n * Get the `preprocessing` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMPreprocessingOptions *htm_conversion_options_preprocessing(const HTMConversionOptions *ptr);\n\n/**\n * Get the `encoding` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_encoding(const HTMConversionOptions *ptr);\n\n/**\n * Get the `debug` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_debug(const HTMConversionOptions *ptr);\n\n/**\n * Get the `strip_tags` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_strip_tags(const HTMConversionOptions *ptr);\n\n/**\n * Get the `preserve_tags` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_preserve_tags(const HTMConversionOptions *ptr);\n\n/**\n * Get the `skip_images` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_skip_images(const HTMConversionOptions *ptr);\n\n/**\n * Get the `link_style` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMLinkStyle *htm_conversion_options_link_style(const HTMConversionOptions *ptr);\n\n/**\n * Get the `output_format` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMOutputFormat *htm_conversion_options_output_format(const HTMConversionOptions *ptr);\n\n/**\n * Get the `include_document_structure` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_include_document_structure(const HTMConversionOptions *ptr);\n\n/**\n * Get the `extract_images` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_extract_images(const HTMConversionOptions *ptr);\n\n/**\n * Get the `max_image_size` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint64_t htm_conversion_options_max_image_size(const HTMConversionOptions *ptr);\n\n/**\n * Get the `capture_svg` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_capture_svg(const HTMConversionOptions *ptr);\n\n/**\n * Get the `infer_dimensions` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_infer_dimensions(const HTMConversionOptions *ptr);\n\n/**\n * Get the `max_depth` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_max_depth(const HTMConversionOptions *ptr);\n\n/**\n * Get the `exclude_selectors` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_exclude_selectors(const HTMConversionOptions *ptr);\n\n/**\n * Get the `visitor` field from a `ConversionOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMVisitorHandle *htm_conversion_options_visitor(const HTMConversionOptions *ptr);\n\n/**\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptions *htm_conversion_options_default(void);\n\n/**\n * Create a new builder with default values.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder(void);\n\n/**\n * Apply a partial update to these conversion options.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nvoid htm_conversion_options_apply_update(HTMConversionOptions *this_,\n                                         const HTMConversionOptionsUpdate *update);\n\n/**\n * Create from a partial update, applying to defaults.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptions *htm_conversion_options_from_update(const HTMConversionOptionsUpdate *update);\n\n/**\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptions *htm_conversion_options_from(const HTMConversionOptionsUpdate *update);\n\n/**\n * Free a `ConversionOptionsBuilder` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_conversion_options_builder_free(HTMConversionOptionsBuilder *ptr);\n\n/**\n * Set the list of HTML tag names whose content is stripped from output.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_strip_tags(HTMConversionOptionsBuilder *this_,\n                                                                       const char *tags);\n\n/**\n * Set the list of HTML tag names that are preserved verbatim in output.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_preserve_tags(HTMConversionOptionsBuilder *this_,\n                                                                          const char *tags);\n\n/**\n * Set the list of HTML tag names whose `<img>` children are kept inline.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_keep_inline_images_in(HTMConversionOptionsBuilder *this_,\n                                                                                  const char *tags);\n\n/**\n * Set the list of CSS selectors for elements to exclude entirely from output.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_exclude_selectors(HTMConversionOptionsBuilder *this_,\n                                                                              const char *selectors);\n\n/**\n * Set the visitor used during conversion.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_visitor(HTMConversionOptionsBuilder *this_,\n                                                                    const HTMVisitorHandle *visitor);\n\n/**\n * Set the pre-processing options applied to the HTML before conversion.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptionsBuilder *htm_conversion_options_builder_preprocessing(HTMConversionOptionsBuilder *this_,\n                                                                          const HTMPreprocessingOptions *preprocessing);\n\n/**\n * Build the final [`ConversionOptions`].\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMConversionOptions *htm_conversion_options_builder_build(HTMConversionOptionsBuilder *this_);\n\n/**\n * Create a `ConversionOptionsUpdate` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_conversion_options_update_free`.\n */\nHTMConversionOptionsUpdate *htm_conversion_options_update_from_json(const char *json);\n\n/**\n * Free a `ConversionOptionsUpdate` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_conversion_options_update_free(HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `heading_style` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMHeadingStyle *htm_conversion_options_update_heading_style(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `list_indent_type` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMListIndentType *htm_conversion_options_update_list_indent_type(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `list_indent_width` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_update_list_indent_width(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `bullets` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_bullets(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `strong_em_symbol` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_strong_em_symbol(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `escape_asterisks` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_escape_asterisks(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `escape_underscores` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_escape_underscores(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `escape_misc` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_escape_misc(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `escape_ascii` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_escape_ascii(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `code_language` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_code_language(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `autolinks` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_autolinks(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `default_title` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_default_title(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `br_in_tables` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_br_in_tables(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `highlight_style` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMHighlightStyle *htm_conversion_options_update_highlight_style(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `extract_metadata` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_extract_metadata(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `whitespace_mode` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMWhitespaceMode *htm_conversion_options_update_whitespace_mode(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `strip_newlines` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_strip_newlines(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `wrap` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_wrap(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `wrap_width` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_update_wrap_width(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `convert_as_inline` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_convert_as_inline(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `sub_symbol` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_sub_symbol(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `sup_symbol` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_sup_symbol(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `newline_style` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMNewlineStyle *htm_conversion_options_update_newline_style(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `code_block_style` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMCodeBlockStyle *htm_conversion_options_update_code_block_style(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `keep_inline_images_in` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_keep_inline_images_in(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `preprocessing` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMPreprocessingOptionsUpdate *htm_conversion_options_update_preprocessing(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `encoding` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_encoding(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `debug` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_debug(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `strip_tags` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_strip_tags(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `preserve_tags` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_preserve_tags(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `skip_images` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_skip_images(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `link_style` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMLinkStyle *htm_conversion_options_update_link_style(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `output_format` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMOutputFormat *htm_conversion_options_update_output_format(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `include_document_structure` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_include_document_structure(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `extract_images` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_extract_images(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `max_image_size` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint64_t htm_conversion_options_update_max_image_size(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `capture_svg` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_capture_svg(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `infer_dimensions` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_conversion_options_update_infer_dimensions(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `max_depth` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_conversion_options_update_max_depth(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `exclude_selectors` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_options_update_exclude_selectors(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Get the `visitor` field from a `ConversionOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMVisitorHandle *htm_conversion_options_update_visitor(const HTMConversionOptionsUpdate *ptr);\n\n/**\n * Create a `PreprocessingOptions` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_preprocessing_options_free`.\n */\nHTMPreprocessingOptions *htm_preprocessing_options_from_json(const char *json);\n\n/**\n * Serialize a `PreprocessingOptions` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_preprocessing_options_to_json(const HTMPreprocessingOptions *ptr);\n\n/**\n * Free a `PreprocessingOptions` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_preprocessing_options_free(HTMPreprocessingOptions *ptr);\n\n/**\n * Get the `enabled` field from a `PreprocessingOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_enabled(const HTMPreprocessingOptions *ptr);\n\n/**\n * Get the `preset` field from a `PreprocessingOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMPreprocessingPreset *htm_preprocessing_options_preset(const HTMPreprocessingOptions *ptr);\n\n/**\n * Get the `remove_navigation` field from a `PreprocessingOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_remove_navigation(const HTMPreprocessingOptions *ptr);\n\n/**\n * Get the `remove_forms` field from a `PreprocessingOptions`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_remove_forms(const HTMPreprocessingOptions *ptr);\n\n/**\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMPreprocessingOptions *htm_preprocessing_options_default(void);\n\n/**\n * Apply a partial update to these preprocessing options.\n *\n * Any specified fields in the update will override the current values.\n * Unspecified fields (None) are left unchanged.\n *\n * # Arguments\n *\n * * `update` - Partial preprocessing options update\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nvoid htm_preprocessing_options_apply_update(HTMPreprocessingOptions *this_,\n                                            const HTMPreprocessingOptionsUpdate *update);\n\n/**\n * Create new preprocessing options from a partial update.\n *\n * Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n * Fields not specified in the update keep their default values.\n *\n * # Arguments\n *\n * * `update` - Partial preprocessing options update\n *\n * # Returns\n *\n * New `PreprocessingOptions` with specified updates applied to defaults\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMPreprocessingOptions *htm_preprocessing_options_from_update(const HTMPreprocessingOptionsUpdate *update);\n\n/**\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nHTMPreprocessingOptions *htm_preprocessing_options_from(const HTMPreprocessingOptionsUpdate *update);\n\n/**\n * Create a `PreprocessingOptionsUpdate` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_preprocessing_options_update_free`.\n */\nHTMPreprocessingOptionsUpdate *htm_preprocessing_options_update_from_json(const char *json);\n\n/**\n * Free a `PreprocessingOptionsUpdate` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_preprocessing_options_update_free(HTMPreprocessingOptionsUpdate *ptr);\n\n/**\n * Get the `enabled` field from a `PreprocessingOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_update_enabled(const HTMPreprocessingOptionsUpdate *ptr);\n\n/**\n * Get the `preset` field from a `PreprocessingOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMPreprocessingPreset *htm_preprocessing_options_update_preset(const HTMPreprocessingOptionsUpdate *ptr);\n\n/**\n * Get the `remove_navigation` field from a `PreprocessingOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_update_remove_navigation(const HTMPreprocessingOptionsUpdate *ptr);\n\n/**\n * Get the `remove_forms` field from a `PreprocessingOptionsUpdate`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_preprocessing_options_update_remove_forms(const HTMPreprocessingOptionsUpdate *ptr);\n\n/**\n * Create a `DocumentStructure` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_document_structure_free`.\n */\nHTMDocumentStructure *htm_document_structure_from_json(const char *json);\n\n/**\n * Serialize a `DocumentStructure` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_document_structure_to_json(const HTMDocumentStructure *ptr);\n\n/**\n * Free a `DocumentStructure` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_document_structure_free(HTMDocumentStructure *ptr);\n\n/**\n * Get the `nodes` field from a `DocumentStructure`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_structure_nodes(const HTMDocumentStructure *ptr);\n\n/**\n * Get the `source_format` field from a `DocumentStructure`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_structure_source_format(const HTMDocumentStructure *ptr);\n\n/**\n * Create a `DocumentNode` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_document_node_free`.\n */\nHTMDocumentNode *htm_document_node_from_json(const char *json);\n\n/**\n * Serialize a `DocumentNode` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_document_node_to_json(const HTMDocumentNode *ptr);\n\n/**\n * Free a `DocumentNode` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_document_node_free(HTMDocumentNode *ptr);\n\n/**\n * Get the `id` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_node_id(const HTMDocumentNode *ptr);\n\n/**\n * Get the `content` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMNodeContent *htm_document_node_content(const HTMDocumentNode *ptr);\n\n/**\n * Get the `parent` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_document_node_parent(const HTMDocumentNode *ptr);\n\n/**\n * Get the `children` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_node_children(const HTMDocumentNode *ptr);\n\n/**\n * Get the `annotations` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_node_annotations(const HTMDocumentNode *ptr);\n\n/**\n * Get the `attributes` field from a `DocumentNode`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_document_node_attributes(const HTMDocumentNode *ptr);\n\n/**\n * Create a `TextAnnotation` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_text_annotation_free`.\n */\nHTMTextAnnotation *htm_text_annotation_from_json(const char *json);\n\n/**\n * Serialize a `TextAnnotation` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_text_annotation_to_json(const HTMTextAnnotation *ptr);\n\n/**\n * Free a `TextAnnotation` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_text_annotation_free(HTMTextAnnotation *ptr);\n\n/**\n * Get the `start` field from a `TextAnnotation`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_text_annotation_start(const HTMTextAnnotation *ptr);\n\n/**\n * Get the `end` field from a `TextAnnotation`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_text_annotation_end(const HTMTextAnnotation *ptr);\n\n/**\n * Get the `kind` field from a `TextAnnotation`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMAnnotationKind *htm_text_annotation_kind(const HTMTextAnnotation *ptr);\n\n/**\n * Create a `ConversionResult` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_conversion_result_free`.\n */\nHTMConversionResult *htm_conversion_result_from_json(const char *json);\n\n/**\n * Serialize a `ConversionResult` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_conversion_result_to_json(const HTMConversionResult *ptr);\n\n/**\n * Free a `ConversionResult` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_conversion_result_free(HTMConversionResult *ptr);\n\n/**\n * Get the `content` field from a `ConversionResult`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_result_content(const HTMConversionResult *ptr);\n\n/**\n * Get the `document` field from a `ConversionResult`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMDocumentStructure *htm_conversion_result_document(const HTMConversionResult *ptr);\n\n/**\n * Get the `metadata` field from a `ConversionResult`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMHtmlMetadata *htm_conversion_result_metadata(const HTMConversionResult *ptr);\n\n/**\n * Get the `tables` field from a `ConversionResult`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_result_tables(const HTMConversionResult *ptr);\n\n/**\n * Get the `warnings` field from a `ConversionResult`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_conversion_result_warnings(const HTMConversionResult *ptr);\n\n/**\n * Create a `TableGrid` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_table_grid_free`.\n */\nHTMTableGrid *htm_table_grid_from_json(const char *json);\n\n/**\n * Serialize a `TableGrid` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_table_grid_to_json(const HTMTableGrid *ptr);\n\n/**\n * Free a `TableGrid` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_table_grid_free(HTMTableGrid *ptr);\n\n/**\n * Get the `rows` field from a `TableGrid`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_table_grid_rows(const HTMTableGrid *ptr);\n\n/**\n * Get the `cols` field from a `TableGrid`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_table_grid_cols(const HTMTableGrid *ptr);\n\n/**\n * Get the `cells` field from a `TableGrid`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_table_grid_cells(const HTMTableGrid *ptr);\n\n/**\n * Create a `GridCell` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_grid_cell_free`.\n */\nHTMGridCell *htm_grid_cell_from_json(const char *json);\n\n/**\n * Serialize a `GridCell` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_grid_cell_to_json(const HTMGridCell *ptr);\n\n/**\n * Free a `GridCell` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_grid_cell_free(HTMGridCell *ptr);\n\n/**\n * Get the `content` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_grid_cell_content(const HTMGridCell *ptr);\n\n/**\n * Get the `row` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_grid_cell_row(const HTMGridCell *ptr);\n\n/**\n * Get the `col` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_grid_cell_col(const HTMGridCell *ptr);\n\n/**\n * Get the `row_span` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_grid_cell_row_span(const HTMGridCell *ptr);\n\n/**\n * Get the `col_span` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuint32_t htm_grid_cell_col_span(const HTMGridCell *ptr);\n\n/**\n * Get the `is_header` field from a `GridCell`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_grid_cell_is_header(const HTMGridCell *ptr);\n\n/**\n * Create a `TableData` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_table_data_free`.\n */\nHTMTableData *htm_table_data_from_json(const char *json);\n\n/**\n * Serialize a `TableData` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_table_data_to_json(const HTMTableData *ptr);\n\n/**\n * Free a `TableData` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_table_data_free(HTMTableData *ptr);\n\n/**\n * Get the `grid` field from a `TableData`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMTableGrid *htm_table_data_grid(const HTMTableData *ptr);\n\n/**\n * Get the `markdown` field from a `TableData`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_table_data_markdown(const HTMTableData *ptr);\n\n/**\n * Create a `ProcessingWarning` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_processing_warning_free`.\n */\nHTMProcessingWarning *htm_processing_warning_from_json(const char *json);\n\n/**\n * Serialize a `ProcessingWarning` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_processing_warning_to_json(const HTMProcessingWarning *ptr);\n\n/**\n * Free a `ProcessingWarning` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_processing_warning_free(HTMProcessingWarning *ptr);\n\n/**\n * Get the `message` field from a `ProcessingWarning`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_processing_warning_message(const HTMProcessingWarning *ptr);\n\n/**\n * Get the `kind` field from a `ProcessingWarning`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMWarningKind *htm_processing_warning_kind(const HTMProcessingWarning *ptr);\n\n/**\n * Free a `VisitorHandle` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_visitor_handle_free(HTMVisitorHandle *ptr);\n\n/**\n * Create a `NodeContext` from a JSON string. Returns null on failure.\n * # Safety\n * JSON string must be valid UTF-8 and null-terminated.\n * Returned handle must be freed with `htm_node_context_free`.\n */\nHTMNodeContext *htm_node_context_from_json(const char *json);\n\n/**\n * Serialize a `NodeContext` to a JSON string. Returns null on failure.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_node_context_to_json(const HTMNodeContext *ptr);\n\n/**\n * Free a `NodeContext` handle.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_node_context_free(HTMNodeContext *ptr);\n\n/**\n * Get the `node_type` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nHTMNodeType *htm_node_context_node_type(const HTMNodeContext *ptr);\n\n/**\n * Get the `tag_name` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_node_context_tag_name(const HTMNodeContext *ptr);\n\n/**\n * Get the `attributes` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_node_context_attributes(const HTMNodeContext *ptr);\n\n/**\n * Get the `depth` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_node_context_depth(const HTMNodeContext *ptr);\n\n/**\n * Get the `index_in_parent` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nuintptr_t htm_node_context_index_in_parent(const HTMNodeContext *ptr);\n\n/**\n * Get the `parent_tag` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nchar *htm_node_context_parent_tag(const HTMNodeContext *ptr);\n\n/**\n * Get the `is_inline` field from a `NodeContext`.\n * # Safety\n * Pointer must be a valid handle returned by this library.\n */\nint32_t htm_node_context_is_inline(const HTMNodeContext *ptr);\n\n/**\n * Convert an integer to a `TextDirection` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_text_direction_from_i32(int32_t value);\n\n/**\n * Convert a `TextDirection` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_text_direction_from_str(const char *name);\n\n/**\n * Convert an integer to a `LinkType` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_link_type_from_i32(int32_t value);\n\n/**\n * Convert a `LinkType` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_link_type_from_str(const char *name);\n\n/**\n * Convert an integer to a `ImageType` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_image_type_from_i32(int32_t value);\n\n/**\n * Convert a `ImageType` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_image_type_from_str(const char *name);\n\n/**\n * Convert an integer to a `StructuredDataType` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_structured_data_type_from_i32(int32_t value);\n\n/**\n * Convert a `StructuredDataType` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_structured_data_type_from_str(const char *name);\n\n/**\n * Convert an integer to a `PreprocessingPreset` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_preprocessing_preset_from_i32(int32_t value);\n\n/**\n * Convert a `PreprocessingPreset` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_preprocessing_preset_from_str(const char *name);\n\n/**\n * Convert an integer to a `HeadingStyle` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_heading_style_from_i32(int32_t value);\n\n/**\n * Convert a `HeadingStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_heading_style_from_str(const char *name);\n\n/**\n * Convert an integer to a `ListIndentType` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_list_indent_type_from_i32(int32_t value);\n\n/**\n * Convert a `ListIndentType` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_list_indent_type_from_str(const char *name);\n\n/**\n * Convert an integer to a `WhitespaceMode` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_whitespace_mode_from_i32(int32_t value);\n\n/**\n * Convert a `WhitespaceMode` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_whitespace_mode_from_str(const char *name);\n\n/**\n * Convert an integer to a `NewlineStyle` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_newline_style_from_i32(int32_t value);\n\n/**\n * Convert a `NewlineStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_newline_style_from_str(const char *name);\n\n/**\n * Convert an integer to a `CodeBlockStyle` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_code_block_style_from_i32(int32_t value);\n\n/**\n * Convert a `CodeBlockStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_code_block_style_from_str(const char *name);\n\n/**\n * Convert an integer to a `HighlightStyle` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_highlight_style_from_i32(int32_t value);\n\n/**\n * Convert a `HighlightStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_highlight_style_from_str(const char *name);\n\n/**\n * Convert an integer to a `LinkStyle` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_link_style_from_i32(int32_t value);\n\n/**\n * Convert a `LinkStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_link_style_from_str(const char *name);\n\n/**\n * Convert an integer to a `OutputFormat` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_output_format_from_i32(int32_t value);\n\n/**\n * Convert a `OutputFormat` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_output_format_from_str(const char *name);\n\n/**\n * Convert an integer to a `NodeContent` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_node_content_from_i32(int32_t value);\n\n/**\n * Convert a `NodeContent` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_node_content_from_str(const char *name);\n\n/**\n * Convert an integer to a `AnnotationKind` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_annotation_kind_from_i32(int32_t value);\n\n/**\n * Convert a `AnnotationKind` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_annotation_kind_from_str(const char *name);\n\n/**\n * Convert an integer to a `WarningKind` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_warning_kind_from_i32(int32_t value);\n\n/**\n * Convert a `WarningKind` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_warning_kind_from_str(const char *name);\n\n/**\n * Convert an integer to a `NodeType` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_node_type_from_i32(int32_t value);\n\n/**\n * Convert a `NodeType` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_node_type_from_str(const char *name);\n\n/**\n * Convert an integer to a `VisitResult` variant. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure all pointer arguments are valid or null.\n * Returned pointers must be freed with the appropriate free function.\n */\nint32_t htm_visit_result_from_i32(int32_t value);\n\n/**\n * Convert a `VisitResult` variant name (C string) to its integer value. Returns -1 on invalid input.\n * # Safety\n * Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n */\nint32_t htm_visit_result_from_str(const char *name);\n\n/**\n * Free a heap-allocated `LinkType` returned by a pointer-returning FFI function.\n * # Safety\n * Pointer must have been returned by this library, or be null.\n */\nvoid htm_link_type_free(HTMLinkType *ptr);\n\n/**\n * Serialize a heap-allocated `LinkType` to a JSON string.\n * # Safety\n * `ptr` must be a valid, non-null pointer returned by a `htm` function.\n * The returned string must be freed with `htm_free_string`.\n */\nchar *htm_link_type_to_json(const HTMLinkType *ptr);\n\n/**\n * Attach a vtable visitor bridge to a `ConversionOptions` options struct.\n *\n * The `HtmHtmlVisitorBridge` encapsulates a set of C function pointers that receive visit\n * callbacks during HTML-to-Markdown conversion.  Call this setter before `htm_convert`\n * to activate visitor callbacks.  Pass `visitor = null` to clear a previously attached visitor.\n *\n * Neither pointer is consumed: the caller retains ownership of both `options` and `visitor`\n * and must free them independently after conversion completes.\n *\n * # Safety\n *\n * `options` must be a non-null pointer returned by `htm_conversion_options_new` (or\n * equivalent), valid for write access.  `visitor` must be a non-null pointer returned by\n * `htm_htm_html_visitor_bridge_new`, or null.  Both must remain valid for the duration of any\n * subsequent `htm_convert` call.\n */\nvoid htm_options_set_visitor(HTMConversionOptions *options,\n                             struct HTMHtmHtmlVisitorBridge *visitor);\n\n/**\n * Convert HTML to Markdown.\n *\n * Returns a heap-allocated [`ConversionResult`] on success, or null on failure.\n * Check `htm_last_error_code` / `htm_last_error_context` for error details.\n * The returned pointer must be freed with `htm_conversion_result_free`.\n *\n * If a visitor was attached to `options` via `htm_options_set_visitor`, it will\n * receive callbacks during conversion.\n *\n * # Arguments\n *\n * - `html`: null-terminated, UTF-8 HTML input. Must not be null.\n * - `options`: optional conversion options (with optional embedded visitor); pass null for defaults.\n *\n * # Safety\n *\n * `html` must be a valid, non-null, null-terminated UTF-8 string.\n * `options` must be a valid pointer or null.\n * Returned pointer must be freed with `htm_conversion_result_free`.\n */\nHTMConversionResult *htm_convert(const char *html,\n                                 const HTMConversionOptions *options);\n\n#endif  /* HTM_H */\n"
  },
  {
    "path": "crates/html-to-markdown-ffi/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:9c01874ed3f71b3ae6f1c20b29e56a0a0af9ca6ba6a1966818c87aa4dcd1119a\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables, unused_mut)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::redundant_locals,\n    dropping_references,\n    clippy::unnecessary_cast,\n    clippy::unused_unit,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions,\n    clippy::useless_conversion\n)]\n\nuse std::cell::RefCell;\nuse std::ffi::c_void;\nuse std::ffi::{c_char, CStr, CString};\nuse std::sync::Arc;\n\nthread_local! {\n    static LAST_ERROR_CODE: RefCell<i32> = const { RefCell::new(0) };\n    static LAST_ERROR_CONTEXT: RefCell<Option<CString>> = const { RefCell::new(None) };\n}\n\nfn set_last_error(code: i32, message: &str) {\n    LAST_ERROR_CODE.with_borrow_mut(|c| *c = code);\n    LAST_ERROR_CONTEXT.with_borrow_mut(|c| *c = CString::new(message).ok());\n}\n\nfn clear_last_error() {\n    LAST_ERROR_CODE.with_borrow_mut(|c| *c = 0);\n    LAST_ERROR_CONTEXT.with_borrow_mut(|c| *c = None);\n}\n\n/// Return the last error code (0 means no error).\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_last_error_code() -> i32 {\n    LAST_ERROR_CODE.with_borrow(|c| *c)\n}\n\n/// Return the last error message. The pointer is valid until the next FFI call on this thread.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_last_error_context() -> *const c_char {\n    LAST_ERROR_CONTEXT.with_borrow(|ctx| ctx.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()))\n}\n\n/// Free a string previously returned by this library.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_free_string(ptr: *mut c_char) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by CString::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(CString::from_raw(ptr));\n        }\n    }\n}\n\n/// Return the library version string. The pointer is static and must NOT be freed.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_version() -> *const c_char {\n    static VERSION: &str = concat!(env!(\"CARGO_PKG_VERSION\"), \"\\0\");\n    VERSION.as_ptr() as *const c_char\n}\n\n/// Create a `DocumentMetadata` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_document_metadata_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::DocumentMetadata {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::DocumentMetadata>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `DocumentMetadata` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_to_json(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `DocumentMetadata` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_free(ptr: *mut html_to_markdown_rs::metadata::DocumentMetadata) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `title` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_title(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.title {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `description` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_description(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.description {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `keywords` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_keywords(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.keywords) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `author` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_author(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.author {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `canonical_url` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_canonical_url(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.canonical_url {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `base_href` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_base_href(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.base_href {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `language` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_language(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.language {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `text_direction` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_text_direction(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut html_to_markdown_rs::metadata::TextDirection {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.text_direction {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `open_graph` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_open_graph(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.open_graph) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `twitter_card` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_twitter_card(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.twitter_card) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `meta_tags` field from a `DocumentMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_metadata_meta_tags(\n    ptr: *const html_to_markdown_rs::metadata::DocumentMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.meta_tags) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `HeaderMetadata` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_header_metadata_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::HeaderMetadata {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::HeaderMetadata>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `HeaderMetadata` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_to_json(\n    ptr: *const html_to_markdown_rs::metadata::HeaderMetadata,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `HeaderMetadata` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_free(ptr: *mut html_to_markdown_rs::metadata::HeaderMetadata) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `level` field from a `HeaderMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_level(ptr: *const html_to_markdown_rs::metadata::HeaderMetadata) -> u8 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.level\n}\n\n/// Get the `text` field from a `HeaderMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_text(\n    ptr: *const html_to_markdown_rs::metadata::HeaderMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.text.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `id` field from a `HeaderMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_id(\n    ptr: *const html_to_markdown_rs::metadata::HeaderMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.id {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `depth` field from a `HeaderMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_depth(ptr: *const html_to_markdown_rs::metadata::HeaderMetadata) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.depth\n}\n\n/// Get the `html_offset` field from a `HeaderMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_html_offset(\n    ptr: *const html_to_markdown_rs::metadata::HeaderMetadata,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.html_offset\n}\n\n/// Validate that the header level is within valid range (1-6).\n///\n/// # Returns\n///\n/// `true` if level is 1-6, `false` otherwise.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HeaderMetadata;\n/// let valid = HeaderMetadata {\n///     level: 3,\n///     text: \"Title\".to_string(),\n///     id: None,\n///     depth: 2,\n///     html_offset: 100,\n/// };\n/// assert!(valid.is_valid());\n///\n/// let invalid = HeaderMetadata {\n///     level: 7,  // Invalid\n///     text: \"Title\".to_string(),\n///     id: None,\n///     depth: 2,\n///     html_offset: 100,\n/// };\n/// assert!(!invalid.is_valid());\n/// ```\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_header_metadata_is_valid(\n    this: *const html_to_markdown_rs::metadata::HeaderMetadata,\n) -> i32 {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return 0;\n    }\n    // SAFETY: null check above guarantees this is a valid pointer.\n    let obj = unsafe { &*this };\n    let result = obj.is_valid();\n    if result {\n        1\n    } else {\n        0\n    }\n}\n\n/// Create a `LinkMetadata` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_link_metadata_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::LinkMetadata {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::LinkMetadata>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `LinkMetadata` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_to_json(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `LinkMetadata` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_free(ptr: *mut html_to_markdown_rs::metadata::LinkMetadata) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `href` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_href(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.href.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `text` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_text(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.text.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `title` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_title(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.title {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `link_type` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_link_type(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut html_to_markdown_rs::metadata::LinkType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.link_type))\n}\n\n/// Get the `rel` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_rel(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.rel) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `attributes` field from a `LinkMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_attributes(\n    ptr: *const html_to_markdown_rs::metadata::LinkMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.attributes) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Classify a link based on href value.\n///\n/// # Arguments\n///\n/// * `href` - The href attribute value\n///\n/// # Returns\n///\n/// Appropriate [`LinkType`] based on protocol and content.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n/// assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n/// assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n/// assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n/// assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n/// ```\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_metadata_classify_link(\n    href: *const std::ffi::c_char,\n) -> *mut html_to_markdown_rs::metadata::LinkType {\n    clear_last_error();\n    if href.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'href'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees href is a valid pointer; string is valid UTF-8 from caller.\n    let href_rs = match unsafe { CStr::from_ptr(href) }.to_str() {\n        Ok(s) => s.to_string(),\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in parameter 'href'\");\n            return std::ptr::null_mut();\n        }\n    };\n    let result = html_to_markdown_rs::metadata::LinkMetadata::classify_link(&href_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Create a `ImageMetadata` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_image_metadata_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::ImageMetadata {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::ImageMetadata>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `ImageMetadata` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_to_json(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `ImageMetadata` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_free(ptr: *mut html_to_markdown_rs::metadata::ImageMetadata) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `src` field from a `ImageMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_src(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.src.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `alt` field from a `ImageMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_alt(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.alt {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `title` field from a `ImageMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_title(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.title {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `image_type` field from a `ImageMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_image_type(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut html_to_markdown_rs::metadata::ImageType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.image_type))\n}\n\n/// Get the `attributes` field from a `ImageMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_metadata_attributes(\n    ptr: *const html_to_markdown_rs::metadata::ImageMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.attributes) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `StructuredData` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_structured_data_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::StructuredData {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::StructuredData>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `StructuredData` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_to_json(\n    ptr: *const html_to_markdown_rs::metadata::StructuredData,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `StructuredData` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_free(ptr: *mut html_to_markdown_rs::metadata::StructuredData) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `data_type` field from a `StructuredData`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_data_type(\n    ptr: *const html_to_markdown_rs::metadata::StructuredData,\n) -> *mut html_to_markdown_rs::metadata::StructuredDataType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.data_type))\n}\n\n/// Get the `raw_json` field from a `StructuredData`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_raw_json(\n    ptr: *const html_to_markdown_rs::metadata::StructuredData,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.raw_json.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `schema_type` field from a `StructuredData`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_schema_type(\n    ptr: *const html_to_markdown_rs::metadata::StructuredData,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.schema_type {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `HtmlMetadata` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_html_metadata_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::metadata::HtmlMetadata {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::metadata::HtmlMetadata>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `HtmlMetadata` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_to_json(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `HtmlMetadata` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_free(ptr: *mut html_to_markdown_rs::metadata::HtmlMetadata) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `document` field from a `HtmlMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_document(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut html_to_markdown_rs::metadata::DocumentMetadata {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.document.clone()))\n}\n\n/// Get the `headers` field from a `HtmlMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_headers(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.headers) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `links` field from a `HtmlMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_links(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.links) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `images` field from a `HtmlMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_images(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.images) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `structured_data` field from a `HtmlMetadata`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_html_metadata_structured_data(\n    ptr: *const html_to_markdown_rs::metadata::HtmlMetadata,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.structured_data) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Serialize a `ConversionOptions` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_to_json(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `ConversionOptions` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_free(ptr: *mut html_to_markdown_rs::options::ConversionOptions) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `heading_style` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_heading_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::HeadingStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.heading_style))\n}\n\n/// Get the `list_indent_type` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_list_indent_type(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::ListIndentType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.list_indent_type))\n}\n\n/// Get the `list_indent_width` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_list_indent_width(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.list_indent_width\n}\n\n/// Get the `bullets` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_bullets(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.bullets.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `strong_em_symbol` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_strong_em_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.strong_em_symbol.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `escape_asterisks` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_escape_asterisks(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.escape_asterisks as i32\n}\n\n/// Get the `escape_underscores` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_escape_underscores(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.escape_underscores as i32\n}\n\n/// Get the `escape_misc` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_escape_misc(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.escape_misc as i32\n}\n\n/// Get the `escape_ascii` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_escape_ascii(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.escape_ascii as i32\n}\n\n/// Get the `code_language` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_code_language(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.code_language.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `autolinks` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_autolinks(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.autolinks as i32\n}\n\n/// Get the `default_title` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_default_title(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.default_title as i32\n}\n\n/// Get the `br_in_tables` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_br_in_tables(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.br_in_tables as i32\n}\n\n/// Get the `highlight_style` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_highlight_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::HighlightStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.highlight_style))\n}\n\n/// Get the `extract_metadata` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_extract_metadata(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.extract_metadata as i32\n}\n\n/// Get the `whitespace_mode` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_whitespace_mode(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::WhitespaceMode {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.whitespace_mode))\n}\n\n/// Get the `strip_newlines` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_strip_newlines(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.strip_newlines as i32\n}\n\n/// Get the `wrap` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_wrap(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.wrap as i32\n}\n\n/// Get the `wrap_width` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_wrap_width(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.wrap_width\n}\n\n/// Get the `convert_as_inline` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_convert_as_inline(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.convert_as_inline as i32\n}\n\n/// Get the `sub_symbol` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_sub_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.sub_symbol.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `sup_symbol` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_sup_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.sup_symbol.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `newline_style` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_newline_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::NewlineStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.newline_style))\n}\n\n/// Get the `code_block_style` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_code_block_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::CodeBlockStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.code_block_style))\n}\n\n/// Get the `keep_inline_images_in` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_keep_inline_images_in(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.keep_inline_images_in) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `preprocessing` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_preprocessing(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptions {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.preprocessing.clone()))\n}\n\n/// Get the `encoding` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_encoding(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.encoding.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `debug` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_debug(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.debug as i32\n}\n\n/// Get the `strip_tags` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_strip_tags(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.strip_tags) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `preserve_tags` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_preserve_tags(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.preserve_tags) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `skip_images` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_skip_images(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.skip_images as i32\n}\n\n/// Get the `link_style` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_link_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::LinkStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.link_style))\n}\n\n/// Get the `output_format` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_output_format(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::options::OutputFormat {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.output_format))\n}\n\n/// Get the `include_document_structure` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_include_document_structure(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.include_document_structure as i32\n}\n\n/// Get the `extract_images` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_extract_images(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.extract_images as i32\n}\n\n/// Get the `max_image_size` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_max_image_size(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> u64 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.max_image_size\n}\n\n/// Get the `capture_svg` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_capture_svg(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.capture_svg as i32\n}\n\n/// Get the `infer_dimensions` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_infer_dimensions(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.infer_dimensions as i32\n}\n\n/// Get the `max_depth` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_max_depth(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.max_depth {\n        Some(val) => *val,\n        None => 0,\n    }\n}\n\n/// Get the `exclude_selectors` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_exclude_selectors(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.exclude_selectors) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `visitor` field from a `ConversionOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_visitor(\n    ptr: *const html_to_markdown_rs::options::ConversionOptions,\n) -> *mut html_to_markdown_rs::visitor::VisitorHandle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.visitor {\n        Some(val) => val as *const _ as *mut _,\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_default() -> *mut html_to_markdown_rs::options::ConversionOptions {\n    clear_last_error();\n    let result = html_to_markdown_rs::options::ConversionOptions::default();\n    Box::into_raw(Box::new(result))\n}\n\n/// Create a new builder with default values.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder() -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder\n{\n    clear_last_error();\n    let result = html_to_markdown_rs::options::ConversionOptions::builder();\n    Box::into_raw(Box::new(result))\n}\n\n/// Apply a partial update to these conversion options.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_apply_update(\n    this: *mut html_to_markdown_rs::options::ConversionOptions,\n    update: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return;\n    }\n    // SAFETY: null check above guarantees this is a valid pointer; caller ensures exclusive access.\n    let obj = unsafe { &mut *this };\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return;\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = obj.apply_update(update_rs);\n}\n\n/// Create from a partial update, applying to defaults.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_from_update(\n    update: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::ConversionOptions {\n    clear_last_error();\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = html_to_markdown_rs::options::ConversionOptions::from_update(update_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_from(\n    update: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::ConversionOptions {\n    clear_last_error();\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = html_to_markdown_rs::options::ConversionOptions::from(update_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Free a `ConversionOptionsBuilder` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_free(\n    ptr: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Set the list of HTML tag names whose content is stripped from output.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_strip_tags(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    tags: *const std::ffi::c_char,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    if tags.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'tags'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees tags is a valid pointer; string is valid UTF-8 from caller.\n    let tags_rs_str = match unsafe { CStr::from_ptr(tags) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in parameter 'tags'\");\n            return std::ptr::null_mut();\n        }\n    };\n    let tags_rs = match serde_json::from_str(tags_rs_str) {\n        Ok(v) => v,\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            return std::ptr::null_mut();\n        }\n    };\n    let result = obj.strip_tags(tags_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Set the list of HTML tag names that are preserved verbatim in output.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_preserve_tags(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    tags: *const std::ffi::c_char,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    if tags.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'tags'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees tags is a valid pointer; string is valid UTF-8 from caller.\n    let tags_rs_str = match unsafe { CStr::from_ptr(tags) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in parameter 'tags'\");\n            return std::ptr::null_mut();\n        }\n    };\n    let tags_rs = match serde_json::from_str(tags_rs_str) {\n        Ok(v) => v,\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            return std::ptr::null_mut();\n        }\n    };\n    let result = obj.preserve_tags(tags_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Set the list of HTML tag names whose `<img>` children are kept inline.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_keep_inline_images_in(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    tags: *const std::ffi::c_char,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    if tags.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'tags'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees tags is a valid pointer; string is valid UTF-8 from caller.\n    let tags_rs_str = match unsafe { CStr::from_ptr(tags) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in parameter 'tags'\");\n            return std::ptr::null_mut();\n        }\n    };\n    let tags_rs = match serde_json::from_str(tags_rs_str) {\n        Ok(v) => v,\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            return std::ptr::null_mut();\n        }\n    };\n    let result = obj.keep_inline_images_in(tags_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Set the list of CSS selectors for elements to exclude entirely from output.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_exclude_selectors(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    selectors: *const std::ffi::c_char,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    if selectors.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'selectors'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees selectors is a valid pointer; string is valid UTF-8 from caller.\n    let selectors_rs_str = match unsafe { CStr::from_ptr(selectors) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in parameter 'selectors'\");\n            return std::ptr::null_mut();\n        }\n    };\n    let selectors_rs = match serde_json::from_str(selectors_rs_str) {\n        Ok(v) => v,\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            return std::ptr::null_mut();\n        }\n    };\n    let result = obj.exclude_selectors(selectors_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Set the visitor used during conversion.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_visitor(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    visitor: *const html_to_markdown_rs::visitor::VisitorHandle,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    let visitor_rs = if visitor.is_null() {\n        None\n    } else {\n        // SAFETY: null check above guarantees visitor is a valid pointer.\n        Some(unsafe { &*visitor }.clone())\n    };\n    let result = obj.visitor(visitor_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Set the pre-processing options applied to the HTML before conversion.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_preprocessing(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n    preprocessing: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsBuilder {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    if preprocessing.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'preprocessing'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees preprocessing is a valid pointer.\n    let preprocessing_rs = unsafe { &*preprocessing }.clone();\n    let result = obj.preprocessing(preprocessing_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Build the final [`ConversionOptions`].\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_builder_build(\n    this: *mut html_to_markdown_rs::options::ConversionOptionsBuilder,\n) -> *mut html_to_markdown_rs::options::ConversionOptions {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees this is a valid pointer originally from Box::into_raw.\n    let obj = unsafe { *Box::from_raw(this) };\n    let result = obj.build();\n    Box::into_raw(Box::new(result))\n}\n\n/// Create a `ConversionOptionsUpdate` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_conversion_options_update_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::options::ConversionOptionsUpdate {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::options::ConversionOptionsUpdate>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `ConversionOptionsUpdate` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_free(\n    ptr: *mut html_to_markdown_rs::options::ConversionOptionsUpdate,\n) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `heading_style` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_heading_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::HeadingStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.heading_style {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `list_indent_type` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_list_indent_type(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::ListIndentType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.list_indent_type {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `list_indent_width` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_list_indent_width(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.list_indent_width {\n        Some(val) => *val,\n        None => 0,\n    }\n}\n\n/// Get the `bullets` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_bullets(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.bullets {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `strong_em_symbol` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_strong_em_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.strong_em_symbol {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `escape_asterisks` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_escape_asterisks(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.escape_asterisks {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `escape_underscores` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_escape_underscores(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.escape_underscores {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `escape_misc` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_escape_misc(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.escape_misc {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `escape_ascii` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_escape_ascii(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.escape_ascii {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `code_language` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_code_language(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.code_language {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `autolinks` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_autolinks(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.autolinks {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `default_title` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_default_title(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.default_title {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `br_in_tables` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_br_in_tables(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.br_in_tables {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `highlight_style` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_highlight_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::HighlightStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.highlight_style {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `extract_metadata` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_extract_metadata(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.extract_metadata {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `whitespace_mode` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_whitespace_mode(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::WhitespaceMode {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.whitespace_mode {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `strip_newlines` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_strip_newlines(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.strip_newlines {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `wrap` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_wrap(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.wrap {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `wrap_width` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_wrap_width(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.wrap_width {\n        Some(val) => *val,\n        None => 0,\n    }\n}\n\n/// Get the `convert_as_inline` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_convert_as_inline(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.convert_as_inline {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `sub_symbol` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_sub_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.sub_symbol {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `sup_symbol` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_sup_symbol(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.sup_symbol {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `newline_style` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_newline_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::NewlineStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.newline_style {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `code_block_style` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_code_block_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::CodeBlockStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.code_block_style {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `keep_inline_images_in` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_keep_inline_images_in(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.keep_inline_images_in {\n        Some(val) => match serde_json::to_string(&val) {\n            Ok(s) => match CString::new(s) {\n                Ok(cs) => cs.into_raw(),\n                Err(_) => std::ptr::null_mut(),\n            },\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `preprocessing` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_preprocessing(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.preprocessing {\n        Some(val) => Box::into_raw(Box::new(val.clone())),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `encoding` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_encoding(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.encoding {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `debug` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_debug(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.debug {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `strip_tags` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_strip_tags(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.strip_tags {\n        Some(val) => match serde_json::to_string(&val) {\n            Ok(s) => match CString::new(s) {\n                Ok(cs) => cs.into_raw(),\n                Err(_) => std::ptr::null_mut(),\n            },\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `preserve_tags` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_preserve_tags(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.preserve_tags {\n        Some(val) => match serde_json::to_string(&val) {\n            Ok(s) => match CString::new(s) {\n                Ok(cs) => cs.into_raw(),\n                Err(_) => std::ptr::null_mut(),\n            },\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `skip_images` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_skip_images(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.skip_images {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `link_style` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_link_style(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::LinkStyle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.link_style {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `output_format` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_output_format(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::OutputFormat {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.output_format {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `include_document_structure` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_include_document_structure(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.include_document_structure {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `extract_images` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_extract_images(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.extract_images {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `max_image_size` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_max_image_size(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> u64 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.max_image_size {\n        Some(val) => *val,\n        None => 0,\n    }\n}\n\n/// Get the `capture_svg` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_capture_svg(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.capture_svg {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `infer_dimensions` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_infer_dimensions(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.infer_dimensions {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `max_depth` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_max_depth(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.max_depth {\n        Some(Some(inner_val)) => *inner_val,\n        Some(None) => 0,\n        None => 0,\n    }\n}\n\n/// Get the `exclude_selectors` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_exclude_selectors(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.exclude_selectors {\n        Some(val) => match serde_json::to_string(&val) {\n            Ok(s) => match CString::new(s) {\n                Ok(cs) => cs.into_raw(),\n                Err(_) => std::ptr::null_mut(),\n            },\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `visitor` field from a `ConversionOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_options_update_visitor(\n    ptr: *const html_to_markdown_rs::options::ConversionOptionsUpdate,\n) -> *mut html_to_markdown_rs::visitor::VisitorHandle {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.visitor {\n        Some(val) => val as *const _ as *mut _,\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `PreprocessingOptions` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_preprocessing_options_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptions {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::options::PreprocessingOptions>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `PreprocessingOptions` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_to_json(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `PreprocessingOptions` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_free(ptr: *mut html_to_markdown_rs::options::PreprocessingOptions) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `enabled` field from a `PreprocessingOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_enabled(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.enabled as i32\n}\n\n/// Get the `preset` field from a `PreprocessingOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_preset(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> *mut html_to_markdown_rs::options::PreprocessingPreset {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.preset))\n}\n\n/// Get the `remove_navigation` field from a `PreprocessingOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_remove_navigation(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.remove_navigation as i32\n}\n\n/// Get the `remove_forms` field from a `PreprocessingOptions`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_remove_forms(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptions,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.remove_forms as i32\n}\n\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_default() -> *mut html_to_markdown_rs::options::PreprocessingOptions\n{\n    clear_last_error();\n    let result = html_to_markdown_rs::options::PreprocessingOptions::default();\n    Box::into_raw(Box::new(result))\n}\n\n/// Apply a partial update to these preprocessing options.\n///\n/// Any specified fields in the update will override the current values.\n/// Unspecified fields (None) are left unchanged.\n///\n/// # Arguments\n///\n/// * `update` - Partial preprocessing options update\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_apply_update(\n    this: *mut html_to_markdown_rs::options::PreprocessingOptions,\n    update: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) {\n    clear_last_error();\n    if this.is_null() {\n        set_last_error(1, \"Null pointer passed for self\");\n        return;\n    }\n    // SAFETY: null check above guarantees this is a valid pointer; caller ensures exclusive access.\n    let obj = unsafe { &mut *this };\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return;\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = obj.apply_update(update_rs);\n}\n\n/// Create new preprocessing options from a partial update.\n///\n/// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n/// Fields not specified in the update keep their default values.\n///\n/// # Arguments\n///\n/// * `update` - Partial preprocessing options update\n///\n/// # Returns\n///\n/// New `PreprocessingOptions` with specified updates applied to defaults\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_from_update(\n    update: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptions {\n    clear_last_error();\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = html_to_markdown_rs::options::PreprocessingOptions::from_update(update_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_from(\n    update: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptions {\n    clear_last_error();\n    if update.is_null() {\n        set_last_error(1, \"Null pointer passed for parameter 'update'\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees update is a valid pointer.\n    let update_rs = unsafe { &*update }.clone();\n    let result = html_to_markdown_rs::options::PreprocessingOptions::from(update_rs);\n    Box::into_raw(Box::new(result))\n}\n\n/// Create a `PreprocessingOptionsUpdate` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_preprocessing_options_update_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::options::PreprocessingOptionsUpdate>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `PreprocessingOptionsUpdate` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_free(\n    ptr: *mut html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `enabled` field from a `PreprocessingOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_enabled(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.enabled {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `preset` field from a `PreprocessingOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_preset(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> *mut html_to_markdown_rs::options::PreprocessingPreset {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.preset {\n        Some(val) => Box::into_raw(Box::new(*val)),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `remove_navigation` field from a `PreprocessingOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_remove_navigation(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.remove_navigation {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Get the `remove_forms` field from a `PreprocessingOptionsUpdate`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_options_update_remove_forms(\n    ptr: *const html_to_markdown_rs::options::PreprocessingOptionsUpdate,\n) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.remove_forms {\n        Some(val) => *val as i32,\n        None => 0,\n    }\n}\n\n/// Create a `DocumentStructure` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_document_structure_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_structure_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::DocumentStructure {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::DocumentStructure>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `DocumentStructure` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_structure_to_json(\n    ptr: *const html_to_markdown_rs::DocumentStructure,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `DocumentStructure` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_structure_free(ptr: *mut html_to_markdown_rs::DocumentStructure) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `nodes` field from a `DocumentStructure`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_structure_nodes(\n    ptr: *const html_to_markdown_rs::DocumentStructure,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.nodes) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `source_format` field from a `DocumentStructure`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_structure_source_format(\n    ptr: *const html_to_markdown_rs::DocumentStructure,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.source_format {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `DocumentNode` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_document_node_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_from_json(json: *const c_char) -> *mut html_to_markdown_rs::DocumentNode {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::DocumentNode>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `DocumentNode` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_to_json(ptr: *const html_to_markdown_rs::DocumentNode) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `DocumentNode` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_free(ptr: *mut html_to_markdown_rs::DocumentNode) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `id` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_id(ptr: *const html_to_markdown_rs::DocumentNode) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.id.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `content` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_content(\n    ptr: *const html_to_markdown_rs::DocumentNode,\n) -> *mut html_to_markdown_rs::NodeContent {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.content.clone()))\n}\n\n/// Get the `parent` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_parent(ptr: *const html_to_markdown_rs::DocumentNode) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.parent {\n        Some(val) => *val,\n        None => 0,\n    }\n}\n\n/// Get the `children` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_children(\n    ptr: *const html_to_markdown_rs::DocumentNode,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.children) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `annotations` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_annotations(\n    ptr: *const html_to_markdown_rs::DocumentNode,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.annotations) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `attributes` field from a `DocumentNode`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_document_node_attributes(\n    ptr: *const html_to_markdown_rs::DocumentNode,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.attributes {\n        Some(val) => match serde_json::to_string(&val) {\n            Ok(s) => match CString::new(s) {\n                Ok(cs) => cs.into_raw(),\n                Err(_) => std::ptr::null_mut(),\n            },\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `TextAnnotation` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_text_annotation_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::TextAnnotation {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::TextAnnotation>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `TextAnnotation` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_to_json(ptr: *const html_to_markdown_rs::TextAnnotation) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `TextAnnotation` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_free(ptr: *mut html_to_markdown_rs::TextAnnotation) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `start` field from a `TextAnnotation`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_start(ptr: *const html_to_markdown_rs::TextAnnotation) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.start\n}\n\n/// Get the `end` field from a `TextAnnotation`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_end(ptr: *const html_to_markdown_rs::TextAnnotation) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.end\n}\n\n/// Get the `kind` field from a `TextAnnotation`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_annotation_kind(\n    ptr: *const html_to_markdown_rs::TextAnnotation,\n) -> *mut html_to_markdown_rs::AnnotationKind {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.kind.clone()))\n}\n\n/// Create a `ConversionResult` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_conversion_result_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::ConversionResult {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::ConversionResult>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `ConversionResult` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_to_json(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `ConversionResult` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_free(ptr: *mut html_to_markdown_rs::ConversionResult) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `content` field from a `ConversionResult`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_content(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.content {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `document` field from a `ConversionResult`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_document(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut html_to_markdown_rs::DocumentStructure {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.document {\n        Some(val) => Box::into_raw(Box::new(val.clone())),\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `metadata` field from a `ConversionResult`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_metadata(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut html_to_markdown_rs::metadata::HtmlMetadata {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.metadata.clone()))\n}\n\n/// Get the `tables` field from a `ConversionResult`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_tables(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.tables) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `warnings` field from a `ConversionResult`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_conversion_result_warnings(\n    ptr: *const html_to_markdown_rs::ConversionResult,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.warnings) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `TableGrid` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_table_grid_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_from_json(json: *const c_char) -> *mut html_to_markdown_rs::TableGrid {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::TableGrid>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `TableGrid` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_to_json(ptr: *const html_to_markdown_rs::TableGrid) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `TableGrid` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_free(ptr: *mut html_to_markdown_rs::TableGrid) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `rows` field from a `TableGrid`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_rows(ptr: *const html_to_markdown_rs::TableGrid) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.rows\n}\n\n/// Get the `cols` field from a `TableGrid`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_cols(ptr: *const html_to_markdown_rs::TableGrid) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.cols\n}\n\n/// Get the `cells` field from a `TableGrid`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_grid_cells(ptr: *const html_to_markdown_rs::TableGrid) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.cells) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `GridCell` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_grid_cell_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_from_json(json: *const c_char) -> *mut html_to_markdown_rs::GridCell {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::GridCell>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `GridCell` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_to_json(ptr: *const html_to_markdown_rs::GridCell) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `GridCell` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_free(ptr: *mut html_to_markdown_rs::GridCell) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `content` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_content(ptr: *const html_to_markdown_rs::GridCell) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.content.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `row` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_row(ptr: *const html_to_markdown_rs::GridCell) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.row\n}\n\n/// Get the `col` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_col(ptr: *const html_to_markdown_rs::GridCell) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.col\n}\n\n/// Get the `row_span` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_row_span(ptr: *const html_to_markdown_rs::GridCell) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.row_span\n}\n\n/// Get the `col_span` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_col_span(ptr: *const html_to_markdown_rs::GridCell) -> u32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.col_span\n}\n\n/// Get the `is_header` field from a `GridCell`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_grid_cell_is_header(ptr: *const html_to_markdown_rs::GridCell) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.is_header as i32\n}\n\n/// Create a `TableData` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_table_data_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_data_from_json(json: *const c_char) -> *mut html_to_markdown_rs::TableData {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::TableData>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `TableData` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_data_to_json(ptr: *const html_to_markdown_rs::TableData) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `TableData` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_data_free(ptr: *mut html_to_markdown_rs::TableData) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `grid` field from a `TableData`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_data_grid(\n    ptr: *const html_to_markdown_rs::TableData,\n) -> *mut html_to_markdown_rs::TableGrid {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.grid.clone()))\n}\n\n/// Get the `markdown` field from a `TableData`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_table_data_markdown(ptr: *const html_to_markdown_rs::TableData) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.markdown.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Create a `ProcessingWarning` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_processing_warning_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_processing_warning_from_json(\n    json: *const c_char,\n) -> *mut html_to_markdown_rs::ProcessingWarning {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::ProcessingWarning>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `ProcessingWarning` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_processing_warning_to_json(\n    ptr: *const html_to_markdown_rs::ProcessingWarning,\n) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `ProcessingWarning` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_processing_warning_free(ptr: *mut html_to_markdown_rs::ProcessingWarning) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `message` field from a `ProcessingWarning`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_processing_warning_message(\n    ptr: *const html_to_markdown_rs::ProcessingWarning,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.message.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `kind` field from a `ProcessingWarning`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_processing_warning_kind(\n    ptr: *const html_to_markdown_rs::ProcessingWarning,\n) -> *mut html_to_markdown_rs::WarningKind {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.kind))\n}\n\n/// Free a `VisitorHandle` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_visitor_handle_free(ptr: *mut html_to_markdown_rs::visitor::VisitorHandle) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Create a `NodeContext` from a JSON string. Returns null on failure.\n/// # Safety\n/// JSON string must be valid UTF-8 and null-terminated.\n/// Returned handle must be freed with `htm_node_context_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_from_json(json: *const c_char) -> *mut html_to_markdown_rs::NodeContext {\n    clear_last_error();\n    if json.is_null() {\n        set_last_error(1, \"Null pointer passed for JSON string\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees json is a valid pointer; string is valid UTF-8 from caller.\n    let c_str = match unsafe { CStr::from_ptr(json) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in JSON string\");\n            return std::ptr::null_mut();\n        }\n    };\n    match serde_json::from_str::<html_to_markdown_rs::NodeContext>(c_str) {\n        Ok(val) => Box::into_raw(Box::new(val)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Serialize a `NodeContext` to a JSON string. Returns null on failure.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_to_json(ptr: *const html_to_markdown_rs::NodeContext) -> *mut c_char {\n    clear_last_error();\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(e) => {\n                set_last_error(2, &e.to_string());\n                std::ptr::null_mut()\n            }\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Free a `NodeContext` handle.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_free(ptr: *mut html_to_markdown_rs::NodeContext) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Get the `node_type` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_node_type(\n    ptr: *const html_to_markdown_rs::NodeContext,\n) -> *mut html_to_markdown_rs::NodeType {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    Box::into_raw(Box::new(obj.node_type))\n}\n\n/// Get the `tag_name` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_tag_name(\n    ptr: *const html_to_markdown_rs::NodeContext,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match CString::new(obj.tag_name.to_string()) {\n        Ok(cs) => cs.into_raw(),\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `attributes` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_attributes(\n    ptr: *const html_to_markdown_rs::NodeContext,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match serde_json::to_string(&obj.attributes) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(_) => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `depth` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_depth(ptr: *const html_to_markdown_rs::NodeContext) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.depth\n}\n\n/// Get the `index_in_parent` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_index_in_parent(ptr: *const html_to_markdown_rs::NodeContext) -> usize {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.index_in_parent\n}\n\n/// Get the `parent_tag` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_parent_tag(\n    ptr: *const html_to_markdown_rs::NodeContext,\n) -> *mut std::ffi::c_char {\n    if ptr.is_null() {\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    match &obj.parent_tag {\n        Some(val) => match CString::new(val.to_string()) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        None => std::ptr::null_mut(),\n    }\n}\n\n/// Get the `is_inline` field from a `NodeContext`.\n/// # Safety\n/// Pointer must be a valid handle returned by this library.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_context_is_inline(ptr: *const html_to_markdown_rs::NodeContext) -> i32 {\n    if ptr.is_null() {\n        return 0;\n    }\n    // SAFETY: null check above guarantees ptr is a valid pointer.\n    let obj = unsafe { &*ptr };\n    obj.is_inline as i32\n}\n\n/// Convert an integer to a `TextDirection` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_direction_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // LeftToRight\n        1 => 1, // RightToLeft\n        2 => 2, // Auto\n        _ => {\n            set_last_error(1, \"Invalid TextDirection variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `TextDirection` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_text_direction_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"LeftToRight\" => 0,\n        \"RightToLeft\" => 1,\n        \"Auto\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown TextDirection variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `LinkType` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_type_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Anchor\n        1 => 1, // Internal\n        2 => 2, // External\n        3 => 3, // Email\n        4 => 4, // Phone\n        5 => 5, // Other\n        _ => {\n            set_last_error(1, \"Invalid LinkType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `LinkType` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_type_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Anchor\" => 0,\n        \"Internal\" => 1,\n        \"External\" => 2,\n        \"Email\" => 3,\n        \"Phone\" => 4,\n        \"Other\" => 5,\n        _ => {\n            set_last_error(1, \"Unknown LinkType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `ImageType` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_type_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // DataUri\n        1 => 1, // InlineSvg\n        2 => 2, // External\n        3 => 3, // Relative\n        _ => {\n            set_last_error(1, \"Invalid ImageType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `ImageType` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_image_type_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"DataUri\" => 0,\n        \"InlineSvg\" => 1,\n        \"External\" => 2,\n        \"Relative\" => 3,\n        _ => {\n            set_last_error(1, \"Unknown ImageType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `StructuredDataType` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_type_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // JsonLd\n        1 => 1, // Microdata\n        2 => 2, // RDFa\n        _ => {\n            set_last_error(1, \"Invalid StructuredDataType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `StructuredDataType` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_structured_data_type_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"JsonLd\" => 0,\n        \"Microdata\" => 1,\n        \"RDFa\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown StructuredDataType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `PreprocessingPreset` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_preset_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Minimal\n        1 => 1, // Standard\n        2 => 2, // Aggressive\n        _ => {\n            set_last_error(1, \"Invalid PreprocessingPreset variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `PreprocessingPreset` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_preprocessing_preset_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Minimal\" => 0,\n        \"Standard\" => 1,\n        \"Aggressive\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown PreprocessingPreset variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `HeadingStyle` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_heading_style_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Underlined\n        1 => 1, // Atx\n        2 => 2, // AtxClosed\n        _ => {\n            set_last_error(1, \"Invalid HeadingStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `HeadingStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_heading_style_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Underlined\" => 0,\n        \"Atx\" => 1,\n        \"AtxClosed\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown HeadingStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `ListIndentType` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_list_indent_type_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Spaces\n        1 => 1, // Tabs\n        _ => {\n            set_last_error(1, \"Invalid ListIndentType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `ListIndentType` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_list_indent_type_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Spaces\" => 0,\n        \"Tabs\" => 1,\n        _ => {\n            set_last_error(1, \"Unknown ListIndentType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `WhitespaceMode` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_whitespace_mode_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Normalized\n        1 => 1, // Strict\n        _ => {\n            set_last_error(1, \"Invalid WhitespaceMode variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `WhitespaceMode` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_whitespace_mode_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Normalized\" => 0,\n        \"Strict\" => 1,\n        _ => {\n            set_last_error(1, \"Unknown WhitespaceMode variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `NewlineStyle` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_newline_style_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Spaces\n        1 => 1, // Backslash\n        _ => {\n            set_last_error(1, \"Invalid NewlineStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `NewlineStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_newline_style_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Spaces\" => 0,\n        \"Backslash\" => 1,\n        _ => {\n            set_last_error(1, \"Unknown NewlineStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `CodeBlockStyle` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_code_block_style_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Indented\n        1 => 1, // Backticks\n        2 => 2, // Tildes\n        _ => {\n            set_last_error(1, \"Invalid CodeBlockStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `CodeBlockStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_code_block_style_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Indented\" => 0,\n        \"Backticks\" => 1,\n        \"Tildes\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown CodeBlockStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `HighlightStyle` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_highlight_style_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // DoubleEqual\n        1 => 1, // Html\n        2 => 2, // Bold\n        3 => 3, // None\n        _ => {\n            set_last_error(1, \"Invalid HighlightStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `HighlightStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_highlight_style_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"DoubleEqual\" => 0,\n        \"Html\" => 1,\n        \"Bold\" => 2,\n        \"None\" => 3,\n        _ => {\n            set_last_error(1, \"Unknown HighlightStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `LinkStyle` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_style_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Inline\n        1 => 1, // Reference\n        _ => {\n            set_last_error(1, \"Invalid LinkStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `LinkStyle` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_style_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Inline\" => 0,\n        \"Reference\" => 1,\n        _ => {\n            set_last_error(1, \"Unknown LinkStyle variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `OutputFormat` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_output_format_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Markdown\n        1 => 1, // Djot\n        2 => 2, // Plain\n        _ => {\n            set_last_error(1, \"Invalid OutputFormat variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `OutputFormat` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_output_format_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Markdown\" => 0,\n        \"Djot\" => 1,\n        \"Plain\" => 2,\n        _ => {\n            set_last_error(1, \"Unknown OutputFormat variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `NodeContent` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_content_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0,   // Heading\n        1 => 1,   // Paragraph\n        2 => 2,   // List\n        3 => 3,   // ListItem\n        4 => 4,   // Table\n        5 => 5,   // Image\n        6 => 6,   // Code\n        7 => 7,   // Quote\n        8 => 8,   // DefinitionList\n        9 => 9,   // DefinitionItem\n        10 => 10, // RawBlock\n        11 => 11, // MetadataBlock\n        12 => 12, // Group\n        _ => {\n            set_last_error(1, \"Invalid NodeContent variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `NodeContent` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_content_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Heading\" => 0,\n        \"Paragraph\" => 1,\n        \"List\" => 2,\n        \"ListItem\" => 3,\n        \"Table\" => 4,\n        \"Image\" => 5,\n        \"Code\" => 6,\n        \"Quote\" => 7,\n        \"DefinitionList\" => 8,\n        \"DefinitionItem\" => 9,\n        \"RawBlock\" => 10,\n        \"MetadataBlock\" => 11,\n        \"Group\" => 12,\n        _ => {\n            set_last_error(1, \"Unknown NodeContent variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `AnnotationKind` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_annotation_kind_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Bold\n        1 => 1, // Italic\n        2 => 2, // Underline\n        3 => 3, // Strikethrough\n        4 => 4, // Code\n        5 => 5, // Subscript\n        6 => 6, // Superscript\n        7 => 7, // Highlight\n        8 => 8, // Link\n        _ => {\n            set_last_error(1, \"Invalid AnnotationKind variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `AnnotationKind` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_annotation_kind_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Bold\" => 0,\n        \"Italic\" => 1,\n        \"Underline\" => 2,\n        \"Strikethrough\" => 3,\n        \"Code\" => 4,\n        \"Subscript\" => 5,\n        \"Superscript\" => 6,\n        \"Highlight\" => 7,\n        \"Link\" => 8,\n        _ => {\n            set_last_error(1, \"Unknown AnnotationKind variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `WarningKind` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_warning_kind_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // ImageExtractionFailed\n        1 => 1, // EncodingFallback\n        2 => 2, // TruncatedInput\n        3 => 3, // MalformedHtml\n        4 => 4, // SanitizationApplied\n        5 => 5, // DepthLimitExceeded\n        _ => {\n            set_last_error(1, \"Invalid WarningKind variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `WarningKind` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_warning_kind_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"ImageExtractionFailed\" => 0,\n        \"EncodingFallback\" => 1,\n        \"TruncatedInput\" => 2,\n        \"MalformedHtml\" => 3,\n        \"SanitizationApplied\" => 4,\n        \"DepthLimitExceeded\" => 5,\n        _ => {\n            set_last_error(1, \"Unknown WarningKind variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `NodeType` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_type_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0,   // Text\n        1 => 1,   // Element\n        2 => 2,   // Heading\n        3 => 3,   // Paragraph\n        4 => 4,   // Div\n        5 => 5,   // Blockquote\n        6 => 6,   // Pre\n        7 => 7,   // Hr\n        8 => 8,   // List\n        9 => 9,   // ListItem\n        10 => 10, // DefinitionList\n        11 => 11, // DefinitionTerm\n        12 => 12, // DefinitionDescription\n        13 => 13, // Table\n        14 => 14, // TableRow\n        15 => 15, // TableCell\n        16 => 16, // TableHeader\n        17 => 17, // TableBody\n        18 => 18, // TableHead\n        19 => 19, // TableFoot\n        20 => 20, // Link\n        21 => 21, // Image\n        22 => 22, // Strong\n        23 => 23, // Em\n        24 => 24, // Code\n        25 => 25, // Strikethrough\n        26 => 26, // Underline\n        27 => 27, // Subscript\n        28 => 28, // Superscript\n        29 => 29, // Mark\n        30 => 30, // Small\n        31 => 31, // Br\n        32 => 32, // Span\n        33 => 33, // Article\n        34 => 34, // Section\n        35 => 35, // Nav\n        36 => 36, // Aside\n        37 => 37, // Header\n        38 => 38, // Footer\n        39 => 39, // Main\n        40 => 40, // Figure\n        41 => 41, // Figcaption\n        42 => 42, // Time\n        43 => 43, // Details\n        44 => 44, // Summary\n        45 => 45, // Form\n        46 => 46, // Input\n        47 => 47, // Select\n        48 => 48, // Option\n        49 => 49, // Button\n        50 => 50, // Textarea\n        51 => 51, // Label\n        52 => 52, // Fieldset\n        53 => 53, // Legend\n        54 => 54, // Audio\n        55 => 55, // Video\n        56 => 56, // Picture\n        57 => 57, // Source\n        58 => 58, // Iframe\n        59 => 59, // Svg\n        60 => 60, // Canvas\n        61 => 61, // Ruby\n        62 => 62, // Rt\n        63 => 63, // Rp\n        64 => 64, // Abbr\n        65 => 65, // Kbd\n        66 => 66, // Samp\n        67 => 67, // Var\n        68 => 68, // Cite\n        69 => 69, // Q\n        70 => 70, // Del\n        71 => 71, // Ins\n        72 => 72, // Data\n        73 => 73, // Meter\n        74 => 74, // Progress\n        75 => 75, // Output\n        76 => 76, // Template\n        77 => 77, // Slot\n        78 => 78, // Html\n        79 => 79, // Head\n        80 => 80, // Body\n        81 => 81, // Title\n        82 => 82, // Meta\n        83 => 83, // LinkTag\n        84 => 84, // Style\n        85 => 85, // Script\n        86 => 86, // Base\n        87 => 87, // Custom\n        _ => {\n            set_last_error(1, \"Invalid NodeType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `NodeType` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_node_type_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Text\" => 0,\n        \"Element\" => 1,\n        \"Heading\" => 2,\n        \"Paragraph\" => 3,\n        \"Div\" => 4,\n        \"Blockquote\" => 5,\n        \"Pre\" => 6,\n        \"Hr\" => 7,\n        \"List\" => 8,\n        \"ListItem\" => 9,\n        \"DefinitionList\" => 10,\n        \"DefinitionTerm\" => 11,\n        \"DefinitionDescription\" => 12,\n        \"Table\" => 13,\n        \"TableRow\" => 14,\n        \"TableCell\" => 15,\n        \"TableHeader\" => 16,\n        \"TableBody\" => 17,\n        \"TableHead\" => 18,\n        \"TableFoot\" => 19,\n        \"Link\" => 20,\n        \"Image\" => 21,\n        \"Strong\" => 22,\n        \"Em\" => 23,\n        \"Code\" => 24,\n        \"Strikethrough\" => 25,\n        \"Underline\" => 26,\n        \"Subscript\" => 27,\n        \"Superscript\" => 28,\n        \"Mark\" => 29,\n        \"Small\" => 30,\n        \"Br\" => 31,\n        \"Span\" => 32,\n        \"Article\" => 33,\n        \"Section\" => 34,\n        \"Nav\" => 35,\n        \"Aside\" => 36,\n        \"Header\" => 37,\n        \"Footer\" => 38,\n        \"Main\" => 39,\n        \"Figure\" => 40,\n        \"Figcaption\" => 41,\n        \"Time\" => 42,\n        \"Details\" => 43,\n        \"Summary\" => 44,\n        \"Form\" => 45,\n        \"Input\" => 46,\n        \"Select\" => 47,\n        \"Option\" => 48,\n        \"Button\" => 49,\n        \"Textarea\" => 50,\n        \"Label\" => 51,\n        \"Fieldset\" => 52,\n        \"Legend\" => 53,\n        \"Audio\" => 54,\n        \"Video\" => 55,\n        \"Picture\" => 56,\n        \"Source\" => 57,\n        \"Iframe\" => 58,\n        \"Svg\" => 59,\n        \"Canvas\" => 60,\n        \"Ruby\" => 61,\n        \"Rt\" => 62,\n        \"Rp\" => 63,\n        \"Abbr\" => 64,\n        \"Kbd\" => 65,\n        \"Samp\" => 66,\n        \"Var\" => 67,\n        \"Cite\" => 68,\n        \"Q\" => 69,\n        \"Del\" => 70,\n        \"Ins\" => 71,\n        \"Data\" => 72,\n        \"Meter\" => 73,\n        \"Progress\" => 74,\n        \"Output\" => 75,\n        \"Template\" => 76,\n        \"Slot\" => 77,\n        \"Html\" => 78,\n        \"Head\" => 79,\n        \"Body\" => 80,\n        \"Title\" => 81,\n        \"Meta\" => 82,\n        \"LinkTag\" => 83,\n        \"Style\" => 84,\n        \"Script\" => 85,\n        \"Base\" => 86,\n        \"Custom\" => 87,\n        _ => {\n            set_last_error(1, \"Unknown NodeType variant\");\n            -1\n        }\n    }\n}\n\n/// Convert an integer to a `VisitResult` variant. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure all pointer arguments are valid or null.\n/// Returned pointers must be freed with the appropriate free function.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_visit_result_from_i32(value: i32) -> i32 {\n    match value {\n        0 => 0, // Continue\n        1 => 1, // Custom\n        2 => 2, // Skip\n        3 => 3, // PreserveHtml\n        4 => 4, // Error\n        _ => {\n            set_last_error(1, \"Invalid VisitResult variant\");\n            -1\n        }\n    }\n}\n\n/// Convert a `VisitResult` variant name (C string) to its integer value. Returns -1 on invalid input.\n/// # Safety\n/// Caller must ensure `ptr` is a valid pointer to a `c_char` or null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_visit_result_from_str(name: *const c_char) -> i32 {\n    if name.is_null() {\n        set_last_error(1, \"Null pointer passed for enum name\");\n        return -1;\n    }\n    // SAFETY: null check above guarantees name is a valid pointer; string is valid UTF-8 from caller.\n    let s = match unsafe { CStr::from_ptr(name) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in enum name\");\n            return -1;\n        }\n    };\n    match s {\n        \"Continue\" => 0,\n        \"Custom\" => 1,\n        \"Skip\" => 2,\n        \"PreserveHtml\" => 3,\n        \"Error\" => 4,\n        _ => {\n            set_last_error(1, \"Unknown VisitResult variant\");\n            -1\n        }\n    }\n}\n\n/// Free a heap-allocated `LinkType` returned by a pointer-returning FFI function.\n/// # Safety\n/// Pointer must have been returned by this library, or be null.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_type_free(ptr: *mut html_to_markdown_rs::metadata::LinkType) {\n    if !ptr.is_null() {\n        // SAFETY: ptr was allocated by Box::into_raw; caller ensures no aliases.\n        unsafe {\n            drop(Box::from_raw(ptr));\n        }\n    }\n}\n\n/// Serialize a heap-allocated `LinkType` to a JSON string.\n/// # Safety\n/// `ptr` must be a valid, non-null pointer returned by a `htm` function.\n/// The returned string must be freed with `htm_free_string`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_link_type_to_json(ptr: *const html_to_markdown_rs::metadata::LinkType) -> *mut c_char {\n    if ptr.is_null() {\n        set_last_error(1, \"Null pointer passed to htm_link_type_to_json\");\n        return std::ptr::null_mut();\n    }\n    // SAFETY: null check above guarantees ptr is valid; no mutable aliases held.\n    let val = unsafe { &*ptr };\n    match serde_json::to_string(val) {\n        Ok(s) => match CString::new(s) {\n            Ok(cs) => cs.into_raw(),\n            Err(_) => std::ptr::null_mut(),\n        },\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n\n/// Write an error message string into an FFI out-error pointer.\n///\n/// # Safety\n///\n/// `out_error` must be null or a valid writable `*mut *mut c_char` pointer.\nunsafe fn ffi_set_out_error(out_error: *mut *mut std::ffi::c_char, msg: &str) {\n    if !out_error.is_null() {\n        if let Ok(cs) = std::ffi::CString::new(msg) {\n            // SAFETY: out_error is non-null; caller must free this string.\n            unsafe {\n                *out_error = cs.into_raw();\n            }\n        }\n    }\n}\n\n/// VTable for C plugin bridges implementing the `HtmlVisitor` trait.\n///\n/// # Safety\n///\n/// All function pointers must be valid for the lifetime of any bridge created from\n/// this vtable.  `free_user_data`, when non-null, is called once with `user_data`\n/// when the bridge is dropped.\n#[derive(Copy, Clone)]\n#[repr(C)]\npub struct HtmHtmlVisitorVTable {\n    /// Called before entering any element.\n    ///\n    /// This is the first callback invoked for every HTML element, allowing\n    /// visitors to implement generic element handling before tag-specific logic.\n    pub visit_element_start: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called after exiting any element.\n    ///\n    /// Receives the default markdown output that would be generated.\n    /// Visitors can inspect or replace this output.\n    pub visit_element_end: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _output: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit text nodes (most frequent callback - ~100+ per document).\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context (will have `node_type: NodeType::Text`)\n    /// - `text`: The raw text content (HTML entities already decoded)\n    pub visit_text: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit anchor links `<a href=\"...\">`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with link element metadata\n    /// - `href`: The link URL (from `href` attribute)\n    /// - `text`: The link text content (already converted to markdown)\n    /// - `title`: Optional title attribute\n    pub visit_link: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _href: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            _title: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit images `<img src=\"...\">`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with image element metadata\n    /// - `src`: The image source URL\n    /// - `alt`: The alt text\n    /// - `title`: Optional title attribute\n    pub visit_image: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _src: *const std::ffi::c_char,\n            _alt: *const std::ffi::c_char,\n            _title: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit heading elements `<h1>` through `<h6>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context with heading metadata\n    /// - `level`: Heading level (1-6)\n    /// - `text`: The heading text content\n    /// - `id`: Optional id attribute (for anchor links)\n    pub visit_heading: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _level: u32,\n            _text: *const std::ffi::c_char,\n            _id: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit code blocks `<pre><code>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `lang`: Optional language specifier (from class attribute)\n    /// - `code`: The code content\n    pub visit_code_block: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _lang: *const std::ffi::c_char,\n            _code: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit inline code `<code>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `code`: The code content\n    pub visit_code_inline: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _code: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit list items `<li>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `ordered`: Whether this is an ordered list item\n    /// - `marker`: The list marker (e.g., \"-\", \"1.\", \"a)\")\n    /// - `text`: The list item content (already converted)\n    pub visit_list_item: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _ordered: i32,\n            _marker: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called before processing a list `<ul>` or `<ol>`.\n    pub visit_list_start: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _ordered: i32,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called after processing a list `</ul>` or `</ol>`.\n    pub visit_list_end: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _ordered: i32,\n            _output: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called before processing a table `<table>`.\n    pub visit_table_start: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit table rows `<tr>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `cells`: Cell contents (already converted to markdown)\n    /// - `is_header`: Whether this row is in `<thead>`\n    pub visit_table_row: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _cells: *const std::ffi::c_char,\n            _is_header: i32,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called after processing a table `</table>`.\n    pub visit_table_end: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _output: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit blockquote elements `<blockquote>`.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `content`: The blockquote content (already converted)\n    /// - `depth`: Nesting depth (for nested blockquotes)\n    pub visit_blockquote: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _content: *const std::ffi::c_char,\n            _depth: usize,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit strong/bold elements `<strong>`, `<b>`.\n    pub visit_strong: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit emphasis/italic elements `<em>`, `<i>`.\n    pub visit_emphasis: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit strikethrough elements `<s>`, `<del>`, `<strike>`.\n    pub visit_strikethrough: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit underline elements `<u>`, `<ins>`.\n    pub visit_underline: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit subscript elements `<sub>`.\n    pub visit_subscript: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit superscript elements `<sup>`.\n    pub visit_superscript: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit mark/highlight elements `<mark>`.\n    pub visit_mark: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit line break elements `<br>`.\n    pub visit_line_break: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit horizontal rule elements `<hr>`.\n    pub visit_horizontal_rule: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit custom elements (web components) or unknown tags.\n    ///\n    /// # Arguments\n    /// - `ctx`: Node context\n    /// - `tag_name`: The custom element's tag name\n    /// - `html`: The raw HTML of this element\n    pub visit_custom_element: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _tag_name: *const std::ffi::c_char,\n            _html: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit definition list `<dl>`.\n    pub visit_definition_list_start: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit definition term `<dt>`.\n    pub visit_definition_term: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit definition description `<dd>`.\n    pub visit_definition_description: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called after processing a definition list `</dl>`.\n    pub visit_definition_list_end: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _output: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit form elements `<form>`.\n    pub visit_form: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _action: *const std::ffi::c_char,\n            _method: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit input elements `<input>`.\n    pub visit_input: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _input_type: *const std::ffi::c_char,\n            _name: *const std::ffi::c_char,\n            _value: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit button elements `<button>`.\n    pub visit_button: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit audio elements `<audio>`.\n    pub visit_audio: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _src: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit video elements `<video>`.\n    pub visit_video: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _src: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit iframe elements `<iframe>`.\n    pub visit_iframe: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _src: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit details elements `<details>`.\n    pub visit_details: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _open: i32,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit summary elements `<summary>`.\n    pub visit_summary: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit figure elements `<figure>`.\n    pub visit_figure_start: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Visit figcaption elements `<figcaption>`.\n    pub visit_figcaption: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _text: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Called after processing a figure `</figure>`.\n    pub visit_figure_end: Option<\n        unsafe extern \"C\" fn(\n            user_data: *const std::ffi::c_void,\n            _ctx: *const std::ffi::c_char,\n            _output: *const std::ffi::c_char,\n            out_result: *mut *mut std::ffi::c_char,\n        ) -> i32,\n    >,\n    /// Optional destructor: called once with `user_data` when the bridge is dropped.\n    pub free_user_data: Option<unsafe extern \"C\" fn(*mut std::ffi::c_void)>,\n}\n// SAFETY: all fields are function pointers and free_user_data, which are Send + Sync.\nunsafe impl Send for HtmHtmlVisitorVTable {}\nunsafe impl Sync for HtmHtmlVisitorVTable {}\n\n/// Rust-side bridge that holds a C vtable pointer and opaque `user_data`.\n///\n/// Implements `HtmlVisitor` by forwarding calls through the vtable.\npub struct HtmHtmlVisitorBridge {\n    vtable: HtmHtmlVisitorVTable,\n    user_data: *const std::ffi::c_void,\n    cached_name: String,\n    cached_version: String,\n}\n\nimpl std::fmt::Debug for HtmHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"HtmHtmlVisitorBridge\")\n            .field(\"cached_name\", &self.cached_name)\n            .field(\"cached_version\", &self.cached_version)\n            .finish_non_exhaustive()\n    }\n}\n\n// SAFETY: The caller is responsible for ensuring `user_data` is safe to send across\n// thread boundaries. This is documented in `HtmHtmlVisitorVTable` and the registration function.\nunsafe impl Send for HtmHtmlVisitorBridge {}\nunsafe impl Sync for HtmHtmlVisitorBridge {}\n\nimpl Drop for HtmHtmlVisitorBridge {\n    fn drop(&mut self) {\n        if let Some(free_fn) = self.vtable.free_user_data {\n            // SAFETY: free_fn is a valid function pointer; user_data is the pointer\n            // originally provided at registration. Called exactly once here.\n            unsafe { free_fn(self.user_data as *mut std::ffi::c_void) }\n        }\n    }\n}\n\nimpl HtmHtmlVisitorBridge {\n    /// Create a new bridge from a vtable and opaque user_data pointer.\n    ///\n    /// # Safety\n    ///\n    /// `vtable` must remain valid for the lifetime of the returned bridge.\n    /// `user_data` must be valid for any thread that calls methods on this bridge.\n    /// All required fn pointers in `vtable` must be non-null.\n    pub unsafe fn new(name: String, vtable: HtmHtmlVisitorVTable, user_data: *const std::ffi::c_void) -> Self {\n        Self {\n            vtable,\n            user_data,\n            cached_name: name,\n            cached_version: String::new(),\n        }\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for HtmHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_element_start else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_element_end else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __output_cs = match std::ffi::CString::new(_output) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _output_ptr = __output_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _output_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_text else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_link else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __href_cs = match std::ffi::CString::new(_href) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _href_ptr = __href_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let __title_storage: Option<std::ffi::CString> = _title.and_then(|v| std::ffi::CString::new(v).ok());\n        let _title_ptr: *const std::ffi::c_char = __title_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe {\n            fp(\n                self.user_data,\n                _ctx_ptr,\n                _href_ptr,\n                _text_ptr,\n                _title_ptr,\n                &mut _out_result,\n            )\n        };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_image else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __src_cs = match std::ffi::CString::new(_src) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _src_ptr = __src_cs.as_ptr();\n        let __alt_cs = match std::ffi::CString::new(_alt) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _alt_ptr = __alt_cs.as_ptr();\n        let __title_storage: Option<std::ffi::CString> = _title.and_then(|v| std::ffi::CString::new(v).ok());\n        let _title_ptr: *const std::ffi::c_char = __title_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe {\n            fp(\n                self.user_data,\n                _ctx_ptr,\n                _src_ptr,\n                _alt_ptr,\n                _title_ptr,\n                &mut _out_result,\n            )\n        };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_heading else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let __id_storage: Option<std::ffi::CString> = _id.and_then(|v| std::ffi::CString::new(v).ok());\n        let _id_ptr: *const std::ffi::c_char = __id_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _level, _text_ptr, _id_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_code_block else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __lang_storage: Option<std::ffi::CString> = _lang.and_then(|v| std::ffi::CString::new(v).ok());\n        let _lang_ptr: *const std::ffi::c_char = __lang_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let __code_cs = match std::ffi::CString::new(_code) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _code_ptr = __code_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _lang_ptr, _code_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_code_inline else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __code_cs = match std::ffi::CString::new(_code) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _code_ptr = __code_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _code_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_list_item else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __marker_cs = match std::ffi::CString::new(_marker) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _marker_ptr = __marker_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe {\n            fp(\n                self.user_data,\n                _ctx_ptr,\n                _ordered as i32,\n                _marker_ptr,\n                _text_ptr,\n                &mut _out_result,\n            )\n        };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_list_start else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _ordered as i32, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_list_end else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __output_cs = match std::ffi::CString::new(_output) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _output_ptr = __output_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _ordered as i32, _output_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_table_start else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_table_row else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __cells_json = serde_json::to_string(&_cells).unwrap_or_default();\n        let __cells_cs = match std::ffi::CString::new(__cells_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _cells_ptr = __cells_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe {\n            fp(\n                self.user_data,\n                _ctx_ptr,\n                _cells_ptr,\n                _is_header as i32,\n                &mut _out_result,\n            )\n        };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_table_end else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __output_cs = match std::ffi::CString::new(_output) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _output_ptr = __output_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _output_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_blockquote else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __content_cs = match std::ffi::CString::new(_content) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _content_ptr = __content_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _content_ptr, _depth, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_strong else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_emphasis else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_strikethrough else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_underline else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_subscript else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_superscript else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_mark else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_line_break else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_horizontal_rule else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_custom_element else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __tag_name_cs = match std::ffi::CString::new(_tag_name) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _tag_name_ptr = __tag_name_cs.as_ptr();\n        let __html_cs = match std::ffi::CString::new(_html) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _html_ptr = __html_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _tag_name_ptr, _html_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_definition_list_start else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_definition_term else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_definition_description else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_definition_list_end else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __output_cs = match std::ffi::CString::new(_output) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _output_ptr = __output_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _output_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_form else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __action_storage: Option<std::ffi::CString> = _action.and_then(|v| std::ffi::CString::new(v).ok());\n        let _action_ptr: *const std::ffi::c_char = __action_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let __method_storage: Option<std::ffi::CString> = _method.and_then(|v| std::ffi::CString::new(v).ok());\n        let _method_ptr: *const std::ffi::c_char = __method_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _action_ptr, _method_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_input else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __input_type_cs = match std::ffi::CString::new(_input_type) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _input_type_ptr = __input_type_cs.as_ptr();\n        let __name_storage: Option<std::ffi::CString> = _name.and_then(|v| std::ffi::CString::new(v).ok());\n        let _name_ptr: *const std::ffi::c_char = __name_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let __value_storage: Option<std::ffi::CString> = _value.and_then(|v| std::ffi::CString::new(v).ok());\n        let _value_ptr: *const std::ffi::c_char = __value_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe {\n            fp(\n                self.user_data,\n                _ctx_ptr,\n                _input_type_ptr,\n                _name_ptr,\n                _value_ptr,\n                &mut _out_result,\n            )\n        };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_button else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_audio else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __src_storage: Option<std::ffi::CString> = _src.and_then(|v| std::ffi::CString::new(v).ok());\n        let _src_ptr: *const std::ffi::c_char = __src_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _src_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_video else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __src_storage: Option<std::ffi::CString> = _src.and_then(|v| std::ffi::CString::new(v).ok());\n        let _src_ptr: *const std::ffi::c_char = __src_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _src_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_iframe else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __src_storage: Option<std::ffi::CString> = _src.and_then(|v| std::ffi::CString::new(v).ok());\n        let _src_ptr: *const std::ffi::c_char = __src_storage.as_ref().map_or(std::ptr::null(), |cs| cs.as_ptr());\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _src_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_details else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _open as i32, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_summary else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_figure_start else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_figcaption else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __text_cs = match std::ffi::CString::new(_text) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _text_ptr = __text_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _text_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let Some(fp) = self.vtable.visit_figure_end else {\n            return Default::default();\n        };\n        let __ctx_json = serde_json::to_string(&_ctx).unwrap_or_default();\n        let __ctx_cs = match std::ffi::CString::new(__ctx_json) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _ctx_ptr = __ctx_cs.as_ptr();\n        let __output_cs = match std::ffi::CString::new(_output) {\n            Ok(s) => s,\n            Err(_) => {\n                return Default::default();\n            }\n        };\n        let _output_ptr = __output_cs.as_ptr();\n        let mut _out_result: *mut std::ffi::c_char = std::ptr::null_mut();\n        // SAFETY: fp is a valid non-null function pointer; all temporaries outlive this call;\n        // user_data validity is the caller's responsibility (documented in the vtable API).\n        let _rc = unsafe { fp(self.user_data, _ctx_ptr, _output_ptr, &mut _out_result) };\n        if _out_result.is_null() {\n            return Default::default();\n        }\n        // SAFETY: out_result was written by the callee as a valid CString.\n        let cs = unsafe { std::ffi::CString::from_raw(_out_result) };\n        let json = cs.to_string_lossy();\n        serde_json::from_str::<html_to_markdown_rs::VisitResult>(&json).unwrap_or_default()\n    }\n}\n\n/// Attach a vtable visitor bridge to a `ConversionOptions` options struct.\n///\n/// The `HtmHtmlVisitorBridge` encapsulates a set of C function pointers that receive visit\n/// callbacks during HTML-to-Markdown conversion.  Call this setter before `htm_convert`\n/// to activate visitor callbacks.  Pass `visitor = null` to clear a previously attached visitor.\n///\n/// Neither pointer is consumed: the caller retains ownership of both `options` and `visitor`\n/// and must free them independently after conversion completes.\n///\n/// # Safety\n///\n/// `options` must be a non-null pointer returned by `htm_conversion_options_new` (or\n/// equivalent), valid for write access.  `visitor` must be a non-null pointer returned by\n/// `htm_htm_html_visitor_bridge_new`, or null.  Both must remain valid for the duration of any\n/// subsequent `htm_convert` call.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_options_set_visitor(\n    options: *mut html_to_markdown_rs::ConversionOptions,\n    visitor: *mut HtmHtmlVisitorBridge,\n) {\n    if options.is_null() {\n        return;\n    }\n    // SAFETY: null check above guarantees options is a valid, aligned, initialised pointer.\n    let opts = unsafe { &mut *options };\n\n    if visitor.is_null() {\n        opts.visitor = None;\n        return;\n    }\n\n    // Wrap the raw bridge pointer in a thin delegating type that implements the trait.\n    // `VtableRef` borrows the bridge by raw pointer and must not outlive the bridge handle.\n    struct VtableRef(*mut HtmHtmlVisitorBridge);\n\n    impl std::fmt::Debug for VtableRef {\n        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n            f.debug_tuple(\"VtableRef\").finish()\n        }\n    }\n\n    // SAFETY: HtmHtmlVisitorBridge is `Send + Sync` (unsafe impl generated by gen_trait_bridge).\n    // The caller guarantees the pointer remains valid while options is in use.\n    unsafe impl Send for VtableRef {}\n\n    impl html_to_markdown_rs::visitor::HtmlVisitor for VtableRef {\n        fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_element_start(_ctx) }\n        }\n        fn visit_element_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_element_end(_ctx, _output) }\n        }\n        fn visit_text(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_text(_ctx, _text) }\n        }\n        fn visit_link(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _href: &str,\n            _text: &str,\n            _title: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_link(_ctx, _href, _text, _title) }\n        }\n        fn visit_image(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: &str,\n            _alt: &str,\n            _title: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_image(_ctx, _src, _alt, _title) }\n        }\n        fn visit_heading(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _level: u32,\n            _text: &str,\n            _id: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_heading(_ctx, _level, _text, _id) }\n        }\n        fn visit_code_block(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _lang: Option<&str>,\n            _code: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_code_block(_ctx, _lang, _code) }\n        }\n        fn visit_code_inline(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _code: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_code_inline(_ctx, _code) }\n        }\n        fn visit_list_item(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n            _marker: &str,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_list_item(_ctx, _ordered, _marker, _text) }\n        }\n        fn visit_list_start(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_list_start(_ctx, _ordered) }\n        }\n        fn visit_list_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_list_end(_ctx, _ordered, _output) }\n        }\n        fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_table_start(_ctx) }\n        }\n        fn visit_table_row(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _cells: &[String],\n            _is_header: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_table_row(_ctx, _cells, _is_header) }\n        }\n        fn visit_table_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_table_end(_ctx, _output) }\n        }\n        fn visit_blockquote(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _content: &str,\n            _depth: usize,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_blockquote(_ctx, _content, _depth) }\n        }\n        fn visit_strong(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_strong(_ctx, _text) }\n        }\n        fn visit_emphasis(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_emphasis(_ctx, _text) }\n        }\n        fn visit_strikethrough(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_strikethrough(_ctx, _text) }\n        }\n        fn visit_underline(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_underline(_ctx, _text) }\n        }\n        fn visit_subscript(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_subscript(_ctx, _text) }\n        }\n        fn visit_superscript(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_superscript(_ctx, _text) }\n        }\n        fn visit_mark(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_mark(_ctx, _text) }\n        }\n        fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_line_break(_ctx) }\n        }\n        fn visit_horizontal_rule(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_horizontal_rule(_ctx) }\n        }\n        fn visit_custom_element(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _tag_name: &str,\n            _html: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_custom_element(_ctx, _tag_name, _html) }\n        }\n        fn visit_definition_list_start(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_definition_list_start(_ctx) }\n        }\n        fn visit_definition_term(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_definition_term(_ctx, _text) }\n        }\n        fn visit_definition_description(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_definition_description(_ctx, _text) }\n        }\n        fn visit_definition_list_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_definition_list_end(_ctx, _output) }\n        }\n        fn visit_form(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _action: Option<&str>,\n            _method: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_form(_ctx, _action, _method) }\n        }\n        fn visit_input(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _input_type: &str,\n            _name: Option<&str>,\n            _value: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_input(_ctx, _input_type, _name, _value) }\n        }\n        fn visit_button(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_button(_ctx, _text) }\n        }\n        fn visit_audio(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_audio(_ctx, _src) }\n        }\n        fn visit_video(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_video(_ctx, _src) }\n        }\n        fn visit_iframe(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_iframe(_ctx, _src) }\n        }\n        fn visit_details(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _open: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_details(_ctx, _open) }\n        }\n        fn visit_summary(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_summary(_ctx, _text) }\n        }\n        fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_figure_start(_ctx) }\n        }\n        fn visit_figcaption(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_figcaption(_ctx, _text) }\n        }\n        fn visit_figure_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            // SAFETY: self.0 is a valid pointer for the duration of the conversion call.\n            unsafe { (*self.0).visit_figure_end(_ctx, _output) }\n        }\n    }\n\n    // SAFETY: visitor is non-null; wrapping in Rc<RefCell<_>> is safe for single-threaded use.\n    opts.visitor = Some(std::rc::Rc::new(std::cell::RefCell::new(VtableRef(visitor))));\n}\n\n/// Convert HTML to Markdown.\n///\n/// Returns a heap-allocated [`ConversionResult`] on success, or null on failure.\n/// Check `htm_last_error_code` / `htm_last_error_context` for error details.\n/// The returned pointer must be freed with `htm_conversion_result_free`.\n///\n/// If a visitor was attached to `options` via `htm_options_set_visitor`, it will\n/// receive callbacks during conversion.\n///\n/// # Arguments\n///\n/// - `html`: null-terminated, UTF-8 HTML input. Must not be null.\n/// - `options`: optional conversion options (with optional embedded visitor); pass null for defaults.\n///\n/// # Safety\n///\n/// `html` must be a valid, non-null, null-terminated UTF-8 string.\n/// `options` must be a valid pointer or null.\n/// Returned pointer must be freed with `htm_conversion_result_free`.\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn htm_convert(\n    html: *const std::ffi::c_char,\n    options: *const html_to_markdown_rs::ConversionOptions,\n) -> *mut html_to_markdown_rs::ConversionResult {\n    clear_last_error();\n\n    if html.is_null() {\n        set_last_error(1, \"Null pointer passed for html\");\n        return std::ptr::null_mut();\n    }\n\n    // SAFETY: null check above guarantees html is a valid pointer; string is valid UTF-8 from caller.\n    let html_str = match unsafe { std::ffi::CStr::from_ptr(html) }.to_str() {\n        Ok(s) => s,\n        Err(_) => {\n            set_last_error(1, \"Invalid UTF-8 in html parameter\");\n            return std::ptr::null_mut();\n        }\n    };\n\n    // Clone options out of the pointer.  Any visitor attached via\n    // `htm_options_set_visitor` is embedded in options.visitor and will be\n    // picked up automatically by the core convert call.\n    let options_rs: Option<html_to_markdown_rs::ConversionOptions> = if options.is_null() {\n        None\n    } else {\n        // SAFETY: null check above guarantees options is a valid pointer.\n        Some(unsafe { &*options }.clone())\n    };\n\n    match html_to_markdown_rs::convert(html_str, options_rs) {\n        Ok(result) => Box::into_raw(Box::new(result)),\n        Err(e) => {\n            set_last_error(2, &e.to_string());\n            std::ptr::null_mut()\n        }\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-node\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter - Node.js bindings\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\", \"web-programming\"]\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../html-to-markdown\", features = [\n    \"full\",\n    \"metadata\",\n    \"visitor\",\n    \"serde\",\n    \"inline-images\",\n] }\nnapi = { version = \"3\", features = [\"async\"] }\nnapi-derive = \"3\"\n\n[build-dependencies]\nnapi-build = \"2\"\n"
  },
  {
    "path": "crates/html-to-markdown-node/index.d.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:10f5bd56ef85b7e317bb3706a9a6274d334a1e6eb4bfd80e4975405fc8e01186\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n/* eslint-disable */\n\n/**\n * Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n * and warnings.\n *\n * # Arguments\n *\n * * `html` — the HTML string to convert.\n * * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n * When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n * attached via the `visitor` field on `ConversionOptions`.\n *\n * # Example\n *\n * ```\n * use html_to_markdown_rs::convert;\n *\n * let html = \"<h1>Hello World</h1>\";\n * let result = convert(html, None).unwrap();\n * assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n * ```\n *\n * # Errors\n *\n * Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n */\nexport declare function convert(html: string, options?: JsConversionOptions | undefined | null): JsConversionResult;\n\n/**\n * The type of an inline text annotation.\n *\n * Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n */\nexport type JsAnnotationKind =\n  | { annotation_type: 'Bold' }\n  | { annotation_type: 'Italic' }\n  | { annotation_type: 'Underline' }\n  | { annotation_type: 'Strikethrough' }\n  | { annotation_type: 'Code' }\n  | { annotation_type: 'Subscript' }\n  | { annotation_type: 'Superscript' }\n  | { annotation_type: 'Highlight' }\n  | { annotation_type: 'Link'; url: string; title: string }\n\n/**\n * Code block fence style in Markdown output.\n *\n * Determines how code blocks (`<pre><code>`) are rendered in Markdown.\n */\nexport declare enum JsCodeBlockStyle {\n  /** Indented code blocks (4 spaces). `CommonMark` standard. */\n  Indented = \"Indented\",\n  /** Fenced code blocks with backticks (```). Default (GFM). Supports language hints. */\n  Backticks = \"Backticks\",\n  /** Fenced code blocks with tildes (~~~). Supports language hints. */\n  Tildes = \"Tildes\",\n}\n\n/**\n * Main conversion options for HTML to Markdown conversion.\n *\n * Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n *\n * # Example\n *\n * ```text\n * use html_to_markdown_rs::ConversionOptions;\n *\n * let options = ConversionOptions::builder()\n * .heading_style(HeadingStyle::Atx)\n * .wrap(true)\n * .wrap_width(100)\n * .build();\n * ```\n */\nexport interface JsConversionOptions {\n  /** Heading style to use in Markdown output (ATX `#` or Setext underline). */\n  headingStyle: JsHeadingStyle\n  /** How to indent nested list items (spaces or tab). */\n  listIndentType: JsListIndentType\n  /** Number of spaces (or tabs) to use for each level of list indentation. */\n  listIndentWidth: number\n  /** Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). */\n  bullets: string\n  /** Character used for bold/italic emphasis markers (`*` or `_`). */\n  strongEmSymbol: string\n  /** Escape `*` characters in plain text to avoid unintended bold/italic. */\n  escapeAsterisks: boolean\n  /** Escape `_` characters in plain text to avoid unintended bold/italic. */\n  escapeUnderscores: boolean\n  /** Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. */\n  escapeMisc: boolean\n  /** Escape ASCII characters that have special meaning in certain Markdown dialects. */\n  escapeAscii: boolean\n  /** Default language annotation for fenced code blocks that have no language hint. */\n  codeLanguage: string\n  /** Automatically convert bare URLs into Markdown autolinks. */\n  autolinks: boolean\n  /** Emit a default title when no `<title>` tag is present. */\n  defaultTitle: boolean\n  /** Render `<br>` elements inside table cells as literal line breaks. */\n  brInTables: boolean\n  /** Style used for `<mark>` / highlighted text (e.g. `==text==`). */\n  highlightStyle: JsHighlightStyle\n  /** Extract `<meta>` and `<head>` information into the result metadata. */\n  extractMetadata: boolean\n  /** Controls how whitespace is normalised during conversion. */\n  whitespaceMode: JsWhitespaceMode\n  /** Strip all newlines from the output, producing a single-line result. */\n  stripNewlines: boolean\n  /** Wrap long lines at [`wrap_width`](Self::wrap_width) characters. */\n  wrap: boolean\n  /** Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`). */\n  wrapWidth: number\n  /** Treat the entire document as inline content (no block-level wrappers). */\n  convertAsInline: boolean\n  /** Markdown notation for subscript text (e.g. `\"~\"`). */\n  subSymbol: string\n  /** Markdown notation for superscript text (e.g. `\"^\"`). */\n  supSymbol: string\n  /** How to encode hard line breaks (`<br>`) in Markdown. */\n  newlineStyle: JsNewlineStyle\n  /** Style used for fenced code blocks (backticks or tilde). */\n  codeBlockStyle: JsCodeBlockStyle\n  /** HTML tag names whose `<img>` children are kept inline instead of block. */\n  keepInlineImagesIn: Array<string>\n  /** Pre-processing options applied to the HTML before conversion. */\n  preprocessing: JsPreprocessingOptions\n  /** Expected character encoding of the input HTML (default `\"utf-8\"`). */\n  encoding: string\n  /** Emit debug information during conversion. */\n  debug: boolean\n  /** HTML tag names whose content is stripped from the output entirely. */\n  stripTags: Array<string>\n  /** HTML tag names that are preserved verbatim in the output. */\n  preserveTags: Array<string>\n  /** Skip conversion of `<img>` elements (omit images from output). */\n  skipImages: boolean\n  /** Link rendering style (inline or reference). */\n  linkStyle: JsLinkStyle\n  /** Target output format (Markdown, plain text, etc.). */\n  outputFormat: JsOutputFormat\n  /** Include structured document tree in result. */\n  includeDocumentStructure: boolean\n  /** Extract inline images from data URIs and SVGs. */\n  extractImages: boolean\n  /** Maximum decoded image size in bytes (default 5MB). */\n  maxImageSize: number\n  /** Capture SVG elements as images. */\n  captureSvg: boolean\n  /** Infer image dimensions from data. */\n  inferDimensions: boolean\n  /**\n   * Maximum DOM traversal depth. `None` means unlimited.\n   * When set, subtrees beyond this depth are silently truncated.\n   */\n  maxDepth: number\n  /**\n   * CSS selectors for elements to exclude entirely (element + all content).\n   *\n   * Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n   * excluded elements and all their descendants are dropped from the output.\n   * Supports any CSS selector that `tl` supports: tag names, `.class`,\n   * `#id`, `[attribute]`, etc.\n   *\n   * Invalid selectors are silently skipped at conversion time.\n   *\n   * Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n   */\n  excludeSelectors: Array<string>\n  /**\n   * Optional visitor for custom traversal logic.\n   *\n   * When set, the visitor's callbacks are invoked for matching HTML elements\n   * during conversion, allowing custom output, skipping, or HTML preservation.\n   * See [`crate::visitor::HtmlVisitor`].\n   */\n  visitor: JsVisitorHandle\n}\n\n/**\n * Builder for [`ConversionOptions`].\n *\n * All fields start with default values. Call `.build()` to produce the final options.\n */\nexport declare class JsConversionOptionsBuilder {\n  /** Set the list of HTML tag names whose content is stripped from output. */\n  stripTags(tags: Array<string>): JsConversionOptionsBuilder\n  /** Set the list of HTML tag names that are preserved verbatim in output. */\n  preserveTags(tags: Array<string>): JsConversionOptionsBuilder\n  /** Set the list of HTML tag names whose `<img>` children are kept inline. */\n  keepInlineImagesIn(tags: Array<string>): JsConversionOptionsBuilder\n  /** Set the list of CSS selectors for elements to exclude entirely from output. */\n  excludeSelectors(selectors: Array<string>): JsConversionOptionsBuilder\n  /** Set the visitor used during conversion. */\n  visitor(visitor?: JsVisitorHandle | undefined | null): JsConversionOptionsBuilder\n  /** Set the pre-processing options applied to the HTML before conversion. */\n  preprocessing(preprocessing: JsPreprocessingOptions): JsConversionOptionsBuilder\n  /** Build the final [`ConversionOptions`]. */\n  build(): JsConversionOptions\n}\n\n/**\n * Partial update for `ConversionOptions`.\n *\n * Uses `Option<T>` fields for selective updates. Bindings use this to construct\n * options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n */\nexport interface JsConversionOptionsUpdate {\n  /** Optional override for [`ConversionOptions::heading_style`]. */\n  headingStyle: JsHeadingStyle\n  /** Optional override for [`ConversionOptions::list_indent_type`]. */\n  listIndentType: JsListIndentType\n  /** Optional override for [`ConversionOptions::list_indent_width`]. */\n  listIndentWidth: number\n  /** Optional override for [`ConversionOptions::bullets`]. */\n  bullets: string\n  /** Optional override for [`ConversionOptions::strong_em_symbol`]. */\n  strongEmSymbol: string\n  /** Optional override for [`ConversionOptions::escape_asterisks`]. */\n  escapeAsterisks: boolean\n  /** Optional override for [`ConversionOptions::escape_underscores`]. */\n  escapeUnderscores: boolean\n  /** Optional override for [`ConversionOptions::escape_misc`]. */\n  escapeMisc: boolean\n  /** Optional override for [`ConversionOptions::escape_ascii`]. */\n  escapeAscii: boolean\n  /** Optional override for [`ConversionOptions::code_language`]. */\n  codeLanguage: string\n  /** Optional override for [`ConversionOptions::autolinks`]. */\n  autolinks: boolean\n  /** Optional override for [`ConversionOptions::default_title`]. */\n  defaultTitle: boolean\n  /** Optional override for [`ConversionOptions::br_in_tables`]. */\n  brInTables: boolean\n  /** Optional override for [`ConversionOptions::highlight_style`]. */\n  highlightStyle: JsHighlightStyle\n  /** Optional override for [`ConversionOptions::extract_metadata`]. */\n  extractMetadata: boolean\n  /** Optional override for [`ConversionOptions::whitespace_mode`]. */\n  whitespaceMode: JsWhitespaceMode\n  /** Optional override for [`ConversionOptions::strip_newlines`]. */\n  stripNewlines: boolean\n  /** Optional override for [`ConversionOptions::wrap`]. */\n  wrap: boolean\n  /** Optional override for [`ConversionOptions::wrap_width`]. */\n  wrapWidth: number\n  /** Optional override for [`ConversionOptions::convert_as_inline`]. */\n  convertAsInline: boolean\n  /** Optional override for [`ConversionOptions::sub_symbol`]. */\n  subSymbol: string\n  /** Optional override for [`ConversionOptions::sup_symbol`]. */\n  supSymbol: string\n  /** Optional override for [`ConversionOptions::newline_style`]. */\n  newlineStyle: JsNewlineStyle\n  /** Optional override for [`ConversionOptions::code_block_style`]. */\n  codeBlockStyle: JsCodeBlockStyle\n  /** Optional override for [`ConversionOptions::keep_inline_images_in`]. */\n  keepInlineImagesIn: Array<string>\n  /** Optional override for [`ConversionOptions::preprocessing`]. */\n  preprocessing: JsPreprocessingOptionsUpdate\n  /** Optional override for [`ConversionOptions::encoding`]. */\n  encoding: string\n  /** Optional override for [`ConversionOptions::debug`]. */\n  debug: boolean\n  /** Optional override for [`ConversionOptions::strip_tags`]. */\n  stripTags: Array<string>\n  /** Optional override for [`ConversionOptions::preserve_tags`]. */\n  preserveTags: Array<string>\n  /** Optional override for [`ConversionOptions::skip_images`]. */\n  skipImages: boolean\n  /** Optional override for [`ConversionOptions::link_style`]. */\n  linkStyle: JsLinkStyle\n  /** Optional override for [`ConversionOptions::output_format`]. */\n  outputFormat: JsOutputFormat\n  /** Optional override for [`ConversionOptions::include_document_structure`]. */\n  includeDocumentStructure: boolean\n  /** Optional override for [`ConversionOptions::extract_images`]. */\n  extractImages: boolean\n  /** Optional override for [`ConversionOptions::max_image_size`]. */\n  maxImageSize: number\n  /** Optional override for [`ConversionOptions::capture_svg`]. */\n  captureSvg: boolean\n  /** Optional override for [`ConversionOptions::infer_dimensions`]. */\n  inferDimensions: boolean\n  /** Optional override for [`ConversionOptions::max_depth`]. */\n  maxDepth?: number | undefined | null\n  /** Optional override for [`ConversionOptions::exclude_selectors`]. */\n  excludeSelectors: Array<string>\n  /** Optional override for [`ConversionOptions::visitor`]. */\n  visitor: JsVisitorHandle\n}\n\n/**\n * The primary result of HTML conversion and extraction.\n *\n * Contains the converted text output, optional structured document tree,\n * metadata, extracted tables, images, and processing warnings.\n *\n * # Example\n *\n * ```text\n * use html_to_markdown_rs::{convert, ConversionOptions};\n *\n * let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n * assert!(result.content.is_some());\n * assert!(result.warnings.is_empty());\n * ```\n */\nexport interface JsConversionResult {\n  /**\n   * Converted text output (markdown, djot, or plain text).\n   *\n   * `None` when `output_format` is set to `OutputFormat::None`,\n   * indicating extraction-only mode.\n   */\n  content: string\n  /**\n   * Structured document tree with semantic elements.\n   *\n   * Populated when `include_document_structure` is `true` in options.\n   */\n  document: JsDocumentStructure\n  /** Extracted HTML metadata (title, OG, links, images, structured data). */\n  metadata: JsHtmlMetadata\n  /** Extracted tables with structured cell data and markdown representation. */\n  tables: Array<JsTableData>\n  /** Non-fatal processing warnings. */\n  warnings: Array<JsProcessingWarning>\n}\n\n/**\n * Document-level metadata extracted from `<head>` and top-level elements.\n *\n * Contains all metadata typically used by search engines, social media platforms,\n * and browsers for document indexing and presentation.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::DocumentMetadata;\n * let doc = DocumentMetadata {\n * title: Some(\"My Article\".to_string()),\n * description: Some(\"A great article about Rust\".to_string()),\n * keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n * ..Default::default()\n * };\n *\n * assert_eq!(doc.title, Some(\"My Article\".to_string()));\n * ```\n */\nexport interface JsDocumentMetadata {\n  /** Document title from `<title>` tag */\n  title: string\n  /** Document description from `<meta name=\"description\">` tag */\n  description: string\n  /** Document keywords from `<meta name=\"keywords\">` tag, split on commas */\n  keywords: Array<string>\n  /** Document author from `<meta name=\"author\">` tag */\n  author: string\n  /** Canonical URL from `<link rel=\"canonical\">` tag */\n  canonicalUrl: string\n  /** Base URL from `<base href=\"\">` tag for resolving relative URLs */\n  baseHref: string\n  /** Document language from `lang` attribute */\n  language: string\n  /** Document text direction from `dir` attribute */\n  textDirection: JsTextDirection\n  /**\n   * Open Graph metadata (og:* properties) for social media\n   * Keys like \"title\", \"description\", \"image\", \"url\", etc.\n   */\n  openGraph: Record<string, string>\n  /**\n   * Twitter Card metadata (twitter:* properties)\n   * Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n   */\n  twitterCard: Record<string, string>\n  /**\n   * Additional meta tags not covered by specific fields\n   * Keys are meta name/property attributes, values are content\n   */\n  metaTags: Record<string, string>\n}\n\n/** A single node in the document tree. */\nexport interface JsDocumentNode {\n  /** Deterministic node identifier. */\n  id: string\n  /** The semantic content of this node. */\n  content: JsNodeContent\n  /** Index of the parent node (None for root nodes). */\n  parent: number\n  /** Indices of child nodes in reading order. */\n  children: Array<number>\n  /** Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. */\n  annotations: Array<JsTextAnnotation>\n  /** Format-specific attributes (e.g. class, id, data-* attributes). */\n  attributes: Record<string, string>\n}\n\n/**\n * A structured document tree representing the semantic content of an HTML document.\n *\n * Uses a flat node array with index-based parent/child references for efficient traversal.\n */\nexport interface JsDocumentStructure {\n  /** All nodes in document reading order. */\n  nodes: Array<JsDocumentNode>\n  /** The source format (always \"html\" for this crate). */\n  sourceFormat: string\n}\n\n/** A single cell in a table grid. */\nexport interface JsGridCell {\n  /** The text content of the cell. */\n  content: string\n  /** 0-indexed row position. */\n  row: number\n  /** 0-indexed column position. */\n  col: number\n  /** Number of rows this cell spans (default 1). */\n  rowSpan: number\n  /** Number of columns this cell spans (default 1). */\n  colSpan: number\n  /** Whether this is a header cell (`<th>`). */\n  isHeader: boolean\n}\n\n/**\n * Header element metadata with hierarchy tracking.\n *\n * Captures heading elements (h1-h6) with their text content, identifiers,\n * and position in the document structure.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::HeaderMetadata;\n * let header = HeaderMetadata {\n * level: 1,\n * text: \"Main Title\".to_string(),\n * id: Some(\"main-title\".to_string()),\n * depth: 0,\n * html_offset: 145,\n * };\n *\n * assert_eq!(header.level, 1);\n * assert!(header.is_valid());\n * ```\n */\nexport interface JsHeaderMetadata {\n  /** Header level: 1 (h1) through 6 (h6) */\n  level: number\n  /** Normalized text content of the header */\n  text: string\n  /** HTML id attribute if present */\n  id: string\n  /** Document tree depth at the header element */\n  depth: number\n  /** Byte offset in original HTML document */\n  htmlOffset: number\n}\n\n/**\n * Heading style options for Markdown output.\n *\n * Controls how headings (h1-h6) are rendered in the output Markdown.\n */\nexport declare enum JsHeadingStyle {\n  /** Underlined style (=== for h1, --- for h2). */\n  Underlined = \"Underlined\",\n  /** ATX style (# for h1, ## for h2, etc.). Default. */\n  Atx = \"Atx\",\n  /** ATX closed style (# title #, with closing hashes). */\n  AtxClosed = \"AtxClosed\",\n}\n\n/**\n * Highlight rendering style for `<mark>` elements.\n *\n * Controls how highlighted text is rendered in Markdown output.\n */\nexport declare enum JsHighlightStyle {\n  /** Double equals syntax (==text==). Default. Pandoc-compatible. */\n  DoubleEqual = \"DoubleEqual\",\n  /** Preserve as HTML (==text==). Original HTML tag. */\n  Html = \"Html\",\n  /** Render as bold (**text**). Uses strong emphasis. */\n  Bold = \"Bold\",\n  /** Strip formatting, render as plain text. No markup. */\n  None = \"None\",\n}\n\n/**\n * Comprehensive metadata extraction result from HTML document.\n *\n * Contains all extracted metadata types in a single structure,\n * suitable for serialization and transmission across language boundaries.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::HtmlMetadata;\n * let metadata = HtmlMetadata {\n * document: Default::default(),\n * headers: Vec::new(),\n * links: Vec::new(),\n * images: Vec::new(),\n * structured_data: Vec::new(),\n * };\n *\n * assert!(metadata.headers.is_empty());\n * ```\n */\nexport interface JsHtmlMetadata {\n  /** Document-level metadata (title, description, canonical, etc.) */\n  document: JsDocumentMetadata\n  /** Extracted header elements with hierarchy */\n  headers: Array<JsHeaderMetadata>\n  /** Extracted hyperlinks with type classification */\n  links: Array<JsLinkMetadata>\n  /** Extracted images with source and dimensions */\n  images: Array<JsImageMetadata>\n  /** Extracted structured data blocks */\n  structuredData: Array<JsStructuredData>\n}\n\n/**\n * Visitor trait for HTML→Markdown conversion.\n *\n * Implement this trait to customize the conversion behavior for any HTML element type.\n * All methods have default implementations that return `VisitResult::Continue`, allowing\n * selective override of only the elements you care about.\n *\n * # Method Naming Convention\n *\n * - `visit_*_start`: Called before entering an element (pre-order traversal)\n * - `visit_*_end`: Called after exiting an element (post-order traversal)\n * - `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n *\n * # Execution Order\n *\n * For a typical element like `<div><p>text</p></div>`:\n * 1. `visit_element_start` for `<div>`\n * 2. `visit_element_start` for `<p>`\n * 3. `visit_text` for \"text\"\n * 4. `visit_element_end` for `<p>`\n * 5. `visit_element_end` for `</div>`\n *\n * # Performance Notes\n *\n * - `visit_text` is the most frequently called method (~100+ times per document)\n * - Return `VisitResult::Continue` quickly for elements you don't need to customize\n * - Avoid heavy computation in visitor methods; consider caching if needed\n */\nexport declare class JsHtmlVisitor {\n  /**\n   * Called before entering any element.\n   *\n   * This is the first callback invoked for every HTML element, allowing\n   * visitors to implement generic element handling before tag-specific logic.\n   */\n  visitElementStart(ctx: JsNodeContext): JsVisitResult\n  /**\n   * Called after exiting any element.\n   *\n   * Receives the default markdown output that would be generated.\n   * Visitors can inspect or replace this output.\n   */\n  visitElementEnd(ctx: JsNodeContext, output: string): JsVisitResult\n  /**\n   * Visit text nodes (most frequent callback - ~100+ per document).\n   *\n   * # Arguments\n   * - `ctx`: Node context (will have `node_type: NodeType::Text`)\n   * - `text`: The raw text content (HTML entities already decoded)\n   */\n  visitText(ctx: JsNodeContext, text: string): JsVisitResult\n  /**\n   * Visit anchor links `<a href=\"...\">`.\n   *\n   * # Arguments\n   * - `ctx`: Node context with link element metadata\n   * - `href`: The link URL (from `href` attribute)\n   * - `text`: The link text content (already converted to markdown)\n   * - `title`: Optional title attribute\n   */\n  visitLink(ctx: JsNodeContext, href: string, text: string, title?: string | undefined | null): JsVisitResult\n  /**\n   * Visit images `<img src=\"...\">`.\n   *\n   * # Arguments\n   * - `ctx`: Node context with image element metadata\n   * - `src`: The image source URL\n   * - `alt`: The alt text\n   * - `title`: Optional title attribute\n   */\n  visitImage(ctx: JsNodeContext, src: string, alt: string, title?: string | undefined | null): JsVisitResult\n  /**\n   * Visit heading elements `<h1>` through `<h6>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context with heading metadata\n   * - `level`: Heading level (1-6)\n   * - `text`: The heading text content\n   * - `id`: Optional id attribute (for anchor links)\n   */\n  visitHeading(ctx: JsNodeContext, level: number, text: string, id?: string | undefined | null): JsVisitResult\n  /**\n   * Visit code blocks `<pre><code>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `lang`: Optional language specifier (from class attribute)\n   * - `code`: The code content\n   */\n  visitCodeBlock(ctx: JsNodeContext, code: string, lang?: string | undefined | null): JsVisitResult\n  /**\n   * Visit inline code `<code>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `code`: The code content\n   */\n  visitCodeInline(ctx: JsNodeContext, code: string): JsVisitResult\n  /**\n   * Visit list items `<li>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `ordered`: Whether this is an ordered list item\n   * - `marker`: The list marker (e.g., \"-\", \"1.\", \"a)\")\n   * - `text`: The list item content (already converted)\n   */\n  visitListItem(ctx: JsNodeContext, ordered: boolean, marker: string, text: string): JsVisitResult\n  /** Called before processing a list `<ul>` or `<ol>`. */\n  visitListStart(ctx: JsNodeContext, ordered: boolean): JsVisitResult\n  /** Called after processing a list `</ul>` or `</ol>`. */\n  visitListEnd(ctx: JsNodeContext, ordered: boolean, output: string): JsVisitResult\n  /** Called before processing a table `<table>`. */\n  visitTableStart(ctx: JsNodeContext): JsVisitResult\n  /**\n   * Visit table rows `<tr>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `cells`: Cell contents (already converted to markdown)\n   * - `is_header`: Whether this row is in `<thead>`\n   */\n  visitTableRow(ctx: JsNodeContext, cells: Array<string>, isHeader: boolean): JsVisitResult\n  /** Called after processing a table `</table>`. */\n  visitTableEnd(ctx: JsNodeContext, output: string): JsVisitResult\n  /**\n   * Visit blockquote elements `<blockquote>`.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `content`: The blockquote content (already converted)\n   * - `depth`: Nesting depth (for nested blockquotes)\n   */\n  visitBlockquote(ctx: JsNodeContext, content: string, depth: number): JsVisitResult\n  /** Visit strong/bold elements `<strong>`, `<b>`. */\n  visitStrong(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit emphasis/italic elements `<em>`, `<i>`. */\n  visitEmphasis(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit strikethrough elements `<s>`, `<del>`, `<strike>`. */\n  visitStrikethrough(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit underline elements `<u>`, `<ins>`. */\n  visitUnderline(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit subscript elements `<sub>`. */\n  visitSubscript(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit superscript elements `<sup>`. */\n  visitSuperscript(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit mark/highlight elements `<mark>`. */\n  visitMark(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit line break elements `<br>`. */\n  visitLineBreak(ctx: JsNodeContext): JsVisitResult\n  /** Visit horizontal rule elements `<hr>`. */\n  visitHorizontalRule(ctx: JsNodeContext): JsVisitResult\n  /**\n   * Visit custom elements (web components) or unknown tags.\n   *\n   * # Arguments\n   * - `ctx`: Node context\n   * - `tag_name`: The custom element's tag name\n   * - `html`: The raw HTML of this element\n   */\n  visitCustomElement(ctx: JsNodeContext, tagName: string, html: string): JsVisitResult\n  /** Visit definition list `<dl>`. */\n  visitDefinitionListStart(ctx: JsNodeContext): JsVisitResult\n  /** Visit definition term `<dt>`. */\n  visitDefinitionTerm(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit definition description `<dd>`. */\n  visitDefinitionDescription(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Called after processing a definition list `</dl>`. */\n  visitDefinitionListEnd(ctx: JsNodeContext, output: string): JsVisitResult\n  /** Visit form elements `<form>`. */\n  visitForm(ctx: JsNodeContext, action?: string | undefined | null, method?: string | undefined | null): JsVisitResult\n  /** Visit input elements `<input>`. */\n  visitInput(ctx: JsNodeContext, inputType: string, name?: string | undefined | null, value?: string | undefined | null): JsVisitResult\n  /** Visit button elements `<button>`. */\n  visitButton(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit audio elements `<audio>`. */\n  visitAudio(ctx: JsNodeContext, src?: string | undefined | null): JsVisitResult\n  /** Visit video elements `<video>`. */\n  visitVideo(ctx: JsNodeContext, src?: string | undefined | null): JsVisitResult\n  /** Visit iframe elements `<iframe>`. */\n  visitIframe(ctx: JsNodeContext, src?: string | undefined | null): JsVisitResult\n  /** Visit details elements `<details>`. */\n  visitDetails(ctx: JsNodeContext, open: boolean): JsVisitResult\n  /** Visit summary elements `<summary>`. */\n  visitSummary(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Visit figure elements `<figure>`. */\n  visitFigureStart(ctx: JsNodeContext): JsVisitResult\n  /** Visit figcaption elements `<figcaption>`. */\n  visitFigcaption(ctx: JsNodeContext, text: string): JsVisitResult\n  /** Called after processing a figure `</figure>`. */\n  visitFigureEnd(ctx: JsNodeContext, output: string): JsVisitResult\n}\n\n/**\n * Image metadata with source and dimensions.\n *\n * Captures `<img>` elements and inline `<svg>` elements with metadata\n * for image analysis and optimization.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n * let img = ImageMetadata {\n * src: \"https://example.com/image.jpg\".to_string(),\n * alt: Some(\"An example image\".to_string()),\n * title: Some(\"Example\".to_string()),\n * dimensions: Some((800, 600)),\n * image_type: ImageType::External,\n * attributes: Default::default(),\n * };\n *\n * assert_eq!(img.image_type, ImageType::External);\n * ```\n */\nexport interface JsImageMetadata {\n  /** Image source (URL, data URI, or SVG content identifier) */\n  src: string\n  /** Alternative text from alt attribute (for accessibility) */\n  alt: string\n  /** Title attribute (often shown as tooltip) */\n  title: string\n  /** Image type classification */\n  imageType: JsImageType\n  /** Additional HTML attributes */\n  attributes: Record<string, string>\n}\n\n/**\n * Image source classification for proper handling and processing.\n *\n * Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n */\nexport declare enum JsImageType {\n  /** Data URI embedded image (base64 or other encoding) */\n  DataUri = \"DataUri\",\n  /** Inline SVG element */\n  InlineSvg = \"InlineSvg\",\n  /** External image URL (http/https) */\n  External = \"External\",\n  /** Relative image path */\n  Relative = \"Relative\",\n}\n\n/**\n * Hyperlink metadata with categorization and attributes.\n *\n * Represents `<a>` elements with parsed href values, text content, and link type classification.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n * let link = LinkMetadata {\n * href: \"https://example.com\".to_string(),\n * text: \"Example\".to_string(),\n * title: Some(\"Visit Example\".to_string()),\n * link_type: LinkType::External,\n * rel: vec![\"nofollow\".to_string()],\n * attributes: Default::default(),\n * };\n *\n * assert_eq!(link.link_type, LinkType::External);\n * assert_eq!(link.text, \"Example\");\n * ```\n */\nexport interface JsLinkMetadata {\n  /** The href URL value */\n  href: string\n  /** Link text content (normalized, concatenated if mixed with elements) */\n  text: string\n  /** Optional title attribute (often shown as tooltip) */\n  title: string\n  /** Link type classification */\n  linkType: JsLinkType\n  /** Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") */\n  rel: Array<string>\n  /** Additional HTML attributes */\n  attributes: Record<string, string>\n}\n\n/**\n * Link rendering style in Markdown output.\n *\n * Controls whether links and images use inline `[text](url)` syntax or\n * reference-style `[text][1]` syntax with definitions collected at the end.\n */\nexport declare enum JsLinkStyle {\n  /** Inline links: `[text](url)`. Default. */\n  Inline = \"Inline\",\n  /** Reference-style links: `[text][1]` with `[1]: url` at end of document. */\n  Reference = \"Reference\",\n}\n\n/**\n * Link classification based on href value and document context.\n *\n * Used to categorize links during extraction for filtering and analysis.\n */\nexport declare enum JsLinkType {\n  /** Anchor link within same document (href starts with #) */\n  Anchor = \"Anchor\",\n  /** Internal link within same domain */\n  Internal = \"Internal\",\n  /** External link to different domain */\n  External = \"External\",\n  /** Email link (mailto:) */\n  Email = \"Email\",\n  /** Phone link (tel:) */\n  Phone = \"Phone\",\n  /** Other protocol or unclassifiable */\n  Other = \"Other\",\n}\n\n/**\n * List indentation character type.\n *\n * Controls whether list items are indented with spaces or tabs.\n */\nexport declare enum JsListIndentType {\n  /** Use spaces for indentation. Default. Width controlled by `list_indent_width`. */\n  Spaces = \"Spaces\",\n  /** Use tabs for indentation. */\n  Tabs = \"Tabs\",\n}\n\n/**\n * Line break syntax in Markdown output.\n *\n * Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n */\nexport declare enum JsNewlineStyle {\n  /** Two trailing spaces at end of line. Default. Standard Markdown syntax. */\n  Spaces = \"Spaces\",\n  /** Backslash at end of line. Alternative Markdown syntax. */\n  Backslash = \"Backslash\",\n}\n\n/**\n * The semantic content type of a document node.\n *\n * Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n */\nexport type JsNodeContent =\n  | { node_type: 'Heading'; level: number; text: string }\n  | { node_type: 'Paragraph'; text: string }\n  | { node_type: 'List'; ordered: boolean }\n  | { node_type: 'ListItem'; text: string }\n  | { node_type: 'Table'; grid: JsTableGrid }\n  | { node_type: 'Image'; description: string; src: string; imageIndex: number }\n  | { node_type: 'Code'; text: string; language: string }\n  | { node_type: 'Quote' }\n  | { node_type: 'DefinitionList' }\n  | { node_type: 'DefinitionItem'; term: string; definition: string }\n  | { node_type: 'RawBlock'; format: string; content: string }\n  | { node_type: 'MetadataBlock'; entries: Array<string> }\n  | { node_type: 'Group'; label: string; headingLevel: number; headingText: string }\n\n/**\n * Context information passed to all visitor methods.\n *\n * Provides comprehensive metadata about the current node being visited,\n * including its type, attributes, position in the DOM tree, and parent context.\n */\nexport interface JsNodeContext {\n  /** Coarse-grained node type classification */\n  nodeType: JsNodeType\n  /** Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") */\n  tagName: string\n  /** All HTML attributes as key-value pairs */\n  attributes: Record<string, string>\n  /** Depth in the DOM tree (0 = root) */\n  depth: number\n  /** Index among siblings (0-based) */\n  indexInParent: number\n  /** Parent element's tag name (None if root) */\n  parentTag: string\n  /** Whether this element is treated as inline vs block */\n  isInline: boolean\n}\n\n/**\n * Node type enumeration covering all HTML element types.\n *\n * This enum categorizes all HTML elements that the converter recognizes,\n * providing a coarse-grained classification for visitor dispatch.\n */\nexport declare enum JsNodeType {\n  /** Text node (most frequent - 100+ per document) */\n  Text = \"Text\",\n  /** Generic element node */\n  Element = \"Element\",\n  /** Heading elements (h1-h6) */\n  Heading = \"Heading\",\n  /** Paragraph element */\n  Paragraph = \"Paragraph\",\n  /** Generic div container */\n  Div = \"Div\",\n  /** Blockquote element */\n  Blockquote = \"Blockquote\",\n  /** Preformatted text block */\n  Pre = \"Pre\",\n  /** Horizontal rule */\n  Hr = \"Hr\",\n  /** Ordered or unordered list (ul, ol) */\n  List = \"List\",\n  /** List item (li) */\n  ListItem = \"ListItem\",\n  /** Definition list (dl) */\n  DefinitionList = \"DefinitionList\",\n  /** Definition term (dt) */\n  DefinitionTerm = \"DefinitionTerm\",\n  /** Definition description (dd) */\n  DefinitionDescription = \"DefinitionDescription\",\n  /** Table element */\n  Table = \"Table\",\n  /** Table row (tr) */\n  TableRow = \"TableRow\",\n  /** Table cell (td, th) */\n  TableCell = \"TableCell\",\n  /** Table header cell (th) */\n  TableHeader = \"TableHeader\",\n  /** Table body (tbody) */\n  TableBody = \"TableBody\",\n  /** Table head (thead) */\n  TableHead = \"TableHead\",\n  /** Table foot (tfoot) */\n  TableFoot = \"TableFoot\",\n  /** Anchor link (a) */\n  Link = \"Link\",\n  /** Image (img) */\n  Image = \"Image\",\n  /** Strong/bold (strong, b) */\n  Strong = \"Strong\",\n  /** Emphasis/italic (em, i) */\n  Em = \"Em\",\n  /** Inline code (code) */\n  Code = \"Code\",\n  /** Strikethrough (s, del, strike) */\n  Strikethrough = \"Strikethrough\",\n  /** Underline (u, ins) */\n  Underline = \"Underline\",\n  /** Subscript (sub) */\n  Subscript = \"Subscript\",\n  /** Superscript (sup) */\n  Superscript = \"Superscript\",\n  /** Mark/highlight (mark) */\n  Mark = \"Mark\",\n  /** Small text (small) */\n  Small = \"Small\",\n  /** Line break (br) */\n  Br = \"Br\",\n  /** Span element */\n  Span = \"Span\",\n  /** Article element */\n  Article = \"Article\",\n  /** Section element */\n  Section = \"Section\",\n  /** Navigation element */\n  Nav = \"Nav\",\n  /** Aside element */\n  Aside = \"Aside\",\n  /** Header element */\n  Header = \"Header\",\n  /** Footer element */\n  Footer = \"Footer\",\n  /** Main element */\n  Main = \"Main\",\n  /** Figure element */\n  Figure = \"Figure\",\n  /** Figure caption */\n  Figcaption = \"Figcaption\",\n  /** Time element */\n  Time = \"Time\",\n  /** Details element */\n  Details = \"Details\",\n  /** Summary element */\n  Summary = \"Summary\",\n  /** Form element */\n  Form = \"Form\",\n  /** Input element */\n  Input = \"Input\",\n  /** Select element */\n  Select = \"Select\",\n  /** Option element */\n  Option = \"Option\",\n  /** Button element */\n  Button = \"Button\",\n  /** Textarea element */\n  Textarea = \"Textarea\",\n  /** Label element */\n  Label = \"Label\",\n  /** Fieldset element */\n  Fieldset = \"Fieldset\",\n  /** Legend element */\n  Legend = \"Legend\",\n  /** Audio element */\n  Audio = \"Audio\",\n  /** Video element */\n  Video = \"Video\",\n  /** Picture element */\n  Picture = \"Picture\",\n  /** Source element */\n  Source = \"Source\",\n  /** Iframe element */\n  Iframe = \"Iframe\",\n  /** SVG element */\n  Svg = \"Svg\",\n  /** Canvas element */\n  Canvas = \"Canvas\",\n  /** Ruby annotation */\n  Ruby = \"Ruby\",\n  /** Ruby text */\n  Rt = \"Rt\",\n  /** Ruby parenthesis */\n  Rp = \"Rp\",\n  /** Abbreviation */\n  Abbr = \"Abbr\",\n  /** Keyboard input */\n  Kbd = \"Kbd\",\n  /** Sample output */\n  Samp = \"Samp\",\n  /** Variable */\n  Var = \"Var\",\n  /** Citation */\n  Cite = \"Cite\",\n  /** Quote */\n  Q = \"Q\",\n  /** Deleted text */\n  Del = \"Del\",\n  /** Inserted text */\n  Ins = \"Ins\",\n  /** Data element */\n  Data = \"Data\",\n  /** Meter element */\n  Meter = \"Meter\",\n  /** Progress element */\n  Progress = \"Progress\",\n  /** Output element */\n  Output = \"Output\",\n  /** Template element */\n  Template = \"Template\",\n  /** Slot element */\n  Slot = \"Slot\",\n  /** HTML root element */\n  Html = \"Html\",\n  /** Head element */\n  Head = \"Head\",\n  /** Body element */\n  Body = \"Body\",\n  /** Title element */\n  Title = \"Title\",\n  /** Meta element */\n  Meta = \"Meta\",\n  /** Link element (not anchor) */\n  LinkTag = \"LinkTag\",\n  /** Style element */\n  Style = \"Style\",\n  /** Script element */\n  Script = \"Script\",\n  /** Base element */\n  Base = \"Base\",\n  /** Custom element (web components) or unknown tag */\n  Custom = \"Custom\",\n}\n\n/**\n * Output format for conversion.\n *\n * Specifies the target markup language format for the conversion output.\n */\nexport declare enum JsOutputFormat {\n  /** Standard Markdown (CommonMark compatible). Default. */\n  Markdown = \"Markdown\",\n  /** Djot lightweight markup language. */\n  Djot = \"Djot\",\n  /** Plain text output (no markup, visible text only). */\n  Plain = \"Plain\",\n}\n\n/** HTML preprocessing options for document cleanup before conversion. */\nexport interface JsPreprocessingOptions {\n  /** Enable HTML preprocessing globally */\n  enabled: boolean\n  /** Preprocessing preset level (Minimal, Standard, Aggressive) */\n  preset: JsPreprocessingPreset\n  /** Remove navigation elements (nav, breadcrumbs, menus, sidebars) */\n  removeNavigation: boolean\n  /** Remove form elements (forms, inputs, buttons, etc.) */\n  removeForms: boolean\n}\n\n/**\n * Partial update for `PreprocessingOptions`.\n *\n * This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n * Only specified fields (Some values) will override existing options; None values leave the\n * corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n */\nexport interface JsPreprocessingOptionsUpdate {\n  /** Optional global preprocessing enablement override */\n  enabled: boolean\n  /** Optional preprocessing preset level override (Minimal, Standard, Aggressive) */\n  preset: JsPreprocessingPreset\n  /** Optional navigation element removal override (nav, breadcrumbs, menus, sidebars) */\n  removeNavigation: boolean\n  /** Optional form element removal override (forms, inputs, buttons, etc.) */\n  removeForms: boolean\n}\n\n/**\n * HTML preprocessing aggressiveness level.\n *\n * Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n */\nexport declare enum JsPreprocessingPreset {\n  /** Minimal cleanup. Remove only essential noise (scripts, styles). */\n  Minimal = \"Minimal\",\n  /** Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. */\n  Standard = \"Standard\",\n  /** Aggressive cleanup. Remove extensive non-content elements and structure. */\n  Aggressive = \"Aggressive\",\n}\n\n/** A non-fatal warning generated during HTML processing. */\nexport interface JsProcessingWarning {\n  /** Human-readable warning message. */\n  message: string\n  /** The category of warning. */\n  kind: JsWarningKind\n}\n\n/**\n * Structured data block (JSON-LD, Microdata, or RDFa).\n *\n * Represents machine-readable structured data found in the document.\n * JSON-LD blocks are collected as raw JSON strings for flexibility.\n *\n * # Examples\n *\n * ```\n * # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n * let schema = StructuredData {\n * data_type: StructuredDataType::JsonLd,\n * raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n * schema_type: Some(\"Article\".to_string()),\n * };\n *\n * assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n * ```\n */\nexport interface JsStructuredData {\n  /** Type of structured data (JSON-LD, Microdata, RDFa) */\n  dataType: JsStructuredDataType\n  /** Raw JSON string (for JSON-LD) or serialized representation */\n  rawJson: string\n  /** Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") */\n  schemaType: string\n}\n\n/**\n * Structured data format type.\n *\n * Identifies the schema/format used for structured data markup.\n */\nexport declare enum JsStructuredDataType {\n  /** JSON-LD (JSON for Linking Data) script blocks */\n  JsonLd = \"json_ld\",\n  /** HTML5 Microdata attributes (itemscope, itemtype, itemprop) */\n  Microdata = \"Microdata\",\n  /** RDF in Attributes (RDFa) markup */\n  RDFa = \"rdfa\",\n}\n\n/** A top-level extracted table with both structured data and markdown representation. */\nexport interface JsTableData {\n  /** The structured table grid. */\n  grid: JsTableGrid\n  /** The markdown rendering of this table. */\n  markdown: string\n}\n\n/** A structured table grid with cell-level data including spans. */\nexport interface JsTableGrid {\n  /** Number of rows. */\n  rows: number\n  /** Number of columns. */\n  cols: number\n  /** All cells in the table (may be fewer than rows*cols due to spans). */\n  cells: Array<JsGridCell>\n}\n\n/**\n * An inline text annotation with byte-range offsets.\n *\n * Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n */\nexport interface JsTextAnnotation {\n  /** Start byte offset (inclusive) into the parent node's text. */\n  start: number\n  /** End byte offset (exclusive) into the parent node's text. */\n  end: number\n  /** The type of annotation. */\n  kind: JsAnnotationKind\n}\n\n/**\n * Text directionality of document content.\n *\n * Corresponds to the HTML `dir` attribute and `bdi` element directionality.\n */\nexport declare enum JsTextDirection {\n  /** Left-to-right text flow (default for Latin scripts) */\n  LeftToRight = \"ltr\",\n  /** Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) */\n  RightToLeft = \"rtl\",\n  /** Automatic directionality detection */\n  Auto = \"auto\",\n}\n\n/**\n * Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n *\n * This allows visitors to be passed around and shared while still being mutable.\n */\nexport declare class JsVisitorHandle {\n}\n\n/**\n * Result of a visitor callback.\n *\n * Allows visitors to control the conversion flow by either proceeding\n * with default behavior, providing custom output, skipping elements,\n * preserving HTML, or signaling errors.\n */\nexport declare enum JsVisitResult {\n  /** Continue with default conversion behavior */\n  Continue = \"Continue\",\n  /**\n   * Replace default output with custom markdown\n   *\n   * The visitor takes full responsibility for the markdown output\n   * of this node and its children.\n   */\n  Custom = \"Custom\",\n  /**\n   * Skip this element entirely (don't output anything)\n   *\n   * The element and all its children are ignored in the output.\n   */\n  Skip = \"Skip\",\n  /**\n   * Preserve original HTML (don't convert to markdown)\n   *\n   * The element's raw HTML is included verbatim in the output.\n   */\n  PreserveHtml = \"PreserveHtml\",\n  /**\n   * Stop conversion with an error\n   *\n   * The conversion process halts and returns this error message.\n   */\n  Error = \"Error\",\n}\n\n/** Categories of processing warnings. */\nexport declare enum JsWarningKind {\n  /** An image could not be extracted (e.g. invalid data URI, unsupported format). */\n  ImageExtractionFailed = \"ImageExtractionFailed\",\n  /** The input encoding was not recognized; fell back to UTF-8. */\n  EncodingFallback = \"EncodingFallback\",\n  /** The input was truncated due to size limits. */\n  TruncatedInput = \"TruncatedInput\",\n  /** The HTML was malformed but processing continued with best effort. */\n  MalformedHtml = \"MalformedHtml\",\n  /** Sanitization was applied to remove potentially unsafe content. */\n  SanitizationApplied = \"SanitizationApplied\",\n  /** DOM traversal was truncated because max_depth was exceeded. */\n  DepthLimitExceeded = \"DepthLimitExceeded\",\n}\n\n/**\n * Whitespace handling strategy during conversion.\n *\n * Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n */\nexport declare enum JsWhitespaceMode {\n  /** Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. */\n  Normalized = \"Normalized\",\n  /** Preserve all whitespace exactly as it appears in the HTML. */\n  Strict = \"Strict\",\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/index.js",
    "content": "// prettier-ignore\n/* eslint-disable */\n// @ts-nocheck\n/* auto-generated by NAPI-RS */\n\nconst { readFileSync } = require('node:fs')\nlet nativeBinding = null;\nconst loadErrors = [];\n\nconst isMusl = () => {\n  let musl = false;\n  if (process.platform === \"linux\") {\n    musl = isMuslFromFilesystem();\n    if (musl === null) {\n      musl = isMuslFromReport();\n    }\n    if (musl === null) {\n      musl = isMuslFromChildProcess();\n    }\n  }\n  return musl;\n};\n\nconst isFileMusl = (f) => f.includes(\"libc.musl-\") || f.includes(\"ld-musl-\");\n\nconst isMuslFromFilesystem = () => {\n  try {\n    return readFileSync(\"/usr/bin/ldd\", \"utf-8\").includes(\"musl\");\n  } catch {\n    return null;\n  }\n};\n\nconst isMuslFromReport = () => {\n  let report = null;\n  if (typeof process.report?.getReport === \"function\") {\n    process.report.excludeNetwork = true;\n    report = process.report.getReport();\n  }\n  if (!report) {\n    return null;\n  }\n  if (report.header && report.header.glibcVersionRuntime) {\n    return false;\n  }\n  if (Array.isArray(report.sharedObjects)) {\n    if (report.sharedObjects.some(isFileMusl)) {\n      return true;\n    }\n  }\n  return false;\n};\n\nconst isMuslFromChildProcess = () => {\n  try {\n    return require(\"child_process\")\n      .execSync(\"ldd --version\", { encoding: \"utf8\" })\n      .includes(\"musl\");\n  } catch (e) {\n    // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false\n    return false;\n  }\n};\n\nfunction requireNative() {\n  if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {\n    try {\n      return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);\n    } catch (err) {\n      loadErrors.push(err);\n    }\n  } else if (process.platform === \"android\") {\n    if (process.arch === \"arm64\") {\n      try {\n        return require(\"./index.android-arm64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-android-arm64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-android-arm64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"arm\") {\n      try {\n        return require(\"./index.android-arm-eabi.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-android-arm-eabi\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-android-arm-eabi/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`));\n    }\n  } else if (process.platform === \"win32\") {\n    if (process.arch === \"x64\") {\n      if (\n        process.config?.variables?.shlib_suffix === \"dll.a\" ||\n        process.config?.variables?.node_target_type === \"shared_library\"\n      ) {\n        try {\n          return require(\"./index.win32-x64-gnu.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-win32-x64-gnu\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-win32-x64-gnu/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.win32-x64-msvc.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-win32-x64-msvc\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-win32-x64-msvc/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"ia32\") {\n      try {\n        return require(\"./index.win32-ia32-msvc.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-win32-ia32-msvc\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-win32-ia32-msvc/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"arm64\") {\n      try {\n        return require(\"./index.win32-arm64-msvc.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-win32-arm64-msvc\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-win32-arm64-msvc/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`));\n    }\n  } else if (process.platform === \"darwin\") {\n    try {\n      return require(\"./index.darwin-universal.node\");\n    } catch (e) {\n      loadErrors.push(e);\n    }\n    try {\n      const binding = require(\"html-to-markdown-monorepo-darwin-universal\");\n      const bindingPackageVersion =\n        require(\"html-to-markdown-monorepo-darwin-universal/package.json\").version;\n      if (\n        bindingPackageVersion !== \"3.1.0\" &&\n        process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n        process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n      ) {\n        throw new Error(\n          `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n        );\n      }\n      return binding;\n    } catch (e) {\n      loadErrors.push(e);\n    }\n    if (process.arch === \"x64\") {\n      try {\n        return require(\"./index.darwin-x64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-darwin-x64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-darwin-x64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"arm64\") {\n      try {\n        return require(\"./index.darwin-arm64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-darwin-arm64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-darwin-arm64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`));\n    }\n  } else if (process.platform === \"freebsd\") {\n    if (process.arch === \"x64\") {\n      try {\n        return require(\"./index.freebsd-x64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-freebsd-x64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-freebsd-x64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"arm64\") {\n      try {\n        return require(\"./index.freebsd-arm64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-freebsd-arm64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-freebsd-arm64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`));\n    }\n  } else if (process.platform === \"linux\") {\n    if (process.arch === \"x64\") {\n      if (isMusl()) {\n        try {\n          return require(\"./index.linux-x64-musl.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-x64-musl\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-x64-musl/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.linux-x64-gnu.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-x64-gnu\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-x64-gnu/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"arm64\") {\n      if (isMusl()) {\n        try {\n          return require(\"./index.linux-arm64-musl.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-arm64-musl\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-arm64-musl/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.linux-arm64-gnu.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-arm64-gnu\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-arm64-gnu/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"arm\") {\n      if (isMusl()) {\n        try {\n          return require(\"./index.linux-arm-musleabihf.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-arm-musleabihf\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-arm-musleabihf/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.linux-arm-gnueabihf.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-arm-gnueabihf\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-arm-gnueabihf/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"loong64\") {\n      if (isMusl()) {\n        try {\n          return require(\"./index.linux-loong64-musl.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-loong64-musl\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-loong64-musl/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.linux-loong64-gnu.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-loong64-gnu\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-loong64-gnu/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"riscv64\") {\n      if (isMusl()) {\n        try {\n          return require(\"./index.linux-riscv64-musl.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-riscv64-musl\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-riscv64-musl/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      } else {\n        try {\n          return require(\"./index.linux-riscv64-gnu.node\");\n        } catch (e) {\n          loadErrors.push(e);\n        }\n        try {\n          const binding = require(\"html-to-markdown-monorepo-linux-riscv64-gnu\");\n          const bindingPackageVersion =\n            require(\"html-to-markdown-monorepo-linux-riscv64-gnu/package.json\").version;\n          if (\n            bindingPackageVersion !== \"3.1.0\" &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n            process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n          ) {\n            throw new Error(\n              `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n            );\n          }\n          return binding;\n        } catch (e) {\n          loadErrors.push(e);\n        }\n      }\n    } else if (process.arch === \"ppc64\") {\n      try {\n        return require(\"./index.linux-ppc64-gnu.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-linux-ppc64-gnu\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-linux-ppc64-gnu/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"s390x\") {\n      try {\n        return require(\"./index.linux-s390x-gnu.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-linux-s390x-gnu\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-linux-s390x-gnu/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`));\n    }\n  } else if (process.platform === \"openharmony\") {\n    if (process.arch === \"arm64\") {\n      try {\n        return require(\"./index.openharmony-arm64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-openharmony-arm64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-openharmony-arm64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"x64\") {\n      try {\n        return require(\"./index.openharmony-x64.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-openharmony-x64\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-openharmony-x64/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else if (process.arch === \"arm\") {\n      try {\n        return require(\"./index.openharmony-arm.node\");\n      } catch (e) {\n        loadErrors.push(e);\n      }\n      try {\n        const binding = require(\"html-to-markdown-monorepo-openharmony-arm\");\n        const bindingPackageVersion =\n          require(\"html-to-markdown-monorepo-openharmony-arm/package.json\").version;\n        if (\n          bindingPackageVersion !== \"3.1.0\" &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK &&\n          process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== \"0\"\n        ) {\n          throw new Error(\n            `Native binding package version mismatch, expected 3.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`,\n          );\n        }\n        return binding;\n      } catch (e) {\n        loadErrors.push(e);\n      }\n    } else {\n      loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`));\n    }\n  } else {\n    loadErrors.push(\n      new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`),\n    );\n  }\n}\n\nnativeBinding = requireNative();\n\nif (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {\n  let wasiBinding = null;\n  let wasiBindingError = null;\n  try {\n    wasiBinding = require(\"./index.wasi.cjs\");\n    nativeBinding = wasiBinding;\n  } catch (err) {\n    if (process.env.NAPI_RS_FORCE_WASI) {\n      wasiBindingError = err;\n    }\n  }\n  if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {\n    try {\n      wasiBinding = require(\"html-to-markdown-monorepo-wasm32-wasi\");\n      nativeBinding = wasiBinding;\n    } catch (err) {\n      if (process.env.NAPI_RS_FORCE_WASI) {\n        if (!wasiBindingError) {\n          wasiBindingError = err;\n        } else {\n          wasiBindingError.cause = err;\n        }\n        loadErrors.push(err);\n      }\n    }\n  }\n  if (process.env.NAPI_RS_FORCE_WASI === \"error\" && !wasiBinding) {\n    const error = new Error(\"WASI binding not found and NAPI_RS_FORCE_WASI is set to error\");\n    error.cause = wasiBindingError;\n    throw error;\n  }\n}\n\nif (!nativeBinding) {\n  if (loadErrors.length > 0) {\n    throw new Error(\n      `Cannot find native binding. ` +\n        `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +\n        \"Please try `npm i` again after removing both package-lock.json and node_modules directory.\",\n      {\n        cause: loadErrors.reduce((err, cur) => {\n          cur.cause = err;\n          return cur;\n        }),\n      },\n    );\n  }\n  throw new Error(`Failed to load native binding`);\n}\n\nmodule.exports = nativeBinding;\nmodule.exports.convert = nativeBinding.convert;\nmodule.exports.JsConversionOptionsBuilder = nativeBinding.JsConversionOptionsBuilder;\nmodule.exports.JsCodeBlockStyle = nativeBinding.JsCodeBlockStyle;\nmodule.exports.JsHeadingStyle = nativeBinding.JsHeadingStyle;\nmodule.exports.JsHighlightStyle = nativeBinding.JsHighlightStyle;\nmodule.exports.JsImageType = nativeBinding.JsImageType;\nmodule.exports.JsLinkStyle = nativeBinding.JsLinkStyle;\nmodule.exports.JsLinkType = nativeBinding.JsLinkType;\nmodule.exports.JsListIndentType = nativeBinding.JsListIndentType;\nmodule.exports.JsNewlineStyle = nativeBinding.JsNewlineStyle;\nmodule.exports.JsNodeType = nativeBinding.JsNodeType;\nmodule.exports.JsOutputFormat = nativeBinding.JsOutputFormat;\nmodule.exports.JsPreprocessingPreset = nativeBinding.JsPreprocessingPreset;\nmodule.exports.JsStructuredDataType = nativeBinding.JsStructuredDataType;\nmodule.exports.JsTextDirection = nativeBinding.JsTextDirection;\nmodule.exports.JsVisitResult = nativeBinding.JsVisitResult;\nmodule.exports.JsWarningKind = nativeBinding.JsWarningKind;\nmodule.exports.JsWhitespaceMode = nativeBinding.JsWhitespaceMode;\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/darwin-arm64/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-darwin-arm64`\n\nThis is the **aarch64-apple-darwin** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/darwin-arm64/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-darwin-arm64\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.darwin-arm64.node\"\n  ],\n  \"os\": [\n    \"darwin\"\n  ],\n  \"cpu\": [\n    \"arm64\"\n  ],\n  \"main\": \"html-to-markdown-node.darwin-arm64.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/darwin-x64/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-darwin-x64`\n\nThis is the **x86_64-apple-darwin** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/darwin-x64/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-darwin-x64\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.darwin-x64.node\"\n  ],\n  \"os\": [\n    \"darwin\"\n  ],\n  \"cpu\": [\n    \"x64\"\n  ],\n  \"main\": \"html-to-markdown-node.darwin-x64.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm-gnueabihf/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-linux-arm-gnueabihf`\n\nThis is the **armv7-unknown-linux-gnueabihf** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm-gnueabihf/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-linux-arm-gnueabihf\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.linux-arm-gnueabihf.node\"\n  ],\n  \"os\": [\n    \"linux\"\n  ],\n  \"cpu\": [\n    \"arm\"\n  ],\n  \"main\": \"html-to-markdown-node.linux-arm-gnueabihf.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm64-gnu/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-linux-arm64-gnu`\n\nThis is the **aarch64-unknown-linux-gnu** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm64-gnu/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-linux-arm64-gnu\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.linux-arm64-gnu.node\"\n  ],\n  \"os\": [\n    \"linux\"\n  ],\n  \"cpu\": [\n    \"arm64\"\n  ],\n  \"libc\": [\n    \"glibc\"\n  ],\n  \"main\": \"html-to-markdown-node.linux-arm64-gnu.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm64-musl/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-linux-arm64-musl`\n\nThis is the **aarch64-unknown-linux-musl** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-arm64-musl/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-linux-arm64-musl\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.linux-arm64-musl.node\"\n  ],\n  \"os\": [\n    \"linux\"\n  ],\n  \"cpu\": [\n    \"arm64\"\n  ],\n  \"libc\": [\n    \"musl\"\n  ],\n  \"main\": \"html-to-markdown-node.linux-arm64-musl.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-x64-gnu/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-linux-x64-gnu`\n\nThis is the **x86_64-unknown-linux-gnu** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-x64-gnu/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-linux-x64-gnu\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.linux-x64-gnu.node\"\n  ],\n  \"os\": [\n    \"linux\"\n  ],\n  \"cpu\": [\n    \"x64\"\n  ],\n  \"libc\": [\n    \"glibc\"\n  ],\n  \"main\": \"html-to-markdown-node.linux-x64-gnu.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-x64-musl/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-linux-x64-musl`\n\nThis is the **x86_64-unknown-linux-musl** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/linux-x64-musl/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-linux-x64-musl\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.linux-x64-musl.node\"\n  ],\n  \"os\": [\n    \"linux\"\n  ],\n  \"cpu\": [\n    \"x64\"\n  ],\n  \"libc\": [\n    \"musl\"\n  ],\n  \"main\": \"html-to-markdown-node.linux-x64-musl.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/win32-arm64-msvc/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-win32-arm64-msvc`\n\nThis is the **aarch64-pc-windows-msvc** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/win32-arm64-msvc/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-win32-arm64-msvc\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.win32-arm64-msvc.node\"\n  ],\n  \"os\": [\n    \"win32\"\n  ],\n  \"cpu\": [\n    \"arm64\"\n  ],\n  \"main\": \"html-to-markdown-node.win32-arm64-msvc.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/win32-x64-msvc/README.md",
    "content": "# `@kreuzberg/html-to-markdown-node-win32-x64-msvc`\n\nThis is the **x86_64-pc-windows-msvc** binary for `@kreuzberg/html-to-markdown-node`\n"
  },
  {
    "path": "crates/html-to-markdown-node/npm/win32-x64-msvc/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node-win32-x64-msvc\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"html-to-markdown-node.win32-x64-msvc.node\"\n  ],\n  \"os\": [\n    \"win32\"\n  ],\n  \"cpu\": [\n    \"x64\"\n  ],\n  \"main\": \"html-to-markdown-node.win32-x64-msvc.node\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-node\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - Node.js native bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"napi\",\n    \"rust\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"index.js\",\n    \"index.d.ts\",\n    \"*.node\",\n    \"README.md\"\n  ],\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"scripts\": {\n    \"artifacts\": \"napi artifacts\",\n    \"build\": \"napi build --platform --release\",\n    \"build:debug\": \"napi build --platform\",\n    \"prepublishOnly\": \"napi prepublish -t npm\",\n    \"test\": \"node -e \\\"console.log('ok')\\\"\",\n    \"version\": \"napi version\",\n    \"clean\": \"rm -rf *.node\"\n  },\n  \"devDependencies\": {\n    \"@napi-rs/cli\": \"^3.6.2\"\n  },\n  \"napi\": {\n    \"binaryName\": \"html-to-markdown-node\",\n    \"targets\": [\n      \"x86_64-apple-darwin\",\n      \"aarch64-apple-darwin\",\n      \"x86_64-pc-windows-msvc\",\n      \"aarch64-pc-windows-msvc\",\n      \"x86_64-unknown-linux-gnu\",\n      \"x86_64-unknown-linux-musl\",\n      \"aarch64-unknown-linux-gnu\",\n      \"aarch64-unknown-linux-musl\",\n      \"armv7-unknown-linux-gnueabihf\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">= 10\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-node/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:2e611d2c4ce552e9807821321fe2b675f036785d4f05fc22d4dd9384c40ae43f\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unnecessary_cast,\n    clippy::unused_unit,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions\n)]\n#![allow(\n    clippy::cast_possible_wrap,\n    clippy::cast_possible_truncation,\n    clippy::cast_sign_loss,\n    clippy::default_trait_access,\n    clippy::useless_conversion,\n    clippy::unsafe_derive_deserialize,\n    clippy::must_use_candidate,\n    clippy::return_self_not_must_use,\n    clippy::use_self,\n    clippy::missing_const_for_fn,\n    clippy::missing_errors_doc,\n    clippy::needless_pass_by_value,\n    clippy::doc_markdown,\n    clippy::derive_partial_eq_without_eq,\n    clippy::uninlined_format_args,\n    clippy::redundant_clone,\n    clippy::implicit_clone,\n    clippy::redundant_closure_for_method_calls,\n    clippy::wildcard_imports,\n    clippy::option_if_let_else,\n    clippy::too_many_lines\n)]\n\nuse napi::*;\nuse napi_derive::napi;\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsDocumentMetadata {\n    pub title: Option<String>,\n    pub description: Option<String>,\n    pub keywords: Option<Vec<String>>,\n    pub author: Option<String>,\n    #[napi(js_name = \"canonicalUrl\")]\n    pub canonical_url: Option<String>,\n    #[napi(js_name = \"baseHref\")]\n    pub base_href: Option<String>,\n    pub language: Option<String>,\n    #[napi(js_name = \"textDirection\")]\n    pub text_direction: Option<JsTextDirection>,\n    #[napi(js_name = \"openGraph\")]\n    pub open_graph: Option<HashMap<String, String>>,\n    #[napi(js_name = \"twitterCard\")]\n    pub twitter_card: Option<HashMap<String, String>>,\n    #[napi(js_name = \"metaTags\")]\n    pub meta_tags: Option<HashMap<String, String>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsHeaderMetadata {\n    pub level: u8,\n    pub text: String,\n    pub id: Option<String>,\n    pub depth: i64,\n    #[napi(js_name = \"htmlOffset\")]\n    pub html_offset: i64,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsLinkMetadata {\n    pub href: String,\n    pub text: String,\n    pub title: Option<String>,\n    #[napi(js_name = \"linkType\")]\n    pub link_type: JsLinkType,\n    pub rel: Vec<String>,\n    pub attributes: HashMap<String, String>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsImageMetadata {\n    pub src: String,\n    pub alt: Option<String>,\n    pub title: Option<String>,\n    pub dimensions: Option<Vec<u32>>,\n    #[napi(js_name = \"imageType\")]\n    pub image_type: JsImageType,\n    pub attributes: HashMap<String, String>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsStructuredData {\n    #[napi(js_name = \"dataType\")]\n    pub data_type: JsStructuredDataType,\n    #[napi(js_name = \"rawJson\")]\n    pub raw_json: String,\n    #[napi(js_name = \"schemaType\")]\n    pub schema_type: Option<String>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsHtmlMetadata {\n    pub document: Option<JsDocumentMetadata>,\n    pub headers: Option<Vec<JsHeaderMetadata>>,\n    pub links: Option<Vec<JsLinkMetadata>>,\n    pub images: Option<Vec<JsImageMetadata>>,\n    #[napi(js_name = \"structuredData\")]\n    pub structured_data: Option<Vec<JsStructuredData>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsConversionOptions {\n    #[napi(js_name = \"headingStyle\")]\n    pub heading_style: Option<JsHeadingStyle>,\n    #[napi(js_name = \"listIndentType\")]\n    pub list_indent_type: Option<JsListIndentType>,\n    #[napi(js_name = \"listIndentWidth\")]\n    pub list_indent_width: Option<i64>,\n    pub bullets: Option<String>,\n    #[napi(js_name = \"strongEmSymbol\")]\n    pub strong_em_symbol: Option<String>,\n    #[napi(js_name = \"escapeAsterisks\")]\n    pub escape_asterisks: Option<bool>,\n    #[napi(js_name = \"escapeUnderscores\")]\n    pub escape_underscores: Option<bool>,\n    #[napi(js_name = \"escapeMisc\")]\n    pub escape_misc: Option<bool>,\n    #[napi(js_name = \"escapeAscii\")]\n    pub escape_ascii: Option<bool>,\n    #[napi(js_name = \"codeLanguage\")]\n    pub code_language: Option<String>,\n    pub autolinks: Option<bool>,\n    #[napi(js_name = \"defaultTitle\")]\n    pub default_title: Option<bool>,\n    #[napi(js_name = \"brInTables\")]\n    pub br_in_tables: Option<bool>,\n    #[napi(js_name = \"highlightStyle\")]\n    pub highlight_style: Option<JsHighlightStyle>,\n    #[napi(js_name = \"extractMetadata\")]\n    pub extract_metadata: Option<bool>,\n    #[napi(js_name = \"whitespaceMode\")]\n    pub whitespace_mode: Option<JsWhitespaceMode>,\n    #[napi(js_name = \"stripNewlines\")]\n    pub strip_newlines: Option<bool>,\n    pub wrap: Option<bool>,\n    #[napi(js_name = \"wrapWidth\")]\n    pub wrap_width: Option<i64>,\n    #[napi(js_name = \"convertAsInline\")]\n    pub convert_as_inline: Option<bool>,\n    #[napi(js_name = \"subSymbol\")]\n    pub sub_symbol: Option<String>,\n    #[napi(js_name = \"supSymbol\")]\n    pub sup_symbol: Option<String>,\n    #[napi(js_name = \"newlineStyle\")]\n    pub newline_style: Option<JsNewlineStyle>,\n    #[napi(js_name = \"codeBlockStyle\")]\n    pub code_block_style: Option<JsCodeBlockStyle>,\n    #[napi(js_name = \"keepInlineImagesIn\")]\n    pub keep_inline_images_in: Option<Vec<String>>,\n    pub preprocessing: Option<JsPreprocessingOptions>,\n    pub encoding: Option<String>,\n    pub debug: Option<bool>,\n    #[napi(js_name = \"stripTags\")]\n    pub strip_tags: Option<Vec<String>>,\n    #[napi(js_name = \"preserveTags\")]\n    pub preserve_tags: Option<Vec<String>>,\n    #[napi(js_name = \"skipImages\")]\n    pub skip_images: Option<bool>,\n    #[napi(js_name = \"linkStyle\")]\n    pub link_style: Option<JsLinkStyle>,\n    #[napi(js_name = \"outputFormat\")]\n    pub output_format: Option<JsOutputFormat>,\n    #[napi(js_name = \"includeDocumentStructure\")]\n    pub include_document_structure: Option<bool>,\n    #[napi(js_name = \"extractImages\")]\n    pub extract_images: Option<bool>,\n    #[napi(js_name = \"maxImageSize\")]\n    pub max_image_size: Option<i64>,\n    #[napi(js_name = \"captureSvg\")]\n    pub capture_svg: Option<bool>,\n    #[napi(js_name = \"inferDimensions\")]\n    pub infer_dimensions: Option<bool>,\n    #[napi(js_name = \"maxDepth\")]\n    pub max_depth: Option<i64>,\n    #[napi(js_name = \"excludeSelectors\")]\n    pub exclude_selectors: Option<Vec<String>>,\n    pub visitor: Option<napi::bindgen_prelude::Object<'static>>,\n}\n\n#[derive(Clone)]\n#[napi]\npub struct JsConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\n#[napi]\nimpl JsConversionOptionsBuilder {\n    #[napi(js_name = \"stripTags\")]\n    pub fn strip_tags(&self, tags: Vec<String>) -> JsConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().strip_tags(tags)),\n        }\n    }\n\n    #[napi(js_name = \"preserveTags\")]\n    pub fn preserve_tags(&self, tags: Vec<String>) -> JsConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preserve_tags(tags)),\n        }\n    }\n\n    #[napi(js_name = \"keepInlineImagesIn\")]\n    pub fn keep_inline_images_in(&self, tags: Vec<String>) -> JsConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    #[napi(js_name = \"excludeSelectors\")]\n    pub fn exclude_selectors(&self, selectors: Vec<String>) -> JsConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().exclude_selectors(selectors)),\n        }\n    }\n\n    #[napi]\n    pub fn preprocessing(&self, preprocessing: JsPreprocessingOptions) -> JsConversionOptionsBuilder {\n        let preprocessing_core: html_to_markdown_rs::PreprocessingOptions = preprocessing.into();\n        Self {\n            inner: Arc::new((*self.inner).clone().preprocessing(preprocessing_core)),\n        }\n    }\n\n    #[napi]\n    pub fn build(&self) -> JsConversionOptions {\n        (*self.inner).clone().build().into()\n    }\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsConversionOptionsUpdate {\n    #[napi(js_name = \"headingStyle\")]\n    pub heading_style: Option<JsHeadingStyle>,\n    #[napi(js_name = \"listIndentType\")]\n    pub list_indent_type: Option<JsListIndentType>,\n    #[napi(js_name = \"listIndentWidth\")]\n    pub list_indent_width: Option<i64>,\n    pub bullets: Option<String>,\n    #[napi(js_name = \"strongEmSymbol\")]\n    pub strong_em_symbol: Option<String>,\n    #[napi(js_name = \"escapeAsterisks\")]\n    pub escape_asterisks: Option<bool>,\n    #[napi(js_name = \"escapeUnderscores\")]\n    pub escape_underscores: Option<bool>,\n    #[napi(js_name = \"escapeMisc\")]\n    pub escape_misc: Option<bool>,\n    #[napi(js_name = \"escapeAscii\")]\n    pub escape_ascii: Option<bool>,\n    #[napi(js_name = \"codeLanguage\")]\n    pub code_language: Option<String>,\n    pub autolinks: Option<bool>,\n    #[napi(js_name = \"defaultTitle\")]\n    pub default_title: Option<bool>,\n    #[napi(js_name = \"brInTables\")]\n    pub br_in_tables: Option<bool>,\n    #[napi(js_name = \"highlightStyle\")]\n    pub highlight_style: Option<JsHighlightStyle>,\n    #[napi(js_name = \"extractMetadata\")]\n    pub extract_metadata: Option<bool>,\n    #[napi(js_name = \"whitespaceMode\")]\n    pub whitespace_mode: Option<JsWhitespaceMode>,\n    #[napi(js_name = \"stripNewlines\")]\n    pub strip_newlines: Option<bool>,\n    pub wrap: Option<bool>,\n    #[napi(js_name = \"wrapWidth\")]\n    pub wrap_width: Option<i64>,\n    #[napi(js_name = \"convertAsInline\")]\n    pub convert_as_inline: Option<bool>,\n    #[napi(js_name = \"subSymbol\")]\n    pub sub_symbol: Option<String>,\n    #[napi(js_name = \"supSymbol\")]\n    pub sup_symbol: Option<String>,\n    #[napi(js_name = \"newlineStyle\")]\n    pub newline_style: Option<JsNewlineStyle>,\n    #[napi(js_name = \"codeBlockStyle\")]\n    pub code_block_style: Option<JsCodeBlockStyle>,\n    #[napi(js_name = \"keepInlineImagesIn\")]\n    pub keep_inline_images_in: Option<Vec<String>>,\n    pub preprocessing: Option<JsPreprocessingOptionsUpdate>,\n    pub encoding: Option<String>,\n    pub debug: Option<bool>,\n    #[napi(js_name = \"stripTags\")]\n    pub strip_tags: Option<Vec<String>>,\n    #[napi(js_name = \"preserveTags\")]\n    pub preserve_tags: Option<Vec<String>>,\n    #[napi(js_name = \"skipImages\")]\n    pub skip_images: Option<bool>,\n    #[napi(js_name = \"linkStyle\")]\n    pub link_style: Option<JsLinkStyle>,\n    #[napi(js_name = \"outputFormat\")]\n    pub output_format: Option<JsOutputFormat>,\n    #[napi(js_name = \"includeDocumentStructure\")]\n    pub include_document_structure: Option<bool>,\n    #[napi(js_name = \"extractImages\")]\n    pub extract_images: Option<bool>,\n    #[napi(js_name = \"maxImageSize\")]\n    pub max_image_size: Option<i64>,\n    #[napi(js_name = \"captureSvg\")]\n    pub capture_svg: Option<bool>,\n    #[napi(js_name = \"inferDimensions\")]\n    pub infer_dimensions: Option<bool>,\n    #[napi(js_name = \"maxDepth\")]\n    pub max_depth: Option<i64>,\n    #[napi(js_name = \"excludeSelectors\")]\n    pub exclude_selectors: Option<Vec<String>>,\n    pub visitor: Option<napi::bindgen_prelude::Object<'static>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsPreprocessingOptions {\n    pub enabled: Option<bool>,\n    pub preset: Option<JsPreprocessingPreset>,\n    #[napi(js_name = \"removeNavigation\")]\n    pub remove_navigation: Option<bool>,\n    #[napi(js_name = \"removeForms\")]\n    pub remove_forms: Option<bool>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsPreprocessingOptionsUpdate {\n    pub enabled: Option<bool>,\n    pub preset: Option<JsPreprocessingPreset>,\n    #[napi(js_name = \"removeNavigation\")]\n    pub remove_navigation: Option<bool>,\n    #[napi(js_name = \"removeForms\")]\n    pub remove_forms: Option<bool>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsDocumentStructure {\n    pub nodes: Vec<JsDocumentNode>,\n    #[napi(js_name = \"sourceFormat\")]\n    pub source_format: Option<String>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsDocumentNode {\n    pub id: String,\n    pub content: JsNodeContent,\n    pub parent: Option<u32>,\n    pub children: Vec<u32>,\n    pub annotations: Vec<JsTextAnnotation>,\n    pub attributes: Option<HashMap<String, String>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsTextAnnotation {\n    pub start: u32,\n    pub end: u32,\n    pub kind: JsAnnotationKind,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsConversionResult {\n    pub content: Option<String>,\n    pub document: Option<JsDocumentStructure>,\n    pub metadata: Option<JsHtmlMetadata>,\n    pub tables: Option<Vec<JsTableData>>,\n    pub images: Option<Vec<String>>,\n    pub warnings: Option<Vec<JsProcessingWarning>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsTableGrid {\n    pub rows: Option<u32>,\n    pub cols: Option<u32>,\n    pub cells: Option<Vec<JsGridCell>>,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsGridCell {\n    pub content: String,\n    pub row: u32,\n    pub col: u32,\n    #[napi(js_name = \"rowSpan\")]\n    pub row_span: u32,\n    #[napi(js_name = \"colSpan\")]\n    pub col_span: u32,\n    #[napi(js_name = \"isHeader\")]\n    pub is_header: bool,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsTableData {\n    pub grid: JsTableGrid,\n    pub markdown: String,\n}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsProcessingWarning {\n    pub message: String,\n    pub kind: JsWarningKind,\n}\n\n#[derive(Clone)]\n#[napi]\npub struct JsVisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n#[napi]\nimpl JsVisitorHandle {}\n\n#[derive(Clone, Default)]\n#[napi(object)]\npub struct JsNodeContext {\n    #[napi(js_name = \"nodeType\")]\n    pub node_type: JsNodeType,\n    #[napi(js_name = \"tagName\")]\n    pub tag_name: String,\n    pub attributes: HashMap<String, String>,\n    pub depth: i64,\n    #[napi(js_name = \"indexInParent\")]\n    pub index_in_parent: i64,\n    #[napi(js_name = \"parentTag\")]\n    pub parent_tag: Option<String>,\n    #[napi(js_name = \"isInline\")]\n    pub is_inline: bool,\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsTextDirection {\n    LeftToRight,\n    RightToLeft,\n    Auto,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsTextDirection {\n    fn default() -> Self {\n        Self::LeftToRight\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsLinkType {\n    Anchor,\n    Internal,\n    External,\n    Email,\n    Phone,\n    Other,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsLinkType {\n    fn default() -> Self {\n        Self::Anchor\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsImageType {\n    DataUri,\n    InlineSvg,\n    External,\n    Relative,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsImageType {\n    fn default() -> Self {\n        Self::DataUri\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsStructuredDataType {\n    JsonLd,\n    Microdata,\n    RDFa,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsStructuredDataType {\n    fn default() -> Self {\n        Self::JsonLd\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsPreprocessingPreset {\n    Minimal,\n    Standard,\n    Aggressive,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsPreprocessingPreset {\n    fn default() -> Self {\n        Self::Minimal\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsHeadingStyle {\n    Underlined,\n    Atx,\n    AtxClosed,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsHeadingStyle {\n    fn default() -> Self {\n        Self::Underlined\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsListIndentType {\n    Spaces,\n    Tabs,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsListIndentType {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsWhitespaceMode {\n    Normalized,\n    Strict,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsWhitespaceMode {\n    fn default() -> Self {\n        Self::Normalized\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsNewlineStyle {\n    Spaces,\n    Backslash,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsNewlineStyle {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsCodeBlockStyle {\n    Indented,\n    Backticks,\n    Tildes,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsCodeBlockStyle {\n    fn default() -> Self {\n        Self::Indented\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsHighlightStyle {\n    DoubleEqual,\n    Html,\n    Bold,\n    None,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsHighlightStyle {\n    fn default() -> Self {\n        Self::DoubleEqual\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsLinkStyle {\n    Inline,\n    Reference,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsLinkStyle {\n    fn default() -> Self {\n        Self::Inline\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsOutputFormat {\n    Markdown,\n    Djot,\n    Plain,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsOutputFormat {\n    fn default() -> Self {\n        Self::Markdown\n    }\n}\n\n#[derive(Clone)]\n#[napi(object)]\npub struct JsNodeContent {\n    #[napi(js_name = \"node_type\")]\n    pub node_type_tag: String,\n    pub level: Option<u8>,\n    pub text: Option<String>,\n    pub ordered: Option<bool>,\n    pub grid: Option<JsTableGrid>,\n    pub description: Option<String>,\n    pub src: Option<String>,\n    #[napi(js_name = \"imageIndex\")]\n    pub image_index: Option<u32>,\n    pub language: Option<String>,\n    pub term: Option<String>,\n    pub definition: Option<String>,\n    pub format: Option<String>,\n    pub content: Option<String>,\n    pub entries: Option<Vec<String>>,\n    pub label: Option<String>,\n    #[napi(js_name = \"headingLevel\")]\n    pub heading_level: Option<u8>,\n    #[napi(js_name = \"headingText\")]\n    pub heading_text: Option<String>,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsNodeContent {\n    fn default() -> Self {\n        Self {\n            node_type_tag: String::new(),\n            content: None,\n            definition: None,\n            description: None,\n            entries: None,\n            format: None,\n            grid: None,\n            heading_level: None,\n            heading_text: None,\n            image_index: None,\n            label: None,\n            language: None,\n            level: None,\n            ordered: None,\n            src: None,\n            term: None,\n            text: None,\n        }\n    }\n}\n\n#[derive(Clone)]\n#[napi(object)]\npub struct JsAnnotationKind {\n    #[napi(js_name = \"annotation_type\")]\n    pub annotation_type_tag: String,\n    pub url: Option<String>,\n    pub title: Option<String>,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsAnnotationKind {\n    fn default() -> Self {\n        Self {\n            annotation_type_tag: String::new(),\n            title: None,\n            url: None,\n        }\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsWarningKind {\n    ImageExtractionFailed,\n    EncodingFallback,\n    TruncatedInput,\n    MalformedHtml,\n    SanitizationApplied,\n    DepthLimitExceeded,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsWarningKind {\n    fn default() -> Self {\n        Self::ImageExtractionFailed\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsNodeType {\n    Text,\n    Element,\n    Heading,\n    Paragraph,\n    Div,\n    Blockquote,\n    Pre,\n    Hr,\n    List,\n    ListItem,\n    DefinitionList,\n    DefinitionTerm,\n    DefinitionDescription,\n    Table,\n    TableRow,\n    TableCell,\n    TableHeader,\n    TableBody,\n    TableHead,\n    TableFoot,\n    Link,\n    Image,\n    Strong,\n    Em,\n    Code,\n    Strikethrough,\n    Underline,\n    Subscript,\n    Superscript,\n    Mark,\n    Small,\n    Br,\n    Span,\n    Article,\n    Section,\n    Nav,\n    Aside,\n    Header,\n    Footer,\n    Main,\n    Figure,\n    Figcaption,\n    Time,\n    Details,\n    Summary,\n    Form,\n    Input,\n    Select,\n    Option,\n    Button,\n    Textarea,\n    Label,\n    Fieldset,\n    Legend,\n    Audio,\n    Video,\n    Picture,\n    Source,\n    Iframe,\n    Svg,\n    Canvas,\n    Ruby,\n    Rt,\n    Rp,\n    Abbr,\n    Kbd,\n    Samp,\n    Var,\n    Cite,\n    Q,\n    Del,\n    Ins,\n    Data,\n    Meter,\n    Progress,\n    Output,\n    Template,\n    Slot,\n    Html,\n    Head,\n    Body,\n    Title,\n    Meta,\n    LinkTag,\n    Style,\n    Script,\n    Base,\n    Custom,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsNodeType {\n    fn default() -> Self {\n        Self::Text\n    }\n}\n\n#[napi(string_enum)]\n#[derive(Clone)]\npub enum JsVisitResult {\n    Continue,\n    Custom,\n    Skip,\n    PreserveHtml,\n    Error,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for JsVisitResult {\n    fn default() -> Self {\n        Self::Continue\n    }\n}\n\n#[allow(clippy::missing_errors_doc)]\n#[napi]\npub fn convert(html: String, options: Option<JsConversionOptions>) -> Result<JsConversionResult> {\n    let __visitor_obj = options.as_ref().and_then(|opts| opts.visitor.clone());\n    let options_core: Option<html_to_markdown_rs::ConversionOptions> = options.map(|opts| {\n        let mut __c: html_to_markdown_rs::ConversionOptions = opts.into();\n        __c.visitor = None;\n        __c\n    });\n    let options_core = if let Some(__obj) = __visitor_obj {\n        let __bridge = JsHtmlVisitorBridge::new(__obj);\n        let __handle: html_to_markdown_rs::visitor::VisitorHandle = std::rc::Rc::new(std::cell::RefCell::new(__bridge));\n        let mut __opts = options_core.unwrap_or_default();\n        __opts.visitor = Some(__handle);\n        Some(__opts)\n    } else {\n        options_core\n    };\n    html_to_markdown_rs::convert(&html, options_core)\n        .map(|val| val.into())\n        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))\n}\n\n#[allow(unused_imports)]\nuse napi::JsValue;\n#[allow(unused_imports)]\nuse napi::bindgen_prelude::{JsObjectValue, Object, ToNapiValue, Unknown};\n\nfn nodecontext_to_js_object<'e>(\n    env: &'e napi::Env,\n    ctx: &html_to_markdown_rs::visitor::NodeContext,\n) -> napi::Result<napi::bindgen_prelude::Object<'e>> {\n    let mut obj = napi::bindgen_prelude::Object::new(env)?;\n    obj.set_named_property(\"nodeType\", env.create_string(&format!(\"{:?}\", ctx.node_type))?)?;\n    obj.set_named_property(\"tagName\", env.create_string(&ctx.tag_name)?)?;\n    obj.set_named_property(\"depth\", env.create_uint32(ctx.depth as u32)?)?;\n    obj.set_named_property(\"indexInParent\", env.create_uint32(ctx.index_in_parent as u32)?)?;\n    obj.set_named_property(\"isInline\", ctx.is_inline)?;\n    let parent_tag = match &ctx.parent_tag {\n        Some(s) => env.create_string(s)?.to_unknown(),\n        None => {\n            // SAFETY: napi_get_null returns a valid napi_value for the given env.\n            let raw =\n                unsafe { napi::bindgen_prelude::ToNapiValue::to_napi_value(env.raw(), napi::bindgen_prelude::Null)? };\n            unsafe { napi::bindgen_prelude::Unknown::from_raw_unchecked(env.raw(), raw) }\n        }\n    };\n    obj.set_named_property(\"parentTag\", parent_tag)?;\n    let mut attrs = napi::bindgen_prelude::Object::new(env)?;\n    for (k, v) in &ctx.attributes {\n        attrs.set_named_property(k, env.create_string(v)?)?;\n    }\n    obj.set_named_property(\"attributes\", attrs)?;\n    Ok(obj)\n}\n\npub struct JsHtmlVisitorBridge {\n    obj: napi::bindgen_prelude::Object<'static>,\n}\n\nimpl std::fmt::Debug for JsHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"JsHtmlVisitorBridge\")\n    }\n}\n\nimpl JsHtmlVisitorBridge {\n    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> Self {\n        // SAFETY: The JS object is owned by the Node.js runtime and lives for\n        // the duration of the enclosing #[napi] call. The bridge is only used\n        // synchronously during that same call, so 'static is safe here.\n        let obj: napi::bindgen_prelude::Object<'static> = unsafe { std::mem::transmute(js_obj) };\n        Self { obj }\n    }\n\n    fn env(&self) -> napi::Env {\n        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.\n        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.obj) };\n        napi::Env::from_raw(raw[0] as napi::sys::napi_env)\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for JsHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitElementStart\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitElementStart\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitElementEnd\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitElementEnd\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_output) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitText\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitText\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitLink\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitLink\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_href) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_3: napi::bindgen_prelude::Unknown = match _title {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2, arg_3));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitImage\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitImage\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_src) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_alt) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_3: napi::bindgen_prelude::Unknown = match _title {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2, arg_3));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitHeading\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitHeading\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_uint32(_level) {\n            Ok(n) => n.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_3: napi::bindgen_prelude::Unknown = match _id {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2, arg_3));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitCodeBlock\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitCodeBlock\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match _lang {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_code) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitCodeInline\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitCodeInline\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_code) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitListItem\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitListItem\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = unsafe {\n            let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(__env.raw(), _ordered)\n                .unwrap_or(std::ptr::null_mut());\n            napi::bindgen_prelude::Unknown::from_raw_unchecked(__env.raw(), r)\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_marker) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_3: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2, arg_3));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitListStart\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitListStart\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = unsafe {\n            let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(__env.raw(), _ordered)\n                .unwrap_or(std::ptr::null_mut());\n            napi::bindgen_prelude::Unknown::from_raw_unchecked(__env.raw(), r)\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitListEnd\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitListEnd\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = unsafe {\n            let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(__env.raw(), _ordered)\n                .unwrap_or(std::ptr::null_mut());\n            napi::bindgen_prelude::Unknown::from_raw_unchecked(__env.raw(), r)\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_output) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitTableStart\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitTableStart\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitTableRow\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitTableRow\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(&format!(\"{:?}\", _cells)) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = unsafe {\n            let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(__env.raw(), _is_header)\n                .unwrap_or(std::ptr::null_mut());\n            napi::bindgen_prelude::Unknown::from_raw_unchecked(__env.raw(), r)\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitTableEnd\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitTableEnd\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_output) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitBlockquote\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitBlockquote\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_content) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_uint32(_depth as u32) {\n            Ok(n) => n.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitStrong\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitStrong\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitEmphasis\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitEmphasis\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitStrikethrough\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitStrikethrough\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitUnderline\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitUnderline\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitSubscript\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitSubscript\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitSuperscript\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitSuperscript\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitMark\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitMark\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitLineBreak\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitLineBreak\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitHorizontalRule\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitHorizontalRule\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitCustomElement\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitCustomElement\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_tag_name) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match __env.create_string(_html) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitDefinitionListStart\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitDefinitionListStart\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitDefinitionTerm\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitDefinitionTerm\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self\n            .obj\n            .has_named_property(\"visitDefinitionDescription\")\n            .unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitDefinitionDescription\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitDefinitionListEnd\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitDefinitionListEnd\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_output) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitForm\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitForm\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match _action {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match _method {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitInput\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n                napi::bindgen_prelude::Unknown,\n            ),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitInput\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_input_type) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_2: napi::bindgen_prelude::Unknown = match _name {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_3: napi::bindgen_prelude::Unknown = match _value {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1, arg_2, arg_3));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitButton\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitButton\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitAudio\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitAudio\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match _src {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitVideo\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitVideo\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match _src {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitIframe\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitIframe\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match _src {\n            Some(s) => match __env.create_string(s) {\n                Ok(v) => v.to_unknown(),\n                Err(_) => unsafe {\n                    let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                        std::ptr::null_mut(),\n                        napi::bindgen_prelude::Null,\n                    )\n                    .unwrap_or(std::ptr::null_mut());\n                    napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n                },\n            },\n            None => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitDetails\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitDetails\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = unsafe {\n            let r =\n                napi::bindgen_prelude::ToNapiValue::to_napi_value(__env.raw(), _open).unwrap_or(std::ptr::null_mut());\n            napi::bindgen_prelude::Unknown::from_raw_unchecked(__env.raw(), r)\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitSummary\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitSummary\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitFigureStart\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<(napi::bindgen_prelude::Unknown,), napi::bindgen_prelude::Unknown> =\n            match self.obj.get_named_property(\"visitFigureStart\") {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0,));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitFigcaption\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitFigcaption\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_text) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let has_method = self.obj.has_named_property(\"visitFigureEnd\").unwrap_or(false);\n        if !has_method {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let func: napi::bindgen_prelude::Function<\n            (napi::bindgen_prelude::Unknown, napi::bindgen_prelude::Unknown),\n            napi::bindgen_prelude::Unknown,\n        > = match self.obj.get_named_property(\"visitFigureEnd\") {\n            Ok(f) => f,\n            Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let __env = self.env();\n        let arg_0: napi::bindgen_prelude::Unknown = match nodecontext_to_js_object(&__env, _ctx) {\n            Ok(o) => o.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let arg_1: napi::bindgen_prelude::Unknown = match __env.create_string(_output) {\n            Ok(s) => s.to_unknown(),\n            Err(_) => unsafe {\n                let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(\n                    std::ptr::null_mut(),\n                    napi::bindgen_prelude::Null,\n                )\n                .unwrap_or(std::ptr::null_mut());\n                napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r)\n            },\n        };\n        let result = func.call((arg_0, arg_1));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Ok(s) = val\n                    .coerce_to_string()\n                    .and_then(|s| s.into_utf8())\n                    .and_then(|s| s.into_owned())\n                {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else {\n                    html_to_markdown_rs::VisitResult::Continue\n                }\n            }\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsDocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: JsDocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords.unwrap_or_default(),\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.unwrap_or_default().into_iter().collect(),\n            twitter_card: val.twitter_card.unwrap_or_default().into_iter().collect(),\n            meta_tags: val.meta_tags.unwrap_or_default().into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for JsDocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: Some(val.keywords),\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: Some(val.open_graph.into_iter().collect()),\n            twitter_card: Some(val.twitter_card.into_iter().collect()),\n            meta_tags: Some(val.meta_tags.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsHeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: JsHeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth as usize,\n            html_offset: val.html_offset as usize,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for JsHeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth as i64,\n            html_offset: val.html_offset as i64,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsLinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: JsLinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for JsLinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: JsImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for JsImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsStructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: JsStructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for JsStructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsHtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: JsHtmlMetadata) -> Self {\n        Self {\n            document: val.document.map(Into::into).unwrap_or_default(),\n            headers: val\n                .headers\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n            links: val\n                .links\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n            images: val\n                .images\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n            structured_data: val\n                .structured_data\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for JsHtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: Some(val.document.into()),\n            headers: Some(val.headers.into_iter().map(Into::into).collect()),\n            links: Some(val.links.into_iter().map(Into::into).collect()),\n            images: Some(val.images.into_iter().map(Into::into).collect()),\n            structured_data: Some(val.structured_data.into_iter().map(Into::into).collect()),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: JsConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into).unwrap_or_default(),\n            list_indent_type: val.list_indent_type.map(Into::into).unwrap_or_default(),\n            list_indent_width: val.list_indent_width.map(|v| v as usize).unwrap_or_default(),\n            bullets: val.bullets.unwrap_or_default(),\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()).unwrap_or('*'),\n            escape_asterisks: val.escape_asterisks.unwrap_or_default(),\n            escape_underscores: val.escape_underscores.unwrap_or_default(),\n            escape_misc: val.escape_misc.unwrap_or_default(),\n            escape_ascii: val.escape_ascii.unwrap_or_default(),\n            code_language: val.code_language.unwrap_or_default(),\n            autolinks: val.autolinks.unwrap_or_default(),\n            default_title: val.default_title.unwrap_or_default(),\n            br_in_tables: val.br_in_tables.unwrap_or_default(),\n            highlight_style: val.highlight_style.map(Into::into).unwrap_or_default(),\n            extract_metadata: val.extract_metadata.unwrap_or_default(),\n            whitespace_mode: val.whitespace_mode.map(Into::into).unwrap_or_default(),\n            strip_newlines: val.strip_newlines.unwrap_or_default(),\n            wrap: val.wrap.unwrap_or_default(),\n            wrap_width: val.wrap_width.map(|v| v as usize).unwrap_or_default(),\n            convert_as_inline: val.convert_as_inline.unwrap_or_default(),\n            sub_symbol: val.sub_symbol.unwrap_or_default(),\n            sup_symbol: val.sup_symbol.unwrap_or_default(),\n            newline_style: val.newline_style.map(Into::into).unwrap_or_default(),\n            code_block_style: val.code_block_style.map(Into::into).unwrap_or_default(),\n            keep_inline_images_in: val.keep_inline_images_in.unwrap_or_default(),\n            preprocessing: val.preprocessing.map(Into::into).unwrap_or_default(),\n            encoding: val.encoding.unwrap_or_default(),\n            debug: val.debug.unwrap_or_default(),\n            strip_tags: val.strip_tags.unwrap_or_default(),\n            preserve_tags: val.preserve_tags.unwrap_or_default(),\n            skip_images: val.skip_images.unwrap_or_default(),\n            link_style: val.link_style.map(Into::into).unwrap_or_default(),\n            output_format: val.output_format.map(Into::into).unwrap_or_default(),\n            include_document_structure: val.include_document_structure.unwrap_or_default(),\n            extract_images: val.extract_images.unwrap_or_default(),\n            max_image_size: val.max_image_size.map(|v| v as u64).unwrap_or_default(),\n            capture_svg: val.capture_svg.unwrap_or_default(),\n            infer_dimensions: val.infer_dimensions.unwrap_or_default(),\n            max_depth: val.max_depth.map(|v| v as usize),\n            exclude_selectors: val.exclude_selectors.unwrap_or_default(),\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for JsConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: Some(val.heading_style.into()),\n            list_indent_type: Some(val.list_indent_type.into()),\n            list_indent_width: Some(val.list_indent_width as i64),\n            bullets: Some(val.bullets),\n            strong_em_symbol: Some(val.strong_em_symbol.to_string()),\n            escape_asterisks: Some(val.escape_asterisks),\n            escape_underscores: Some(val.escape_underscores),\n            escape_misc: Some(val.escape_misc),\n            escape_ascii: Some(val.escape_ascii),\n            code_language: Some(val.code_language),\n            autolinks: Some(val.autolinks),\n            default_title: Some(val.default_title),\n            br_in_tables: Some(val.br_in_tables),\n            highlight_style: Some(val.highlight_style.into()),\n            extract_metadata: Some(val.extract_metadata),\n            whitespace_mode: Some(val.whitespace_mode.into()),\n            strip_newlines: Some(val.strip_newlines),\n            wrap: Some(val.wrap),\n            wrap_width: Some(val.wrap_width as i64),\n            convert_as_inline: Some(val.convert_as_inline),\n            sub_symbol: Some(val.sub_symbol),\n            sup_symbol: Some(val.sup_symbol),\n            newline_style: Some(val.newline_style.into()),\n            code_block_style: Some(val.code_block_style.into()),\n            keep_inline_images_in: Some(val.keep_inline_images_in),\n            preprocessing: Some(val.preprocessing.into()),\n            encoding: Some(val.encoding),\n            debug: Some(val.debug),\n            strip_tags: Some(val.strip_tags),\n            preserve_tags: Some(val.preserve_tags),\n            skip_images: Some(val.skip_images),\n            link_style: Some(val.link_style.into()),\n            output_format: Some(val.output_format.into()),\n            include_document_structure: Some(val.include_document_structure),\n            extract_images: Some(val.extract_images),\n            max_image_size: Some(val.max_image_size as i64),\n            capture_svg: Some(val.capture_svg),\n            infer_dimensions: Some(val.infer_dimensions),\n            max_depth: val.max_depth.map(|v| v as i64),\n            exclude_selectors: Some(val.exclude_selectors),\n            visitor: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: JsConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width.map(|v| v as usize),\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width.map(|v| v as usize),\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size.map(|v| v as u64),\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth.map(|v| v as usize)).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for JsConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width.map(|v| v as i64),\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width.map(|v| v as i64),\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size.map(|v| v as i64),\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten().map(|v| v as i64),\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsPreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: JsPreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled.unwrap_or_default(),\n            preset: val.preset.map(Into::into).unwrap_or_default(),\n            remove_navigation: val.remove_navigation.unwrap_or_default(),\n            remove_forms: val.remove_forms.unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for JsPreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: Some(val.enabled),\n            preset: Some(val.preset.into()),\n            remove_navigation: Some(val.remove_navigation),\n            remove_forms: Some(val.remove_forms),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsPreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: JsPreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for JsPreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsDocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: JsDocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for JsDocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsDocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: JsDocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for JsDocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsTextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: JsTextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for JsTextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: JsConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.map(Into::into).unwrap_or_default(),\n            tables: val\n                .tables\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n            images: Default::default(),\n            warnings: val\n                .warnings\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for JsConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: Some(val.metadata.into()),\n            tables: Some(val.tables.into_iter().map(Into::into).collect()),\n            images: Some(val.images.iter().map(|i| format!(\"{:?}\", i)).collect()),\n            warnings: Some(val.warnings.into_iter().map(Into::into).collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsTableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: JsTableGrid) -> Self {\n        Self {\n            rows: val.rows.unwrap_or_default(),\n            cols: val.cols.unwrap_or_default(),\n            cells: val\n                .cells\n                .map(|v| v.into_iter().map(Into::into).collect())\n                .unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for JsTableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: Some(val.rows),\n            cols: Some(val.cols),\n            cells: Some(val.cells.into_iter().map(Into::into).collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsGridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: JsGridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for JsGridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsTableData> for html_to_markdown_rs::TableData {\n    fn from(val: JsTableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for JsTableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<JsProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: JsProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for JsProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for JsNodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: val.attributes.into_iter().collect(),\n            depth: val.depth as i64,\n            index_in_parent: val.index_in_parent as i64,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<JsTextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: JsTextDirection) -> Self {\n        match val {\n            JsTextDirection::LeftToRight => Self::LeftToRight,\n            JsTextDirection::RightToLeft => Self::RightToLeft,\n            JsTextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for JsTextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<JsLinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: JsLinkType) -> Self {\n        match val {\n            JsLinkType::Anchor => Self::Anchor,\n            JsLinkType::Internal => Self::Internal,\n            JsLinkType::External => Self::External,\n            JsLinkType::Email => Self::Email,\n            JsLinkType::Phone => Self::Phone,\n            JsLinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for JsLinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<JsImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: JsImageType) -> Self {\n        match val {\n            JsImageType::DataUri => Self::DataUri,\n            JsImageType::InlineSvg => Self::InlineSvg,\n            JsImageType::External => Self::External,\n            JsImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for JsImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<JsStructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: JsStructuredDataType) -> Self {\n        match val {\n            JsStructuredDataType::JsonLd => Self::JsonLd,\n            JsStructuredDataType::Microdata => Self::Microdata,\n            JsStructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for JsStructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<JsPreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: JsPreprocessingPreset) -> Self {\n        match val {\n            JsPreprocessingPreset::Minimal => Self::Minimal,\n            JsPreprocessingPreset::Standard => Self::Standard,\n            JsPreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for JsPreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<JsHeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: JsHeadingStyle) -> Self {\n        match val {\n            JsHeadingStyle::Underlined => Self::Underlined,\n            JsHeadingStyle::Atx => Self::Atx,\n            JsHeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for JsHeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<JsListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: JsListIndentType) -> Self {\n        match val {\n            JsListIndentType::Spaces => Self::Spaces,\n            JsListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for JsListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<JsWhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: JsWhitespaceMode) -> Self {\n        match val {\n            JsWhitespaceMode::Normalized => Self::Normalized,\n            JsWhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for JsWhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<JsNewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: JsNewlineStyle) -> Self {\n        match val {\n            JsNewlineStyle::Spaces => Self::Spaces,\n            JsNewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for JsNewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<JsCodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: JsCodeBlockStyle) -> Self {\n        match val {\n            JsCodeBlockStyle::Indented => Self::Indented,\n            JsCodeBlockStyle::Backticks => Self::Backticks,\n            JsCodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for JsCodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<JsHighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: JsHighlightStyle) -> Self {\n        match val {\n            JsHighlightStyle::DoubleEqual => Self::DoubleEqual,\n            JsHighlightStyle::Html => Self::Html,\n            JsHighlightStyle::Bold => Self::Bold,\n            JsHighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for JsHighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<JsLinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: JsLinkStyle) -> Self {\n        match val {\n            JsLinkStyle::Inline => Self::Inline,\n            JsLinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for JsLinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<JsOutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: JsOutputFormat) -> Self {\n        match val {\n            JsOutputFormat::Markdown => Self::Markdown,\n            JsOutputFormat::Djot => Self::Djot,\n            JsOutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for JsOutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<JsNodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: JsNodeContent) -> Self {\n        match val.node_type_tag.as_str() {\n            \"heading\" => Self::Heading {\n                level: val.level.unwrap_or_default(),\n                text: val.text.unwrap_or_default(),\n            },\n            \"paragraph\" => Self::Paragraph {\n                text: val.text.unwrap_or_default(),\n            },\n            \"list\" => Self::List {\n                ordered: val.ordered.unwrap_or_default(),\n            },\n            \"listitem\" => Self::ListItem {\n                text: val.text.unwrap_or_default(),\n            },\n            \"table\" => Self::Table {\n                grid: val.grid.map(|v| v.into()).unwrap_or_default(),\n            },\n            \"image\" => Self::Image {\n                description: val.description,\n                src: val.src,\n                image_index: val.image_index,\n            },\n            \"code\" => Self::Code {\n                text: val.text.unwrap_or_default(),\n                language: val.language,\n            },\n            \"quote\" => Self::Quote,\n            \"definitionlist\" => Self::DefinitionList,\n            \"definitionitem\" => Self::DefinitionItem {\n                term: val.term.unwrap_or_default(),\n                definition: val.definition.unwrap_or_default(),\n            },\n            \"rawblock\" => Self::RawBlock {\n                format: val.format.unwrap_or_default(),\n                content: val.content.unwrap_or_default(),\n            },\n            \"metadatablock\" => Self::MetadataBlock {\n                entries: Default::default(),\n            },\n            \"group\" => Self::Group {\n                label: val.label,\n                heading_level: val.heading_level,\n                heading_text: val.heading_text,\n            },\n            _ => Self::Heading {\n                level: Default::default(),\n                text: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for JsNodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { level, text } => Self {\n                node_type_tag: \"heading\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: Some(level),\n                ordered: None,\n                src: None,\n                term: None,\n                text: Some(text),\n            },\n            html_to_markdown_rs::NodeContent::Paragraph { text } => Self {\n                node_type_tag: \"paragraph\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: Some(text),\n            },\n            html_to_markdown_rs::NodeContent::List { ordered } => Self {\n                node_type_tag: \"list\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: Some(ordered),\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::ListItem { text } => Self {\n                node_type_tag: \"listitem\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: Some(text),\n            },\n            html_to_markdown_rs::NodeContent::Table { grid } => Self {\n                node_type_tag: \"table\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: Some(grid.into()),\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self {\n                node_type_tag: \"image\".to_string(),\n                content: None,\n                definition: None,\n                description,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::Code { text, language } => Self {\n                node_type_tag: \"code\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: Some(text),\n            },\n            html_to_markdown_rs::NodeContent::Quote => Self {\n                node_type_tag: \"quote\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::DefinitionList => Self {\n                node_type_tag: \"definitionlist\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::DefinitionItem { term, definition } => Self {\n                node_type_tag: \"definitionitem\".to_string(),\n                content: None,\n                definition: Some(definition),\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: Some(term),\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::RawBlock { format, content } => Self {\n                node_type_tag: \"rawblock\".to_string(),\n                content: Some(content),\n                definition: None,\n                description: None,\n                entries: None,\n                format: Some(format),\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::MetadataBlock { entries: _entries } => Self {\n                node_type_tag: \"metadatablock\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level: None,\n                heading_text: None,\n                image_index: None,\n                label: None,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n            html_to_markdown_rs::NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self {\n                node_type_tag: \"group\".to_string(),\n                content: None,\n                definition: None,\n                description: None,\n                entries: None,\n                format: None,\n                grid: None,\n                heading_level,\n                heading_text,\n                image_index: None,\n                label,\n                language: None,\n                level: None,\n                ordered: None,\n                src: None,\n                term: None,\n                text: None,\n            },\n        }\n    }\n}\n\nimpl From<JsAnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: JsAnnotationKind) -> Self {\n        match val.annotation_type_tag.as_str() {\n            \"bold\" => Self::Bold,\n            \"italic\" => Self::Italic,\n            \"underline\" => Self::Underline,\n            \"strikethrough\" => Self::Strikethrough,\n            \"code\" => Self::Code,\n            \"subscript\" => Self::Subscript,\n            \"superscript\" => Self::Superscript,\n            \"highlight\" => Self::Highlight,\n            \"link\" => Self::Link {\n                url: val.url.unwrap_or_default(),\n                title: val.title,\n            },\n            _ => Self::Bold,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for JsAnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self {\n                annotation_type_tag: \"bold\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Italic => Self {\n                annotation_type_tag: \"italic\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Underline => Self {\n                annotation_type_tag: \"underline\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self {\n                annotation_type_tag: \"strikethrough\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Code => Self {\n                annotation_type_tag: \"code\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Subscript => Self {\n                annotation_type_tag: \"subscript\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Superscript => Self {\n                annotation_type_tag: \"superscript\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Highlight => Self {\n                annotation_type_tag: \"highlight\".to_string(),\n                title: None,\n                url: None,\n            },\n            html_to_markdown_rs::AnnotationKind::Link { url, title } => Self {\n                annotation_type_tag: \"link\".to_string(),\n                title,\n                url: Some(url),\n            },\n        }\n    }\n}\n\nimpl From<JsWarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: JsWarningKind) -> Self {\n        match val {\n            JsWarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            JsWarningKind::EncodingFallback => Self::EncodingFallback,\n            JsWarningKind::TruncatedInput => Self::TruncatedInput,\n            JsWarningKind::MalformedHtml => Self::MalformedHtml,\n            JsWarningKind::SanitizationApplied => Self::SanitizationApplied,\n            JsWarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for JsWarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for JsNodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for JsVisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        match val {\n            html_to_markdown_rs::VisitResult::Continue => Self::Continue,\n            html_to_markdown_rs::VisitResult::Custom(..) => Self::Custom,\n            html_to_markdown_rs::VisitResult::Skip => Self::Skip,\n            html_to_markdown_rs::VisitResult::PreserveHtml => Self::PreserveHtml,\n            html_to_markdown_rs::VisitResult::Error(..) => Self::Error,\n        }\n    }\n}\n\n// Error variant name constants\npub const CONVERSION_ERROR_ERROR_PARSE_ERROR: &str = \"ParseError\";\npub const CONVERSION_ERROR_ERROR_SANITIZATION_ERROR: &str = \"SanitizationError\";\npub const CONVERSION_ERROR_ERROR_CONFIG_ERROR: &str = \"ConfigError\";\npub const CONVERSION_ERROR_ERROR_IO_ERROR: &str = \"IoError\";\npub const CONVERSION_ERROR_ERROR_PANIC: &str = \"Panic\";\npub const CONVERSION_ERROR_ERROR_INVALID_INPUT: &str = \"InvalidInput\";\npub const CONVERSION_ERROR_ERROR_OTHER: &str = \"Other\";\n\n/// Convert a `html_to_markdown_rs::error::ConversionError` error to a NAPI error.\n#[allow(dead_code)]\nfn conversion_error_to_napi_err(e: html_to_markdown_rs::error::ConversionError) -> napi::Error {\n    let msg = e.to_string();\n    #[allow(unreachable_patterns)]\n    match &e {\n        html_to_markdown_rs::error::ConversionError::ParseError(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[ParseError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::SanitizationError(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[SanitizationError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::ConfigError(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[ConfigError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::IoError(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[IoError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::Panic(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[Panic] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::InvalidInput(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[InvalidInput] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::Other(..) => {\n            napi::Error::new(napi::Status::GenericFailure, format!(\"[Other] {}\", msg))\n        }\n        _ => napi::Error::new(napi::Status::GenericFailure, msg),\n    }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-php/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-php\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter - PHP bindings\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\", \"web-programming\"]\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\next-php-rs = \"0.15.12\"\nhtml-to-markdown-rs = { path = \"../html-to-markdown\", features = [\n    \"full\",\n    \"metadata\",\n    \"visitor\",\n    \"serde\",\n    \"inline-images\",\n] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n"
  },
  {
    "path": "crates/html-to-markdown-php/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:7569f28cc1b0a0768d040b952c19955c2a981c653289ad417eaba4c704a08f57\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unnecessary_cast,\n    clippy::unused_unit,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions\n)]\n#![cfg_attr(windows, feature(abi_vectorcall))]\n\nuse ext_php_rs::prelude::*;\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\DocumentMetadata\")]\npub struct DocumentMetadata {\n    /// Document title from `<title>` tag\n    #[php(prop, name = \"title\")]\n    pub title: Option<String>,\n    /// Document description from `<meta name=\"description\">` tag\n    #[php(prop, name = \"description\")]\n    pub description: Option<String>,\n    /// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n    #[php(prop, name = \"keywords\")]\n    pub keywords: Vec<String>,\n    /// Document author from `<meta name=\"author\">` tag\n    #[php(prop, name = \"author\")]\n    pub author: Option<String>,\n    /// Canonical URL from `<link rel=\"canonical\">` tag\n    #[php(prop, name = \"canonical_url\")]\n    pub canonical_url: Option<String>,\n    /// Base URL from `<base href=\"\">` tag for resolving relative URLs\n    #[php(prop, name = \"base_href\")]\n    pub base_href: Option<String>,\n    /// Document language from `lang` attribute\n    #[php(prop, name = \"language\")]\n    pub language: Option<String>,\n    /// Document text direction from `dir` attribute\n    #[php(prop, name = \"text_direction\")]\n    pub text_direction: Option<String>,\n    /// Open Graph metadata (og:* properties) for social media\n    /// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n    pub open_graph: HashMap<String, String>,\n    /// Twitter Card metadata (twitter:* properties)\n    /// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n    pub twitter_card: HashMap<String, String>,\n    /// Additional meta tags not covered by specific fields\n    /// Keys are meta name/property attributes, values are content\n    pub meta_tags: HashMap<String, String>,\n}\n\n#[php_impl]\nimpl DocumentMetadata {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_open_graph(&self) -> HashMap<String, String> {\n        self.open_graph.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_twitter_card(&self) -> HashMap<String, String> {\n        self.twitter_card.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_meta_tags(&self) -> HashMap<String, String> {\n        self.meta_tags.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\HeaderMetadata\")]\npub struct HeaderMetadata {\n    /// Header level: 1 (h1) through 6 (h6)\n    #[php(prop, name = \"level\")]\n    pub level: u8,\n    /// Normalized text content of the header\n    #[php(prop, name = \"text\")]\n    pub text: String,\n    /// HTML id attribute if present\n    #[php(prop, name = \"id\")]\n    pub id: Option<String>,\n    /// Document tree depth at the header element\n    #[php(prop, name = \"depth\")]\n    pub depth: i64,\n    /// Byte offset in original HTML document\n    #[php(prop, name = \"html_offset\")]\n    pub html_offset: i64,\n}\n\n#[php_impl]\nimpl HeaderMetadata {\n    pub fn __construct(level: u8, text: String, depth: i64, html_offset: i64, id: Option<String>) -> Self {\n        Self {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        }\n    }\n\n    pub fn is_valid(&self) -> bool {\n        let core_self = html_to_markdown_rs::metadata::HeaderMetadata {\n            level: self.level,\n            text: self.text.clone(),\n            id: self.id.clone(),\n            depth: self.depth as usize,\n            html_offset: self.html_offset as usize,\n        };\n        core_self.is_valid()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\LinkMetadata\")]\npub struct LinkMetadata {\n    /// The href URL value\n    #[php(prop, name = \"href\")]\n    pub href: String,\n    /// Link text content (normalized, concatenated if mixed with elements)\n    #[php(prop, name = \"text\")]\n    pub text: String,\n    /// Optional title attribute (often shown as tooltip)\n    #[php(prop, name = \"title\")]\n    pub title: Option<String>,\n    /// Link type classification\n    #[php(prop, name = \"link_type\")]\n    pub link_type: String,\n    /// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n    #[php(prop, name = \"rel\")]\n    pub rel: Vec<String>,\n    /// Additional HTML attributes\n    pub attributes: HashMap<String, String>,\n}\n\n#[php_impl]\nimpl LinkMetadata {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n\n    /**\n     * Classify a link based on href value.\n     *\n     * # Arguments\n     *\n     * * `href` - The href attribute value\n     *\n     * # Returns\n     *\n     * Appropriate [`LinkType`] based on protocol and content.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n     * assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n     * assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n     * assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n     * assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n     * ```\n     */\n    pub fn classify_link(href: String) -> String {\n        format!(\n            \"{:?}\",\n            html_to_markdown_rs::metadata::LinkMetadata::classify_link(&href)\n        )\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ImageMetadata\")]\npub struct ImageMetadata {\n    /// Image source (URL, data URI, or SVG content identifier)\n    #[php(prop, name = \"src\")]\n    pub src: String,\n    /// Alternative text from alt attribute (for accessibility)\n    #[php(prop, name = \"alt\")]\n    pub alt: Option<String>,\n    /// Title attribute (often shown as tooltip)\n    #[php(prop, name = \"title\")]\n    pub title: Option<String>,\n    /// Image dimensions as (width, height) if available\n    #[php(prop, name = \"dimensions\")]\n    #[serde(skip)]\n    pub dimensions: Option<Vec<u32>>,\n    /// Image type classification\n    #[php(prop, name = \"image_type\")]\n    pub image_type: String,\n    /// Additional HTML attributes\n    pub attributes: HashMap<String, String>,\n}\n\n#[php_impl]\nimpl ImageMetadata {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\StructuredData\")]\npub struct StructuredData {\n    /// Type of structured data (JSON-LD, Microdata, RDFa)\n    #[php(prop, name = \"data_type\")]\n    pub data_type: String,\n    /// Raw JSON string (for JSON-LD) or serialized representation\n    #[php(prop, name = \"raw_json\")]\n    pub raw_json: String,\n    /// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n    #[php(prop, name = \"schema_type\")]\n    pub schema_type: Option<String>,\n}\n\n#[php_impl]\nimpl StructuredData {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\HtmlMetadata\")]\npub struct HtmlMetadata {\n    /// Document-level metadata (title, description, canonical, etc.)\n    pub document: DocumentMetadata,\n    /// Extracted header elements with hierarchy\n    pub headers: Vec<HeaderMetadata>,\n    /// Extracted hyperlinks with type classification\n    pub links: Vec<LinkMetadata>,\n    /// Extracted images with source and dimensions\n    pub images: Vec<ImageMetadata>,\n    /// Extracted structured data blocks\n    pub structured_data: Vec<StructuredData>,\n}\n\n#[php_impl]\nimpl HtmlMetadata {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_document(&self) -> DocumentMetadata {\n        self.document.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_headers(&self) -> Vec<HeaderMetadata> {\n        self.headers.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_links(&self) -> Vec<LinkMetadata> {\n        self.links.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_images(&self) -> Vec<ImageMetadata> {\n        self.images.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_structured_data(&self) -> Vec<StructuredData> {\n        self.structured_data.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ConversionOptions\")]\n#[allow(clippy::similar_names)]\npub struct ConversionOptions {\n    /// Heading style to use in Markdown output (ATX `#` or Setext underline).\n    #[php(prop, name = \"heading_style\")]\n    pub heading_style: String,\n    /// How to indent nested list items (spaces or tab).\n    #[php(prop, name = \"list_indent_type\")]\n    pub list_indent_type: String,\n    /// Number of spaces (or tabs) to use for each level of list indentation.\n    #[php(prop, name = \"list_indent_width\")]\n    pub list_indent_width: i64,\n    /// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n    #[php(prop, name = \"bullets\")]\n    pub bullets: String,\n    /// Character used for bold/italic emphasis markers (`*` or `_`).\n    #[php(prop, name = \"strong_em_symbol\")]\n    pub strong_em_symbol: String,\n    /// Escape `*` characters in plain text to avoid unintended bold/italic.\n    #[php(prop, name = \"escape_asterisks\")]\n    pub escape_asterisks: bool,\n    /// Escape `_` characters in plain text to avoid unintended bold/italic.\n    #[php(prop, name = \"escape_underscores\")]\n    pub escape_underscores: bool,\n    /// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n    #[php(prop, name = \"escape_misc\")]\n    pub escape_misc: bool,\n    /// Escape ASCII characters that have special meaning in certain Markdown dialects.\n    #[php(prop, name = \"escape_ascii\")]\n    pub escape_ascii: bool,\n    /// Default language annotation for fenced code blocks that have no language hint.\n    #[php(prop, name = \"code_language\")]\n    pub code_language: String,\n    /// Automatically convert bare URLs into Markdown autolinks.\n    #[php(prop, name = \"autolinks\")]\n    pub autolinks: bool,\n    /// Emit a default title when no `<title>` tag is present.\n    #[php(prop, name = \"default_title\")]\n    pub default_title: bool,\n    /// Render `<br>` elements inside table cells as literal line breaks.\n    #[php(prop, name = \"br_in_tables\")]\n    pub br_in_tables: bool,\n    /// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n    #[php(prop, name = \"highlight_style\")]\n    pub highlight_style: String,\n    /// Extract `<meta>` and `<head>` information into the result metadata.\n    #[php(prop, name = \"extract_metadata\")]\n    pub extract_metadata: bool,\n    /// Controls how whitespace is normalised during conversion.\n    #[php(prop, name = \"whitespace_mode\")]\n    pub whitespace_mode: String,\n    /// Strip all newlines from the output, producing a single-line result.\n    #[php(prop, name = \"strip_newlines\")]\n    pub strip_newlines: bool,\n    /// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n    #[php(prop, name = \"wrap\")]\n    pub wrap: bool,\n    /// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n    #[php(prop, name = \"wrap_width\")]\n    pub wrap_width: i64,\n    /// Treat the entire document as inline content (no block-level wrappers).\n    #[php(prop, name = \"convert_as_inline\")]\n    pub convert_as_inline: bool,\n    /// Markdown notation for subscript text (e.g. `\"~\"`).\n    #[php(prop, name = \"sub_symbol\")]\n    pub sub_symbol: String,\n    /// Markdown notation for superscript text (e.g. `\"^\"`).\n    #[php(prop, name = \"sup_symbol\")]\n    pub sup_symbol: String,\n    /// How to encode hard line breaks (`<br>`) in Markdown.\n    #[php(prop, name = \"newline_style\")]\n    pub newline_style: String,\n    /// Style used for fenced code blocks (backticks or tilde).\n    #[php(prop, name = \"code_block_style\")]\n    pub code_block_style: String,\n    /// HTML tag names whose `<img>` children are kept inline instead of block.\n    #[php(prop, name = \"keep_inline_images_in\")]\n    pub keep_inline_images_in: Vec<String>,\n    /// Pre-processing options applied to the HTML before conversion.\n    pub preprocessing: PreprocessingOptions,\n    /// Expected character encoding of the input HTML (default `\"utf-8\"`).\n    #[php(prop, name = \"encoding\")]\n    pub encoding: String,\n    /// Emit debug information during conversion.\n    #[php(prop, name = \"debug\")]\n    pub debug: bool,\n    /// HTML tag names whose content is stripped from the output entirely.\n    #[php(prop, name = \"strip_tags\")]\n    pub strip_tags: Vec<String>,\n    /// HTML tag names that are preserved verbatim in the output.\n    #[php(prop, name = \"preserve_tags\")]\n    pub preserve_tags: Vec<String>,\n    /// Skip conversion of `<img>` elements (omit images from output).\n    #[php(prop, name = \"skip_images\")]\n    pub skip_images: bool,\n    /// Link rendering style (inline or reference).\n    #[php(prop, name = \"link_style\")]\n    pub link_style: String,\n    /// Target output format (Markdown, plain text, etc.).\n    #[php(prop, name = \"output_format\")]\n    pub output_format: String,\n    /// Include structured document tree in result.\n    #[php(prop, name = \"include_document_structure\")]\n    pub include_document_structure: bool,\n    /// Extract inline images from data URIs and SVGs.\n    #[php(prop, name = \"extract_images\")]\n    pub extract_images: bool,\n    /// Maximum decoded image size in bytes (default 5MB).\n    #[php(prop, name = \"max_image_size\")]\n    pub max_image_size: i64,\n    /// Capture SVG elements as images.\n    #[php(prop, name = \"capture_svg\")]\n    pub capture_svg: bool,\n    /// Infer image dimensions from data.\n    #[php(prop, name = \"infer_dimensions\")]\n    pub infer_dimensions: bool,\n    /// Maximum DOM traversal depth. `None` means unlimited.\n    /// When set, subtrees beyond this depth are silently truncated.\n    #[php(prop, name = \"max_depth\")]\n    pub max_depth: Option<i64>,\n    /// CSS selectors for elements to exclude entirely (element + all content).\n    ///\n    /// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n    /// excluded elements and all their descendants are dropped from the output.\n    /// Supports any CSS selector that `tl` supports: tag names, `.class`,\n    /// `#id`, `[attribute]`, etc.\n    ///\n    /// Invalid selectors are silently skipped at conversion time.\n    ///\n    /// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n    #[php(prop, name = \"exclude_selectors\")]\n    pub exclude_selectors: Vec<String>,\n    /// Optional visitor for custom traversal logic.\n    ///\n    /// When set, the visitor's callbacks are invoked for matching HTML elements\n    /// during conversion, allowing custom output, skipping, or HTML preservation.\n    /// See [`crate::visitor::HtmlVisitor`].\n    #[serde(skip)]\n    pub visitor: Option<VisitorHandle>,\n}\n\n#[php_impl]\nimpl ConversionOptions {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_preprocessing(&self) -> PreprocessingOptions {\n        self.preprocessing.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_visitor(&self) -> Option<VisitorHandle> {\n        self.visitor.clone()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn default() -> ConversionOptions {\n        html_to_markdown_rs::options::ConversionOptions::default().into()\n    }\n\n    /**\n     * Create a new builder with default values.\n     */\n    pub fn builder() -> ConversionOptionsBuilder {\n        ConversionOptionsBuilder {\n            inner: Arc::new(html_to_markdown_rs::options::ConversionOptions::builder()),\n        }\n    }\n}\n\n#[derive(Clone)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ConversionOptionsBuilder\")]\npub struct ConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\n#[php_impl]\nimpl ConversionOptionsBuilder {\n    /**\n     * Set the list of HTML tag names whose content is stripped from output.\n     */\n    pub fn strip_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().strip_tags(tags)),\n        }\n    }\n\n    /**\n     * Set the list of HTML tag names that are preserved verbatim in output.\n     */\n    pub fn preserve_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preserve_tags(tags)),\n        }\n    }\n\n    /**\n     * Set the list of HTML tag names whose `<img>` children are kept inline.\n     */\n    pub fn keep_inline_images_in(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    /**\n     * Set the list of CSS selectors for elements to exclude entirely from output.\n     */\n    pub fn exclude_selectors(&self, selectors: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().exclude_selectors(selectors)),\n        }\n    }\n\n    /**\n     * Set the visitor used during conversion.\n     */\n    pub fn visitor(&self, visitor: Option<&VisitorHandle>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().visitor(visitor.map(|v| (*v.inner).clone()))),\n        }\n    }\n\n    /**\n     * Set the pre-processing options applied to the HTML before conversion.\n     */\n    pub fn preprocessing(&self, preprocessing: &PreprocessingOptions) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preprocessing(preprocessing.clone().into())),\n        }\n    }\n\n    /**\n     * Build the final [`ConversionOptions`].\n     */\n    pub fn build(&self) -> ConversionOptions {\n        (*self.inner).clone().build().into()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ConversionOptionsUpdate\")]\n#[allow(clippy::similar_names)]\npub struct ConversionOptionsUpdate {\n    /// Optional override for [`ConversionOptions::heading_style`].\n    #[php(prop, name = \"heading_style\")]\n    pub heading_style: Option<String>,\n    /// Optional override for [`ConversionOptions::list_indent_type`].\n    #[php(prop, name = \"list_indent_type\")]\n    pub list_indent_type: Option<String>,\n    /// Optional override for [`ConversionOptions::list_indent_width`].\n    #[php(prop, name = \"list_indent_width\")]\n    pub list_indent_width: Option<i64>,\n    /// Optional override for [`ConversionOptions::bullets`].\n    #[php(prop, name = \"bullets\")]\n    pub bullets: Option<String>,\n    /// Optional override for [`ConversionOptions::strong_em_symbol`].\n    #[php(prop, name = \"strong_em_symbol\")]\n    pub strong_em_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::escape_asterisks`].\n    #[php(prop, name = \"escape_asterisks\")]\n    pub escape_asterisks: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_underscores`].\n    #[php(prop, name = \"escape_underscores\")]\n    pub escape_underscores: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_misc`].\n    #[php(prop, name = \"escape_misc\")]\n    pub escape_misc: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_ascii`].\n    #[php(prop, name = \"escape_ascii\")]\n    pub escape_ascii: Option<bool>,\n    /// Optional override for [`ConversionOptions::code_language`].\n    #[php(prop, name = \"code_language\")]\n    pub code_language: Option<String>,\n    /// Optional override for [`ConversionOptions::autolinks`].\n    #[php(prop, name = \"autolinks\")]\n    pub autolinks: Option<bool>,\n    /// Optional override for [`ConversionOptions::default_title`].\n    #[php(prop, name = \"default_title\")]\n    pub default_title: Option<bool>,\n    /// Optional override for [`ConversionOptions::br_in_tables`].\n    #[php(prop, name = \"br_in_tables\")]\n    pub br_in_tables: Option<bool>,\n    /// Optional override for [`ConversionOptions::highlight_style`].\n    #[php(prop, name = \"highlight_style\")]\n    pub highlight_style: Option<String>,\n    /// Optional override for [`ConversionOptions::extract_metadata`].\n    #[php(prop, name = \"extract_metadata\")]\n    pub extract_metadata: Option<bool>,\n    /// Optional override for [`ConversionOptions::whitespace_mode`].\n    #[php(prop, name = \"whitespace_mode\")]\n    pub whitespace_mode: Option<String>,\n    /// Optional override for [`ConversionOptions::strip_newlines`].\n    #[php(prop, name = \"strip_newlines\")]\n    pub strip_newlines: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap`].\n    #[php(prop, name = \"wrap\")]\n    pub wrap: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap_width`].\n    #[php(prop, name = \"wrap_width\")]\n    pub wrap_width: Option<i64>,\n    /// Optional override for [`ConversionOptions::convert_as_inline`].\n    #[php(prop, name = \"convert_as_inline\")]\n    pub convert_as_inline: Option<bool>,\n    /// Optional override for [`ConversionOptions::sub_symbol`].\n    #[php(prop, name = \"sub_symbol\")]\n    pub sub_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::sup_symbol`].\n    #[php(prop, name = \"sup_symbol\")]\n    pub sup_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::newline_style`].\n    #[php(prop, name = \"newline_style\")]\n    pub newline_style: Option<String>,\n    /// Optional override for [`ConversionOptions::code_block_style`].\n    #[php(prop, name = \"code_block_style\")]\n    pub code_block_style: Option<String>,\n    /// Optional override for [`ConversionOptions::keep_inline_images_in`].\n    #[php(prop, name = \"keep_inline_images_in\")]\n    pub keep_inline_images_in: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preprocessing`].\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    /// Optional override for [`ConversionOptions::encoding`].\n    #[php(prop, name = \"encoding\")]\n    pub encoding: Option<String>,\n    /// Optional override for [`ConversionOptions::debug`].\n    #[php(prop, name = \"debug\")]\n    pub debug: Option<bool>,\n    /// Optional override for [`ConversionOptions::strip_tags`].\n    #[php(prop, name = \"strip_tags\")]\n    pub strip_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preserve_tags`].\n    #[php(prop, name = \"preserve_tags\")]\n    pub preserve_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::skip_images`].\n    #[php(prop, name = \"skip_images\")]\n    pub skip_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::link_style`].\n    #[php(prop, name = \"link_style\")]\n    pub link_style: Option<String>,\n    /// Optional override for [`ConversionOptions::output_format`].\n    #[php(prop, name = \"output_format\")]\n    pub output_format: Option<String>,\n    /// Optional override for [`ConversionOptions::include_document_structure`].\n    #[php(prop, name = \"include_document_structure\")]\n    pub include_document_structure: Option<bool>,\n    /// Optional override for [`ConversionOptions::extract_images`].\n    #[php(prop, name = \"extract_images\")]\n    pub extract_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_image_size`].\n    #[php(prop, name = \"max_image_size\")]\n    pub max_image_size: Option<i64>,\n    /// Optional override for [`ConversionOptions::capture_svg`].\n    #[php(prop, name = \"capture_svg\")]\n    pub capture_svg: Option<bool>,\n    /// Optional override for [`ConversionOptions::infer_dimensions`].\n    #[php(prop, name = \"infer_dimensions\")]\n    pub infer_dimensions: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_depth`].\n    #[php(prop, name = \"max_depth\")]\n    pub max_depth: Option<i64>,\n    /// Optional override for [`ConversionOptions::exclude_selectors`].\n    #[php(prop, name = \"exclude_selectors\")]\n    pub exclude_selectors: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::visitor`].\n    #[serde(skip)]\n    pub visitor: Option<VisitorHandle>,\n}\n\n#[php_impl]\nimpl ConversionOptionsUpdate {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_preprocessing(&self) -> Option<PreprocessingOptionsUpdate> {\n        self.preprocessing.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_visitor(&self) -> Option<VisitorHandle> {\n        self.visitor.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\PreprocessingOptions\")]\npub struct PreprocessingOptions {\n    /// Enable HTML preprocessing globally\n    #[php(prop, name = \"enabled\")]\n    pub enabled: bool,\n    /// Preprocessing preset level (Minimal, Standard, Aggressive)\n    #[php(prop, name = \"preset\")]\n    pub preset: String,\n    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n    #[php(prop, name = \"remove_navigation\")]\n    pub remove_navigation: bool,\n    /// Remove form elements (forms, inputs, buttons, etc.)\n    #[php(prop, name = \"remove_forms\")]\n    pub remove_forms: bool,\n}\n\n#[php_impl]\nimpl PreprocessingOptions {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn default() -> PreprocessingOptions {\n        html_to_markdown_rs::options::PreprocessingOptions::default().into()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\PreprocessingOptionsUpdate\")]\npub struct PreprocessingOptionsUpdate {\n    /// Optional global preprocessing enablement override\n    #[php(prop, name = \"enabled\")]\n    pub enabled: Option<bool>,\n    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n    #[php(prop, name = \"preset\")]\n    pub preset: Option<String>,\n    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n    #[php(prop, name = \"remove_navigation\")]\n    pub remove_navigation: Option<bool>,\n    /// Optional form element removal override (forms, inputs, buttons, etc.)\n    #[php(prop, name = \"remove_forms\")]\n    pub remove_forms: Option<bool>,\n}\n\n#[php_impl]\nimpl PreprocessingOptionsUpdate {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\DocumentStructure\")]\npub struct DocumentStructure {\n    /// All nodes in document reading order.\n    pub nodes: Vec<DocumentNode>,\n    /// The source format (always \"html\" for this crate).\n    #[php(prop, name = \"source_format\")]\n    pub source_format: Option<String>,\n}\n\n#[php_impl]\nimpl DocumentStructure {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_nodes(&self) -> Vec<DocumentNode> {\n        self.nodes.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\DocumentNode\")]\npub struct DocumentNode {\n    /// Deterministic node identifier.\n    #[php(prop, name = \"id\")]\n    pub id: String,\n    /// The semantic content of this node.\n    pub content: NodeContent,\n    /// Index of the parent node (None for root nodes).\n    #[php(prop, name = \"parent\")]\n    pub parent: Option<u32>,\n    /// Indices of child nodes in reading order.\n    #[php(prop, name = \"children\")]\n    pub children: Vec<u32>,\n    /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n    pub annotations: Vec<TextAnnotation>,\n    /// Format-specific attributes (e.g. class, id, data-* attributes).\n    pub attributes: Option<HashMap<String, String>>,\n}\n\n#[php_impl]\nimpl DocumentNode {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_content(&self) -> NodeContent {\n        self.content.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_annotations(&self) -> Vec<TextAnnotation> {\n        self.annotations.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_attributes(&self) -> Option<HashMap<String, String>> {\n        self.attributes.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\TextAnnotation\")]\npub struct TextAnnotation {\n    /// Start byte offset (inclusive) into the parent node's text.\n    #[php(prop, name = \"start\")]\n    pub start: u32,\n    /// End byte offset (exclusive) into the parent node's text.\n    #[php(prop, name = \"end\")]\n    pub end: u32,\n    /// The type of annotation.\n    pub kind: AnnotationKind,\n}\n\n#[php_impl]\nimpl TextAnnotation {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_kind(&self) -> AnnotationKind {\n        self.kind.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ConversionResult\")]\npub struct ConversionResult {\n    /// Converted text output (markdown, djot, or plain text).\n    ///\n    /// `None` when `output_format` is set to `OutputFormat::None`,\n    /// indicating extraction-only mode.\n    #[php(prop, name = \"content\")]\n    pub content: Option<String>,\n    /// Structured document tree with semantic elements.\n    ///\n    /// Populated when `include_document_structure` is `true` in options.\n    pub document: Option<DocumentStructure>,\n    /// Extracted HTML metadata (title, OG, links, images, structured data).\n    pub metadata: HtmlMetadata,\n    /// Extracted tables with structured cell data and markdown representation.\n    pub tables: Vec<TableData>,\n    /// Extracted inline images (data URIs and SVGs).\n    ///\n    /// Populated when `extract_images` is `true` in options.\n    #[php(prop, name = \"images\")]\n    #[serde(skip)]\n    pub images: Vec<String>,\n    /// Non-fatal processing warnings.\n    pub warnings: Vec<ProcessingWarning>,\n}\n\n#[php_impl]\nimpl ConversionResult {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_document(&self) -> Option<DocumentStructure> {\n        self.document.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_metadata(&self) -> HtmlMetadata {\n        self.metadata.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_tables(&self) -> Vec<TableData> {\n        self.tables.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_warnings(&self) -> Vec<ProcessingWarning> {\n        self.warnings.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\TableGrid\")]\n#[allow(clippy::similar_names)]\npub struct TableGrid {\n    /// Number of rows.\n    #[php(prop, name = \"rows\")]\n    pub rows: u32,\n    /// Number of columns.\n    #[php(prop, name = \"cols\")]\n    pub cols: u32,\n    /// All cells in the table (may be fewer than rows*cols due to spans).\n    pub cells: Vec<GridCell>,\n}\n\n#[php_impl]\nimpl TableGrid {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_cells(&self) -> Vec<GridCell> {\n        self.cells.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\GridCell\")]\n#[allow(clippy::similar_names)]\npub struct GridCell {\n    /// The text content of the cell.\n    #[php(prop, name = \"content\")]\n    pub content: String,\n    /// 0-indexed row position.\n    #[php(prop, name = \"row\")]\n    pub row: u32,\n    /// 0-indexed column position.\n    #[php(prop, name = \"col\")]\n    pub col: u32,\n    /// Number of rows this cell spans (default 1).\n    #[php(prop, name = \"row_span\")]\n    pub row_span: u32,\n    /// Number of columns this cell spans (default 1).\n    #[php(prop, name = \"col_span\")]\n    pub col_span: u32,\n    /// Whether this is a header cell (`<th>`).\n    #[php(prop, name = \"is_header\")]\n    pub is_header: bool,\n}\n\n#[php_impl]\nimpl GridCell {\n    pub fn __construct(content: String, row: u32, col: u32, row_span: u32, col_span: u32, is_header: bool) -> Self {\n        Self {\n            content,\n            row,\n            col,\n            row_span,\n            col_span,\n            is_header,\n        }\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\TableData\")]\npub struct TableData {\n    /// The structured table grid.\n    pub grid: TableGrid,\n    /// The markdown rendering of this table.\n    #[php(prop, name = \"markdown\")]\n    pub markdown: String,\n}\n\n#[php_impl]\nimpl TableData {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_grid(&self) -> TableGrid {\n        self.grid.clone()\n    }\n}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\ProcessingWarning\")]\npub struct ProcessingWarning {\n    /// Human-readable warning message.\n    #[php(prop, name = \"message\")]\n    pub message: String,\n    /// The category of warning.\n    #[php(prop, name = \"kind\")]\n    pub kind: String,\n}\n\n#[php_impl]\nimpl ProcessingWarning {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n}\n\n#[derive(Clone)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\VisitorHandle\")]\npub struct VisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n#[php_impl]\nimpl VisitorHandle {}\n\n#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\NodeContext\")]\npub struct NodeContext {\n    /// Coarse-grained node type classification\n    #[php(prop, name = \"node_type\")]\n    pub node_type: String,\n    /// Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n    #[php(prop, name = \"tag_name\")]\n    pub tag_name: String,\n    /// All HTML attributes as key-value pairs\n    pub attributes: HashMap<String, String>,\n    /// Depth in the DOM tree (0 = root)\n    #[php(prop, name = \"depth\")]\n    pub depth: i64,\n    /// Index among siblings (0-based)\n    #[php(prop, name = \"index_in_parent\")]\n    pub index_in_parent: i64,\n    /// Parent element's tag name (None if root)\n    #[php(prop, name = \"parent_tag\")]\n    pub parent_tag: Option<String>,\n    /// Whether this element is treated as inline vs block\n    #[php(prop, name = \"is_inline\")]\n    pub is_inline: bool,\n}\n\n#[php_impl]\nimpl NodeContext {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n}\n\n// TextDirection enum values\npub const TEXTDIRECTION_LEFTTORIGHT: &str = \"LeftToRight\";\npub const TEXTDIRECTION_RIGHTTOLEFT: &str = \"RightToLeft\";\npub const TEXTDIRECTION_AUTO: &str = \"Auto\";\n\n// LinkType enum values\npub const LINKTYPE_ANCHOR: &str = \"Anchor\";\npub const LINKTYPE_INTERNAL: &str = \"Internal\";\npub const LINKTYPE_EXTERNAL: &str = \"External\";\npub const LINKTYPE_EMAIL: &str = \"Email\";\npub const LINKTYPE_PHONE: &str = \"Phone\";\npub const LINKTYPE_OTHER: &str = \"Other\";\n\n// ImageType enum values\npub const IMAGETYPE_DATAURI: &str = \"DataUri\";\npub const IMAGETYPE_INLINESVG: &str = \"InlineSvg\";\npub const IMAGETYPE_EXTERNAL: &str = \"External\";\npub const IMAGETYPE_RELATIVE: &str = \"Relative\";\n\n// StructuredDataType enum values\npub const STRUCTUREDDATATYPE_JSONLD: &str = \"JsonLd\";\npub const STRUCTUREDDATATYPE_MICRODATA: &str = \"Microdata\";\npub const STRUCTUREDDATATYPE_RDFA: &str = \"RDFa\";\n\n// PreprocessingPreset enum values\npub const PREPROCESSINGPRESET_MINIMAL: &str = \"Minimal\";\npub const PREPROCESSINGPRESET_STANDARD: &str = \"Standard\";\npub const PREPROCESSINGPRESET_AGGRESSIVE: &str = \"Aggressive\";\n\n// HeadingStyle enum values\npub const HEADINGSTYLE_UNDERLINED: &str = \"Underlined\";\npub const HEADINGSTYLE_ATX: &str = \"Atx\";\npub const HEADINGSTYLE_ATXCLOSED: &str = \"AtxClosed\";\n\n// ListIndentType enum values\npub const LISTINDENTTYPE_SPACES: &str = \"Spaces\";\npub const LISTINDENTTYPE_TABS: &str = \"Tabs\";\n\n// WhitespaceMode enum values\npub const WHITESPACEMODE_NORMALIZED: &str = \"Normalized\";\npub const WHITESPACEMODE_STRICT: &str = \"Strict\";\n\n// NewlineStyle enum values\npub const NEWLINESTYLE_SPACES: &str = \"Spaces\";\npub const NEWLINESTYLE_BACKSLASH: &str = \"Backslash\";\n\n// CodeBlockStyle enum values\npub const CODEBLOCKSTYLE_INDENTED: &str = \"Indented\";\npub const CODEBLOCKSTYLE_BACKTICKS: &str = \"Backticks\";\npub const CODEBLOCKSTYLE_TILDES: &str = \"Tildes\";\n\n// HighlightStyle enum values\npub const HIGHLIGHTSTYLE_DOUBLEEQUAL: &str = \"DoubleEqual\";\npub const HIGHLIGHTSTYLE_HTML: &str = \"Html\";\npub const HIGHLIGHTSTYLE_BOLD: &str = \"Bold\";\npub const HIGHLIGHTSTYLE_NONE: &str = \"None\";\n\n// LinkStyle enum values\npub const LINKSTYLE_INLINE: &str = \"Inline\";\npub const LINKSTYLE_REFERENCE: &str = \"Reference\";\n\n// OutputFormat enum values\npub const OUTPUTFORMAT_MARKDOWN: &str = \"Markdown\";\npub const OUTPUTFORMAT_DJOT: &str = \"Djot\";\npub const OUTPUTFORMAT_PLAIN: &str = \"Plain\";\n\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\NodeContent\")]\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct NodeContent {\n    #[php(prop, name = \"node_type\")]\n    #[serde(rename = \"node_type\")]\n    pub node_type_tag: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub level: Option<u8>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub text: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub ordered: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub grid: Option<TableGrid>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub description: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub src: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub image_index: Option<u32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub language: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub term: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub definition: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub format: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub content: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub entries: Option<Vec<String>>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub label: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub heading_level: Option<u8>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub heading_text: Option<String>,\n}\n\n#[php_impl]\nimpl NodeContent {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_node_type_tag(&self) -> String {\n        self.node_type_tag.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_level(&self) -> Option<u8> {\n        self.level\n    }\n\n    #[php(getter)]\n    pub fn get_text(&self) -> Option<String> {\n        self.text.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_ordered(&self) -> Option<bool> {\n        self.ordered\n    }\n\n    #[php(getter)]\n    pub fn get_grid(&self) -> Option<TableGrid> {\n        self.grid.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_description(&self) -> Option<String> {\n        self.description.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_src(&self) -> Option<String> {\n        self.src.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_image_index(&self) -> Option<u32> {\n        self.image_index\n    }\n\n    #[php(getter)]\n    pub fn get_language(&self) -> Option<String> {\n        self.language.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_term(&self) -> Option<String> {\n        self.term.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_definition(&self) -> Option<String> {\n        self.definition.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_format(&self) -> Option<String> {\n        self.format.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_content(&self) -> Option<String> {\n        self.content.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_entries(&self) -> Option<Vec<String>> {\n        self.entries.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_label(&self) -> Option<String> {\n        self.label.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_heading_level(&self) -> Option<u8> {\n        self.heading_level\n    }\n\n    #[php(getter)]\n    pub fn get_heading_text(&self) -> Option<String> {\n        self.heading_text.clone()\n    }\n}\n\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\AnnotationKind\")]\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct AnnotationKind {\n    #[php(prop, name = \"annotation_type\")]\n    #[serde(rename = \"annotation_type\")]\n    pub annotation_type_tag: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub url: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub title: Option<String>,\n}\n\n#[php_impl]\nimpl AnnotationKind {\n    pub fn from_json(json: String) -> PhpResult<Self> {\n        serde_json::from_str(&json).map_err(|e| PhpException::default(e.to_string()))\n    }\n\n    #[php(getter)]\n    pub fn get_annotation_type_tag(&self) -> String {\n        self.annotation_type_tag.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_url(&self) -> Option<String> {\n        self.url.clone()\n    }\n\n    #[php(getter)]\n    pub fn get_title(&self) -> Option<String> {\n        self.title.clone()\n    }\n}\n\n// WarningKind enum values\npub const WARNINGKIND_IMAGEEXTRACTIONFAILED: &str = \"ImageExtractionFailed\";\npub const WARNINGKIND_ENCODINGFALLBACK: &str = \"EncodingFallback\";\npub const WARNINGKIND_TRUNCATEDINPUT: &str = \"TruncatedInput\";\npub const WARNINGKIND_MALFORMEDHTML: &str = \"MalformedHtml\";\npub const WARNINGKIND_SANITIZATIONAPPLIED: &str = \"SanitizationApplied\";\npub const WARNINGKIND_DEPTHLIMITEXCEEDED: &str = \"DepthLimitExceeded\";\n\n// NodeType enum values\npub const NODETYPE_TEXT: &str = \"Text\";\npub const NODETYPE_ELEMENT: &str = \"Element\";\npub const NODETYPE_HEADING: &str = \"Heading\";\npub const NODETYPE_PARAGRAPH: &str = \"Paragraph\";\npub const NODETYPE_DIV: &str = \"Div\";\npub const NODETYPE_BLOCKQUOTE: &str = \"Blockquote\";\npub const NODETYPE_PRE: &str = \"Pre\";\npub const NODETYPE_HR: &str = \"Hr\";\npub const NODETYPE_LIST: &str = \"List\";\npub const NODETYPE_LISTITEM: &str = \"ListItem\";\npub const NODETYPE_DEFINITIONLIST: &str = \"DefinitionList\";\npub const NODETYPE_DEFINITIONTERM: &str = \"DefinitionTerm\";\npub const NODETYPE_DEFINITIONDESCRIPTION: &str = \"DefinitionDescription\";\npub const NODETYPE_TABLE: &str = \"Table\";\npub const NODETYPE_TABLEROW: &str = \"TableRow\";\npub const NODETYPE_TABLECELL: &str = \"TableCell\";\npub const NODETYPE_TABLEHEADER: &str = \"TableHeader\";\npub const NODETYPE_TABLEBODY: &str = \"TableBody\";\npub const NODETYPE_TABLEHEAD: &str = \"TableHead\";\npub const NODETYPE_TABLEFOOT: &str = \"TableFoot\";\npub const NODETYPE_LINK: &str = \"Link\";\npub const NODETYPE_IMAGE: &str = \"Image\";\npub const NODETYPE_STRONG: &str = \"Strong\";\npub const NODETYPE_EM: &str = \"Em\";\npub const NODETYPE_CODE: &str = \"Code\";\npub const NODETYPE_STRIKETHROUGH: &str = \"Strikethrough\";\npub const NODETYPE_UNDERLINE: &str = \"Underline\";\npub const NODETYPE_SUBSCRIPT: &str = \"Subscript\";\npub const NODETYPE_SUPERSCRIPT: &str = \"Superscript\";\npub const NODETYPE_MARK: &str = \"Mark\";\npub const NODETYPE_SMALL: &str = \"Small\";\npub const NODETYPE_BR: &str = \"Br\";\npub const NODETYPE_SPAN: &str = \"Span\";\npub const NODETYPE_ARTICLE: &str = \"Article\";\npub const NODETYPE_SECTION: &str = \"Section\";\npub const NODETYPE_NAV: &str = \"Nav\";\npub const NODETYPE_ASIDE: &str = \"Aside\";\npub const NODETYPE_HEADER: &str = \"Header\";\npub const NODETYPE_FOOTER: &str = \"Footer\";\npub const NODETYPE_MAIN: &str = \"Main\";\npub const NODETYPE_FIGURE: &str = \"Figure\";\npub const NODETYPE_FIGCAPTION: &str = \"Figcaption\";\npub const NODETYPE_TIME: &str = \"Time\";\npub const NODETYPE_DETAILS: &str = \"Details\";\npub const NODETYPE_SUMMARY: &str = \"Summary\";\npub const NODETYPE_FORM: &str = \"Form\";\npub const NODETYPE_INPUT: &str = \"Input\";\npub const NODETYPE_SELECT: &str = \"Select\";\npub const NODETYPE_OPTION: &str = \"Option\";\npub const NODETYPE_BUTTON: &str = \"Button\";\npub const NODETYPE_TEXTAREA: &str = \"Textarea\";\npub const NODETYPE_LABEL: &str = \"Label\";\npub const NODETYPE_FIELDSET: &str = \"Fieldset\";\npub const NODETYPE_LEGEND: &str = \"Legend\";\npub const NODETYPE_AUDIO: &str = \"Audio\";\npub const NODETYPE_VIDEO: &str = \"Video\";\npub const NODETYPE_PICTURE: &str = \"Picture\";\npub const NODETYPE_SOURCE: &str = \"Source\";\npub const NODETYPE_IFRAME: &str = \"Iframe\";\npub const NODETYPE_SVG: &str = \"Svg\";\npub const NODETYPE_CANVAS: &str = \"Canvas\";\npub const NODETYPE_RUBY: &str = \"Ruby\";\npub const NODETYPE_RT: &str = \"Rt\";\npub const NODETYPE_RP: &str = \"Rp\";\npub const NODETYPE_ABBR: &str = \"Abbr\";\npub const NODETYPE_KBD: &str = \"Kbd\";\npub const NODETYPE_SAMP: &str = \"Samp\";\npub const NODETYPE_VAR: &str = \"Var\";\npub const NODETYPE_CITE: &str = \"Cite\";\npub const NODETYPE_Q: &str = \"Q\";\npub const NODETYPE_DEL: &str = \"Del\";\npub const NODETYPE_INS: &str = \"Ins\";\npub const NODETYPE_DATA: &str = \"Data\";\npub const NODETYPE_METER: &str = \"Meter\";\npub const NODETYPE_PROGRESS: &str = \"Progress\";\npub const NODETYPE_OUTPUT: &str = \"Output\";\npub const NODETYPE_TEMPLATE: &str = \"Template\";\npub const NODETYPE_SLOT: &str = \"Slot\";\npub const NODETYPE_HTML: &str = \"Html\";\npub const NODETYPE_HEAD: &str = \"Head\";\npub const NODETYPE_BODY: &str = \"Body\";\npub const NODETYPE_TITLE: &str = \"Title\";\npub const NODETYPE_META: &str = \"Meta\";\npub const NODETYPE_LINKTAG: &str = \"LinkTag\";\npub const NODETYPE_STYLE: &str = \"Style\";\npub const NODETYPE_SCRIPT: &str = \"Script\";\npub const NODETYPE_BASE: &str = \"Base\";\npub const NODETYPE_CUSTOM: &str = \"Custom\";\n\n// VisitResult enum values\npub const VISITRESULT_CONTINUE: &str = \"Continue\";\npub const VISITRESULT_CUSTOM: &str = \"Custom\";\npub const VISITRESULT_SKIP: &str = \"Skip\";\npub const VISITRESULT_PRESERVEHTML: &str = \"PreserveHtml\";\npub const VISITRESULT_ERROR: &str = \"Error\";\n\n#[php_class]\n#[php(name = \"HtmlToMarkdown\\\\HtmlToMarkdownApi\")]\npub struct HtmlToMarkdownApi;\n\n#[php_impl]\nimpl HtmlToMarkdownApi {\n    #[allow(clippy::missing_errors_doc)]\n    pub fn convert(\n        html: String,\n        options: Option<&mut ConversionOptions>,\n        visitor_obj: Option<&mut ext_php_rs::types::ZendObject>,\n    ) -> PhpResult<ConversionResult> {\n        let visitor_obj = visitor_obj.map(|v| {\n            let bridge = PhpHtmlVisitorBridge::new(v);\n            std::rc::Rc::new(std::cell::RefCell::new(bridge)) as html_to_markdown_rs::visitor::VisitorHandle\n        });\n        let mut options_core: Option<html_to_markdown_rs::ConversionOptions> = options\n            .map(|v| {\n                let json = serde_json::to_string(&v)\n                    .map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))?;\n                serde_json::from_str(&json).map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))\n            })\n            .transpose()?;\n        if let (Some(ref mut opts), Some(v)) = (options_core.as_mut(), visitor_obj) {\n            opts.visitor = Some(v);\n        }\n        html_to_markdown_rs::convert(&html, options_core)\n            .map(|val| val.into())\n            .map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))\n    }\n}\n\nfn nodecontext_to_php_array(\n    ctx: &html_to_markdown_rs::visitor::NodeContext,\n) -> ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendHashTable> {\n    let mut arr = ext_php_rs::types::ZendHashTable::new();\n    arr.insert(\n        \"nodeType\",\n        ext_php_rs::types::Zval::try_from(format!(\"{:?}\", ctx.node_type)).unwrap_or_default(),\n    )\n    .ok();\n    arr.insert(\n        \"tagName\",\n        ext_php_rs::types::Zval::try_from(ctx.tag_name.clone()).unwrap_or_default(),\n    )\n    .ok();\n    arr.insert(\n        \"depth\",\n        ext_php_rs::types::Zval::try_from(ctx.depth as i64).unwrap_or_default(),\n    )\n    .ok();\n    arr.insert(\n        \"indexInParent\",\n        ext_php_rs::types::Zval::try_from(ctx.index_in_parent as i64).unwrap_or_default(),\n    )\n    .ok();\n    arr.insert(\n        \"isInline\",\n        ext_php_rs::types::Zval::try_from(ctx.is_inline).unwrap_or_default(),\n    )\n    .ok();\n    if let Some(ref pt) = ctx.parent_tag {\n        arr.insert(\n            \"parentTag\",\n            ext_php_rs::types::Zval::try_from(pt.clone()).unwrap_or_default(),\n        )\n        .ok();\n    }\n    let mut attrs = ext_php_rs::types::ZendHashTable::new();\n    for (k, v) in &ctx.attributes {\n        attrs\n            .insert(\n                k.as_str(),\n                ext_php_rs::types::Zval::try_from(v.clone()).unwrap_or_default(),\n            )\n            .ok();\n    }\n    let mut attrs_zval = ext_php_rs::types::Zval::new();\n    attrs_zval.set_hashtable(attrs);\n    arr.insert(\"attributes\", attrs_zval).ok();\n    arr\n}\n\npub struct PhpHtmlVisitorBridge {\n    php_obj: *mut ext_php_rs::types::ZendObject,\n    cached_name: String,\n}\n\n// SAFETY: PHP objects are single-threaded; the bridge is used\n// only within a single PHP request, never across threads.\nunsafe impl Send for PhpHtmlVisitorBridge {}\nunsafe impl Sync for PhpHtmlVisitorBridge {}\n\nimpl Clone for PhpHtmlVisitorBridge {\n    fn clone(&self) -> Self {\n        Self {\n            php_obj: self.php_obj,\n            cached_name: self.cached_name.clone(),\n        }\n    }\n}\n\nimpl std::fmt::Debug for PhpHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"PhpHtmlVisitorBridge\")\n    }\n}\n\nimpl PhpHtmlVisitorBridge {\n    pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {\n        let cached_name = php_obj\n            .try_call_method(\"name\", vec![])\n            .ok()\n            .and_then(|v| v.string())\n            .unwrap_or(\"unknown\".into())\n            .to_string();\n        Self {\n            php_obj: php_obj as *mut _,\n            cached_name,\n        }\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for PhpHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitElementStart\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_output.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitElementEnd\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitText\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_href.to_string()).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        args.push(match _title {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitLink\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_src.to_string()).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_alt.to_string()).unwrap_or_default());\n        args.push(match _title {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitImage\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(format!(\"{:?}\", _level)).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        args.push(match _id {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitHeading\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(match _lang {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        args.push(ext_php_rs::types::Zval::try_from(_code.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitCodeBlock\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_code.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitCodeInline\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        {\n            let mut _zv = ext_php_rs::types::Zval::new();\n            _zv.set_bool(_ordered);\n            args.push(_zv);\n        }\n        args.push(ext_php_rs::types::Zval::try_from(_marker.to_string()).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitListItem\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        {\n            let mut _zv = ext_php_rs::types::Zval::new();\n            _zv.set_bool(_ordered);\n            args.push(_zv);\n        }\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitListStart\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        {\n            let mut _zv = ext_php_rs::types::Zval::new();\n            _zv.set_bool(_ordered);\n            args.push(_zv);\n        }\n        args.push(ext_php_rs::types::Zval::try_from(_output.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitListEnd\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitTableStart\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(format!(\"{:?}\", _cells)).unwrap_or_default());\n        {\n            let mut _zv = ext_php_rs::types::Zval::new();\n            _zv.set_bool(_is_header);\n            args.push(_zv);\n        }\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitTableRow\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_output.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitTableEnd\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_content.to_string()).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(format!(\"{:?}\", _depth)).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitBlockquote\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitStrong\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitEmphasis\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitStrikethrough\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitUnderline\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitSubscript\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitSuperscript\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitMark\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitLineBreak\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitHorizontalRule\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_tag_name.to_string()).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_html.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitCustomElement\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitDefinitionListStart\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitDefinitionTerm\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitDefinitionDescription\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_output.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitDefinitionListEnd\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(match _action {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        args.push(match _method {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitForm\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_input_type.to_string()).unwrap_or_default());\n        args.push(match _name {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        args.push(match _value {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitInput\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitButton\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(match _src {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitAudio\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(match _src {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitVideo\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(match _src {\n            Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(),\n            None => ext_php_rs::types::Zval::new(),\n        });\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitIframe\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        {\n            let mut _zv = ext_php_rs::types::Zval::new();\n            _zv.set_bool(_open);\n            args.push(_zv);\n        }\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitDetails\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitSummary\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitFigureStart\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_text.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitFigcaption\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n        let php_obj_ref = unsafe { &mut *self.php_obj };\n        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n        let ctx_arr = nodecontext_to_php_array(_ctx);\n        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());\n        args.push(ext_php_rs::types::Zval::try_from(_output.to_string()).unwrap_or_default());\n        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args\n            .iter()\n            .map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn)\n            .collect();\n        let result = php_obj_ref.try_call_method(\"visitFigureEnd\", dyn_args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s = val.string().unwrap_or_default().to_lowercase();\n                match s.as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n}\n\nimpl From<DocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: DocumentMetadata) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for DocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth as usize,\n            html_offset: val.html_offset as usize,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for HeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth as i64,\n            html_offset: val.html_offset as i64,\n        }\n    }\n}\n\nimpl From<LinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: LinkMetadata) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for LinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: serde_json::to_value(val.link_type)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\nimpl From<ImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: ImageMetadata) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for ImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: serde_json::to_value(val.image_type)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\nimpl From<StructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: StructuredData) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for StructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: serde_json::to_value(val.data_type)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\nimpl From<HtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: HtmlMetadata) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for HtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\nimpl From<ConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: ConversionOptions) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for ConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: serde_json::to_value(val.heading_style)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            list_indent_type: serde_json::to_value(val.list_indent_type)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            list_indent_width: val.list_indent_width as i64,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: serde_json::to_value(val.highlight_style)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: serde_json::to_value(val.whitespace_mode)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width as i64,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: serde_json::to_value(val.newline_style)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            code_block_style: serde_json::to_value(val.code_block_style)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: serde_json::to_value(val.link_style)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            output_format: serde_json::to_value(val.output_format)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size as i64,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.map(|v| v as i64),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\nimpl From<ConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: ConversionOptionsUpdate) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for ConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            list_indent_type: val.list_indent_type.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            list_indent_width: val.list_indent_width.map(|v| v as i64),\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width.map(|v| v as i64),\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            code_block_style: val.code_block_style.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            output_format: val.output_format.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size.map(|v| v as i64),\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten().map(|v| v as i64),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\nimpl From<PreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: PreprocessingOptions) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for PreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: serde_json::to_value(val.preset)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\nimpl From<PreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: PreprocessingOptionsUpdate) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for PreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.as_ref().map(|v| {\n                serde_json::to_value(v)\n                    .ok()\n                    .and_then(|s| s.as_str().map(String::from))\n                    .unwrap_or_default()\n            }),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for DocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for DocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for TextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\nimpl From<ConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: ConversionResult) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for ConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for TableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<GridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for GridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableData> for html_to_markdown_rs::TableData {\n    fn from(val: TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for TableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\nimpl From<ProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: ProcessingWarning) -> Self {\n        let json = serde_json::to_string(&val).expect(\"alef: serialize binding type\");\n        serde_json::from_str(&json).expect(\"alef: deserialize to core type\")\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for ProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: serde_json::to_value(val.kind)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for NodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: serde_json::to_value(val.node_type)\n                .ok()\n                .and_then(|s| s.as_str().map(String::from))\n                .unwrap_or_default(),\n            tag_name: val.tag_name,\n            attributes: val.attributes.into_iter().collect(),\n            depth: val.depth as i64,\n            index_in_parent: val.index_in_parent as i64,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for NodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { level, text } => Self {\n                node_type_tag: \"Heading\".to_string(),\n                level: Some(level),\n                text: Some(text),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Paragraph { text } => Self {\n                node_type_tag: \"Paragraph\".to_string(),\n                text: Some(text),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::List { ordered } => Self {\n                node_type_tag: \"List\".to_string(),\n                ordered: Some(ordered),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::ListItem { text } => Self {\n                node_type_tag: \"ListItem\".to_string(),\n                text: Some(text),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Table { grid } => Self {\n                node_type_tag: \"Table\".to_string(),\n                grid: Some(grid.into()),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self {\n                node_type_tag: \"Image\".to_string(),\n                description,\n                src,\n                image_index,\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Code { text, language } => Self {\n                node_type_tag: \"Code\".to_string(),\n                text: Some(text),\n                language,\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Quote => Self {\n                node_type_tag: \"Quote\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::DefinitionList => Self {\n                node_type_tag: \"DefinitionList\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::DefinitionItem { term, definition } => Self {\n                node_type_tag: \"DefinitionItem\".to_string(),\n                term: Some(term),\n                definition: Some(definition),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::RawBlock { format, content } => Self {\n                node_type_tag: \"RawBlock\".to_string(),\n                format: Some(format),\n                content: Some(content),\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::MetadataBlock { entries: _entries } => Self {\n                node_type_tag: \"MetadataBlock\".to_string(),\n                entries: None,\n                ..Default::default()\n            },\n            html_to_markdown_rs::NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self {\n                node_type_tag: \"Group\".to_string(),\n                label,\n                heading_level,\n                heading_text,\n                ..Default::default()\n            },\n        }\n    }\n}\n\nimpl From<NodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: NodeContent) -> Self {\n        match val.node_type_tag.as_str() {\n            \"Heading\" => html_to_markdown_rs::NodeContent::Heading {\n                level: val.level.unwrap_or_default(),\n                text: val.text.unwrap_or_default(),\n            },\n            \"Paragraph\" => html_to_markdown_rs::NodeContent::Paragraph {\n                text: val.text.unwrap_or_default(),\n            },\n            \"List\" => html_to_markdown_rs::NodeContent::List {\n                ordered: val.ordered.unwrap_or_default(),\n            },\n            \"ListItem\" => html_to_markdown_rs::NodeContent::ListItem {\n                text: val.text.unwrap_or_default(),\n            },\n            \"Table\" => html_to_markdown_rs::NodeContent::Table {\n                grid: val.grid.map(Into::into).unwrap_or_default(),\n            },\n            \"Image\" => html_to_markdown_rs::NodeContent::Image {\n                description: val.description,\n                src: val.src,\n                image_index: val.image_index,\n            },\n            \"Code\" => html_to_markdown_rs::NodeContent::Code {\n                text: val.text.unwrap_or_default(),\n                language: val.language,\n            },\n            \"Quote\" => html_to_markdown_rs::NodeContent::Quote,\n            \"DefinitionList\" => html_to_markdown_rs::NodeContent::DefinitionList,\n            \"DefinitionItem\" => html_to_markdown_rs::NodeContent::DefinitionItem {\n                term: val.term.unwrap_or_default(),\n                definition: val.definition.unwrap_or_default(),\n            },\n            \"RawBlock\" => html_to_markdown_rs::NodeContent::RawBlock {\n                format: val.format.unwrap_or_default(),\n                content: val.content.unwrap_or_default(),\n            },\n            \"MetadataBlock\" => html_to_markdown_rs::NodeContent::MetadataBlock {\n                entries: Default::default(),\n            },\n            \"Group\" => html_to_markdown_rs::NodeContent::Group {\n                label: val.label,\n                heading_level: val.heading_level,\n                heading_text: val.heading_text,\n            },\n            _ => html_to_markdown_rs::NodeContent::Heading {\n                level: Default::default(),\n                text: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for AnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self {\n                annotation_type_tag: \"Bold\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Italic => Self {\n                annotation_type_tag: \"Italic\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Underline => Self {\n                annotation_type_tag: \"Underline\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self {\n                annotation_type_tag: \"Strikethrough\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Code => Self {\n                annotation_type_tag: \"Code\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Subscript => Self {\n                annotation_type_tag: \"Subscript\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Superscript => Self {\n                annotation_type_tag: \"Superscript\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Highlight => Self {\n                annotation_type_tag: \"Highlight\".to_string(),\n                ..Default::default()\n            },\n            html_to_markdown_rs::AnnotationKind::Link { url, title } => Self {\n                annotation_type_tag: \"Link\".to_string(),\n                url: Some(url),\n                title,\n            },\n        }\n    }\n}\n\nimpl From<AnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: AnnotationKind) -> Self {\n        match val.annotation_type_tag.as_str() {\n            \"Bold\" => html_to_markdown_rs::AnnotationKind::Bold,\n            \"Italic\" => html_to_markdown_rs::AnnotationKind::Italic,\n            \"Underline\" => html_to_markdown_rs::AnnotationKind::Underline,\n            \"Strikethrough\" => html_to_markdown_rs::AnnotationKind::Strikethrough,\n            \"Code\" => html_to_markdown_rs::AnnotationKind::Code,\n            \"Subscript\" => html_to_markdown_rs::AnnotationKind::Subscript,\n            \"Superscript\" => html_to_markdown_rs::AnnotationKind::Superscript,\n            \"Highlight\" => html_to_markdown_rs::AnnotationKind::Highlight,\n            \"Link\" => html_to_markdown_rs::AnnotationKind::Link {\n                url: val.url.unwrap_or_default(),\n                title: val.title,\n            },\n            _ => html_to_markdown_rs::AnnotationKind::Bold,\n        }\n    }\n}\n\n/// Convert a `html_to_markdown_rs::error::ConversionError` error to a PHP exception.\n#[allow(dead_code)]\nfn conversion_error_to_php_err(e: html_to_markdown_rs::error::ConversionError) -> ext_php_rs::exception::PhpException {\n    let msg = e.to_string();\n    #[allow(unreachable_patterns)]\n    match &e {\n        html_to_markdown_rs::error::ConversionError::ParseError(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[ParseError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::SanitizationError(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[SanitizationError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::ConfigError(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[ConfigError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::IoError(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[IoError] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::Panic(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[Panic] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::InvalidInput(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[InvalidInput] {}\", msg))\n        }\n        html_to_markdown_rs::error::ConversionError::Other(..) => {\n            ext_php_rs::exception::PhpException::default(format!(\"[Other] {}\", msg))\n        }\n        _ => ext_php_rs::exception::PhpException::default(msg),\n    }\n}\n\n#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {\n    module\n        .class::<DocumentMetadata>()\n        .class::<HeaderMetadata>()\n        .class::<LinkMetadata>()\n        .class::<ImageMetadata>()\n        .class::<StructuredData>()\n        .class::<HtmlMetadata>()\n        .class::<ConversionOptions>()\n        .class::<ConversionOptionsBuilder>()\n        .class::<ConversionOptionsUpdate>()\n        .class::<PreprocessingOptions>()\n        .class::<PreprocessingOptionsUpdate>()\n        .class::<DocumentStructure>()\n        .class::<DocumentNode>()\n        .class::<TextAnnotation>()\n        .class::<ConversionResult>()\n        .class::<TableGrid>()\n        .class::<GridCell>()\n        .class::<TableData>()\n        .class::<ProcessingWarning>()\n        .class::<VisitorHandle>()\n        .class::<NodeContext>()\n        .class::<HtmlToMarkdownApi>()\n        .class::<NodeContent>()\n        .class::<AnnotationKind>()\n}\n"
  },
  {
    "path": "crates/html-to-markdown-py/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-py\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter - Python bindings\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\", \"web-programming\"]\n\n[lib]\nname = \"_html_to_markdown\"\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../html-to-markdown\", features = [\n    \"full\",\n    \"metadata\",\n    \"visitor\",\n    \"serde\",\n    \"inline-images\",\n] }\npyo3 = { workspace = true, features = [\"extension-module\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n"
  },
  {
    "path": "crates/html-to-markdown-py/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:f4f6dc599a4fb55d35cd86092986652834b0b903c509d635809bb02892e8921a\n// Re-generate with: alef generate\n#![allow(missing_docs)]\n#![allow(deprecated, dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::default_trait_access,\n    clippy::cast_possible_wrap,\n    clippy::cast_possible_truncation,\n    clippy::cast_sign_loss,\n    clippy::just_underscores_and_digits,\n    clippy::unused_unit,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::too_many_arguments,\n    clippy::map_identity,\n    clippy::unnecessary_cast,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions,\n    clippy::useless_conversion\n)]\n#![allow(\n    clippy::unsafe_derive_deserialize,\n    clippy::must_use_candidate,\n    clippy::return_self_not_must_use,\n    clippy::use_self,\n    clippy::missing_const_for_fn,\n    clippy::missing_errors_doc,\n    clippy::needless_pass_by_value,\n    clippy::doc_markdown,\n    clippy::derive_partial_eq_without_eq,\n    clippy::uninlined_format_args,\n    clippy::redundant_clone,\n    clippy::implicit_clone,\n    clippy::redundant_closure_for_method_calls,\n    clippy::wildcard_imports,\n    clippy::option_if_let_else,\n    clippy::too_many_lines\n)]\n\nuse pyo3::prelude::*;\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::sync::Mutex;\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct DocumentMetadata {\n    /// Document title from `<title>` tag\n    #[pyo3(get)]\n    pub title: Option<String>,\n    /// Document description from `<meta name=\"description\">` tag\n    #[pyo3(get)]\n    pub description: Option<String>,\n    /// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n    #[pyo3(get)]\n    pub keywords: Vec<String>,\n    /// Document author from `<meta name=\"author\">` tag\n    #[pyo3(get)]\n    pub author: Option<String>,\n    /// Canonical URL from `<link rel=\"canonical\">` tag\n    #[pyo3(get)]\n    pub canonical_url: Option<String>,\n    /// Base URL from `<base href=\"\">` tag for resolving relative URLs\n    #[pyo3(get)]\n    pub base_href: Option<String>,\n    /// Document language from `lang` attribute\n    #[pyo3(get)]\n    pub language: Option<String>,\n    /// Document text direction from `dir` attribute\n    #[pyo3(get)]\n    pub text_direction: Option<TextDirection>,\n    /// Open Graph metadata (og:* properties) for social media\n    /// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n    #[pyo3(get)]\n    pub open_graph: HashMap<String, String>,\n    /// Twitter Card metadata (twitter:* properties)\n    /// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n    #[pyo3(get)]\n    pub twitter_card: HashMap<String, String>,\n    /// Additional meta tags not covered by specific fields\n    /// Keys are meta name/property attributes, values are content\n    #[pyo3(get)]\n    pub meta_tags: HashMap<String, String>,\n}\n\n#[pymethods]\nimpl DocumentMetadata {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n    #[pyo3(signature = (keywords=None, open_graph=None, twitter_card=None, meta_tags=None, title=None, description=None, author=None, canonical_url=None, base_href=None, language=None, text_direction=None))]\n    #[new]\n    pub fn new(\n        keywords: Option<Vec<String>>,\n        open_graph: Option<HashMap<String, String>>,\n        twitter_card: Option<HashMap<String, String>>,\n        meta_tags: Option<HashMap<String, String>>,\n        title: Option<String>,\n        description: Option<String>,\n        author: Option<String>,\n        canonical_url: Option<String>,\n        base_href: Option<String>,\n        language: Option<String>,\n        text_direction: Option<TextDirection>,\n    ) -> Self {\n        Self {\n            title,\n            description,\n            keywords: keywords.unwrap_or_default(),\n            author,\n            canonical_url,\n            base_href,\n            language,\n            text_direction,\n            open_graph: open_graph.unwrap_or_default(),\n            twitter_card: twitter_card.unwrap_or_default(),\n            meta_tags: meta_tags.unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct HeaderMetadata {\n    /// Header level: 1 (h1) through 6 (h6)\n    #[pyo3(get)]\n    pub level: u8,\n    /// Normalized text content of the header\n    #[pyo3(get)]\n    pub text: String,\n    /// HTML id attribute if present\n    #[pyo3(get)]\n    pub id: Option<String>,\n    /// Document tree depth at the header element\n    #[pyo3(get)]\n    pub depth: usize,\n    /// Byte offset in original HTML document\n    #[pyo3(get)]\n    pub html_offset: usize,\n}\n\n#[pymethods]\nimpl HeaderMetadata {\n    #[must_use]\n    #[pyo3(signature = (level, text, depth, html_offset, id=None))]\n    #[new]\n    pub fn new(level: u8, text: String, depth: usize, html_offset: usize, id: Option<String>) -> Self {\n        Self {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        }\n    }\n\n    #[pyo3(signature = ())]\n    pub fn is_valid(&self) -> bool {\n        let core_self = html_to_markdown_rs::metadata::HeaderMetadata {\n            level: self.level,\n            text: self.text.clone(),\n            id: self.id.clone(),\n            depth: self.depth,\n            html_offset: self.html_offset,\n        };\n        core_self.is_valid()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct LinkMetadata {\n    /// The href URL value\n    #[pyo3(get)]\n    pub href: String,\n    /// Link text content (normalized, concatenated if mixed with elements)\n    #[pyo3(get)]\n    pub text: String,\n    /// Optional title attribute (often shown as tooltip)\n    #[pyo3(get)]\n    pub title: Option<String>,\n    /// Link type classification\n    #[pyo3(get)]\n    pub link_type: LinkType,\n    /// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n    #[pyo3(get)]\n    pub rel: Vec<String>,\n    /// Additional HTML attributes\n    #[pyo3(get)]\n    pub attributes: HashMap<String, String>,\n}\n\n#[pymethods]\nimpl LinkMetadata {\n    #[must_use]\n    #[pyo3(signature = (href, text, link_type, rel, attributes, title=None))]\n    #[new]\n    pub fn new(\n        href: String,\n        text: String,\n        link_type: LinkType,\n        rel: Vec<String>,\n        attributes: HashMap<String, String>,\n        title: Option<String>,\n    ) -> Self {\n        Self {\n            href,\n            text,\n            title,\n            link_type,\n            rel,\n            attributes,\n        }\n    }\n\n    #[staticmethod]\n    #[pyo3(signature = (href))]\n    pub fn classify_link(href: String) -> LinkType {\n        html_to_markdown_rs::metadata::LinkMetadata::classify_link(&href).into()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct ImageMetadata {\n    /// Image source (URL, data URI, or SVG content identifier)\n    #[pyo3(get)]\n    pub src: String,\n    /// Alternative text from alt attribute (for accessibility)\n    #[pyo3(get)]\n    pub alt: Option<String>,\n    /// Title attribute (often shown as tooltip)\n    #[pyo3(get)]\n    pub title: Option<String>,\n    /// Image dimensions as (width, height) if available\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub dimensions: Option<Vec<u32>>,\n    /// Image type classification\n    #[pyo3(get)]\n    pub image_type: ImageType,\n    /// Additional HTML attributes\n    #[pyo3(get)]\n    pub attributes: HashMap<String, String>,\n}\n\n#[pymethods]\nimpl ImageMetadata {\n    #[must_use]\n    #[pyo3(signature = (src, image_type, attributes, alt=None, title=None, dimensions=None))]\n    #[new]\n    pub fn new(\n        src: String,\n        image_type: ImageType,\n        attributes: HashMap<String, String>,\n        alt: Option<String>,\n        title: Option<String>,\n        dimensions: Option<Vec<u32>>,\n    ) -> Self {\n        Self {\n            src,\n            alt,\n            title,\n            dimensions,\n            image_type,\n            attributes,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct StructuredData {\n    /// Type of structured data (JSON-LD, Microdata, RDFa)\n    #[pyo3(get)]\n    pub data_type: StructuredDataType,\n    /// Raw JSON string (for JSON-LD) or serialized representation\n    #[pyo3(get)]\n    pub raw_json: String,\n    /// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n    #[pyo3(get)]\n    pub schema_type: Option<String>,\n}\n\n#[pymethods]\nimpl StructuredData {\n    #[must_use]\n    #[pyo3(signature = (data_type, raw_json, schema_type=None))]\n    #[new]\n    pub fn new(data_type: StructuredDataType, raw_json: String, schema_type: Option<String>) -> Self {\n        Self {\n            data_type,\n            raw_json,\n            schema_type,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct HtmlMetadata {\n    /// Document-level metadata (title, description, canonical, etc.)\n    #[pyo3(get)]\n    pub document: DocumentMetadata,\n    /// Extracted header elements with hierarchy\n    #[pyo3(get)]\n    pub headers: Vec<HeaderMetadata>,\n    /// Extracted hyperlinks with type classification\n    #[pyo3(get)]\n    pub links: Vec<LinkMetadata>,\n    /// Extracted images with source and dimensions\n    #[pyo3(get)]\n    pub images: Vec<ImageMetadata>,\n    /// Extracted structured data blocks\n    #[pyo3(get)]\n    pub structured_data: Vec<StructuredData>,\n}\n\n#[pymethods]\nimpl HtmlMetadata {\n    #[must_use]\n    #[pyo3(signature = (document=None, headers=None, links=None, images=None, structured_data=None))]\n    #[new]\n    pub fn new(\n        document: Option<DocumentMetadata>,\n        headers: Option<Vec<HeaderMetadata>>,\n        links: Option<Vec<LinkMetadata>>,\n        images: Option<Vec<ImageMetadata>>,\n        structured_data: Option<Vec<StructuredData>>,\n    ) -> Self {\n        Self {\n            document: document.unwrap_or_default(),\n            headers: headers.unwrap_or_default(),\n            links: links.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            structured_data: structured_data.unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(unsendable, from_py_object)]\n#[allow(clippy::similar_names)]\npub struct ConversionOptions {\n    /// Heading style to use in Markdown output (ATX `#` or Setext underline).\n    #[pyo3(get)]\n    pub heading_style: HeadingStyle,\n    /// How to indent nested list items (spaces or tab).\n    #[pyo3(get)]\n    pub list_indent_type: ListIndentType,\n    /// Number of spaces (or tabs) to use for each level of list indentation.\n    #[pyo3(get)]\n    pub list_indent_width: usize,\n    /// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n    #[pyo3(get)]\n    pub bullets: String,\n    /// Character used for bold/italic emphasis markers (`*` or `_`).\n    #[pyo3(get)]\n    pub strong_em_symbol: String,\n    /// Escape `*` characters in plain text to avoid unintended bold/italic.\n    #[pyo3(get)]\n    pub escape_asterisks: bool,\n    /// Escape `_` characters in plain text to avoid unintended bold/italic.\n    #[pyo3(get)]\n    pub escape_underscores: bool,\n    /// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n    #[pyo3(get)]\n    pub escape_misc: bool,\n    /// Escape ASCII characters that have special meaning in certain Markdown dialects.\n    #[pyo3(get)]\n    pub escape_ascii: bool,\n    /// Default language annotation for fenced code blocks that have no language hint.\n    #[pyo3(get)]\n    pub code_language: String,\n    /// Automatically convert bare URLs into Markdown autolinks.\n    #[pyo3(get)]\n    pub autolinks: bool,\n    /// Emit a default title when no `<title>` tag is present.\n    #[pyo3(get)]\n    pub default_title: bool,\n    /// Render `<br>` elements inside table cells as literal line breaks.\n    #[pyo3(get)]\n    pub br_in_tables: bool,\n    /// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n    #[pyo3(get)]\n    pub highlight_style: HighlightStyle,\n    /// Extract `<meta>` and `<head>` information into the result metadata.\n    #[pyo3(get)]\n    pub extract_metadata: bool,\n    /// Controls how whitespace is normalised during conversion.\n    #[pyo3(get)]\n    pub whitespace_mode: WhitespaceMode,\n    /// Strip all newlines from the output, producing a single-line result.\n    #[pyo3(get)]\n    pub strip_newlines: bool,\n    /// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n    #[pyo3(get)]\n    pub wrap: bool,\n    /// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n    #[pyo3(get)]\n    pub wrap_width: usize,\n    /// Treat the entire document as inline content (no block-level wrappers).\n    #[pyo3(get)]\n    pub convert_as_inline: bool,\n    /// Markdown notation for subscript text (e.g. `\"~\"`).\n    #[pyo3(get)]\n    pub sub_symbol: String,\n    /// Markdown notation for superscript text (e.g. `\"^\"`).\n    #[pyo3(get)]\n    pub sup_symbol: String,\n    /// How to encode hard line breaks (`<br>`) in Markdown.\n    #[pyo3(get)]\n    pub newline_style: NewlineStyle,\n    /// Style used for fenced code blocks (backticks or tilde).\n    #[pyo3(get)]\n    pub code_block_style: CodeBlockStyle,\n    /// HTML tag names whose `<img>` children are kept inline instead of block.\n    #[pyo3(get)]\n    pub keep_inline_images_in: Vec<String>,\n    /// Pre-processing options applied to the HTML before conversion.\n    #[pyo3(get)]\n    pub preprocessing: PreprocessingOptions,\n    /// Expected character encoding of the input HTML (default `\"utf-8\"`).\n    #[pyo3(get)]\n    pub encoding: String,\n    /// Emit debug information during conversion.\n    #[pyo3(get)]\n    pub debug: bool,\n    /// HTML tag names whose content is stripped from the output entirely.\n    #[pyo3(get)]\n    pub strip_tags: Vec<String>,\n    /// HTML tag names that are preserved verbatim in the output.\n    #[pyo3(get)]\n    pub preserve_tags: Vec<String>,\n    /// Skip conversion of `<img>` elements (omit images from output).\n    #[pyo3(get)]\n    pub skip_images: bool,\n    /// Link rendering style (inline or reference).\n    #[pyo3(get)]\n    pub link_style: LinkStyle,\n    /// Target output format (Markdown, plain text, etc.).\n    #[pyo3(get)]\n    pub output_format: OutputFormat,\n    /// Include structured document tree in result.\n    #[pyo3(get)]\n    pub include_document_structure: bool,\n    /// Extract inline images from data URIs and SVGs.\n    #[pyo3(get)]\n    pub extract_images: bool,\n    /// Maximum decoded image size in bytes (default 5MB).\n    #[pyo3(get)]\n    pub max_image_size: u64,\n    /// Capture SVG elements as images.\n    #[pyo3(get)]\n    pub capture_svg: bool,\n    /// Infer image dimensions from data.\n    #[pyo3(get)]\n    pub infer_dimensions: bool,\n    /// Maximum DOM traversal depth. `None` means unlimited.\n    /// When set, subtrees beyond this depth are silently truncated.\n    #[pyo3(get)]\n    pub max_depth: Option<usize>,\n    /// CSS selectors for elements to exclude entirely (element + all content).\n    ///\n    /// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n    /// excluded elements and all their descendants are dropped from the output.\n    /// Supports any CSS selector that `tl` supports: tag names, `.class`,\n    /// `#id`, `[attribute]`, etc.\n    ///\n    /// Invalid selectors are silently skipped at conversion time.\n    ///\n    /// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n    #[pyo3(get)]\n    pub exclude_selectors: Vec<String>,\n    /// Optional visitor for custom traversal logic.\n    ///\n    /// When set, the visitor's callbacks are invoked for matching HTML elements\n    /// during conversion, allowing custom output, skipping, or HTML preservation.\n    /// See [`crate::visitor::HtmlVisitor`].\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub visitor: Option<Py<PyAny>>,\n}\n\nimpl Clone for ConversionOptions {\n    fn clone(&self) -> Self {\n        Self {\n            heading_style: self.heading_style.clone(),\n            list_indent_type: self.list_indent_type.clone(),\n            list_indent_width: self.list_indent_width.clone(),\n            bullets: self.bullets.clone(),\n            strong_em_symbol: self.strong_em_symbol.clone(),\n            escape_asterisks: self.escape_asterisks.clone(),\n            escape_underscores: self.escape_underscores.clone(),\n            escape_misc: self.escape_misc.clone(),\n            escape_ascii: self.escape_ascii.clone(),\n            code_language: self.code_language.clone(),\n            autolinks: self.autolinks.clone(),\n            default_title: self.default_title.clone(),\n            br_in_tables: self.br_in_tables.clone(),\n            highlight_style: self.highlight_style.clone(),\n            extract_metadata: self.extract_metadata.clone(),\n            whitespace_mode: self.whitespace_mode.clone(),\n            strip_newlines: self.strip_newlines.clone(),\n            wrap: self.wrap.clone(),\n            wrap_width: self.wrap_width.clone(),\n            convert_as_inline: self.convert_as_inline.clone(),\n            sub_symbol: self.sub_symbol.clone(),\n            sup_symbol: self.sup_symbol.clone(),\n            newline_style: self.newline_style.clone(),\n            code_block_style: self.code_block_style.clone(),\n            keep_inline_images_in: self.keep_inline_images_in.clone(),\n            preprocessing: self.preprocessing.clone(),\n            encoding: self.encoding.clone(),\n            debug: self.debug.clone(),\n            strip_tags: self.strip_tags.clone(),\n            preserve_tags: self.preserve_tags.clone(),\n            skip_images: self.skip_images.clone(),\n            link_style: self.link_style.clone(),\n            output_format: self.output_format.clone(),\n            include_document_structure: self.include_document_structure.clone(),\n            extract_images: self.extract_images.clone(),\n            max_image_size: self.max_image_size.clone(),\n            capture_svg: self.capture_svg.clone(),\n            infer_dimensions: self.infer_dimensions.clone(),\n            max_depth: self.max_depth.clone(),\n            exclude_selectors: self.exclude_selectors.clone(),\n            visitor: self.visitor.as_ref().map(|v| Python::attach(|py| v.clone_ref(py))),\n        }\n    }\n}\n\n#[pymethods]\nimpl ConversionOptions {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n    #[pyo3(signature = (heading_style=None, list_indent_type=None, list_indent_width=None, bullets=None, strong_em_symbol=None, escape_asterisks=None, escape_underscores=None, escape_misc=None, escape_ascii=None, code_language=None, autolinks=None, default_title=None, br_in_tables=None, highlight_style=None, extract_metadata=None, whitespace_mode=None, strip_newlines=None, wrap=None, wrap_width=None, convert_as_inline=None, sub_symbol=None, sup_symbol=None, newline_style=None, code_block_style=None, keep_inline_images_in=None, preprocessing=None, encoding=None, debug=None, strip_tags=None, preserve_tags=None, skip_images=None, link_style=None, output_format=None, include_document_structure=None, extract_images=None, max_image_size=None, capture_svg=None, infer_dimensions=None, exclude_selectors=None, max_depth=None, visitor=None))]\n    #[new]\n    pub fn new(\n        heading_style: Option<HeadingStyle>,\n        list_indent_type: Option<ListIndentType>,\n        list_indent_width: Option<usize>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<HighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<usize>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<NewlineStyle>,\n        code_block_style: Option<CodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<PreprocessingOptions>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<LinkStyle>,\n        output_format: Option<OutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<u64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        exclude_selectors: Option<Vec<String>>,\n        max_depth: Option<usize>,\n        visitor: Option<Py<PyAny>>,\n    ) -> Self {\n        Self {\n            heading_style: heading_style.unwrap_or_default(),\n            list_indent_type: list_indent_type.unwrap_or_default(),\n            list_indent_width: list_indent_width.unwrap_or(2),\n            bullets: bullets.unwrap_or_else(|| \"-*+\".to_string()),\n            strong_em_symbol: strong_em_symbol.unwrap_or_else(|| \"*\".to_string()),\n            escape_asterisks: escape_asterisks.unwrap_or(false),\n            escape_underscores: escape_underscores.unwrap_or(false),\n            escape_misc: escape_misc.unwrap_or(false),\n            escape_ascii: escape_ascii.unwrap_or(false),\n            code_language: code_language.unwrap_or_else(|| \"\".to_string()),\n            autolinks: autolinks.unwrap_or(true),\n            default_title: default_title.unwrap_or(false),\n            br_in_tables: br_in_tables.unwrap_or(false),\n            highlight_style: highlight_style.unwrap_or_default(),\n            extract_metadata: extract_metadata.unwrap_or(true),\n            whitespace_mode: whitespace_mode.unwrap_or_default(),\n            strip_newlines: strip_newlines.unwrap_or(false),\n            wrap: wrap.unwrap_or(false),\n            wrap_width: wrap_width.unwrap_or(80),\n            convert_as_inline: convert_as_inline.unwrap_or(false),\n            sub_symbol: sub_symbol.unwrap_or_else(|| \"\".to_string()),\n            sup_symbol: sup_symbol.unwrap_or_else(|| \"\".to_string()),\n            newline_style: newline_style.unwrap_or_default(),\n            code_block_style: code_block_style.unwrap_or_default(),\n            keep_inline_images_in: keep_inline_images_in.unwrap_or_default(),\n            preprocessing: preprocessing.unwrap_or_default(),\n            encoding: encoding.unwrap_or_else(|| \"utf-8\".to_string()),\n            debug: debug.unwrap_or(false),\n            strip_tags: strip_tags.unwrap_or_default(),\n            preserve_tags: preserve_tags.unwrap_or_default(),\n            skip_images: skip_images.unwrap_or(false),\n            link_style: link_style.unwrap_or_default(),\n            output_format: output_format.unwrap_or_default(),\n            include_document_structure: include_document_structure.unwrap_or(false),\n            extract_images: extract_images.unwrap_or(false),\n            max_image_size: max_image_size.unwrap_or(5242880),\n            capture_svg: capture_svg.unwrap_or(false),\n            infer_dimensions: infer_dimensions.unwrap_or(true),\n            max_depth,\n            exclude_selectors: exclude_selectors.unwrap_or_default(),\n            visitor,\n        }\n    }\n\n    #[pyo3(signature = (update))]\n    pub fn apply_update(&self, update: ConversionOptionsUpdate) -> Self {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        #[allow(clippy::needless_update)]\n        let mut core_self = html_to_markdown_rs::options::ConversionOptions {\n            heading_style: self.heading_style.clone().into(),\n            list_indent_type: self.list_indent_type.clone().into(),\n            list_indent_width: self.list_indent_width,\n            bullets: self.bullets.clone(),\n            strong_em_symbol: self.strong_em_symbol.chars().next().unwrap_or('*'),\n            escape_asterisks: self.escape_asterisks,\n            escape_underscores: self.escape_underscores,\n            escape_misc: self.escape_misc,\n            escape_ascii: self.escape_ascii,\n            code_language: self.code_language.clone(),\n            autolinks: self.autolinks,\n            default_title: self.default_title,\n            br_in_tables: self.br_in_tables,\n            highlight_style: self.highlight_style.clone().into(),\n            extract_metadata: self.extract_metadata,\n            whitespace_mode: self.whitespace_mode.clone().into(),\n            strip_newlines: self.strip_newlines,\n            wrap: self.wrap,\n            wrap_width: self.wrap_width,\n            convert_as_inline: self.convert_as_inline,\n            sub_symbol: self.sub_symbol.clone(),\n            sup_symbol: self.sup_symbol.clone(),\n            newline_style: self.newline_style.clone().into(),\n            code_block_style: self.code_block_style.clone().into(),\n            keep_inline_images_in: self.keep_inline_images_in.clone(),\n            preprocessing: self.preprocessing.clone().into(),\n            encoding: self.encoding.clone(),\n            debug: self.debug,\n            strip_tags: self.strip_tags.clone(),\n            preserve_tags: self.preserve_tags.clone(),\n            skip_images: self.skip_images,\n            link_style: self.link_style.clone().into(),\n            output_format: self.output_format.clone().into(),\n            include_document_structure: self.include_document_structure,\n            extract_images: self.extract_images,\n            max_image_size: self.max_image_size,\n            capture_svg: self.capture_svg,\n            infer_dimensions: self.infer_dimensions,\n            max_depth: self.max_depth,\n            exclude_selectors: self.exclude_selectors.clone(),\n            visitor: Default::default(),\n            ..Default::default()\n        };\n        core_self.apply_update(update_core);\n        core_self.into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[staticmethod]\n    #[pyo3(signature = ())]\n    pub fn default() -> ConversionOptions {\n        html_to_markdown_rs::options::ConversionOptions::default().into()\n    }\n\n    #[staticmethod]\n    #[pyo3(signature = ())]\n    pub fn builder() -> ConversionOptionsBuilder {\n        ConversionOptionsBuilder {\n            inner: Arc::new(html_to_markdown_rs::options::ConversionOptions::builder()),\n        }\n    }\n\n    #[staticmethod]\n    #[pyo3(signature = (update))]\n    pub fn from_update(update: ConversionOptionsUpdate) -> ConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::options::ConversionOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[staticmethod]\n    #[pyo3(signature = (update))]\n    pub fn from(update: ConversionOptionsUpdate) -> ConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::options::ConversionOptions::from(update_core).into()\n    }\n}\n\n#[derive(Clone)]\n#[pyclass(unsendable, from_py_object)]\npub struct ConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\n#[pymethods]\nimpl ConversionOptionsBuilder {\n    #[pyo3(signature = (tags))]\n    pub fn strip_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().strip_tags(tags)),\n        }\n    }\n\n    #[pyo3(signature = (tags))]\n    pub fn preserve_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preserve_tags(tags)),\n        }\n    }\n\n    #[pyo3(signature = (tags))]\n    pub fn keep_inline_images_in(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    #[pyo3(signature = (selectors))]\n    pub fn exclude_selectors(&self, selectors: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().exclude_selectors(selectors)),\n        }\n    }\n\n    #[pyo3(signature = (visitor=None))]\n    pub fn visitor(&self, visitor: Option<VisitorHandle>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().visitor(visitor.map(|v| (*v.inner).clone()))),\n        }\n    }\n\n    #[pyo3(signature = (preprocessing))]\n    pub fn preprocessing(&self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder {\n        let preprocessing_core: html_to_markdown_rs::PreprocessingOptions = preprocessing.into();\n        Self {\n            inner: Arc::new((*self.inner).clone().preprocessing(preprocessing_core)),\n        }\n    }\n\n    #[pyo3(signature = ())]\n    pub fn build(&self) -> ConversionOptions {\n        (*self.inner).clone().build().into()\n    }\n}\n\n#[derive(Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(unsendable, from_py_object)]\n#[allow(clippy::similar_names)]\npub struct ConversionOptionsUpdate {\n    /// Optional override for [`ConversionOptions::heading_style`].\n    #[pyo3(get)]\n    pub heading_style: Option<HeadingStyle>,\n    /// Optional override for [`ConversionOptions::list_indent_type`].\n    #[pyo3(get)]\n    pub list_indent_type: Option<ListIndentType>,\n    /// Optional override for [`ConversionOptions::list_indent_width`].\n    #[pyo3(get)]\n    pub list_indent_width: Option<usize>,\n    /// Optional override for [`ConversionOptions::bullets`].\n    #[pyo3(get)]\n    pub bullets: Option<String>,\n    /// Optional override for [`ConversionOptions::strong_em_symbol`].\n    #[pyo3(get)]\n    pub strong_em_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::escape_asterisks`].\n    #[pyo3(get)]\n    pub escape_asterisks: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_underscores`].\n    #[pyo3(get)]\n    pub escape_underscores: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_misc`].\n    #[pyo3(get)]\n    pub escape_misc: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_ascii`].\n    #[pyo3(get)]\n    pub escape_ascii: Option<bool>,\n    /// Optional override for [`ConversionOptions::code_language`].\n    #[pyo3(get)]\n    pub code_language: Option<String>,\n    /// Optional override for [`ConversionOptions::autolinks`].\n    #[pyo3(get)]\n    pub autolinks: Option<bool>,\n    /// Optional override for [`ConversionOptions::default_title`].\n    #[pyo3(get)]\n    pub default_title: Option<bool>,\n    /// Optional override for [`ConversionOptions::br_in_tables`].\n    #[pyo3(get)]\n    pub br_in_tables: Option<bool>,\n    /// Optional override for [`ConversionOptions::highlight_style`].\n    #[pyo3(get)]\n    pub highlight_style: Option<HighlightStyle>,\n    /// Optional override for [`ConversionOptions::extract_metadata`].\n    #[pyo3(get)]\n    pub extract_metadata: Option<bool>,\n    /// Optional override for [`ConversionOptions::whitespace_mode`].\n    #[pyo3(get)]\n    pub whitespace_mode: Option<WhitespaceMode>,\n    /// Optional override for [`ConversionOptions::strip_newlines`].\n    #[pyo3(get)]\n    pub strip_newlines: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap`].\n    #[pyo3(get)]\n    pub wrap: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap_width`].\n    #[pyo3(get)]\n    pub wrap_width: Option<usize>,\n    /// Optional override for [`ConversionOptions::convert_as_inline`].\n    #[pyo3(get)]\n    pub convert_as_inline: Option<bool>,\n    /// Optional override for [`ConversionOptions::sub_symbol`].\n    #[pyo3(get)]\n    pub sub_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::sup_symbol`].\n    #[pyo3(get)]\n    pub sup_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::newline_style`].\n    #[pyo3(get)]\n    pub newline_style: Option<NewlineStyle>,\n    /// Optional override for [`ConversionOptions::code_block_style`].\n    #[pyo3(get)]\n    pub code_block_style: Option<CodeBlockStyle>,\n    /// Optional override for [`ConversionOptions::keep_inline_images_in`].\n    #[pyo3(get)]\n    pub keep_inline_images_in: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preprocessing`].\n    #[pyo3(get)]\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    /// Optional override for [`ConversionOptions::encoding`].\n    #[pyo3(get)]\n    pub encoding: Option<String>,\n    /// Optional override for [`ConversionOptions::debug`].\n    #[pyo3(get)]\n    pub debug: Option<bool>,\n    /// Optional override for [`ConversionOptions::strip_tags`].\n    #[pyo3(get)]\n    pub strip_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preserve_tags`].\n    #[pyo3(get)]\n    pub preserve_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::skip_images`].\n    #[pyo3(get)]\n    pub skip_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::link_style`].\n    #[pyo3(get)]\n    pub link_style: Option<LinkStyle>,\n    /// Optional override for [`ConversionOptions::output_format`].\n    #[pyo3(get)]\n    pub output_format: Option<OutputFormat>,\n    /// Optional override for [`ConversionOptions::include_document_structure`].\n    #[pyo3(get)]\n    pub include_document_structure: Option<bool>,\n    /// Optional override for [`ConversionOptions::extract_images`].\n    #[pyo3(get)]\n    pub extract_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_image_size`].\n    #[pyo3(get)]\n    pub max_image_size: Option<u64>,\n    /// Optional override for [`ConversionOptions::capture_svg`].\n    #[pyo3(get)]\n    pub capture_svg: Option<bool>,\n    /// Optional override for [`ConversionOptions::infer_dimensions`].\n    #[pyo3(get)]\n    pub infer_dimensions: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_depth`].\n    #[pyo3(get)]\n    pub max_depth: Option<usize>,\n    /// Optional override for [`ConversionOptions::exclude_selectors`].\n    #[pyo3(get)]\n    pub exclude_selectors: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::visitor`].\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub visitor: Option<Py<PyAny>>,\n}\n\nimpl Clone for ConversionOptionsUpdate {\n    fn clone(&self) -> Self {\n        Self {\n            heading_style: self.heading_style.clone(),\n            list_indent_type: self.list_indent_type.clone(),\n            list_indent_width: self.list_indent_width.clone(),\n            bullets: self.bullets.clone(),\n            strong_em_symbol: self.strong_em_symbol.clone(),\n            escape_asterisks: self.escape_asterisks.clone(),\n            escape_underscores: self.escape_underscores.clone(),\n            escape_misc: self.escape_misc.clone(),\n            escape_ascii: self.escape_ascii.clone(),\n            code_language: self.code_language.clone(),\n            autolinks: self.autolinks.clone(),\n            default_title: self.default_title.clone(),\n            br_in_tables: self.br_in_tables.clone(),\n            highlight_style: self.highlight_style.clone(),\n            extract_metadata: self.extract_metadata.clone(),\n            whitespace_mode: self.whitespace_mode.clone(),\n            strip_newlines: self.strip_newlines.clone(),\n            wrap: self.wrap.clone(),\n            wrap_width: self.wrap_width.clone(),\n            convert_as_inline: self.convert_as_inline.clone(),\n            sub_symbol: self.sub_symbol.clone(),\n            sup_symbol: self.sup_symbol.clone(),\n            newline_style: self.newline_style.clone(),\n            code_block_style: self.code_block_style.clone(),\n            keep_inline_images_in: self.keep_inline_images_in.clone(),\n            preprocessing: self.preprocessing.clone(),\n            encoding: self.encoding.clone(),\n            debug: self.debug.clone(),\n            strip_tags: self.strip_tags.clone(),\n            preserve_tags: self.preserve_tags.clone(),\n            skip_images: self.skip_images.clone(),\n            link_style: self.link_style.clone(),\n            output_format: self.output_format.clone(),\n            include_document_structure: self.include_document_structure.clone(),\n            extract_images: self.extract_images.clone(),\n            max_image_size: self.max_image_size.clone(),\n            capture_svg: self.capture_svg.clone(),\n            infer_dimensions: self.infer_dimensions.clone(),\n            max_depth: self.max_depth.clone(),\n            exclude_selectors: self.exclude_selectors.clone(),\n            visitor: self.visitor.as_ref().map(|v| Python::attach(|py| v.clone_ref(py))),\n        }\n    }\n}\n\n#[pymethods]\nimpl ConversionOptionsUpdate {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n    #[pyo3(signature = (heading_style=None, list_indent_type=None, list_indent_width=None, bullets=None, strong_em_symbol=None, escape_asterisks=None, escape_underscores=None, escape_misc=None, escape_ascii=None, code_language=None, autolinks=None, default_title=None, br_in_tables=None, highlight_style=None, extract_metadata=None, whitespace_mode=None, strip_newlines=None, wrap=None, wrap_width=None, convert_as_inline=None, sub_symbol=None, sup_symbol=None, newline_style=None, code_block_style=None, keep_inline_images_in=None, preprocessing=None, encoding=None, debug=None, strip_tags=None, preserve_tags=None, skip_images=None, link_style=None, output_format=None, include_document_structure=None, extract_images=None, max_image_size=None, capture_svg=None, infer_dimensions=None, max_depth=None, exclude_selectors=None, visitor=None))]\n    #[new]\n    pub fn new(\n        heading_style: Option<HeadingStyle>,\n        list_indent_type: Option<ListIndentType>,\n        list_indent_width: Option<usize>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<HighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<usize>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<NewlineStyle>,\n        code_block_style: Option<CodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<PreprocessingOptionsUpdate>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<LinkStyle>,\n        output_format: Option<OutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<u64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        max_depth: Option<usize>,\n        exclude_selectors: Option<Vec<String>>,\n        visitor: Option<Py<PyAny>>,\n    ) -> Self {\n        Self {\n            heading_style,\n            list_indent_type,\n            list_indent_width,\n            bullets,\n            strong_em_symbol,\n            escape_asterisks,\n            escape_underscores,\n            escape_misc,\n            escape_ascii,\n            code_language,\n            autolinks,\n            default_title,\n            br_in_tables,\n            highlight_style,\n            extract_metadata,\n            whitespace_mode,\n            strip_newlines,\n            wrap,\n            wrap_width,\n            convert_as_inline,\n            sub_symbol,\n            sup_symbol,\n            newline_style,\n            code_block_style,\n            keep_inline_images_in,\n            preprocessing,\n            encoding,\n            debug,\n            strip_tags,\n            preserve_tags,\n            skip_images,\n            link_style,\n            output_format,\n            include_document_structure,\n            extract_images,\n            max_image_size,\n            capture_svg,\n            infer_dimensions,\n            max_depth,\n            exclude_selectors,\n            visitor,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct PreprocessingOptions {\n    /// Enable HTML preprocessing globally\n    #[pyo3(get)]\n    pub enabled: bool,\n    /// Preprocessing preset level (Minimal, Standard, Aggressive)\n    #[pyo3(get)]\n    pub preset: PreprocessingPreset,\n    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n    #[pyo3(get)]\n    pub remove_navigation: bool,\n    /// Remove form elements (forms, inputs, buttons, etc.)\n    #[pyo3(get)]\n    pub remove_forms: bool,\n}\n\n#[pymethods]\nimpl PreprocessingOptions {\n    #[must_use]\n    #[pyo3(signature = (enabled=None, preset=None, remove_navigation=None, remove_forms=None))]\n    #[new]\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled: enabled.unwrap_or(true),\n            preset: preset.unwrap_or_default(),\n            remove_navigation: remove_navigation.unwrap_or(true),\n            remove_forms: remove_forms.unwrap_or(true),\n        }\n    }\n\n    #[pyo3(signature = (update))]\n    pub fn apply_update(&self, update: PreprocessingOptionsUpdate) -> Self {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        let mut core_self = html_to_markdown_rs::options::PreprocessingOptions {\n            enabled: self.enabled,\n            preset: self.preset.clone().into(),\n            remove_navigation: self.remove_navigation,\n            remove_forms: self.remove_forms,\n        };\n        core_self.apply_update(update_core);\n        core_self.into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[staticmethod]\n    #[pyo3(signature = ())]\n    pub fn default() -> PreprocessingOptions {\n        html_to_markdown_rs::options::PreprocessingOptions::default().into()\n    }\n\n    #[staticmethod]\n    #[pyo3(signature = (update))]\n    pub fn from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::options::PreprocessingOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[staticmethod]\n    #[pyo3(signature = (update))]\n    pub fn from(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::options::PreprocessingOptions::from(update_core).into()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct PreprocessingOptionsUpdate {\n    /// Optional global preprocessing enablement override\n    #[pyo3(get)]\n    pub enabled: Option<bool>,\n    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n    #[pyo3(get)]\n    pub preset: Option<PreprocessingPreset>,\n    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n    #[pyo3(get)]\n    pub remove_navigation: Option<bool>,\n    /// Optional form element removal override (forms, inputs, buttons, etc.)\n    #[pyo3(get)]\n    pub remove_forms: Option<bool>,\n}\n\n#[pymethods]\nimpl PreprocessingOptionsUpdate {\n    #[must_use]\n    #[pyo3(signature = (enabled=None, preset=None, remove_navigation=None, remove_forms=None))]\n    #[new]\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled,\n            preset,\n            remove_navigation,\n            remove_forms,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct DocumentStructure {\n    /// All nodes in document reading order.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub nodes: Vec<DocumentNode>,\n    /// The source format (always \"html\" for this crate).\n    #[pyo3(get)]\n    pub source_format: Option<String>,\n}\n\n#[pymethods]\nimpl DocumentStructure {\n    #[must_use]\n    #[pyo3(signature = (nodes, source_format=None))]\n    #[new]\n    pub fn new(nodes: Vec<DocumentNode>, source_format: Option<String>) -> Self {\n        Self { nodes, source_format }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct DocumentNode {\n    /// Deterministic node identifier.\n    #[pyo3(get)]\n    pub id: String,\n    /// The semantic content of this node.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub content: NodeContent,\n    /// Index of the parent node (None for root nodes).\n    #[pyo3(get)]\n    pub parent: Option<u32>,\n    /// Indices of child nodes in reading order.\n    #[pyo3(get)]\n    pub children: Vec<u32>,\n    /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub annotations: Vec<TextAnnotation>,\n    /// Format-specific attributes (e.g. class, id, data-* attributes).\n    #[pyo3(get)]\n    pub attributes: Option<HashMap<String, String>>,\n}\n\n#[pymethods]\nimpl DocumentNode {\n    #[must_use]\n    #[pyo3(signature = (id, content, children, annotations, parent=None, attributes=None))]\n    #[new]\n    pub fn new(\n        id: String,\n        content: NodeContent,\n        children: Vec<u32>,\n        annotations: Vec<TextAnnotation>,\n        parent: Option<u32>,\n        attributes: Option<HashMap<String, String>>,\n    ) -> Self {\n        Self {\n            id,\n            content,\n            parent,\n            children,\n            annotations,\n            attributes,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct TextAnnotation {\n    /// Start byte offset (inclusive) into the parent node's text.\n    #[pyo3(get)]\n    pub start: u32,\n    /// End byte offset (exclusive) into the parent node's text.\n    #[pyo3(get)]\n    pub end: u32,\n    /// The type of annotation.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub kind: AnnotationKind,\n}\n\n#[pymethods]\nimpl TextAnnotation {\n    #[must_use]\n    #[pyo3(signature = (start, end, kind))]\n    #[new]\n    pub fn new(start: u32, end: u32, kind: AnnotationKind) -> Self {\n        Self { start, end, kind }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct ConversionResult {\n    /// Converted text output (markdown, djot, or plain text).\n    ///\n    /// `None` when `output_format` is set to `OutputFormat::None`,\n    /// indicating extraction-only mode.\n    #[pyo3(get)]\n    pub content: Option<String>,\n    /// Structured document tree with semantic elements.\n    ///\n    /// Populated when `include_document_structure` is `true` in options.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub document: Option<DocumentStructure>,\n    /// Extracted HTML metadata (title, OG, links, images, structured data).\n    #[pyo3(get)]\n    pub metadata: HtmlMetadata,\n    /// Extracted tables with structured cell data and markdown representation.\n    #[pyo3(get)]\n    pub tables: Vec<TableData>,\n    /// Extracted inline images (data URIs and SVGs).\n    ///\n    /// Populated when `extract_images` is `true` in options.\n    #[pyo3(get)]\n    #[serde(skip)]\n    pub images: Vec<String>,\n    /// Non-fatal processing warnings.\n    #[pyo3(get)]\n    pub warnings: Vec<ProcessingWarning>,\n}\n\n#[pymethods]\nimpl ConversionResult {\n    #[must_use]\n    #[pyo3(signature = (metadata=None, tables=None, images=None, warnings=None, content=None, document=None))]\n    #[new]\n    pub fn new(\n        metadata: Option<HtmlMetadata>,\n        tables: Option<Vec<TableData>>,\n        images: Option<Vec<String>>,\n        warnings: Option<Vec<ProcessingWarning>>,\n        content: Option<String>,\n        document: Option<DocumentStructure>,\n    ) -> Self {\n        Self {\n            content,\n            document,\n            metadata: metadata.unwrap_or_default(),\n            tables: tables.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            warnings: warnings.unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\n#[allow(clippy::similar_names)]\npub struct TableGrid {\n    /// Number of rows.\n    #[pyo3(get)]\n    pub rows: u32,\n    /// Number of columns.\n    #[pyo3(get)]\n    pub cols: u32,\n    /// All cells in the table (may be fewer than rows*cols due to spans).\n    #[pyo3(get)]\n    pub cells: Vec<GridCell>,\n}\n\n#[pymethods]\nimpl TableGrid {\n    #[must_use]\n    #[pyo3(signature = (rows=None, cols=None, cells=None))]\n    #[new]\n    pub fn new(rows: Option<u32>, cols: Option<u32>, cells: Option<Vec<GridCell>>) -> Self {\n        Self {\n            rows: rows.unwrap_or_default(),\n            cols: cols.unwrap_or_default(),\n            cells: cells.unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\n#[allow(clippy::similar_names)]\npub struct GridCell {\n    /// The text content of the cell.\n    #[pyo3(get)]\n    pub content: String,\n    /// 0-indexed row position.\n    #[pyo3(get)]\n    pub row: u32,\n    /// 0-indexed column position.\n    #[pyo3(get)]\n    pub col: u32,\n    /// Number of rows this cell spans (default 1).\n    #[pyo3(get)]\n    pub row_span: u32,\n    /// Number of columns this cell spans (default 1).\n    #[pyo3(get)]\n    pub col_span: u32,\n    /// Whether this is a header cell (`<th>`).\n    #[pyo3(get)]\n    pub is_header: bool,\n}\n\n#[pymethods]\nimpl GridCell {\n    #[must_use]\n    #[pyo3(signature = (content, row, col, row_span, col_span, is_header))]\n    #[new]\n    pub fn new(content: String, row: u32, col: u32, row_span: u32, col_span: u32, is_header: bool) -> Self {\n        Self {\n            content,\n            row,\n            col,\n            row_span,\n            col_span,\n            is_header,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct TableData {\n    /// The structured table grid.\n    #[pyo3(get)]\n    pub grid: TableGrid,\n    /// The markdown rendering of this table.\n    #[pyo3(get)]\n    pub markdown: String,\n}\n\n#[pymethods]\nimpl TableData {\n    #[must_use]\n    #[pyo3(signature = (grid, markdown))]\n    #[new]\n    pub fn new(grid: TableGrid, markdown: String) -> Self {\n        Self { grid, markdown }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct ProcessingWarning {\n    /// Human-readable warning message.\n    #[pyo3(get)]\n    pub message: String,\n    /// The category of warning.\n    #[pyo3(get)]\n    pub kind: WarningKind,\n}\n\n#[pymethods]\nimpl ProcessingWarning {\n    #[must_use]\n    #[pyo3(signature = (message, kind))]\n    #[new]\n    pub fn new(message: String, kind: WarningKind) -> Self {\n        Self { message, kind }\n    }\n}\n\n#[derive(Clone)]\n#[pyclass(unsendable, from_py_object)]\npub struct VisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(frozen, from_py_object)]\npub struct NodeContext {\n    /// Coarse-grained node type classification\n    #[pyo3(get)]\n    pub node_type: NodeType,\n    /// Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n    #[pyo3(get)]\n    pub tag_name: String,\n    /// All HTML attributes as key-value pairs\n    #[pyo3(get)]\n    pub attributes: HashMap<String, String>,\n    /// Depth in the DOM tree (0 = root)\n    #[pyo3(get)]\n    pub depth: usize,\n    /// Index among siblings (0-based)\n    #[pyo3(get)]\n    pub index_in_parent: usize,\n    /// Parent element's tag name (None if root)\n    #[pyo3(get)]\n    pub parent_tag: Option<String>,\n    /// Whether this element is treated as inline vs block\n    #[pyo3(get)]\n    pub is_inline: bool,\n}\n\n#[pymethods]\nimpl NodeContext {\n    #[must_use]\n    #[pyo3(signature = (node_type, tag_name, attributes, depth, index_in_parent, is_inline, parent_tag=None))]\n    #[new]\n    pub fn new(\n        node_type: NodeType,\n        tag_name: String,\n        attributes: HashMap<String, String>,\n        depth: usize,\n        index_in_parent: usize,\n        is_inline: bool,\n        parent_tag: Option<String>,\n    ) -> Self {\n        Self {\n            node_type,\n            tag_name,\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline,\n        }\n    }\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum TextDirection {\n    #[default]\n    LeftToRight = 0,\n    RightToLeft = 1,\n    Auto = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum LinkType {\n    #[default]\n    Anchor = 0,\n    Internal = 1,\n    External = 2,\n    Email = 3,\n    Phone = 4,\n    Other = 5,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum ImageType {\n    #[default]\n    DataUri = 0,\n    InlineSvg = 1,\n    External = 2,\n    Relative = 3,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum StructuredDataType {\n    #[default]\n    JsonLd = 0,\n    Microdata = 1,\n    RDFa = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum PreprocessingPreset {\n    #[default]\n    Minimal = 0,\n    Standard = 1,\n    Aggressive = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum HeadingStyle {\n    #[default]\n    Underlined = 0,\n    Atx = 1,\n    AtxClosed = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum ListIndentType {\n    #[default]\n    Spaces = 0,\n    Tabs = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum WhitespaceMode {\n    #[default]\n    Normalized = 0,\n    Strict = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum NewlineStyle {\n    #[default]\n    Spaces = 0,\n    Backslash = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum CodeBlockStyle {\n    #[default]\n    Indented = 0,\n    Backticks = 1,\n    Tildes = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum HighlightStyle {\n    #[default]\n    DoubleEqual = 0,\n    Html = 1,\n    Bold = 2,\n    #[pyo3(name = \"None_\")]\n    None = 3,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum LinkStyle {\n    #[default]\n    Inline = 0,\n    Reference = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum OutputFormat {\n    #[default]\n    Markdown = 0,\n    Djot = 1,\n    Plain = 2,\n}\n\n#[derive(Clone)]\n#[pyclass(frozen)]\npub struct NodeContent {\n    pub(crate) inner: html_to_markdown_rs::NodeContent,\n}\n\n#[pymethods]\nimpl NodeContent {}\n\nimpl From<NodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: NodeContent) -> Self {\n        val.inner\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for NodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        Self { inner: val }\n    }\n}\n\nimpl serde::Serialize for NodeContent {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.inner.serialize(serializer)\n    }\n}\n\nimpl Default for NodeContent {\n    fn default() -> Self {\n        Self {\n            inner: Default::default(),\n        }\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for NodeContent {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        let inner = html_to_markdown_rs::NodeContent::deserialize(deserializer)?;\n        Ok(Self { inner })\n    }\n}\n\n#[derive(Clone)]\n#[pyclass(frozen)]\npub struct AnnotationKind {\n    pub(crate) inner: html_to_markdown_rs::AnnotationKind,\n}\n\n#[pymethods]\nimpl AnnotationKind {\n    #[new]\n    fn new(py: Python<'_>, value: &Bound<'_, pyo3::types::PyDict>) -> PyResult<Self> {\n        let json_mod = py.import(\"json\")?;\n        let json_str: String = json_mod.call_method1(\"dumps\", (value,))?.extract()?;\n        let inner: html_to_markdown_rs::AnnotationKind = serde_json::from_str(&json_str)\n            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!(\"Invalid AnnotationKind: {e}\")))?;\n        Ok(Self { inner })\n    }\n}\n\nimpl From<AnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: AnnotationKind) -> Self {\n        val.inner\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for AnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        Self { inner: val }\n    }\n}\n\nimpl serde::Serialize for AnnotationKind {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.inner.serialize(serializer)\n    }\n}\n\nimpl Default for AnnotationKind {\n    fn default() -> Self {\n        Self {\n            inner: Default::default(),\n        }\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for AnnotationKind {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        let inner = html_to_markdown_rs::AnnotationKind::deserialize(deserializer)?;\n        Ok(Self { inner })\n    }\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum WarningKind {\n    #[default]\n    ImageExtractionFailed = 0,\n    EncodingFallback = 1,\n    TruncatedInput = 2,\n    MalformedHtml = 3,\n    SanitizationApplied = 4,\n    DepthLimitExceeded = 5,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\n#[pyclass(eq, eq_int, from_py_object)]\npub enum NodeType {\n    #[default]\n    Text = 0,\n    Element = 1,\n    Heading = 2,\n    Paragraph = 3,\n    Div = 4,\n    Blockquote = 5,\n    Pre = 6,\n    Hr = 7,\n    List = 8,\n    ListItem = 9,\n    DefinitionList = 10,\n    DefinitionTerm = 11,\n    DefinitionDescription = 12,\n    Table = 13,\n    TableRow = 14,\n    TableCell = 15,\n    TableHeader = 16,\n    TableBody = 17,\n    TableHead = 18,\n    TableFoot = 19,\n    Link = 20,\n    Image = 21,\n    Strong = 22,\n    Em = 23,\n    Code = 24,\n    Strikethrough = 25,\n    Underline = 26,\n    Subscript = 27,\n    Superscript = 28,\n    Mark = 29,\n    Small = 30,\n    Br = 31,\n    Span = 32,\n    Article = 33,\n    Section = 34,\n    Nav = 35,\n    Aside = 36,\n    Header = 37,\n    Footer = 38,\n    Main = 39,\n    Figure = 40,\n    Figcaption = 41,\n    Time = 42,\n    Details = 43,\n    Summary = 44,\n    Form = 45,\n    Input = 46,\n    Select = 47,\n    Option = 48,\n    Button = 49,\n    Textarea = 50,\n    Label = 51,\n    Fieldset = 52,\n    Legend = 53,\n    Audio = 54,\n    Video = 55,\n    Picture = 56,\n    Source = 57,\n    Iframe = 58,\n    Svg = 59,\n    Canvas = 60,\n    Ruby = 61,\n    Rt = 62,\n    Rp = 63,\n    Abbr = 64,\n    Kbd = 65,\n    Samp = 66,\n    Var = 67,\n    Cite = 68,\n    Q = 69,\n    Del = 70,\n    Ins = 71,\n    Data = 72,\n    Meter = 73,\n    Progress = 74,\n    Output = 75,\n    Template = 76,\n    Slot = 77,\n    Html = 78,\n    Head = 79,\n    Body = 80,\n    Title = 81,\n    Meta = 82,\n    LinkTag = 83,\n    Style = 84,\n    Script = 85,\n    Base = 86,\n    Custom = 87,\n}\n\n#[derive(Clone)]\n#[pyclass(frozen)]\npub struct VisitResult {\n    pub(crate) inner: html_to_markdown_rs::VisitResult,\n}\n\n#[pymethods]\nimpl VisitResult {\n    #[new]\n    fn new(py: Python<'_>, value: &Bound<'_, pyo3::types::PyDict>) -> PyResult<Self> {\n        let json_mod = py.import(\"json\")?;\n        let json_str: String = json_mod.call_method1(\"dumps\", (value,))?.extract()?;\n        let inner: html_to_markdown_rs::VisitResult = serde_json::from_str(&json_str)\n            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!(\"Invalid VisitResult: {e}\")))?;\n        Ok(Self { inner })\n    }\n}\n\nimpl From<VisitResult> for html_to_markdown_rs::VisitResult {\n    fn from(val: VisitResult) -> Self {\n        val.inner\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for VisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        Self { inner: val }\n    }\n}\n\nimpl serde::Serialize for VisitResult {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        self.inner.serialize(serializer)\n    }\n}\n\nimpl Default for VisitResult {\n    fn default() -> Self {\n        Self {\n            inner: Default::default(),\n        }\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for VisitResult {\n    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {\n        let inner = html_to_markdown_rs::VisitResult::deserialize(deserializer)?;\n        Ok(Self { inner })\n    }\n}\n\n#[allow(clippy::missing_errors_doc)]\n#[pyfunction]\n#[pyo3(signature = (html, options=None))]\npub fn convert(html: String, options: Option<ConversionOptions>) -> PyResult<ConversionResult> {\n    // Extract visitor from binding options before serde round-trip (visitor is #[serde(skip)]).\n    // Use clone_ref rather than .clone() — Py<PyAny> requires the `py-clone` feature for\n    // Clone, but clone_ref(py) increments the refcount without that feature.\n    let __visitor = options\n        .as_ref()\n        .and_then(|o| o.visitor.as_ref().map(|v| Python::attach(|py| v.clone_ref(py))));\n    let __options_core: Option<html_to_markdown_rs::ConversionOptions> = options\n        .map(|v| {\n            let json =\n                serde_json::to_string(&v).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;\n            serde_json::from_str::<html_to_markdown_rs::ConversionOptions>(&json)\n                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))\n        })\n        .transpose()?;\n    let options_core = __options_core.map(|mut opts| {\n        if let Some(v) = __visitor {\n            let bridge = PyHtmlVisitorBridge::new(v);\n            opts.visitor =\n                Some(std::rc::Rc::new(std::cell::RefCell::new(bridge)) as html_to_markdown_rs::visitor::VisitorHandle);\n        }\n        opts\n    });\n    html_to_markdown_rs::convert(&html, options_core)\n        .map(|val| val.into())\n        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))\n}\n\nfn nodecontext_to_py_dict<'py>(\n    py: Python<'py>,\n    ctx: &html_to_markdown_rs::visitor::NodeContext,\n) -> pyo3::Bound<'py, pyo3::types::PyDict> {\n    let d = pyo3::types::PyDict::new(py);\n    d.set_item(\"node_type\", format!(\"{:?}\", ctx.node_type)).unwrap_or(());\n    d.set_item(\"tag_name\", &ctx.tag_name).unwrap_or(());\n    d.set_item(\"depth\", ctx.depth).unwrap_or(());\n    d.set_item(\"index_in_parent\", ctx.index_in_parent).unwrap_or(());\n    d.set_item(\"is_inline\", ctx.is_inline).unwrap_or(());\n    d.set_item(\"parent_tag\", ctx.parent_tag.as_deref()).unwrap_or(());\n    let attrs = pyo3::types::PyDict::new(py);\n    for (k, v) in &ctx.attributes {\n        attrs.set_item(k, v).unwrap_or(());\n    }\n    d.set_item(\"attributes\", attrs).unwrap_or(());\n    d\n}\n\n#[derive(Debug)]\npub struct PyHtmlVisitorBridge {\n    python_obj: Py<PyAny>,\n}\n\nimpl PyHtmlVisitorBridge {\n    pub fn new(python_obj: Py<PyAny>) -> Self {\n        Self { python_obj }\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for PyHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_element_start\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_element_start\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_element_end\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_element_end\", (nodecontext_to_py_dict(py, _ctx), _output)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_text\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_text\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_link\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_link\", (nodecontext_to_py_dict(py, _ctx), _href, _text, _title)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_image\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_image\", (nodecontext_to_py_dict(py, _ctx), _src, _alt, _title)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_heading\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_heading\", (nodecontext_to_py_dict(py, _ctx), _level, _text, _id)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_code_block\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_code_block\", (nodecontext_to_py_dict(py, _ctx), _lang, _code)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_code_inline\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_code_inline\", (nodecontext_to_py_dict(py, _ctx), _code)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_list_item\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\n                \"visit_list_item\",\n                (nodecontext_to_py_dict(py, _ctx), _ordered, _marker, _text),\n            ) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_list_start\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_list_start\", (nodecontext_to_py_dict(py, _ctx), _ordered)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_list_end\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_list_end\", (nodecontext_to_py_dict(py, _ctx), _ordered, _output)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_table_start\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_table_start\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_table_row\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\n                \"visit_table_row\",\n                (nodecontext_to_py_dict(py, _ctx), _cells, _is_header),\n            ) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_table_end\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_table_end\", (nodecontext_to_py_dict(py, _ctx), _output)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_blockquote\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_blockquote\", (nodecontext_to_py_dict(py, _ctx), _content, _depth)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_strong\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_strong\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_emphasis\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_emphasis\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_strikethrough\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_strikethrough\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_underline\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_underline\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_subscript\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_subscript\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_superscript\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_superscript\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_mark\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_mark\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_line_break\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_line_break\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_horizontal_rule\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_horizontal_rule\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_custom_element\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\n                \"visit_custom_element\",\n                (nodecontext_to_py_dict(py, _ctx), _tag_name, _html),\n            ) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_definition_list_start\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_definition_list_start\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_definition_term\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_definition_term\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_definition_description\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\n                \"visit_definition_description\",\n                (nodecontext_to_py_dict(py, _ctx), _text),\n            ) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_definition_list_end\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_definition_list_end\", (nodecontext_to_py_dict(py, _ctx), _output)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_form\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_form\", (nodecontext_to_py_dict(py, _ctx), _action, _method)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_input\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\n                \"visit_input\",\n                (nodecontext_to_py_dict(py, _ctx), _input_type, _name, _value),\n            ) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_button\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_button\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_audio\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_audio\", (nodecontext_to_py_dict(py, _ctx), _src)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_video\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_video\", (nodecontext_to_py_dict(py, _ctx), _src)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_iframe\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_iframe\", (nodecontext_to_py_dict(py, _ctx), _src)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_details\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_details\", (nodecontext_to_py_dict(py, _ctx), _open)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_summary\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_summary\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_figure_start\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_figure_start\", (nodecontext_to_py_dict(py, _ctx),)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_figcaption\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_figcaption\", (nodecontext_to_py_dict(py, _ctx), _text)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        Python::attach(|py| {\n            let obj = self.python_obj.bind(py);\n            if !obj.hasattr(\"visit_figure_end\").unwrap_or(false) {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            match obj.call_method1(\"visit_figure_end\", (nodecontext_to_py_dict(py, _ctx), _output)) {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(result) => {\n                    if let Ok(s) = result.extract::<String>() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else if result.is_none() {\n                        html_to_markdown_rs::VisitResult::Continue\n                    } else {\n                        let py_dict = result.downcast::<pyo3::types::PyDict>();\n                        if let Ok(d) = py_dict {\n                            if let Some(v) = d.get_item(\"custom\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Custom(v.extract::<String>().unwrap_or_default())\n                            } else if let Some(v) = d.get_item(\"error\").ok().flatten() {\n                                html_to_markdown_rs::VisitResult::Error(v.extract::<String>().unwrap_or_default())\n                            } else {\n                                html_to_markdown_rs::VisitResult::Continue\n                            }\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    }\n                }\n            }\n        })\n    }\n}\n\n// Error types\npyo3::create_exception!(_html_to_markdown, ParseError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, SanitizationError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, ConfigError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, IoError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, PanicError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, InvalidInputError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, OtherError, pyo3::exceptions::PyException);\npyo3::create_exception!(_html_to_markdown, ConversionError, pyo3::exceptions::PyException);\n\n/// Convert a `html_to_markdown_rs::ConversionError` error to a Python exception.\nfn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {\n    let msg = e.to_string();\n    #[allow(unreachable_patterns)]\n    match &e {\n        html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),\n        html_to_markdown_rs::ConversionError::SanitizationError(..) => SanitizationError::new_err(msg),\n        html_to_markdown_rs::ConversionError::ConfigError(..) => ConfigError::new_err(msg),\n        html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),\n        html_to_markdown_rs::ConversionError::Panic(..) => PanicError::new_err(msg),\n        html_to_markdown_rs::ConversionError::InvalidInput(..) => InvalidInputError::new_err(msg),\n        html_to_markdown_rs::ConversionError::Other(..) => OtherError::new_err(msg),\n        _ => ConversionError::new_err(msg),\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for DocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for HeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<LinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for LinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for ImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<StructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for StructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for HtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.chars().next().unwrap_or('*'),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for ConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for ConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten(),\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for PreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for PreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for DocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for DocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for TextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: Default::default(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for ConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for TableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<GridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for GridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableData> for html_to_markdown_rs::TableData {\n    fn from(val: TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for TableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for ProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for NodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: val.attributes.into_iter().collect(),\n            depth: val.depth,\n            index_in_parent: val.index_in_parent,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<TextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: TextDirection) -> Self {\n        match val {\n            TextDirection::LeftToRight => Self::LeftToRight,\n            TextDirection::RightToLeft => Self::RightToLeft,\n            TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for TextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<LinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: LinkType) -> Self {\n        match val {\n            LinkType::Anchor => Self::Anchor,\n            LinkType::Internal => Self::Internal,\n            LinkType::External => Self::External,\n            LinkType::Email => Self::Email,\n            LinkType::Phone => Self::Phone,\n            LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for LinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<ImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: ImageType) -> Self {\n        match val {\n            ImageType::DataUri => Self::DataUri,\n            ImageType::InlineSvg => Self::InlineSvg,\n            ImageType::External => Self::External,\n            ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for ImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<StructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: StructuredDataType) -> Self {\n        match val {\n            StructuredDataType::JsonLd => Self::JsonLd,\n            StructuredDataType::Microdata => Self::Microdata,\n            StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for StructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<PreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: PreprocessingPreset) -> Self {\n        match val {\n            PreprocessingPreset::Minimal => Self::Minimal,\n            PreprocessingPreset::Standard => Self::Standard,\n            PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for PreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<HeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: HeadingStyle) -> Self {\n        match val {\n            HeadingStyle::Underlined => Self::Underlined,\n            HeadingStyle::Atx => Self::Atx,\n            HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for HeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<ListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: ListIndentType) -> Self {\n        match val {\n            ListIndentType::Spaces => Self::Spaces,\n            ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for ListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<WhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: WhitespaceMode) -> Self {\n        match val {\n            WhitespaceMode::Normalized => Self::Normalized,\n            WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for WhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<NewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: NewlineStyle) -> Self {\n        match val {\n            NewlineStyle::Spaces => Self::Spaces,\n            NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for NewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<CodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: CodeBlockStyle) -> Self {\n        match val {\n            CodeBlockStyle::Indented => Self::Indented,\n            CodeBlockStyle::Backticks => Self::Backticks,\n            CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for CodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<HighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: HighlightStyle) -> Self {\n        match val {\n            HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            HighlightStyle::Html => Self::Html,\n            HighlightStyle::Bold => Self::Bold,\n            HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for HighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<LinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: LinkStyle) -> Self {\n        match val {\n            LinkStyle::Inline => Self::Inline,\n            LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for LinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<OutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: OutputFormat) -> Self {\n        match val {\n            OutputFormat::Markdown => Self::Markdown,\n            OutputFormat::Djot => Self::Djot,\n            OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for OutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<WarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: WarningKind) -> Self {\n        match val {\n            WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            WarningKind::EncodingFallback => Self::EncodingFallback,\n            WarningKind::TruncatedInput => Self::TruncatedInput,\n            WarningKind::MalformedHtml => Self::MalformedHtml,\n            WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for WarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for NodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\n#[pymodule]\npub fn _html_to_markdown(m: &Bound<'_, PyModule>) -> PyResult<()> {\n    m.add_class::<DocumentMetadata>()?;\n    m.add_class::<HeaderMetadata>()?;\n    m.add_class::<LinkMetadata>()?;\n    m.add_class::<ImageMetadata>()?;\n    m.add_class::<StructuredData>()?;\n    m.add_class::<HtmlMetadata>()?;\n    m.add_class::<ConversionOptions>()?;\n    m.add_class::<ConversionOptionsBuilder>()?;\n    m.add_class::<ConversionOptionsUpdate>()?;\n    m.add_class::<PreprocessingOptions>()?;\n    m.add_class::<PreprocessingOptionsUpdate>()?;\n    m.add_class::<DocumentStructure>()?;\n    m.add_class::<DocumentNode>()?;\n    m.add_class::<TextAnnotation>()?;\n    m.add_class::<ConversionResult>()?;\n    m.add_class::<TableGrid>()?;\n    m.add_class::<GridCell>()?;\n    m.add_class::<TableData>()?;\n    m.add_class::<ProcessingWarning>()?;\n    m.add_class::<VisitorHandle>()?;\n    m.add_class::<NodeContext>()?;\n    m.add_class::<TextDirection>()?;\n    m.add_class::<LinkType>()?;\n    m.add_class::<ImageType>()?;\n    m.add_class::<StructuredDataType>()?;\n    m.add_class::<PreprocessingPreset>()?;\n    m.add_class::<HeadingStyle>()?;\n    m.add_class::<ListIndentType>()?;\n    m.add_class::<WhitespaceMode>()?;\n    m.add_class::<NewlineStyle>()?;\n    m.add_class::<CodeBlockStyle>()?;\n    m.add_class::<HighlightStyle>()?;\n    m.add_class::<LinkStyle>()?;\n    m.add_class::<OutputFormat>()?;\n    m.add_class::<NodeContent>()?;\n    m.add_class::<AnnotationKind>()?;\n    m.add_class::<WarningKind>()?;\n    m.add_class::<NodeType>()?;\n    m.add_class::<VisitResult>()?;\n    m.add_function(wrap_pyfunction!(convert, m)?)?;\n    m.add(\"ParseError\", m.py().get_type::<ParseError>())?;\n    m.add(\"SanitizationError\", m.py().get_type::<SanitizationError>())?;\n    m.add(\"ConfigError\", m.py().get_type::<ConfigError>())?;\n    m.add(\"IoError\", m.py().get_type::<IoError>())?;\n    m.add(\"PanicError\", m.py().get_type::<PanicError>())?;\n    m.add(\"InvalidInputError\", m.py().get_type::<InvalidInputError>())?;\n    m.add(\"OtherError\", m.py().get_type::<OtherError>())?;\n    m.add(\"ConversionError\", m.py().get_type::<ConversionError>())?;\n    Ok(())\n}\n"
  },
  {
    "path": "crates/html-to-markdown-rs-ffi/README.md",
    "content": "# html-to-markdown-rs - FFI (C/C++) Bindings\n\nHigh-performance HTML to Markdown converter\n\n## Installation\n\nLink against `libhtml-to-markdown-rs_ffi` and include `html_to_markdown.h`.\n\nSee the build instructions in the main repository.\n\n## Quick Start\n\n```c\n#include \"html_to_markdown.h\"\n\nint main(void) {\n    // See https://github.com/kreuzberg-dev/html-to-markdown for usage examples.\n    return 0;\n}\n```\n\n## Documentation\n\nFor full documentation, see the [html-to-markdown-rs repository](https://github.com/kreuzberg-dev/html-to-markdown).\n\n## License\n\nSee the [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE) file in the root repository.\n"
  },
  {
    "path": "crates/html-to-markdown-rs-wasm/README.md",
    "content": "# html-to-markdown-rs - WebAssembly Bindings\n\nHigh-performance HTML to Markdown converter\n\n## Installation\n\n```bash\nnpm install html-to-markdown-rs-wasm\n```\n\n## Quick Start\n\n```javascript\nimport init from 'html-to-markdown-rs-wasm';\n\nawait init();\n// See https://github.com/kreuzberg-dev/html-to-markdown for usage examples.\n```\n\n## Documentation\n\nFor full documentation, see the [html-to-markdown-rs repository](https://github.com/kreuzberg-dev/html-to-markdown).\n\n## License\n\nSee the [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE) file in the root repository.\n"
  },
  {
    "path": "crates/html-to-markdown-wasm/Cargo.toml",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:b7c10ab5e992cb7d5fb57a9460812630b8b7063766ece8f0317e3157704fbdaa\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\n[package]\nname = \"html-to-markdown-wasm\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nkeywords = [\"html\", \"markdown\", \"converter\"]\n\n[package.metadata.cargo-machete]\nignored = [\"futures-util\", \"js-sys\", \"wasm-bindgen-futures\", \"serde_json\"]\n\n[package.metadata.wasm-pack.profile.release]\nwasm-opt = false\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nfutures-util = \"0.3\"\nhtml-to-markdown-rs = { path = \"../html-to-markdown\", default-features = false, features = [\n    \"full\",\n    \"metadata\",\n    \"visitor\",\n    \"serde\",\n    \"inline-images\",\n] }\njs-sys = \"0.3\"\nserde-wasm-bindgen = \"0.6\"\nserde_json = \"1\"\nwasm-bindgen = \"0.2\"\nwasm-bindgen-futures = \"0.4\"\n\n[target.'cfg(target_arch = \"wasm32\")'.dependencies]\ngetrandom = { version = \"0.3\", features = [\"wasm_js\"] }\n"
  },
  {
    "path": "crates/html-to-markdown-wasm/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-wasm\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter - WebAssembly bindings\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\",\n    \"rust\",\n    \"wasm\",\n    \"webassembly\"\n  ],\n  \"homepage\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Na'aman Hirschfeld <naaman@kreuzberg.dev>\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"dist\",\n    \"dist-node\",\n    \"dist-web\",\n    \"README.md\"\n  ],\n  \"main\": \"dist/html_to_markdown_wasm.js\",\n  \"types\": \"dist/html_to_markdown_wasm.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/html_to_markdown_wasm.js\",\n      \"types\": \"./dist/html_to_markdown_wasm.d.ts\",\n      \"default\": \"./dist/html_to_markdown_wasm.js\"\n    },\n    \"./dist-node\": {\n      \"import\": \"./dist-node/html_to_markdown_wasm.js\",\n      \"require\": \"./dist-node/html_to_markdown_wasm.js\",\n      \"types\": \"./dist-node/html_to_markdown_wasm.d.ts\"\n    },\n    \"./dist-node/*\": \"./dist-node/*\",\n    \"./dist-web\": {\n      \"import\": \"./dist-web/html_to_markdown_wasm.js\",\n      \"types\": \"./dist-web/html_to_markdown_wasm.d.ts\"\n    },\n    \"./dist-web/*\": \"./dist-web/*\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"scripts\": {\n    \"build\": \"wasm-pack build --target bundler --out-dir dist && node ./scripts/patch-bundler-entry.js\",\n    \"build:nodejs\": \"wasm-pack build --target nodejs --out-dir dist-node && node ./scripts/patch-bundler-entry.js dist-node --types-only\",\n    \"build:web\": \"wasm-pack build --target web --out-dir dist-web && node ./scripts/patch-bundler-entry.js dist-web --types-only\",\n    \"build:all\": \"pnpm run build && pnpm run build:nodejs && pnpm run build:web && pnpm run cleanup:gitignore\",\n    \"cleanup:gitignore\": \"node ./scripts/cleanup-gitignore.js\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:wasm-pack\": \"wasm-pack test --headless --chrome\",\n    \"clean\": \"rm -rf dist dist-node dist-web node_modules pkg\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^25.6.0\",\n    \"tsx\": \"^4.21.0\",\n    \"vitest\": \"^4.1.5\",\n    \"wasm-pack\": \"^0.14.0\"\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-wasm/scripts/cleanup-gitignore.js",
    "content": "#!/usr/bin/env node\nconst { unlinkSync } = require(\"node:fs\");\nconst { join, dirname } = require(\"node:path\");\n\nconst baseDir = dirname(__filename);\nconst targets = [\"dist\", \"dist-node\", \"dist-web\"];\n\nfor (const dir of targets) {\n  const gitignorePath = join(baseDir, \"..\", dir, \".gitignore\");\n  try {\n    unlinkSync(gitignorePath);\n  } catch (error) {\n    if (error.code !== \"ENOENT\") {\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "crates/html-to-markdown-wasm/scripts/patch-bundler-entry.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Post-process the bundler entry emitted by wasm-pack so we can support runtimes\n * that instantiate WebAssembly modules asynchronously (Cloudflare Workers, esbuild, etc).\n */\n\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\n\nconst rootDir = path.resolve(__dirname, \"..\");\nconst args = process.argv.slice(2);\nlet distArg = args.find((arg) => !arg.startsWith(\"--\"));\ndistArg = distArg || \"dist\";\nconst flags = new Set(args.filter((arg) => arg.startsWith(\"--\")));\nconst typesOnly = flags.has(\"--types-only\");\n\nconst distDir = path.resolve(rootDir, distArg);\nconst entryPath = path.join(distDir, \"html_to_markdown_wasm.js\");\nconst dtsPath = path.join(distDir, \"html_to_markdown_wasm.d.ts\");\nconst bgPath = path.join(distDir, \"html_to_markdown_wasm_bg.js\");\n\nconst typeDefinitions = `\nexport type WasmHeadingStyle = \"underlined\" | \"atx\" | \"atxClosed\";\nexport type WasmListIndentType = \"spaces\" | \"tabs\";\nexport type WasmWhitespaceMode = \"normalized\" | \"strict\";\nexport type WasmNewlineStyle = \"spaces\" | \"backslash\";\nexport type WasmCodeBlockStyle = \"indented\" | \"backticks\" | \"tildes\";\nexport type WasmHighlightStyle = \"doubleEqual\" | \"html\" | \"bold\" | \"none\";\nexport type WasmPreprocessingPreset = \"minimal\" | \"standard\" | \"aggressive\";\nexport type WasmOutputFormat = \"markdown\" | \"djot\" | \"plain\";\n\nexport interface WasmPreprocessingOptions {\n  enabled?: boolean;\n  preset?: WasmPreprocessingPreset;\n  removeNavigation?: boolean;\n  removeForms?: boolean;\n}\n\nexport interface WasmConversionOptions {\n  headingStyle?: WasmHeadingStyle;\n  listIndentType?: WasmListIndentType;\n  listIndentWidth?: number;\n  bullets?: string;\n  strongEmSymbol?: string;\n  escapeAsterisks?: boolean;\n  escapeUnderscores?: boolean;\n  escapeMisc?: boolean;\n  escapeAscii?: boolean;\n  codeLanguage?: string;\n  autolinks?: boolean;\n  defaultTitle?: boolean;\n  brInTables?: boolean;\n  hocrSpatialTables?: boolean;\n  highlightStyle?: WasmHighlightStyle;\n  extractMetadata?: boolean;\n  whitespaceMode?: WasmWhitespaceMode;\n  stripNewlines?: boolean;\n  wrap?: boolean;\n  wrapWidth?: number;\n  convertAsInline?: boolean;\n  subSymbol?: string;\n  supSymbol?: string;\n  newlineStyle?: WasmNewlineStyle;\n  codeBlockStyle?: WasmCodeBlockStyle;\n  keepInlineImagesIn?: string[];\n  preprocessing?: WasmPreprocessingOptions | null;\n  encoding?: string;\n  debug?: boolean;\n  stripTags?: string[];\n  preserveTags?: string[];\n  skipImages?: boolean;\n  outputFormat?: WasmOutputFormat;\n  includeDocumentStructure?: boolean;\n  extractImages?: boolean;\n  maxImageSize?: number;\n  captureSvg?: boolean;\n  inferDimensions?: boolean;\n}\n\n/** A single cell in a structured table grid. */\nexport interface WasmGridCell {\n  content: string;\n  row: number;\n  col: number;\n  rowSpan: number;\n  colSpan: number;\n  isHeader: boolean;\n}\n\n/** Structured table grid with cell-level data. */\nexport interface WasmTableGrid {\n  rows: number;\n  cols: number;\n  cells: WasmGridCell[];\n}\n\n/** A table extracted during conversion. */\nexport interface WasmConversionTable {\n  grid: WasmTableGrid;\n  markdown: string;\n}\n\n/** Non-fatal warning emitted during conversion. */\nexport interface WasmConversionWarning {\n  /** Human-readable warning message. */\n  message: string;\n  /** Warning kind identifier. */\n  kind: string;\n}\n\n/** An extracted inline image from the HTML document. */\nexport interface WasmInlineImage {\n  /** Raw image data as a Uint8Array. */\n  data: Uint8Array;\n  /** Image format (png, jpeg, gif, svg, etc.). */\n  format: string;\n  /** Generated or provided filename, or null. */\n  filename: string | null;\n  /** Alt text or description, or null. */\n  description: string | null;\n  /** Image width in pixels, or null if not available. */\n  width: number | null;\n  /** Image height in pixels, or null if not available. */\n  height: number | null;\n  /** Source type (\"img_data_uri\" or \"svg_element\"). */\n  source: string;\n  /** HTML attributes from the source element. */\n  attributes: Record<string, string>;\n}\n\n/** Result of the convert() API. */\nexport interface WasmConversionResult {\n  /** Converted text output (markdown, djot, or plain text), or null. */\n  content: string | null;\n  /** Structured document tree serialized as a JSON value, or null. */\n  document: unknown | null;\n  /** Extracted HTML metadata serialized as a JSON value, or null. */\n  metadata: unknown | null;\n  /** All tables found in the HTML, in document order. */\n  tables: WasmConversionTable[];\n  /** Extracted inline images (data URIs and SVGs). */\n  images: WasmInlineImage[];\n  /** Non-fatal processing warnings. */\n  warnings: WasmConversionWarning[];\n}\n`;\n\nfunction injectTypedef(content, specifier) {\n  const typedefBlock = `\\n/**\\n * @typedef {import(\"${specifier}\").WasmConversionOptions} WasmConversionOptions\\n */\\n`;\n  if (content.includes(\"WasmConversionOptions} WasmConversionOptions\")) {\n    return content;\n  }\n  if (content.includes(\"let wasm;\")) {\n    return content.replace(\"let wasm;\", `let wasm;${typedefBlock}`);\n  }\n  return `${typedefBlock}${content}`;\n}\n\nfunction patchJsDoc(targetPath, typeSpecifier) {\n  if (!fs.existsSync(targetPath)) {\n    return;\n  }\n  let jsContent = fs.readFileSync(targetPath, \"utf8\");\n  const originalContent = jsContent;\n\n  jsContent = injectTypedef(jsContent, typeSpecifier);\n\n  const optionsPattern = /@param\\s+\\{any\\}\\s+options/g;\n  const optionsReplacement = \"@param {WasmConversionOptions | null | undefined} [options]\";\n  jsContent = jsContent.replace(optionsPattern, optionsReplacement);\n\n  const returnsPattern = /@returns\\s+\\{any\\}/g;\n  const returnsReplacement = \"@returns {WasmConversionResult}\";\n  jsContent = jsContent.replace(returnsPattern, returnsReplacement);\n\n  if (jsContent !== originalContent) {\n    fs.writeFileSync(targetPath, jsContent, \"utf8\");\n  }\n}\n\nif (!typesOnly) {\n  if (!fs.existsSync(entryPath)) {\n    console.error(`[patch-bundler-entry] Missing entry file at ${entryPath}`);\n    process.exit(1);\n  }\n\n  const wrapper = `import * as wasmModule from \"./html_to_markdown_wasm_bg.wasm\";\nexport * from \"./html_to_markdown_wasm_bg.js\";\nimport * as imports_mod from \"./html_to_markdown_wasm_bg.js\";\n\nconst notReadyError = () =>\n  new Error(\"html-to-markdown-wasm: WebAssembly bundle is still initializing. Await initWasm() before calling convert() in runtimes that load WASM asynchronously (e.g., Cloudflare Workers).\");\n\nconst notReadyProxy = new Proxy({}, {\n  get(_target, prop) {\n    if (prop === \"__esModule\") {\n      return true;\n    }\n    throw notReadyError();\n  }\n});\n\nlet wasmExports;\nlet initialized = false;\nlet initPromise;\n\nimports_mod.__wbg_set_wasm(notReadyProxy);\n\nfunction asExports(value) {\n  if (!value) {\n    return null;\n  }\n  if (typeof value.__wbindgen_start === \"function\") {\n    return value;\n  }\n  if (value instanceof WebAssembly.Instance) {\n    return value.exports;\n  }\n  if (typeof value === \"object\") {\n    if (value.instance instanceof WebAssembly.Instance) {\n      return value.instance.exports;\n    }\n    if (value.default instanceof WebAssembly.Instance) {\n      return value.default.exports;\n    }\n    if (value.default && value.default.instance instanceof WebAssembly.Instance) {\n      return value.default.instance.exports;\n    }\n  }\n  return null;\n}\n\nfunction finalize(exports) {\n  wasmExports = exports;\n  imports_mod.__wbg_set_wasm(exports);\n  if (typeof exports.__wbindgen_start === \"function\") {\n    exports.__wbindgen_start();\n  }\n  initialized = true;\n  return exports;\n}\n\nfunction trySyncInit() {\n  try {\n    const exports = asExports(wasmModule);\n    if (exports) {\n      finalize(exports);\n    }\n  } catch {\n    // ignore and fall back to async init\n  }\n}\n\ntrySyncInit();\n\nasync function ensureInitPromise() {\n  if (initialized) {\n    return Promise.resolve(wasmExports);\n  }\n  if (!initPromise) {\n    initPromise = (async () => {\n      let module = wasmModule;\n\n      // Handle promise-wrapped modules\n      if (module && typeof module.then === \"function\") {\n        module = await module;\n      }\n\n      // Handle function loaders (like @rollup/plugin-wasm)\n      if (module && typeof module.default === \"function\") {\n        module = await module.default(module);\n      }\n\n      // Handle WebAssembly.Module (Wrangler/esbuild)\n      if (module && module.default instanceof WebAssembly.Module) {\n        const imports = {};\n        imports[\"./html_to_markdown_wasm_bg.js\"] = {};\n        for (const key in imports_mod) {\n          if ((key.startsWith('__wbg_') || key.startsWith('__wbindgen_')) && key !== '__wbg_set_wasm' && typeof imports_mod[key] === 'function') {\n            imports[\"./html_to_markdown_wasm_bg.js\"][key] = imports_mod[key];\n          }\n        }\n        const instance = await WebAssembly.instantiate(module.default, imports);\n        return finalize(instance.exports);\n      }\n\n      // Try standard export detection\n      const exports = asExports(module);\n      if (!exports) {\n        throw new Error(\"html-to-markdown-wasm: failed to initialize WebAssembly bundle. Call initWasm() with a supported bundler configuration.\");\n      }\n      return finalize(exports);\n    })();\n  }\n  return initPromise;\n}\n\nexport const wasmReady = ensureInitPromise();\n\nexport async function initWasm() {\n  return ensureInitPromise();\n}\n`;\n\n  fs.writeFileSync(entryPath, wrapper, \"utf8\");\n}\n\nif (!fs.existsSync(dtsPath)) {\n  console.error(`[patch-bundler-entry] Missing type definitions at ${dtsPath}`);\n  process.exit(1);\n}\n\nlet content = fs.readFileSync(dtsPath, \"utf8\");\n\nif (!typesOnly && !content.includes(\"initWasm():\")) {\n  const additions = `\\nexport declare function initWasm(): Promise<void>;\\nexport declare const wasmReady: Promise<void>;\\n`;\n  content += additions;\n}\n\nif (content.includes(\"options: any\")) {\n  content = content.replace(/options: any/g, \"options?: WasmConversionOptions | null\");\n}\n\ncontent = content.replace(\n  \"readonly attributes: any;\",\n  \"readonly attributes: Record<string, string>;\",\n);\n\n// Fix return types: wasm-bindgen generates `string` or `any` for Result<JsValue, JsValue>\nif (!content.includes(\"WasmConversionResult\")) {\n  // convert() returns a structured result object, not a string\n  content = content.replace(\n    /export function convert\\(html: string, options\\?: WasmConversionOptions \\| null\\): string;/,\n    \"export function convert(html: string, options?: WasmConversionOptions | null): WasmConversionResult;\",\n  );\n  // Also handle the case where wasm-bindgen emits `any` as the return type\n  content = content.replace(\n    /export function convert\\(html: string, options\\?: WasmConversionOptions \\| null\\): any;/,\n    \"export function convert(html: string, options?: WasmConversionOptions | null): WasmConversionResult;\",\n  );\n  // convertWithMetadata and convertBytesWithMetadata return structured results\n  content = content.replace(\n    /export function convertWithMetadata\\(([^)]*)\\): any;/,\n    \"export function convertWithMetadata($1): WasmConversionResult;\",\n  );\n  content = content.replace(\n    /export function convertBytesWithMetadata\\(([^)]*)\\): any;/,\n    \"export function convertBytesWithMetadata($1): WasmConversionResult;\",\n  );\n}\n\nif (!content.includes(\"interface WasmConversionOptions\")) {\n  content += `\\n${typeDefinitions}`;\n}\n\nfs.writeFileSync(dtsPath, content, \"utf8\");\n\nconst jsDocTarget = fs.existsSync(bgPath) ? bgPath : entryPath;\nconst typeImportSpecifier = \"./html_to_markdown_wasm\";\npatchJsDoc(jsDocTarget, typeImportSpecifier);\n\n// Fix package.json \"files\" field to include all necessary files\nconst pkgJsonPath = path.join(distDir, \"package.json\");\nif (fs.existsSync(pkgJsonPath)) {\n  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, \"utf8\"));\n\n  // Use glob patterns to ensure all generated files are included\n  pkgJson.files = [\"*.wasm\", \"*.js\", \"*.d.ts\"];\n\n  fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + \"\\n\", \"utf8\");\n  console.log(`[patch-bundler-entry] Updated package.json files field in ${distArg}`);\n}\n"
  },
  {
    "path": "crates/html-to-markdown-wasm/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:e14bed08fb014469cc11a7c1441ddfc3a1daa9d4caea8dc2bc71c8a2dd63e890\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unused_unit,\n    clippy::unnecessary_cast,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions,\n    clippy::useless_conversion\n)]\n\nuse std::sync::Arc;\nuse wasm_bindgen::prelude::*;\n\n/// Document-level metadata extracted from `<head>` and top-level elements.\n///\n/// Contains all metadata typically used by search engines, social media platforms,\n/// and browsers for document indexing and presentation.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::DocumentMetadata;\n/// let doc = DocumentMetadata {\n///     title: Some(\"My Article\".to_string()),\n///     description: Some(\"A great article about Rust\".to_string()),\n///     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n///     ..Default::default()\n/// };\n///\n/// assert_eq!(doc.title, Some(\"My Article\".to_string()));\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmDocumentMetadata {\n    title: Option<String>,\n    description: Option<String>,\n    keywords: Vec<String>,\n    author: Option<String>,\n    canonical_url: Option<String>,\n    base_href: Option<String>,\n    language: Option<String>,\n    text_direction: Option<WasmTextDirection>,\n    open_graph: JsValue,\n    twitter_card: JsValue,\n    meta_tags: JsValue,\n}\n\n#[wasm_bindgen]\nimpl WasmDocumentMetadata {\n    #[allow(clippy::too_many_arguments)]\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        keywords: Option<Vec<String>>,\n        open_graph: Option<JsValue>,\n        twitter_card: Option<JsValue>,\n        meta_tags: Option<JsValue>,\n        title: Option<String>,\n        description: Option<String>,\n        author: Option<String>,\n        canonical_url: Option<String>,\n        base_href: Option<String>,\n        language: Option<String>,\n        text_direction: Option<WasmTextDirection>,\n    ) -> WasmDocumentMetadata {\n        WasmDocumentMetadata {\n            title,\n            description,\n            keywords: keywords.unwrap_or_default(),\n            author,\n            canonical_url,\n            base_href,\n            language,\n            text_direction,\n            open_graph: open_graph.unwrap_or_default(),\n            twitter_card: twitter_card.unwrap_or_default(),\n            meta_tags: meta_tags.unwrap_or_default(),\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_title(&mut self, value: Option<String>) {\n        self.title = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn description(&self) -> Option<String> {\n        self.description.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_description(&mut self, value: Option<String>) {\n        self.description = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn keywords(&self) -> Vec<String> {\n        self.keywords.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_keywords(&mut self, value: Vec<String>) {\n        self.keywords = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn author(&self) -> Option<String> {\n        self.author.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_author(&mut self, value: Option<String>) {\n        self.author = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"canonicalUrl\")]\n    pub fn canonical_url(&self) -> Option<String> {\n        self.canonical_url.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"canonicalUrl\")]\n    pub fn set_canonical_url(&mut self, value: Option<String>) {\n        self.canonical_url = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"baseHref\")]\n    pub fn base_href(&self) -> Option<String> {\n        self.base_href.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"baseHref\")]\n    pub fn set_base_href(&mut self, value: Option<String>) {\n        self.base_href = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn language(&self) -> Option<String> {\n        self.language.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_language(&mut self, value: Option<String>) {\n        self.language = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"textDirection\")]\n    pub fn text_direction(&self) -> Option<WasmTextDirection> {\n        self.text_direction\n    }\n\n    #[wasm_bindgen(setter, js_name = \"textDirection\")]\n    pub fn set_text_direction(&mut self, value: Option<WasmTextDirection>) {\n        self.text_direction = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"openGraph\")]\n    pub fn open_graph(&self) -> JsValue {\n        self.open_graph.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"openGraph\")]\n    pub fn set_open_graph(&mut self, value: JsValue) {\n        self.open_graph = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"twitterCard\")]\n    pub fn twitter_card(&self) -> JsValue {\n        self.twitter_card.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"twitterCard\")]\n    pub fn set_twitter_card(&mut self, value: JsValue) {\n        self.twitter_card = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"metaTags\")]\n    pub fn meta_tags(&self) -> JsValue {\n        self.meta_tags.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"metaTags\")]\n    pub fn set_meta_tags(&mut self, value: JsValue) {\n        self.meta_tags = value;\n    }\n}\n\n/// Header element metadata with hierarchy tracking.\n///\n/// Captures heading elements (h1-h6) with their text content, identifiers,\n/// and position in the document structure.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HeaderMetadata;\n/// let header = HeaderMetadata {\n///     level: 1,\n///     text: \"Main Title\".to_string(),\n///     id: Some(\"main-title\".to_string()),\n///     depth: 0,\n///     html_offset: 145,\n/// };\n///\n/// assert_eq!(header.level, 1);\n/// assert!(header.is_valid());\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmHeaderMetadata {\n    level: u8,\n    text: String,\n    id: Option<String>,\n    depth: usize,\n    html_offset: usize,\n}\n\n#[wasm_bindgen]\nimpl WasmHeaderMetadata {\n    #[wasm_bindgen(constructor)]\n    pub fn new(level: u8, text: String, depth: usize, html_offset: usize, id: Option<String>) -> WasmHeaderMetadata {\n        WasmHeaderMetadata {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn level(&self) -> u8 {\n        self.level\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_level(&mut self, value: u8) {\n        self.level = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn text(&self) -> String {\n        self.text.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_text(&mut self, value: String) {\n        self.text = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn id(&self) -> Option<String> {\n        self.id.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_id(&mut self, value: Option<String>) {\n        self.id = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn depth(&self) -> usize {\n        self.depth\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_depth(&mut self, value: usize) {\n        self.depth = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"htmlOffset\")]\n    pub fn html_offset(&self) -> usize {\n        self.html_offset\n    }\n\n    #[wasm_bindgen(setter, js_name = \"htmlOffset\")]\n    pub fn set_html_offset(&mut self, value: usize) {\n        self.html_offset = value;\n    }\n\n    /// Validate that the header level is within valid range (1-6).\n    ///\n    /// # Returns\n    ///\n    /// `true` if level is 1-6, `false` otherwise.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::HeaderMetadata;\n    /// let valid = HeaderMetadata {\n    ///     level: 3,\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(valid.is_valid());\n    ///\n    /// let invalid = HeaderMetadata {\n    ///     level: 7,  // Invalid\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(!invalid.is_valid());\n    /// ```\n    #[wasm_bindgen(js_name = \"isValid\")]\n    pub fn is_valid(&self) -> bool {\n        html_to_markdown_rs::HeaderMetadata::from(self.clone()).is_valid()\n    }\n}\n\n/// Hyperlink metadata with categorization and attributes.\n///\n/// Represents `<a>` elements with parsed href values, text content, and link type classification.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n/// let link = LinkMetadata {\n///     href: \"https://example.com\".to_string(),\n///     text: \"Example\".to_string(),\n///     title: Some(\"Visit Example\".to_string()),\n///     link_type: LinkType::External,\n///     rel: vec![\"nofollow\".to_string()],\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(link.link_type, LinkType::External);\n/// assert_eq!(link.text, \"Example\");\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmLinkMetadata {\n    href: String,\n    text: String,\n    title: Option<String>,\n    link_type: WasmLinkType,\n    rel: Vec<String>,\n    attributes: JsValue,\n}\n\n#[wasm_bindgen]\nimpl WasmLinkMetadata {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        href: String,\n        text: String,\n        link_type: WasmLinkType,\n        rel: Vec<String>,\n        attributes: JsValue,\n        title: Option<String>,\n    ) -> WasmLinkMetadata {\n        WasmLinkMetadata {\n            href,\n            text,\n            title,\n            link_type,\n            rel,\n            attributes,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn href(&self) -> String {\n        self.href.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_href(&mut self, value: String) {\n        self.href = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn text(&self) -> String {\n        self.text.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_text(&mut self, value: String) {\n        self.text = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_title(&mut self, value: Option<String>) {\n        self.title = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"linkType\")]\n    pub fn link_type(&self) -> WasmLinkType {\n        self.link_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"linkType\")]\n    pub fn set_link_type(&mut self, value: WasmLinkType) {\n        self.link_type = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn rel(&self) -> Vec<String> {\n        self.rel.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_rel(&mut self, value: Vec<String>) {\n        self.rel = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn attributes(&self) -> JsValue {\n        self.attributes.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_attributes(&mut self, value: JsValue) {\n        self.attributes = value;\n    }\n\n    /// Classify a link based on href value.\n    ///\n    /// # Arguments\n    ///\n    /// * `href` - The href attribute value\n    ///\n    /// # Returns\n    ///\n    /// Appropriate [`LinkType`] based on protocol and content.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n    /// assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n    /// assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n    /// assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n    /// assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n    /// ```\n    #[wasm_bindgen(js_name = \"classifyLink\")]\n    pub fn classify_link(href: String) -> WasmLinkType {\n        html_to_markdown_rs::LinkMetadata::classify_link(&href).into()\n    }\n}\n\n/// Image metadata with source and dimensions.\n///\n/// Captures `<img>` elements and inline `<svg>` elements with metadata\n/// for image analysis and optimization.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n/// let img = ImageMetadata {\n///     src: \"https://example.com/image.jpg\".to_string(),\n///     alt: Some(\"An example image\".to_string()),\n///     title: Some(\"Example\".to_string()),\n///     dimensions: Some((800, 600)),\n///     image_type: ImageType::External,\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(img.image_type, ImageType::External);\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmImageMetadata {\n    src: String,\n    alt: Option<String>,\n    title: Option<String>,\n    dimensions: Option<Vec<u32>>,\n    image_type: WasmImageType,\n    attributes: JsValue,\n}\n\n#[wasm_bindgen]\nimpl WasmImageMetadata {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        src: String,\n        image_type: WasmImageType,\n        attributes: JsValue,\n        alt: Option<String>,\n        title: Option<String>,\n        dimensions: Option<Vec<u32>>,\n    ) -> WasmImageMetadata {\n        WasmImageMetadata {\n            src,\n            alt,\n            title,\n            dimensions,\n            image_type,\n            attributes,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn src(&self) -> String {\n        self.src.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_src(&mut self, value: String) {\n        self.src = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn alt(&self) -> Option<String> {\n        self.alt.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_alt(&mut self, value: Option<String>) {\n        self.alt = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_title(&mut self, value: Option<String>) {\n        self.title = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn dimensions(&self) -> Option<Vec<u32>> {\n        self.dimensions.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_dimensions(&mut self, value: Option<Vec<u32>>) {\n        self.dimensions = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"imageType\")]\n    pub fn image_type(&self) -> WasmImageType {\n        self.image_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"imageType\")]\n    pub fn set_image_type(&mut self, value: WasmImageType) {\n        self.image_type = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn attributes(&self) -> JsValue {\n        self.attributes.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_attributes(&mut self, value: JsValue) {\n        self.attributes = value;\n    }\n}\n\n/// Structured data block (JSON-LD, Microdata, or RDFa).\n///\n/// Represents machine-readable structured data found in the document.\n/// JSON-LD blocks are collected as raw JSON strings for flexibility.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n/// let schema = StructuredData {\n///     data_type: StructuredDataType::JsonLd,\n///     raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n///     schema_type: Some(\"Article\".to_string()),\n/// };\n///\n/// assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmStructuredData {\n    data_type: WasmStructuredDataType,\n    raw_json: String,\n    schema_type: Option<String>,\n}\n\n#[wasm_bindgen]\nimpl WasmStructuredData {\n    #[wasm_bindgen(constructor)]\n    pub fn new(data_type: WasmStructuredDataType, raw_json: String, schema_type: Option<String>) -> WasmStructuredData {\n        WasmStructuredData {\n            data_type,\n            raw_json,\n            schema_type,\n        }\n    }\n\n    #[wasm_bindgen(getter, js_name = \"dataType\")]\n    pub fn data_type(&self) -> WasmStructuredDataType {\n        self.data_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"dataType\")]\n    pub fn set_data_type(&mut self, value: WasmStructuredDataType) {\n        self.data_type = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"rawJson\")]\n    pub fn raw_json(&self) -> String {\n        self.raw_json.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"rawJson\")]\n    pub fn set_raw_json(&mut self, value: String) {\n        self.raw_json = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"schemaType\")]\n    pub fn schema_type(&self) -> Option<String> {\n        self.schema_type.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"schemaType\")]\n    pub fn set_schema_type(&mut self, value: Option<String>) {\n        self.schema_type = value;\n    }\n}\n\n/// Comprehensive metadata extraction result from HTML document.\n///\n/// Contains all extracted metadata types in a single structure,\n/// suitable for serialization and transmission across language boundaries.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HtmlMetadata;\n/// let metadata = HtmlMetadata {\n///     document: Default::default(),\n///     headers: Vec::new(),\n///     links: Vec::new(),\n///     images: Vec::new(),\n///     structured_data: Vec::new(),\n/// };\n///\n/// assert!(metadata.headers.is_empty());\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmHtmlMetadata {\n    document: WasmDocumentMetadata,\n    headers: Vec<WasmHeaderMetadata>,\n    links: Vec<WasmLinkMetadata>,\n    images: Vec<WasmImageMetadata>,\n    structured_data: Vec<WasmStructuredData>,\n}\n\n#[wasm_bindgen]\nimpl WasmHtmlMetadata {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        document: Option<WasmDocumentMetadata>,\n        headers: Option<Vec<WasmHeaderMetadata>>,\n        links: Option<Vec<WasmLinkMetadata>>,\n        images: Option<Vec<WasmImageMetadata>>,\n        structured_data: Option<Vec<WasmStructuredData>>,\n    ) -> WasmHtmlMetadata {\n        WasmHtmlMetadata {\n            document: document.unwrap_or_default(),\n            headers: headers.unwrap_or_default(),\n            links: links.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            structured_data: structured_data.unwrap_or_default(),\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn document(&self) -> WasmDocumentMetadata {\n        self.document.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_document(&mut self, value: WasmDocumentMetadata) {\n        self.document = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn headers(&self) -> Vec<WasmHeaderMetadata> {\n        self.headers.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_headers(&mut self, value: Vec<WasmHeaderMetadata>) {\n        self.headers = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn links(&self) -> Vec<WasmLinkMetadata> {\n        self.links.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_links(&mut self, value: Vec<WasmLinkMetadata>) {\n        self.links = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn images(&self) -> Vec<WasmImageMetadata> {\n        self.images.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_images(&mut self, value: Vec<WasmImageMetadata>) {\n        self.images = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"structuredData\")]\n    pub fn structured_data(&self) -> Vec<WasmStructuredData> {\n        self.structured_data.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"structuredData\")]\n    pub fn set_structured_data(&mut self, value: Vec<WasmStructuredData>) {\n        self.structured_data = value;\n    }\n}\n\n/// Main conversion options for HTML to Markdown conversion.\n///\n/// Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::ConversionOptions;\n///\n/// let options = ConversionOptions::builder()\n///     .heading_style(HeadingStyle::Atx)\n///     .wrap(true)\n///     .wrap_width(100)\n///     .build();\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmConversionOptions {\n    heading_style: WasmHeadingStyle,\n    list_indent_type: WasmListIndentType,\n    list_indent_width: usize,\n    bullets: String,\n    strong_em_symbol: String,\n    escape_asterisks: bool,\n    escape_underscores: bool,\n    escape_misc: bool,\n    escape_ascii: bool,\n    code_language: String,\n    autolinks: bool,\n    default_title: bool,\n    br_in_tables: bool,\n    highlight_style: WasmHighlightStyle,\n    extract_metadata: bool,\n    whitespace_mode: WasmWhitespaceMode,\n    strip_newlines: bool,\n    wrap: bool,\n    wrap_width: usize,\n    convert_as_inline: bool,\n    sub_symbol: String,\n    sup_symbol: String,\n    newline_style: WasmNewlineStyle,\n    code_block_style: WasmCodeBlockStyle,\n    keep_inline_images_in: Vec<String>,\n    preprocessing: WasmPreprocessingOptions,\n    encoding: String,\n    debug: bool,\n    strip_tags: Vec<String>,\n    preserve_tags: Vec<String>,\n    skip_images: bool,\n    link_style: WasmLinkStyle,\n    output_format: WasmOutputFormat,\n    include_document_structure: bool,\n    extract_images: bool,\n    max_image_size: u64,\n    capture_svg: bool,\n    infer_dimensions: bool,\n    max_depth: Option<usize>,\n    exclude_selectors: Vec<String>,\n    visitor: Option<wasm_bindgen::JsValue>,\n}\n\n#[wasm_bindgen]\nimpl WasmConversionOptions {\n    #[allow(clippy::too_many_arguments)]\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        heading_style: Option<WasmHeadingStyle>,\n        list_indent_type: Option<WasmListIndentType>,\n        list_indent_width: Option<usize>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<WasmHighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WasmWhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<usize>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<WasmNewlineStyle>,\n        code_block_style: Option<WasmCodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<WasmPreprocessingOptions>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<WasmLinkStyle>,\n        output_format: Option<WasmOutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<u64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        exclude_selectors: Option<Vec<String>>,\n        max_depth: Option<usize>,\n    ) -> WasmConversionOptions {\n        WasmConversionOptions {\n            heading_style: heading_style.unwrap_or_default(),\n            list_indent_type: list_indent_type.unwrap_or_default(),\n            list_indent_width: list_indent_width.unwrap_or(2),\n            bullets: bullets.unwrap_or_else(|| \"-*+\".to_string()),\n            strong_em_symbol: strong_em_symbol.unwrap_or_else(|| \"*\".to_string()),\n            escape_asterisks: escape_asterisks.unwrap_or(false),\n            escape_underscores: escape_underscores.unwrap_or(false),\n            escape_misc: escape_misc.unwrap_or(false),\n            escape_ascii: escape_ascii.unwrap_or(false),\n            code_language: code_language.unwrap_or_else(|| \"\".to_string()),\n            autolinks: autolinks.unwrap_or(true),\n            default_title: default_title.unwrap_or(false),\n            br_in_tables: br_in_tables.unwrap_or(false),\n            highlight_style: highlight_style.unwrap_or_default(),\n            extract_metadata: extract_metadata.unwrap_or(true),\n            whitespace_mode: whitespace_mode.unwrap_or_default(),\n            strip_newlines: strip_newlines.unwrap_or(false),\n            wrap: wrap.unwrap_or(false),\n            wrap_width: wrap_width.unwrap_or(80),\n            convert_as_inline: convert_as_inline.unwrap_or(false),\n            sub_symbol: sub_symbol.unwrap_or_else(|| \"\".to_string()),\n            sup_symbol: sup_symbol.unwrap_or_else(|| \"\".to_string()),\n            newline_style: newline_style.unwrap_or_default(),\n            code_block_style: code_block_style.unwrap_or_default(),\n            keep_inline_images_in: keep_inline_images_in.unwrap_or_default(),\n            preprocessing: preprocessing.unwrap_or_default(),\n            encoding: encoding.unwrap_or_else(|| \"utf-8\".to_string()),\n            debug: debug.unwrap_or(false),\n            strip_tags: strip_tags.unwrap_or_default(),\n            preserve_tags: preserve_tags.unwrap_or_default(),\n            skip_images: skip_images.unwrap_or(false),\n            link_style: link_style.unwrap_or_default(),\n            output_format: output_format.unwrap_or_default(),\n            include_document_structure: include_document_structure.unwrap_or(false),\n            extract_images: extract_images.unwrap_or(false),\n            max_image_size: max_image_size.unwrap_or(5242880),\n            capture_svg: capture_svg.unwrap_or(false),\n            infer_dimensions: infer_dimensions.unwrap_or(true),\n            max_depth,\n            exclude_selectors: exclude_selectors.unwrap_or_default(),\n            ..Default::default()\n        }\n    }\n\n    #[wasm_bindgen(getter, js_name = \"headingStyle\")]\n    pub fn heading_style(&self) -> WasmHeadingStyle {\n        self.heading_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"headingStyle\")]\n    pub fn set_heading_style(&mut self, value: WasmHeadingStyle) {\n        self.heading_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"listIndentType\")]\n    pub fn list_indent_type(&self) -> WasmListIndentType {\n        self.list_indent_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"listIndentType\")]\n    pub fn set_list_indent_type(&mut self, value: WasmListIndentType) {\n        self.list_indent_type = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"listIndentWidth\")]\n    pub fn list_indent_width(&self) -> usize {\n        self.list_indent_width\n    }\n\n    #[wasm_bindgen(setter, js_name = \"listIndentWidth\")]\n    pub fn set_list_indent_width(&mut self, value: usize) {\n        self.list_indent_width = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn bullets(&self) -> String {\n        self.bullets.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_bullets(&mut self, value: String) {\n        self.bullets = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"strongEmSymbol\")]\n    pub fn strong_em_symbol(&self) -> String {\n        self.strong_em_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"strongEmSymbol\")]\n    pub fn set_strong_em_symbol(&mut self, value: String) {\n        self.strong_em_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeAsterisks\")]\n    pub fn escape_asterisks(&self) -> bool {\n        self.escape_asterisks\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeAsterisks\")]\n    pub fn set_escape_asterisks(&mut self, value: bool) {\n        self.escape_asterisks = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeUnderscores\")]\n    pub fn escape_underscores(&self) -> bool {\n        self.escape_underscores\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeUnderscores\")]\n    pub fn set_escape_underscores(&mut self, value: bool) {\n        self.escape_underscores = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeMisc\")]\n    pub fn escape_misc(&self) -> bool {\n        self.escape_misc\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeMisc\")]\n    pub fn set_escape_misc(&mut self, value: bool) {\n        self.escape_misc = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeAscii\")]\n    pub fn escape_ascii(&self) -> bool {\n        self.escape_ascii\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeAscii\")]\n    pub fn set_escape_ascii(&mut self, value: bool) {\n        self.escape_ascii = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"codeLanguage\")]\n    pub fn code_language(&self) -> String {\n        self.code_language.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"codeLanguage\")]\n    pub fn set_code_language(&mut self, value: String) {\n        self.code_language = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn autolinks(&self) -> bool {\n        self.autolinks\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_autolinks(&mut self, value: bool) {\n        self.autolinks = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"defaultTitle\")]\n    pub fn default_title(&self) -> bool {\n        self.default_title\n    }\n\n    #[wasm_bindgen(setter, js_name = \"defaultTitle\")]\n    pub fn set_default_title(&mut self, value: bool) {\n        self.default_title = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"brInTables\")]\n    pub fn br_in_tables(&self) -> bool {\n        self.br_in_tables\n    }\n\n    #[wasm_bindgen(setter, js_name = \"brInTables\")]\n    pub fn set_br_in_tables(&mut self, value: bool) {\n        self.br_in_tables = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"highlightStyle\")]\n    pub fn highlight_style(&self) -> WasmHighlightStyle {\n        self.highlight_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"highlightStyle\")]\n    pub fn set_highlight_style(&mut self, value: WasmHighlightStyle) {\n        self.highlight_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"extractMetadata\")]\n    pub fn extract_metadata(&self) -> bool {\n        self.extract_metadata\n    }\n\n    #[wasm_bindgen(setter, js_name = \"extractMetadata\")]\n    pub fn set_extract_metadata(&mut self, value: bool) {\n        self.extract_metadata = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"whitespaceMode\")]\n    pub fn whitespace_mode(&self) -> WasmWhitespaceMode {\n        self.whitespace_mode\n    }\n\n    #[wasm_bindgen(setter, js_name = \"whitespaceMode\")]\n    pub fn set_whitespace_mode(&mut self, value: WasmWhitespaceMode) {\n        self.whitespace_mode = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"stripNewlines\")]\n    pub fn strip_newlines(&self) -> bool {\n        self.strip_newlines\n    }\n\n    #[wasm_bindgen(setter, js_name = \"stripNewlines\")]\n    pub fn set_strip_newlines(&mut self, value: bool) {\n        self.strip_newlines = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn wrap(&self) -> bool {\n        self.wrap\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_wrap(&mut self, value: bool) {\n        self.wrap = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"wrapWidth\")]\n    pub fn wrap_width(&self) -> usize {\n        self.wrap_width\n    }\n\n    #[wasm_bindgen(setter, js_name = \"wrapWidth\")]\n    pub fn set_wrap_width(&mut self, value: usize) {\n        self.wrap_width = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"convertAsInline\")]\n    pub fn convert_as_inline(&self) -> bool {\n        self.convert_as_inline\n    }\n\n    #[wasm_bindgen(setter, js_name = \"convertAsInline\")]\n    pub fn set_convert_as_inline(&mut self, value: bool) {\n        self.convert_as_inline = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"subSymbol\")]\n    pub fn sub_symbol(&self) -> String {\n        self.sub_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"subSymbol\")]\n    pub fn set_sub_symbol(&mut self, value: String) {\n        self.sub_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"supSymbol\")]\n    pub fn sup_symbol(&self) -> String {\n        self.sup_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"supSymbol\")]\n    pub fn set_sup_symbol(&mut self, value: String) {\n        self.sup_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"newlineStyle\")]\n    pub fn newline_style(&self) -> WasmNewlineStyle {\n        self.newline_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"newlineStyle\")]\n    pub fn set_newline_style(&mut self, value: WasmNewlineStyle) {\n        self.newline_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"codeBlockStyle\")]\n    pub fn code_block_style(&self) -> WasmCodeBlockStyle {\n        self.code_block_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"codeBlockStyle\")]\n    pub fn set_code_block_style(&mut self, value: WasmCodeBlockStyle) {\n        self.code_block_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"keepInlineImagesIn\")]\n    pub fn keep_inline_images_in(&self) -> Vec<String> {\n        self.keep_inline_images_in.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"keepInlineImagesIn\")]\n    pub fn set_keep_inline_images_in(&mut self, value: Vec<String>) {\n        self.keep_inline_images_in = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn preprocessing(&self) -> WasmPreprocessingOptions {\n        self.preprocessing.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_preprocessing(&mut self, value: WasmPreprocessingOptions) {\n        self.preprocessing = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn encoding(&self) -> String {\n        self.encoding.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_encoding(&mut self, value: String) {\n        self.encoding = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn debug(&self) -> bool {\n        self.debug\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_debug(&mut self, value: bool) {\n        self.debug = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"stripTags\")]\n    pub fn strip_tags(&self) -> Vec<String> {\n        self.strip_tags.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"stripTags\")]\n    pub fn set_strip_tags(&mut self, value: Vec<String>) {\n        self.strip_tags = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"preserveTags\")]\n    pub fn preserve_tags(&self) -> Vec<String> {\n        self.preserve_tags.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"preserveTags\")]\n    pub fn set_preserve_tags(&mut self, value: Vec<String>) {\n        self.preserve_tags = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"skipImages\")]\n    pub fn skip_images(&self) -> bool {\n        self.skip_images\n    }\n\n    #[wasm_bindgen(setter, js_name = \"skipImages\")]\n    pub fn set_skip_images(&mut self, value: bool) {\n        self.skip_images = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"linkStyle\")]\n    pub fn link_style(&self) -> WasmLinkStyle {\n        self.link_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"linkStyle\")]\n    pub fn set_link_style(&mut self, value: WasmLinkStyle) {\n        self.link_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"outputFormat\")]\n    pub fn output_format(&self) -> WasmOutputFormat {\n        self.output_format\n    }\n\n    #[wasm_bindgen(setter, js_name = \"outputFormat\")]\n    pub fn set_output_format(&mut self, value: WasmOutputFormat) {\n        self.output_format = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"includeDocumentStructure\")]\n    pub fn include_document_structure(&self) -> bool {\n        self.include_document_structure\n    }\n\n    #[wasm_bindgen(setter, js_name = \"includeDocumentStructure\")]\n    pub fn set_include_document_structure(&mut self, value: bool) {\n        self.include_document_structure = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"extractImages\")]\n    pub fn extract_images(&self) -> bool {\n        self.extract_images\n    }\n\n    #[wasm_bindgen(setter, js_name = \"extractImages\")]\n    pub fn set_extract_images(&mut self, value: bool) {\n        self.extract_images = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"maxImageSize\")]\n    pub fn max_image_size(&self) -> u64 {\n        self.max_image_size\n    }\n\n    #[wasm_bindgen(setter, js_name = \"maxImageSize\")]\n    pub fn set_max_image_size(&mut self, value: u64) {\n        self.max_image_size = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"captureSvg\")]\n    pub fn capture_svg(&self) -> bool {\n        self.capture_svg\n    }\n\n    #[wasm_bindgen(setter, js_name = \"captureSvg\")]\n    pub fn set_capture_svg(&mut self, value: bool) {\n        self.capture_svg = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"inferDimensions\")]\n    pub fn infer_dimensions(&self) -> bool {\n        self.infer_dimensions\n    }\n\n    #[wasm_bindgen(setter, js_name = \"inferDimensions\")]\n    pub fn set_infer_dimensions(&mut self, value: bool) {\n        self.infer_dimensions = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"maxDepth\")]\n    pub fn max_depth(&self) -> Option<usize> {\n        self.max_depth\n    }\n\n    #[wasm_bindgen(setter, js_name = \"maxDepth\")]\n    pub fn set_max_depth(&mut self, value: Option<usize>) {\n        self.max_depth = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"excludeSelectors\")]\n    pub fn exclude_selectors(&self) -> Vec<String> {\n        self.exclude_selectors.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"excludeSelectors\")]\n    pub fn set_exclude_selectors(&mut self, value: Vec<String>) {\n        self.exclude_selectors = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn visitor(&self) -> Option<wasm_bindgen::JsValue> {\n        self.visitor.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_visitor(&mut self, value: Option<wasm_bindgen::JsValue>) {\n        self.visitor = value;\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[wasm_bindgen]\n    pub fn default() -> WasmConversionOptions {\n        html_to_markdown_rs::ConversionOptions::default().into()\n    }\n\n    /// Create a new builder with default values.\n    #[wasm_bindgen]\n    pub fn builder() -> WasmConversionOptionsBuilder {\n        WasmConversionOptionsBuilder {\n            inner: Arc::new(html_to_markdown_rs::ConversionOptions::builder()),\n        }\n    }\n\n    /// Apply a partial update to these conversion options.\n    #[wasm_bindgen(js_name = \"applyUpdate\")]\n    pub fn apply_update(&self, _update: WasmConversionOptionsUpdate) -> () {\n        ()\n    }\n\n    /// Create from a partial update, applying to defaults.\n    #[wasm_bindgen(js_name = \"fromUpdate\")]\n    pub fn from_update(update: WasmConversionOptionsUpdate) -> WasmConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::ConversionOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[wasm_bindgen]\n    pub fn from(update: WasmConversionOptionsUpdate) -> WasmConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::ConversionOptions::from(update_core).into()\n    }\n}\n\n/// Builder for [`ConversionOptions`].\n///\n/// All fields start with default values. Call `.build()` to produce the final options.\n#[derive(Clone)]\n#[wasm_bindgen]\npub struct WasmConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\n#[wasm_bindgen]\nimpl WasmConversionOptionsBuilder {\n    /// Set the list of HTML tag names whose content is stripped from output.\n    #[wasm_bindgen(js_name = \"stripTags\")]\n    pub fn strip_tags(&self, tags: Vec<String>) -> WasmConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().strip_tags(tags)),\n        }\n    }\n\n    /// Set the list of HTML tag names that are preserved verbatim in output.\n    #[wasm_bindgen(js_name = \"preserveTags\")]\n    pub fn preserve_tags(&self, tags: Vec<String>) -> WasmConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preserve_tags(tags)),\n        }\n    }\n\n    /// Set the list of HTML tag names whose `<img>` children are kept inline.\n    #[wasm_bindgen(js_name = \"keepInlineImagesIn\")]\n    pub fn keep_inline_images_in(&self, tags: Vec<String>) -> WasmConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    /// Set the list of CSS selectors for elements to exclude entirely from output.\n    #[wasm_bindgen(js_name = \"excludeSelectors\")]\n    pub fn exclude_selectors(&self, selectors: Vec<String>) -> WasmConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().exclude_selectors(selectors)),\n        }\n    }\n\n    /// Set the pre-processing options applied to the HTML before conversion.\n    #[wasm_bindgen]\n    pub fn preprocessing(&self, preprocessing: WasmPreprocessingOptions) -> WasmConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preprocessing(preprocessing.into())),\n        }\n    }\n\n    /// Build the final [`ConversionOptions`].\n    #[wasm_bindgen]\n    pub fn build(&self) -> WasmConversionOptions {\n        (*self.inner).clone().build().into()\n    }\n}\n\n/// Partial update for `ConversionOptions`.\n///\n/// Uses `Option<T>` fields for selective updates. Bindings use this to construct\n/// options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmConversionOptionsUpdate {\n    heading_style: Option<WasmHeadingStyle>,\n    list_indent_type: Option<WasmListIndentType>,\n    list_indent_width: Option<usize>,\n    bullets: Option<String>,\n    strong_em_symbol: Option<String>,\n    escape_asterisks: Option<bool>,\n    escape_underscores: Option<bool>,\n    escape_misc: Option<bool>,\n    escape_ascii: Option<bool>,\n    code_language: Option<String>,\n    autolinks: Option<bool>,\n    default_title: Option<bool>,\n    br_in_tables: Option<bool>,\n    highlight_style: Option<WasmHighlightStyle>,\n    extract_metadata: Option<bool>,\n    whitespace_mode: Option<WasmWhitespaceMode>,\n    strip_newlines: Option<bool>,\n    wrap: Option<bool>,\n    wrap_width: Option<usize>,\n    convert_as_inline: Option<bool>,\n    sub_symbol: Option<String>,\n    sup_symbol: Option<String>,\n    newline_style: Option<WasmNewlineStyle>,\n    code_block_style: Option<WasmCodeBlockStyle>,\n    keep_inline_images_in: Option<Vec<String>>,\n    preprocessing: Option<WasmPreprocessingOptionsUpdate>,\n    encoding: Option<String>,\n    debug: Option<bool>,\n    strip_tags: Option<Vec<String>>,\n    preserve_tags: Option<Vec<String>>,\n    skip_images: Option<bool>,\n    link_style: Option<WasmLinkStyle>,\n    output_format: Option<WasmOutputFormat>,\n    include_document_structure: Option<bool>,\n    extract_images: Option<bool>,\n    max_image_size: Option<u64>,\n    capture_svg: Option<bool>,\n    infer_dimensions: Option<bool>,\n    max_depth: Option<usize>,\n    exclude_selectors: Option<Vec<String>>,\n    visitor: Option<wasm_bindgen::JsValue>,\n}\n\n#[wasm_bindgen]\nimpl WasmConversionOptionsUpdate {\n    #[allow(clippy::too_many_arguments)]\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        heading_style: Option<WasmHeadingStyle>,\n        list_indent_type: Option<WasmListIndentType>,\n        list_indent_width: Option<usize>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<WasmHighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WasmWhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<usize>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<WasmNewlineStyle>,\n        code_block_style: Option<WasmCodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<WasmPreprocessingOptionsUpdate>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<WasmLinkStyle>,\n        output_format: Option<WasmOutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<u64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        max_depth: Option<usize>,\n        exclude_selectors: Option<Vec<String>>,\n    ) -> WasmConversionOptionsUpdate {\n        WasmConversionOptionsUpdate {\n            heading_style,\n            list_indent_type,\n            list_indent_width,\n            bullets,\n            strong_em_symbol,\n            escape_asterisks,\n            escape_underscores,\n            escape_misc,\n            escape_ascii,\n            code_language,\n            autolinks,\n            default_title,\n            br_in_tables,\n            highlight_style,\n            extract_metadata,\n            whitespace_mode,\n            strip_newlines,\n            wrap,\n            wrap_width,\n            convert_as_inline,\n            sub_symbol,\n            sup_symbol,\n            newline_style,\n            code_block_style,\n            keep_inline_images_in,\n            preprocessing,\n            encoding,\n            debug,\n            strip_tags,\n            preserve_tags,\n            skip_images,\n            link_style,\n            output_format,\n            include_document_structure,\n            extract_images,\n            max_image_size,\n            capture_svg,\n            infer_dimensions,\n            max_depth,\n            exclude_selectors,\n            ..Default::default()\n        }\n    }\n\n    #[wasm_bindgen(getter, js_name = \"headingStyle\")]\n    pub fn heading_style(&self) -> Option<WasmHeadingStyle> {\n        self.heading_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"headingStyle\")]\n    pub fn set_heading_style(&mut self, value: Option<WasmHeadingStyle>) {\n        self.heading_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"listIndentType\")]\n    pub fn list_indent_type(&self) -> Option<WasmListIndentType> {\n        self.list_indent_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"listIndentType\")]\n    pub fn set_list_indent_type(&mut self, value: Option<WasmListIndentType>) {\n        self.list_indent_type = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"listIndentWidth\")]\n    pub fn list_indent_width(&self) -> Option<usize> {\n        self.list_indent_width\n    }\n\n    #[wasm_bindgen(setter, js_name = \"listIndentWidth\")]\n    pub fn set_list_indent_width(&mut self, value: Option<usize>) {\n        self.list_indent_width = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn bullets(&self) -> Option<String> {\n        self.bullets.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_bullets(&mut self, value: Option<String>) {\n        self.bullets = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"strongEmSymbol\")]\n    pub fn strong_em_symbol(&self) -> Option<String> {\n        self.strong_em_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"strongEmSymbol\")]\n    pub fn set_strong_em_symbol(&mut self, value: Option<String>) {\n        self.strong_em_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeAsterisks\")]\n    pub fn escape_asterisks(&self) -> Option<bool> {\n        self.escape_asterisks\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeAsterisks\")]\n    pub fn set_escape_asterisks(&mut self, value: Option<bool>) {\n        self.escape_asterisks = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeUnderscores\")]\n    pub fn escape_underscores(&self) -> Option<bool> {\n        self.escape_underscores\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeUnderscores\")]\n    pub fn set_escape_underscores(&mut self, value: Option<bool>) {\n        self.escape_underscores = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeMisc\")]\n    pub fn escape_misc(&self) -> Option<bool> {\n        self.escape_misc\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeMisc\")]\n    pub fn set_escape_misc(&mut self, value: Option<bool>) {\n        self.escape_misc = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"escapeAscii\")]\n    pub fn escape_ascii(&self) -> Option<bool> {\n        self.escape_ascii\n    }\n\n    #[wasm_bindgen(setter, js_name = \"escapeAscii\")]\n    pub fn set_escape_ascii(&mut self, value: Option<bool>) {\n        self.escape_ascii = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"codeLanguage\")]\n    pub fn code_language(&self) -> Option<String> {\n        self.code_language.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"codeLanguage\")]\n    pub fn set_code_language(&mut self, value: Option<String>) {\n        self.code_language = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn autolinks(&self) -> Option<bool> {\n        self.autolinks\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_autolinks(&mut self, value: Option<bool>) {\n        self.autolinks = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"defaultTitle\")]\n    pub fn default_title(&self) -> Option<bool> {\n        self.default_title\n    }\n\n    #[wasm_bindgen(setter, js_name = \"defaultTitle\")]\n    pub fn set_default_title(&mut self, value: Option<bool>) {\n        self.default_title = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"brInTables\")]\n    pub fn br_in_tables(&self) -> Option<bool> {\n        self.br_in_tables\n    }\n\n    #[wasm_bindgen(setter, js_name = \"brInTables\")]\n    pub fn set_br_in_tables(&mut self, value: Option<bool>) {\n        self.br_in_tables = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"highlightStyle\")]\n    pub fn highlight_style(&self) -> Option<WasmHighlightStyle> {\n        self.highlight_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"highlightStyle\")]\n    pub fn set_highlight_style(&mut self, value: Option<WasmHighlightStyle>) {\n        self.highlight_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"extractMetadata\")]\n    pub fn extract_metadata(&self) -> Option<bool> {\n        self.extract_metadata\n    }\n\n    #[wasm_bindgen(setter, js_name = \"extractMetadata\")]\n    pub fn set_extract_metadata(&mut self, value: Option<bool>) {\n        self.extract_metadata = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"whitespaceMode\")]\n    pub fn whitespace_mode(&self) -> Option<WasmWhitespaceMode> {\n        self.whitespace_mode\n    }\n\n    #[wasm_bindgen(setter, js_name = \"whitespaceMode\")]\n    pub fn set_whitespace_mode(&mut self, value: Option<WasmWhitespaceMode>) {\n        self.whitespace_mode = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"stripNewlines\")]\n    pub fn strip_newlines(&self) -> Option<bool> {\n        self.strip_newlines\n    }\n\n    #[wasm_bindgen(setter, js_name = \"stripNewlines\")]\n    pub fn set_strip_newlines(&mut self, value: Option<bool>) {\n        self.strip_newlines = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn wrap(&self) -> Option<bool> {\n        self.wrap\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_wrap(&mut self, value: Option<bool>) {\n        self.wrap = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"wrapWidth\")]\n    pub fn wrap_width(&self) -> Option<usize> {\n        self.wrap_width\n    }\n\n    #[wasm_bindgen(setter, js_name = \"wrapWidth\")]\n    pub fn set_wrap_width(&mut self, value: Option<usize>) {\n        self.wrap_width = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"convertAsInline\")]\n    pub fn convert_as_inline(&self) -> Option<bool> {\n        self.convert_as_inline\n    }\n\n    #[wasm_bindgen(setter, js_name = \"convertAsInline\")]\n    pub fn set_convert_as_inline(&mut self, value: Option<bool>) {\n        self.convert_as_inline = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"subSymbol\")]\n    pub fn sub_symbol(&self) -> Option<String> {\n        self.sub_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"subSymbol\")]\n    pub fn set_sub_symbol(&mut self, value: Option<String>) {\n        self.sub_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"supSymbol\")]\n    pub fn sup_symbol(&self) -> Option<String> {\n        self.sup_symbol.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"supSymbol\")]\n    pub fn set_sup_symbol(&mut self, value: Option<String>) {\n        self.sup_symbol = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"newlineStyle\")]\n    pub fn newline_style(&self) -> Option<WasmNewlineStyle> {\n        self.newline_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"newlineStyle\")]\n    pub fn set_newline_style(&mut self, value: Option<WasmNewlineStyle>) {\n        self.newline_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"codeBlockStyle\")]\n    pub fn code_block_style(&self) -> Option<WasmCodeBlockStyle> {\n        self.code_block_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"codeBlockStyle\")]\n    pub fn set_code_block_style(&mut self, value: Option<WasmCodeBlockStyle>) {\n        self.code_block_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"keepInlineImagesIn\")]\n    pub fn keep_inline_images_in(&self) -> Option<Vec<String>> {\n        self.keep_inline_images_in.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"keepInlineImagesIn\")]\n    pub fn set_keep_inline_images_in(&mut self, value: Option<Vec<String>>) {\n        self.keep_inline_images_in = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn preprocessing(&self) -> Option<WasmPreprocessingOptionsUpdate> {\n        self.preprocessing.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_preprocessing(&mut self, value: Option<WasmPreprocessingOptionsUpdate>) {\n        self.preprocessing = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn encoding(&self) -> Option<String> {\n        self.encoding.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_encoding(&mut self, value: Option<String>) {\n        self.encoding = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn debug(&self) -> Option<bool> {\n        self.debug\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_debug(&mut self, value: Option<bool>) {\n        self.debug = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"stripTags\")]\n    pub fn strip_tags(&self) -> Option<Vec<String>> {\n        self.strip_tags.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"stripTags\")]\n    pub fn set_strip_tags(&mut self, value: Option<Vec<String>>) {\n        self.strip_tags = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"preserveTags\")]\n    pub fn preserve_tags(&self) -> Option<Vec<String>> {\n        self.preserve_tags.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"preserveTags\")]\n    pub fn set_preserve_tags(&mut self, value: Option<Vec<String>>) {\n        self.preserve_tags = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"skipImages\")]\n    pub fn skip_images(&self) -> Option<bool> {\n        self.skip_images\n    }\n\n    #[wasm_bindgen(setter, js_name = \"skipImages\")]\n    pub fn set_skip_images(&mut self, value: Option<bool>) {\n        self.skip_images = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"linkStyle\")]\n    pub fn link_style(&self) -> Option<WasmLinkStyle> {\n        self.link_style\n    }\n\n    #[wasm_bindgen(setter, js_name = \"linkStyle\")]\n    pub fn set_link_style(&mut self, value: Option<WasmLinkStyle>) {\n        self.link_style = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"outputFormat\")]\n    pub fn output_format(&self) -> Option<WasmOutputFormat> {\n        self.output_format\n    }\n\n    #[wasm_bindgen(setter, js_name = \"outputFormat\")]\n    pub fn set_output_format(&mut self, value: Option<WasmOutputFormat>) {\n        self.output_format = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"includeDocumentStructure\")]\n    pub fn include_document_structure(&self) -> Option<bool> {\n        self.include_document_structure\n    }\n\n    #[wasm_bindgen(setter, js_name = \"includeDocumentStructure\")]\n    pub fn set_include_document_structure(&mut self, value: Option<bool>) {\n        self.include_document_structure = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"extractImages\")]\n    pub fn extract_images(&self) -> Option<bool> {\n        self.extract_images\n    }\n\n    #[wasm_bindgen(setter, js_name = \"extractImages\")]\n    pub fn set_extract_images(&mut self, value: Option<bool>) {\n        self.extract_images = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"maxImageSize\")]\n    pub fn max_image_size(&self) -> Option<u64> {\n        self.max_image_size\n    }\n\n    #[wasm_bindgen(setter, js_name = \"maxImageSize\")]\n    pub fn set_max_image_size(&mut self, value: Option<u64>) {\n        self.max_image_size = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"captureSvg\")]\n    pub fn capture_svg(&self) -> Option<bool> {\n        self.capture_svg\n    }\n\n    #[wasm_bindgen(setter, js_name = \"captureSvg\")]\n    pub fn set_capture_svg(&mut self, value: Option<bool>) {\n        self.capture_svg = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"inferDimensions\")]\n    pub fn infer_dimensions(&self) -> Option<bool> {\n        self.infer_dimensions\n    }\n\n    #[wasm_bindgen(setter, js_name = \"inferDimensions\")]\n    pub fn set_infer_dimensions(&mut self, value: Option<bool>) {\n        self.infer_dimensions = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"maxDepth\")]\n    pub fn max_depth(&self) -> Option<usize> {\n        self.max_depth\n    }\n\n    #[wasm_bindgen(setter, js_name = \"maxDepth\")]\n    pub fn set_max_depth(&mut self, value: Option<usize>) {\n        self.max_depth = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"excludeSelectors\")]\n    pub fn exclude_selectors(&self) -> Option<Vec<String>> {\n        self.exclude_selectors.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"excludeSelectors\")]\n    pub fn set_exclude_selectors(&mut self, value: Option<Vec<String>>) {\n        self.exclude_selectors = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn visitor(&self) -> Option<wasm_bindgen::JsValue> {\n        self.visitor.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_visitor(&mut self, value: Option<wasm_bindgen::JsValue>) {\n        self.visitor = value;\n    }\n}\n\n/// HTML preprocessing options for document cleanup before conversion.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmPreprocessingOptions {\n    enabled: bool,\n    preset: WasmPreprocessingPreset,\n    remove_navigation: bool,\n    remove_forms: bool,\n}\n\n#[wasm_bindgen]\nimpl WasmPreprocessingOptions {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<WasmPreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> WasmPreprocessingOptions {\n        WasmPreprocessingOptions {\n            enabled: enabled.unwrap_or(true),\n            preset: preset.unwrap_or_default(),\n            remove_navigation: remove_navigation.unwrap_or(true),\n            remove_forms: remove_forms.unwrap_or(true),\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn enabled(&self) -> bool {\n        self.enabled\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_enabled(&mut self, value: bool) {\n        self.enabled = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn preset(&self) -> WasmPreprocessingPreset {\n        self.preset\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_preset(&mut self, value: WasmPreprocessingPreset) {\n        self.preset = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"removeNavigation\")]\n    pub fn remove_navigation(&self) -> bool {\n        self.remove_navigation\n    }\n\n    #[wasm_bindgen(setter, js_name = \"removeNavigation\")]\n    pub fn set_remove_navigation(&mut self, value: bool) {\n        self.remove_navigation = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"removeForms\")]\n    pub fn remove_forms(&self) -> bool {\n        self.remove_forms\n    }\n\n    #[wasm_bindgen(setter, js_name = \"removeForms\")]\n    pub fn set_remove_forms(&mut self, value: bool) {\n        self.remove_forms = value;\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[wasm_bindgen]\n    pub fn default() -> WasmPreprocessingOptions {\n        html_to_markdown_rs::PreprocessingOptions::default().into()\n    }\n\n    /// Apply a partial update to these preprocessing options.\n    ///\n    /// Any specified fields in the update will override the current values.\n    /// Unspecified fields (None) are left unchanged.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    #[wasm_bindgen(js_name = \"applyUpdate\")]\n    pub fn apply_update(&self, _update: WasmPreprocessingOptionsUpdate) -> () {\n        ()\n    }\n\n    /// Create new preprocessing options from a partial update.\n    ///\n    /// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n    /// Fields not specified in the update keep their default values.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    ///\n    /// # Returns\n    ///\n    /// New `PreprocessingOptions` with specified updates applied to defaults\n    #[wasm_bindgen(js_name = \"fromUpdate\")]\n    pub fn from_update(update: WasmPreprocessingOptionsUpdate) -> WasmPreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::PreprocessingOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    #[wasm_bindgen]\n    pub fn from(update: WasmPreprocessingOptionsUpdate) -> WasmPreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::PreprocessingOptions::from(update_core).into()\n    }\n}\n\n/// Partial update for `PreprocessingOptions`.\n///\n/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n/// Only specified fields (Some values) will override existing options; None values leave the\n/// corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmPreprocessingOptionsUpdate {\n    enabled: Option<bool>,\n    preset: Option<WasmPreprocessingPreset>,\n    remove_navigation: Option<bool>,\n    remove_forms: Option<bool>,\n}\n\n#[wasm_bindgen]\nimpl WasmPreprocessingOptionsUpdate {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<WasmPreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> WasmPreprocessingOptionsUpdate {\n        WasmPreprocessingOptionsUpdate {\n            enabled,\n            preset,\n            remove_navigation,\n            remove_forms,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn enabled(&self) -> Option<bool> {\n        self.enabled\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_enabled(&mut self, value: Option<bool>) {\n        self.enabled = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn preset(&self) -> Option<WasmPreprocessingPreset> {\n        self.preset\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_preset(&mut self, value: Option<WasmPreprocessingPreset>) {\n        self.preset = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"removeNavigation\")]\n    pub fn remove_navigation(&self) -> Option<bool> {\n        self.remove_navigation\n    }\n\n    #[wasm_bindgen(setter, js_name = \"removeNavigation\")]\n    pub fn set_remove_navigation(&mut self, value: Option<bool>) {\n        self.remove_navigation = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"removeForms\")]\n    pub fn remove_forms(&self) -> Option<bool> {\n        self.remove_forms\n    }\n\n    #[wasm_bindgen(setter, js_name = \"removeForms\")]\n    pub fn set_remove_forms(&mut self, value: Option<bool>) {\n        self.remove_forms = value;\n    }\n}\n\n/// A structured document tree representing the semantic content of an HTML document.\n///\n/// Uses a flat node array with index-based parent/child references for efficient traversal.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmDocumentStructure {\n    nodes: Vec<WasmDocumentNode>,\n    source_format: Option<String>,\n}\n\n#[wasm_bindgen]\nimpl WasmDocumentStructure {\n    #[wasm_bindgen(constructor)]\n    pub fn new(nodes: Vec<WasmDocumentNode>, source_format: Option<String>) -> WasmDocumentStructure {\n        WasmDocumentStructure { nodes, source_format }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn nodes(&self) -> Vec<WasmDocumentNode> {\n        self.nodes.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_nodes(&mut self, value: Vec<WasmDocumentNode>) {\n        self.nodes = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"sourceFormat\")]\n    pub fn source_format(&self) -> Option<String> {\n        self.source_format.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"sourceFormat\")]\n    pub fn set_source_format(&mut self, value: Option<String>) {\n        self.source_format = value;\n    }\n}\n\n/// A single node in the document tree.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmDocumentNode {\n    id: String,\n    content: WasmNodeContent,\n    parent: Option<u32>,\n    children: Vec<u32>,\n    annotations: Vec<WasmTextAnnotation>,\n    attributes: Option<JsValue>,\n}\n\n#[wasm_bindgen]\nimpl WasmDocumentNode {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        id: String,\n        content: WasmNodeContent,\n        children: Vec<u32>,\n        annotations: Vec<WasmTextAnnotation>,\n        parent: Option<u32>,\n        attributes: Option<JsValue>,\n    ) -> WasmDocumentNode {\n        WasmDocumentNode {\n            id,\n            content,\n            parent,\n            children,\n            annotations,\n            attributes,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn id(&self) -> String {\n        self.id.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_id(&mut self, value: String) {\n        self.id = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn content(&self) -> WasmNodeContent {\n        self.content\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_content(&mut self, value: WasmNodeContent) {\n        self.content = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn parent(&self) -> Option<u32> {\n        self.parent\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_parent(&mut self, value: Option<u32>) {\n        self.parent = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn children(&self) -> Vec<u32> {\n        self.children.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_children(&mut self, value: Vec<u32>) {\n        self.children = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn annotations(&self) -> Vec<WasmTextAnnotation> {\n        self.annotations.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_annotations(&mut self, value: Vec<WasmTextAnnotation>) {\n        self.annotations = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn attributes(&self) -> Option<JsValue> {\n        self.attributes.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_attributes(&mut self, value: Option<JsValue>) {\n        self.attributes = value;\n    }\n}\n\n/// An inline text annotation with byte-range offsets.\n///\n/// Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmTextAnnotation {\n    start: u32,\n    end: u32,\n    kind: WasmAnnotationKind,\n}\n\n#[wasm_bindgen]\nimpl WasmTextAnnotation {\n    #[wasm_bindgen(constructor)]\n    pub fn new(start: u32, end: u32, kind: WasmAnnotationKind) -> WasmTextAnnotation {\n        WasmTextAnnotation { start, end, kind }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn start(&self) -> u32 {\n        self.start\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_start(&mut self, value: u32) {\n        self.start = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn end(&self) -> u32 {\n        self.end\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_end(&mut self, value: u32) {\n        self.end = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn kind(&self) -> WasmAnnotationKind {\n        self.kind\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_kind(&mut self, value: WasmAnnotationKind) {\n        self.kind = value;\n    }\n}\n\n/// The primary result of HTML conversion and extraction.\n///\n/// Contains the converted text output, optional structured document tree,\n/// metadata, extracted tables, images, and processing warnings.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::{convert, ConversionOptions};\n///\n/// let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n/// assert!(result.content.is_some());\n/// assert!(result.warnings.is_empty());\n/// ```\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmConversionResult {\n    content: Option<String>,\n    document: Option<WasmDocumentStructure>,\n    metadata: WasmHtmlMetadata,\n    tables: Vec<WasmTableData>,\n    images: Vec<String>,\n    warnings: Vec<WasmProcessingWarning>,\n}\n\n#[wasm_bindgen]\nimpl WasmConversionResult {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        metadata: Option<WasmHtmlMetadata>,\n        tables: Option<Vec<WasmTableData>>,\n        images: Option<Vec<String>>,\n        warnings: Option<Vec<WasmProcessingWarning>>,\n        content: Option<String>,\n        document: Option<WasmDocumentStructure>,\n    ) -> WasmConversionResult {\n        WasmConversionResult {\n            content,\n            document,\n            metadata: metadata.unwrap_or_default(),\n            tables: tables.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            warnings: warnings.unwrap_or_default(),\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn content(&self) -> Option<String> {\n        self.content.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_content(&mut self, value: Option<String>) {\n        self.content = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn document(&self) -> Option<WasmDocumentStructure> {\n        self.document.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_document(&mut self, value: Option<WasmDocumentStructure>) {\n        self.document = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn metadata(&self) -> WasmHtmlMetadata {\n        self.metadata.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_metadata(&mut self, value: WasmHtmlMetadata) {\n        self.metadata = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn tables(&self) -> Vec<WasmTableData> {\n        self.tables.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_tables(&mut self, value: Vec<WasmTableData>) {\n        self.tables = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn images(&self) -> Vec<String> {\n        self.images.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_images(&mut self, value: Vec<String>) {\n        self.images = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn warnings(&self) -> Vec<WasmProcessingWarning> {\n        self.warnings.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_warnings(&mut self, value: Vec<WasmProcessingWarning>) {\n        self.warnings = value;\n    }\n}\n\n/// A structured table grid with cell-level data including spans.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmTableGrid {\n    rows: u32,\n    cols: u32,\n    cells: Vec<WasmGridCell>,\n}\n\n#[wasm_bindgen]\nimpl WasmTableGrid {\n    #[wasm_bindgen(constructor)]\n    pub fn new(rows: Option<u32>, cols: Option<u32>, cells: Option<Vec<WasmGridCell>>) -> WasmTableGrid {\n        WasmTableGrid {\n            rows: rows.unwrap_or_default(),\n            cols: cols.unwrap_or_default(),\n            cells: cells.unwrap_or_default(),\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn rows(&self) -> u32 {\n        self.rows\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_rows(&mut self, value: u32) {\n        self.rows = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn cols(&self) -> u32 {\n        self.cols\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_cols(&mut self, value: u32) {\n        self.cols = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn cells(&self) -> Vec<WasmGridCell> {\n        self.cells.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_cells(&mut self, value: Vec<WasmGridCell>) {\n        self.cells = value;\n    }\n}\n\n/// A single cell in a table grid.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmGridCell {\n    content: String,\n    row: u32,\n    col: u32,\n    row_span: u32,\n    col_span: u32,\n    is_header: bool,\n}\n\n#[wasm_bindgen]\nimpl WasmGridCell {\n    #[wasm_bindgen(constructor)]\n    pub fn new(content: String, row: u32, col: u32, row_span: u32, col_span: u32, is_header: bool) -> WasmGridCell {\n        WasmGridCell {\n            content,\n            row,\n            col,\n            row_span,\n            col_span,\n            is_header,\n        }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn content(&self) -> String {\n        self.content.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_content(&mut self, value: String) {\n        self.content = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn row(&self) -> u32 {\n        self.row\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_row(&mut self, value: u32) {\n        self.row = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn col(&self) -> u32 {\n        self.col\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_col(&mut self, value: u32) {\n        self.col = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"rowSpan\")]\n    pub fn row_span(&self) -> u32 {\n        self.row_span\n    }\n\n    #[wasm_bindgen(setter, js_name = \"rowSpan\")]\n    pub fn set_row_span(&mut self, value: u32) {\n        self.row_span = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"colSpan\")]\n    pub fn col_span(&self) -> u32 {\n        self.col_span\n    }\n\n    #[wasm_bindgen(setter, js_name = \"colSpan\")]\n    pub fn set_col_span(&mut self, value: u32) {\n        self.col_span = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"isHeader\")]\n    pub fn is_header(&self) -> bool {\n        self.is_header\n    }\n\n    #[wasm_bindgen(setter, js_name = \"isHeader\")]\n    pub fn set_is_header(&mut self, value: bool) {\n        self.is_header = value;\n    }\n}\n\n/// A top-level extracted table with both structured data and markdown representation.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmTableData {\n    grid: WasmTableGrid,\n    markdown: String,\n}\n\n#[wasm_bindgen]\nimpl WasmTableData {\n    #[wasm_bindgen(constructor)]\n    pub fn new(grid: WasmTableGrid, markdown: String) -> WasmTableData {\n        WasmTableData { grid, markdown }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn grid(&self) -> WasmTableGrid {\n        self.grid.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_grid(&mut self, value: WasmTableGrid) {\n        self.grid = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn markdown(&self) -> String {\n        self.markdown.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_markdown(&mut self, value: String) {\n        self.markdown = value;\n    }\n}\n\n/// A non-fatal warning generated during HTML processing.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmProcessingWarning {\n    message: String,\n    kind: WasmWarningKind,\n}\n\n#[wasm_bindgen]\nimpl WasmProcessingWarning {\n    #[wasm_bindgen(constructor)]\n    pub fn new(message: String, kind: WasmWarningKind) -> WasmProcessingWarning {\n        WasmProcessingWarning { message, kind }\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn message(&self) -> String {\n        self.message.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_message(&mut self, value: String) {\n        self.message = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn kind(&self) -> WasmWarningKind {\n        self.kind\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_kind(&mut self, value: WasmWarningKind) {\n        self.kind = value;\n    }\n}\n\n/// Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n///\n/// This allows visitors to be passed around and shared while still being mutable.\n#[derive(Clone)]\n#[wasm_bindgen]\npub struct WasmVisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n#[wasm_bindgen]\nimpl WasmVisitorHandle {}\n\n/// Context information passed to all visitor methods.\n///\n/// Provides comprehensive metadata about the current node being visited,\n/// including its type, attributes, position in the DOM tree, and parent context.\n#[derive(Clone, Default)]\n#[wasm_bindgen]\npub struct WasmNodeContext {\n    node_type: WasmNodeType,\n    tag_name: String,\n    attributes: JsValue,\n    depth: usize,\n    index_in_parent: usize,\n    parent_tag: Option<String>,\n    is_inline: bool,\n}\n\n#[wasm_bindgen]\nimpl WasmNodeContext {\n    #[wasm_bindgen(constructor)]\n    pub fn new(\n        node_type: WasmNodeType,\n        tag_name: String,\n        attributes: JsValue,\n        depth: usize,\n        index_in_parent: usize,\n        is_inline: bool,\n        parent_tag: Option<String>,\n    ) -> WasmNodeContext {\n        WasmNodeContext {\n            node_type,\n            tag_name,\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline,\n        }\n    }\n\n    #[wasm_bindgen(getter, js_name = \"nodeType\")]\n    pub fn node_type(&self) -> WasmNodeType {\n        self.node_type\n    }\n\n    #[wasm_bindgen(setter, js_name = \"nodeType\")]\n    pub fn set_node_type(&mut self, value: WasmNodeType) {\n        self.node_type = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"tagName\")]\n    pub fn tag_name(&self) -> String {\n        self.tag_name.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"tagName\")]\n    pub fn set_tag_name(&mut self, value: String) {\n        self.tag_name = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn attributes(&self) -> JsValue {\n        self.attributes.clone()\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_attributes(&mut self, value: JsValue) {\n        self.attributes = value;\n    }\n\n    #[wasm_bindgen(getter)]\n    pub fn depth(&self) -> usize {\n        self.depth\n    }\n\n    #[wasm_bindgen(setter)]\n    pub fn set_depth(&mut self, value: usize) {\n        self.depth = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"indexInParent\")]\n    pub fn index_in_parent(&self) -> usize {\n        self.index_in_parent\n    }\n\n    #[wasm_bindgen(setter, js_name = \"indexInParent\")]\n    pub fn set_index_in_parent(&mut self, value: usize) {\n        self.index_in_parent = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"parentTag\")]\n    pub fn parent_tag(&self) -> Option<String> {\n        self.parent_tag.clone()\n    }\n\n    #[wasm_bindgen(setter, js_name = \"parentTag\")]\n    pub fn set_parent_tag(&mut self, value: Option<String>) {\n        self.parent_tag = value;\n    }\n\n    #[wasm_bindgen(getter, js_name = \"isInline\")]\n    pub fn is_inline(&self) -> bool {\n        self.is_inline\n    }\n\n    #[wasm_bindgen(setter, js_name = \"isInline\")]\n    pub fn set_is_inline(&mut self, value: bool) {\n        self.is_inline = value;\n    }\n}\n\n/// Text directionality of document content.\n///\n/// Corresponds to the HTML `dir` attribute and `bdi` element directionality.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmTextDirection {\n    LeftToRight = 0,\n    RightToLeft = 1,\n    Auto = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmTextDirection {\n    fn default() -> Self {\n        Self::LeftToRight\n    }\n}\n\n/// Link classification based on href value and document context.\n///\n/// Used to categorize links during extraction for filtering and analysis.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmLinkType {\n    Anchor = 0,\n    Internal = 1,\n    External = 2,\n    Email = 3,\n    Phone = 4,\n    Other = 5,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmLinkType {\n    fn default() -> Self {\n        Self::Anchor\n    }\n}\n\n/// Image source classification for proper handling and processing.\n///\n/// Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmImageType {\n    DataUri = 0,\n    InlineSvg = 1,\n    External = 2,\n    Relative = 3,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmImageType {\n    fn default() -> Self {\n        Self::DataUri\n    }\n}\n\n/// Structured data format type.\n///\n/// Identifies the schema/format used for structured data markup.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmStructuredDataType {\n    JsonLd = 0,\n    Microdata = 1,\n    RDFa = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmStructuredDataType {\n    fn default() -> Self {\n        Self::JsonLd\n    }\n}\n\n/// HTML preprocessing aggressiveness level.\n///\n/// Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmPreprocessingPreset {\n    Minimal = 0,\n    Standard = 1,\n    Aggressive = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmPreprocessingPreset {\n    fn default() -> Self {\n        Self::Minimal\n    }\n}\n\n/// Heading style options for Markdown output.\n///\n/// Controls how headings (h1-h6) are rendered in the output Markdown.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmHeadingStyle {\n    Underlined = 0,\n    Atx = 1,\n    AtxClosed = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmHeadingStyle {\n    fn default() -> Self {\n        Self::Underlined\n    }\n}\n\n/// List indentation character type.\n///\n/// Controls whether list items are indented with spaces or tabs.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmListIndentType {\n    Spaces = 0,\n    Tabs = 1,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmListIndentType {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n/// Whitespace handling strategy during conversion.\n///\n/// Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmWhitespaceMode {\n    Normalized = 0,\n    Strict = 1,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmWhitespaceMode {\n    fn default() -> Self {\n        Self::Normalized\n    }\n}\n\n/// Line break syntax in Markdown output.\n///\n/// Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmNewlineStyle {\n    Spaces = 0,\n    Backslash = 1,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmNewlineStyle {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n/// Code block fence style in Markdown output.\n///\n/// Determines how code blocks (`<pre><code>`) are rendered in Markdown.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmCodeBlockStyle {\n    Indented = 0,\n    Backticks = 1,\n    Tildes = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmCodeBlockStyle {\n    fn default() -> Self {\n        Self::Indented\n    }\n}\n\n/// Highlight rendering style for `<mark>` elements.\n///\n/// Controls how highlighted text is rendered in Markdown output.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmHighlightStyle {\n    DoubleEqual = 0,\n    Html = 1,\n    Bold = 2,\n    None = 3,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmHighlightStyle {\n    fn default() -> Self {\n        Self::DoubleEqual\n    }\n}\n\n/// Link rendering style in Markdown output.\n///\n/// Controls whether links and images use inline `[text](url)` syntax or\n/// reference-style `[text][1]` syntax with definitions collected at the end.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmLinkStyle {\n    Inline = 0,\n    Reference = 1,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmLinkStyle {\n    fn default() -> Self {\n        Self::Inline\n    }\n}\n\n/// Output format for conversion.\n///\n/// Specifies the target markup language format for the conversion output.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmOutputFormat {\n    Markdown = 0,\n    Djot = 1,\n    Plain = 2,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmOutputFormat {\n    fn default() -> Self {\n        Self::Markdown\n    }\n}\n\n/// The semantic content type of a document node.\n///\n/// Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmNodeContent {\n    Heading = 0,\n    Paragraph = 1,\n    List = 2,\n    ListItem = 3,\n    Table = 4,\n    Image = 5,\n    Code = 6,\n    Quote = 7,\n    DefinitionList = 8,\n    DefinitionItem = 9,\n    RawBlock = 10,\n    MetadataBlock = 11,\n    Group = 12,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmNodeContent {\n    fn default() -> Self {\n        Self::Heading\n    }\n}\n\n/// The type of an inline text annotation.\n///\n/// Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmAnnotationKind {\n    Bold = 0,\n    Italic = 1,\n    Underline = 2,\n    Strikethrough = 3,\n    Code = 4,\n    Subscript = 5,\n    Superscript = 6,\n    Highlight = 7,\n    Link = 8,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmAnnotationKind {\n    fn default() -> Self {\n        Self::Bold\n    }\n}\n\n/// Categories of processing warnings.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmWarningKind {\n    ImageExtractionFailed = 0,\n    EncodingFallback = 1,\n    TruncatedInput = 2,\n    MalformedHtml = 3,\n    SanitizationApplied = 4,\n    DepthLimitExceeded = 5,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmWarningKind {\n    fn default() -> Self {\n        Self::ImageExtractionFailed\n    }\n}\n\n/// Node type enumeration covering all HTML element types.\n///\n/// This enum categorizes all HTML elements that the converter recognizes,\n/// providing a coarse-grained classification for visitor dispatch.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmNodeType {\n    Text = 0,\n    Element = 1,\n    Heading = 2,\n    Paragraph = 3,\n    Div = 4,\n    Blockquote = 5,\n    Pre = 6,\n    Hr = 7,\n    List = 8,\n    ListItem = 9,\n    DefinitionList = 10,\n    DefinitionTerm = 11,\n    DefinitionDescription = 12,\n    Table = 13,\n    TableRow = 14,\n    TableCell = 15,\n    TableHeader = 16,\n    TableBody = 17,\n    TableHead = 18,\n    TableFoot = 19,\n    Link = 20,\n    Image = 21,\n    Strong = 22,\n    Em = 23,\n    Code = 24,\n    Strikethrough = 25,\n    Underline = 26,\n    Subscript = 27,\n    Superscript = 28,\n    Mark = 29,\n    Small = 30,\n    Br = 31,\n    Span = 32,\n    Article = 33,\n    Section = 34,\n    Nav = 35,\n    Aside = 36,\n    Header = 37,\n    Footer = 38,\n    Main = 39,\n    Figure = 40,\n    Figcaption = 41,\n    Time = 42,\n    Details = 43,\n    Summary = 44,\n    Form = 45,\n    Input = 46,\n    Select = 47,\n    Option = 48,\n    Button = 49,\n    Textarea = 50,\n    Label = 51,\n    Fieldset = 52,\n    Legend = 53,\n    Audio = 54,\n    Video = 55,\n    Picture = 56,\n    Source = 57,\n    Iframe = 58,\n    Svg = 59,\n    Canvas = 60,\n    Ruby = 61,\n    Rt = 62,\n    Rp = 63,\n    Abbr = 64,\n    Kbd = 65,\n    Samp = 66,\n    Var = 67,\n    Cite = 68,\n    Q = 69,\n    Del = 70,\n    Ins = 71,\n    Data = 72,\n    Meter = 73,\n    Progress = 74,\n    Output = 75,\n    Template = 76,\n    Slot = 77,\n    Html = 78,\n    Head = 79,\n    Body = 80,\n    Title = 81,\n    Meta = 82,\n    LinkTag = 83,\n    Style = 84,\n    Script = 85,\n    Base = 86,\n    Custom = 87,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmNodeType {\n    fn default() -> Self {\n        Self::Text\n    }\n}\n\n/// Result of a visitor callback.\n///\n/// Allows visitors to control the conversion flow by either proceeding\n/// with default behavior, providing custom output, skipping elements,\n/// preserving HTML, or signaling errors.\n#[wasm_bindgen]\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum WasmVisitResult {\n    Continue = 0,\n    Custom = 1,\n    Skip = 2,\n    PreserveHtml = 3,\n    Error = 4,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WasmVisitResult {\n    fn default() -> Self {\n        Self::Continue\n    }\n}\n\n#[allow(clippy::missing_errors_doc)]\n#[cfg(target_arch = \"wasm32\")]\n#[wasm_bindgen]\npub fn convert(html: String, options: Option<WasmConversionOptions>) -> Result<WasmConversionResult, JsValue> {\n    let mut __opt_binding = options;\n    let visitor_js: Option<wasm_bindgen::JsValue> = __opt_binding.as_mut().and_then(|o| o.visitor.take());\n    let mut options_core: Option<html_to_markdown_rs::ConversionOptions> =\n        __opt_binding.map(html_to_markdown_rs::ConversionOptions::from);\n    if let Some(js_val) = visitor_js {\n        let bridge = WasmHtmlVisitorBridge::new(js_val);\n        let handle = std::rc::Rc::new(std::cell::RefCell::new(bridge)) as html_to_markdown_rs::VisitorHandle;\n        if let Some(ref mut opts) = options_core {\n            opts.visitor = Some(handle);\n        }\n    }\n    html_to_markdown_rs::convert(&html, options_core)\n        .map(|val| val.into())\n        .map_err(|e| wasm_bindgen::JsValue::from_str(&e.to_string()))\n}\n\n#[cfg(target_arch = \"wasm32\")]\nmod __alef_wasm_bridge_htmlvisitor {\n    use super::*;\n\n    fn nodecontext_to_js_value(ctx: &html_to_markdown_rs::visitor::NodeContext) -> wasm_bindgen::JsValue {\n        let obj = js_sys::Object::new();\n        js_sys::Reflect::set(\n            &obj,\n            &wasm_bindgen::JsValue::from_str(\"nodeType\"),\n            &wasm_bindgen::JsValue::from_str(&format!(\"{:?}\", ctx.node_type)),\n        )\n        .ok();\n        js_sys::Reflect::set(\n            &obj,\n            &wasm_bindgen::JsValue::from_str(\"tagName\"),\n            &wasm_bindgen::JsValue::from_str(&ctx.tag_name),\n        )\n        .ok();\n        js_sys::Reflect::set(\n            &obj,\n            &wasm_bindgen::JsValue::from_str(\"depth\"),\n            &wasm_bindgen::JsValue::from_f64(ctx.depth as f64),\n        )\n        .ok();\n        js_sys::Reflect::set(\n            &obj,\n            &wasm_bindgen::JsValue::from_str(\"indexInParent\"),\n            &wasm_bindgen::JsValue::from_f64(ctx.index_in_parent as f64),\n        )\n        .ok();\n        js_sys::Reflect::set(\n            &obj,\n            &wasm_bindgen::JsValue::from_str(\"isInline\"),\n            &wasm_bindgen::JsValue::from_bool(ctx.is_inline),\n        )\n        .ok();\n        let parent_tag_val = match &ctx.parent_tag {\n            Some(s) => wasm_bindgen::JsValue::from_str(s),\n            None => wasm_bindgen::JsValue::null(),\n        };\n        js_sys::Reflect::set(&obj, &wasm_bindgen::JsValue::from_str(\"parentTag\"), &parent_tag_val).ok();\n        let attrs = js_sys::Object::new();\n        for (k, v) in &ctx.attributes {\n            js_sys::Reflect::set(\n                &attrs,\n                &wasm_bindgen::JsValue::from_str(k),\n                &wasm_bindgen::JsValue::from_str(v),\n            )\n            .ok();\n        }\n        js_sys::Reflect::set(&obj, &wasm_bindgen::JsValue::from_str(\"attributes\"), &attrs).ok();\n        obj.into()\n    }\n\n    pub struct WasmHtmlVisitorBridge {\n        js_obj: wasm_bindgen::JsValue,\n    }\n\n    impl std::fmt::Debug for WasmHtmlVisitorBridge {\n        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n            write!(f, \"WasmHtmlVisitorBridge\")\n        }\n    }\n\n    impl WasmHtmlVisitorBridge {\n        pub fn new(js_obj: wasm_bindgen::JsValue) -> Self {\n            Self { js_obj }\n        }\n    }\n\n    impl html_to_markdown_rs::visitor::HtmlVisitor for WasmHtmlVisitorBridge {\n        fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitElementStart\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_element_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitElementEnd\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_output));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_text(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitText\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_link(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _href: &str,\n            _text: &str,\n            _title: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitLink\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_href));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            args.push(&match _title {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_image(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: &str,\n            _alt: &str,\n            _title: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitImage\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_src));\n            args.push(&wasm_bindgen::JsValue::from_str(_alt));\n            args.push(&match _title {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_heading(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _level: u32,\n            _text: &str,\n            _id: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitHeading\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(&format!(\"{:?}\", _level)));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            args.push(&match _id {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_code_block(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _lang: Option<&str>,\n            _code: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitCodeBlock\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&match _lang {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            args.push(&wasm_bindgen::JsValue::from_str(_code));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_code_inline(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _code: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitCodeInline\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_code));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_list_item(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n            _marker: &str,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitListItem\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_bool(_ordered));\n            args.push(&wasm_bindgen::JsValue::from_str(_marker));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_list_start(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitListStart\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_bool(_ordered));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_list_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _ordered: bool,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitListEnd\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_bool(_ordered));\n            args.push(&wasm_bindgen::JsValue::from_str(_output));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitTableStart\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_table_row(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _cells: &[String],\n            _is_header: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitTableRow\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(&format!(\"{:?}\", _cells)));\n            args.push(&wasm_bindgen::JsValue::from_bool(_is_header));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_table_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitTableEnd\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_output));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_blockquote(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _content: &str,\n            _depth: usize,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitBlockquote\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_content));\n            args.push(&wasm_bindgen::JsValue::from_str(&format!(\"{:?}\", _depth)));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_strong(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitStrong\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_emphasis(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitEmphasis\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_strikethrough(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitStrikethrough\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_underline(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitUnderline\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_subscript(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitSubscript\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_superscript(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitSuperscript\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_mark(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitMark\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitLineBreak\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_horizontal_rule(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitHorizontalRule\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_custom_element(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _tag_name: &str,\n            _html: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitCustomElement\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_tag_name));\n            args.push(&wasm_bindgen::JsValue::from_str(_html));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_definition_list_start(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitDefinitionListStart\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_definition_term(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitDefinitionTerm\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_definition_description(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitDefinitionDescription\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_definition_list_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitDefinitionListEnd\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_output));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_form(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _action: Option<&str>,\n            _method: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitForm\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&match _action {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            args.push(&match _method {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_input(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _input_type: &str,\n            _name: Option<&str>,\n            _value: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitInput\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_input_type));\n            args.push(&match _name {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            args.push(&match _value {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_button(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitButton\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_audio(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitAudio\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&match _src {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_video(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitVideo\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&match _src {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_iframe(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _src: Option<&str>,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitIframe\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&match _src {\n                Some(s) => wasm_bindgen::JsValue::from_str(s),\n                None => wasm_bindgen::JsValue::null(),\n            });\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_details(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _open: bool,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitDetails\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_bool(_open));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_summary(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitSummary\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitFigureStart\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_figcaption(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _text: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitFigcaption\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_text));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n\n        fn visit_figure_end(\n            &mut self,\n            _ctx: &html_to_markdown_rs::NodeContext,\n            _output: &str,\n        ) -> html_to_markdown_rs::VisitResult {\n            let key = wasm_bindgen::JsValue::from_str(\"visitFigureEnd\");\n            let has_method = js_sys::Reflect::has(&self.js_obj, &key).unwrap_or(false);\n            if !has_method {\n                return html_to_markdown_rs::VisitResult::Continue;\n            }\n            let func_val = match js_sys::Reflect::get(&self.js_obj, &key) {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let func: js_sys::Function = match func_val.dyn_into() {\n                Ok(f) => f,\n                Err(_) => return html_to_markdown_rs::VisitResult::Continue,\n            };\n            let args = js_sys::Array::new();\n            args.push(&nodecontext_to_js_value(_ctx));\n            args.push(&wasm_bindgen::JsValue::from_str(_output));\n            let result = func.apply(&self.js_obj, &args);\n            match result {\n                Err(_) => html_to_markdown_rs::VisitResult::Continue,\n                Ok(val) => {\n                    if let Some(s) = val.as_string() {\n                        match s.to_lowercase().as_str() {\n                            \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                            \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                            \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                            other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\npub use __alef_wasm_bridge_htmlvisitor::*;\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmDocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: WasmDocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: serde_wasm_bindgen::from_value(val.open_graph.clone()).unwrap_or_default(),\n            twitter_card: serde_wasm_bindgen::from_value(val.twitter_card.clone()).unwrap_or_default(),\n            meta_tags: serde_wasm_bindgen::from_value(val.meta_tags.clone()).unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for WasmDocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: serde_wasm_bindgen::to_value(&val.open_graph).unwrap_or(JsValue::NULL),\n            twitter_card: serde_wasm_bindgen::to_value(&val.twitter_card).unwrap_or(JsValue::NULL),\n            meta_tags: serde_wasm_bindgen::to_value(&val.meta_tags).unwrap_or(JsValue::NULL),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmHeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: WasmHeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for WasmHeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmLinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: WasmLinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: serde_wasm_bindgen::from_value(val.attributes.clone()).unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for WasmLinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: serde_wasm_bindgen::to_value(&val.attributes).unwrap_or(JsValue::NULL),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: WasmImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: serde_wasm_bindgen::from_value(val.attributes.clone()).unwrap_or_default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for WasmImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: serde_wasm_bindgen::to_value(&val.attributes).unwrap_or(JsValue::NULL),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmStructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: WasmStructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for WasmStructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmHtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: WasmHtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for WasmHtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: WasmConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.chars().next().unwrap_or('*'),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for WasmConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: None,\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: WasmConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: Default::default(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for WasmConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten(),\n            exclude_selectors: val.exclude_selectors,\n            visitor: None,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmPreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: WasmPreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for WasmPreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmPreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: WasmPreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for WasmPreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmDocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: WasmDocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for WasmDocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmDocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: WasmDocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val\n                .attributes\n                .as_ref()\n                .and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for WasmDocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val\n                .attributes\n                .as_ref()\n                .and_then(|v| serde_wasm_bindgen::to_value(v).ok()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmTextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: WasmTextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for WasmTextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: WasmConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: Default::default(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for WasmConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmTableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: WasmTableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for WasmTableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmGridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: WasmGridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for WasmGridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmTableData> for html_to_markdown_rs::TableData {\n    fn from(val: WasmTableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for WasmTableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<WasmProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: WasmProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for WasmProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for WasmNodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: serde_wasm_bindgen::to_value(&val.attributes).unwrap_or(JsValue::NULL),\n            depth: val.depth,\n            index_in_parent: val.index_in_parent,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<WasmTextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: WasmTextDirection) -> Self {\n        match val {\n            WasmTextDirection::LeftToRight => Self::LeftToRight,\n            WasmTextDirection::RightToLeft => Self::RightToLeft,\n            WasmTextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for WasmTextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<WasmLinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: WasmLinkType) -> Self {\n        match val {\n            WasmLinkType::Anchor => Self::Anchor,\n            WasmLinkType::Internal => Self::Internal,\n            WasmLinkType::External => Self::External,\n            WasmLinkType::Email => Self::Email,\n            WasmLinkType::Phone => Self::Phone,\n            WasmLinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for WasmLinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<WasmImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: WasmImageType) -> Self {\n        match val {\n            WasmImageType::DataUri => Self::DataUri,\n            WasmImageType::InlineSvg => Self::InlineSvg,\n            WasmImageType::External => Self::External,\n            WasmImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for WasmImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<WasmStructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: WasmStructuredDataType) -> Self {\n        match val {\n            WasmStructuredDataType::JsonLd => Self::JsonLd,\n            WasmStructuredDataType::Microdata => Self::Microdata,\n            WasmStructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for WasmStructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<WasmPreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: WasmPreprocessingPreset) -> Self {\n        match val {\n            WasmPreprocessingPreset::Minimal => Self::Minimal,\n            WasmPreprocessingPreset::Standard => Self::Standard,\n            WasmPreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for WasmPreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<WasmHeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: WasmHeadingStyle) -> Self {\n        match val {\n            WasmHeadingStyle::Underlined => Self::Underlined,\n            WasmHeadingStyle::Atx => Self::Atx,\n            WasmHeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for WasmHeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<WasmListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: WasmListIndentType) -> Self {\n        match val {\n            WasmListIndentType::Spaces => Self::Spaces,\n            WasmListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for WasmListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<WasmWhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: WasmWhitespaceMode) -> Self {\n        match val {\n            WasmWhitespaceMode::Normalized => Self::Normalized,\n            WasmWhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for WasmWhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<WasmNewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: WasmNewlineStyle) -> Self {\n        match val {\n            WasmNewlineStyle::Spaces => Self::Spaces,\n            WasmNewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for WasmNewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<WasmCodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: WasmCodeBlockStyle) -> Self {\n        match val {\n            WasmCodeBlockStyle::Indented => Self::Indented,\n            WasmCodeBlockStyle::Backticks => Self::Backticks,\n            WasmCodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for WasmCodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<WasmHighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: WasmHighlightStyle) -> Self {\n        match val {\n            WasmHighlightStyle::DoubleEqual => Self::DoubleEqual,\n            WasmHighlightStyle::Html => Self::Html,\n            WasmHighlightStyle::Bold => Self::Bold,\n            WasmHighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for WasmHighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<WasmLinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: WasmLinkStyle) -> Self {\n        match val {\n            WasmLinkStyle::Inline => Self::Inline,\n            WasmLinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for WasmLinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<WasmOutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: WasmOutputFormat) -> Self {\n        match val {\n            WasmOutputFormat::Markdown => Self::Markdown,\n            WasmOutputFormat::Djot => Self::Djot,\n            WasmOutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for WasmOutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<WasmNodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: WasmNodeContent) -> Self {\n        match val {\n            WasmNodeContent::Heading => Self::Heading {\n                level: Default::default(),\n                text: Default::default(),\n            },\n            WasmNodeContent::Paragraph => Self::Paragraph {\n                text: Default::default(),\n            },\n            WasmNodeContent::List => Self::List {\n                ordered: Default::default(),\n            },\n            WasmNodeContent::ListItem => Self::ListItem {\n                text: Default::default(),\n            },\n            WasmNodeContent::Table => Self::Table {\n                grid: Default::default(),\n            },\n            WasmNodeContent::Image => Self::Image {\n                description: Default::default(),\n                src: Default::default(),\n                image_index: Default::default(),\n            },\n            WasmNodeContent::Code => Self::Code {\n                text: Default::default(),\n                language: Default::default(),\n            },\n            WasmNodeContent::Quote => Self::Quote,\n            WasmNodeContent::DefinitionList => Self::DefinitionList,\n            WasmNodeContent::DefinitionItem => Self::DefinitionItem {\n                term: Default::default(),\n                definition: Default::default(),\n            },\n            WasmNodeContent::RawBlock => Self::RawBlock {\n                format: Default::default(),\n                content: Default::default(),\n            },\n            WasmNodeContent::MetadataBlock => Self::MetadataBlock {\n                entries: Default::default(),\n            },\n            WasmNodeContent::Group => Self::Group {\n                label: Default::default(),\n                heading_level: Default::default(),\n                heading_text: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for WasmNodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { .. } => Self::Heading,\n            html_to_markdown_rs::NodeContent::Paragraph { .. } => Self::Paragraph,\n            html_to_markdown_rs::NodeContent::List { .. } => Self::List,\n            html_to_markdown_rs::NodeContent::ListItem { .. } => Self::ListItem,\n            html_to_markdown_rs::NodeContent::Table { .. } => Self::Table,\n            html_to_markdown_rs::NodeContent::Image { .. } => Self::Image,\n            html_to_markdown_rs::NodeContent::Code { .. } => Self::Code,\n            html_to_markdown_rs::NodeContent::Quote => Self::Quote,\n            html_to_markdown_rs::NodeContent::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeContent::DefinitionItem { .. } => Self::DefinitionItem,\n            html_to_markdown_rs::NodeContent::RawBlock { .. } => Self::RawBlock,\n            html_to_markdown_rs::NodeContent::MetadataBlock { .. } => Self::MetadataBlock,\n            html_to_markdown_rs::NodeContent::Group { .. } => Self::Group,\n        }\n    }\n}\n\nimpl From<WasmAnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: WasmAnnotationKind) -> Self {\n        match val {\n            WasmAnnotationKind::Bold => Self::Bold,\n            WasmAnnotationKind::Italic => Self::Italic,\n            WasmAnnotationKind::Underline => Self::Underline,\n            WasmAnnotationKind::Strikethrough => Self::Strikethrough,\n            WasmAnnotationKind::Code => Self::Code,\n            WasmAnnotationKind::Subscript => Self::Subscript,\n            WasmAnnotationKind::Superscript => Self::Superscript,\n            WasmAnnotationKind::Highlight => Self::Highlight,\n            WasmAnnotationKind::Link => Self::Link {\n                url: Default::default(),\n                title: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for WasmAnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self::Bold,\n            html_to_markdown_rs::AnnotationKind::Italic => Self::Italic,\n            html_to_markdown_rs::AnnotationKind::Underline => Self::Underline,\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::AnnotationKind::Code => Self::Code,\n            html_to_markdown_rs::AnnotationKind::Subscript => Self::Subscript,\n            html_to_markdown_rs::AnnotationKind::Superscript => Self::Superscript,\n            html_to_markdown_rs::AnnotationKind::Highlight => Self::Highlight,\n            html_to_markdown_rs::AnnotationKind::Link { .. } => Self::Link,\n        }\n    }\n}\n\nimpl From<WasmWarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: WasmWarningKind) -> Self {\n        match val {\n            WasmWarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            WasmWarningKind::EncodingFallback => Self::EncodingFallback,\n            WasmWarningKind::TruncatedInput => Self::TruncatedInput,\n            WasmWarningKind::MalformedHtml => Self::MalformedHtml,\n            WasmWarningKind::SanitizationApplied => Self::SanitizationApplied,\n            WasmWarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for WasmWarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for WasmNodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for WasmVisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        match val {\n            html_to_markdown_rs::VisitResult::Continue => Self::Continue,\n            html_to_markdown_rs::VisitResult::Custom(..) => Self::Custom,\n            html_to_markdown_rs::VisitResult::Skip => Self::Skip,\n            html_to_markdown_rs::VisitResult::PreserveHtml => Self::PreserveHtml,\n            html_to_markdown_rs::VisitResult::Error(..) => Self::Error,\n        }\n    }\n}\n\n/// Return the error code string for a `html_to_markdown_rs::error::ConversionError` variant.\n#[allow(dead_code)]\nfn conversion_error_error_code(e: &html_to_markdown_rs::error::ConversionError) -> &'static str {\n    #[allow(unreachable_patterns)]\n    match e {\n        html_to_markdown_rs::error::ConversionError::ParseError(..) => \"parse_error\",\n        html_to_markdown_rs::error::ConversionError::SanitizationError(..) => \"sanitization_error\",\n        html_to_markdown_rs::error::ConversionError::ConfigError(..) => \"config_error\",\n        html_to_markdown_rs::error::ConversionError::IoError(..) => \"io_error\",\n        html_to_markdown_rs::error::ConversionError::Panic(..) => \"panic\",\n        html_to_markdown_rs::error::ConversionError::InvalidInput(..) => \"invalid_input\",\n        html_to_markdown_rs::error::ConversionError::Other(..) => \"other\",\n        _ => \"conversion_error\",\n    }\n}\n\n/// Convert a `html_to_markdown_rs::error::ConversionError` error to a `JsValue` object with `code` and `message` fields.\n#[allow(dead_code)]\nfn conversion_error_to_js_value(e: html_to_markdown_rs::error::ConversionError) -> wasm_bindgen::JsValue {\n    let code = conversion_error_error_code(&e);\n    let message = e.to_string();\n    let obj = js_sys::Object::new();\n    js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok();\n    js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok();\n    obj.into()\n}\n"
  },
  {
    "path": "deny.toml",
    "content": "[graph]\ntargets = []\nall-features = false\nno-default-features = false\nexclude = [\"html-to-markdown-wasm-wasmtime-tests\"]\n\n[output]\nfeature-depth = 1\n\n[advisories]\nignore = []\n\n[licenses]\nallow = [\n  \"Apache-2.0\",\n  \"Apache-2.0 WITH LLVM-exception\",\n  \"MIT\",\n  \"BSD-2-Clause\",\n  \"BSD-3-Clause\",\n  \"ISC\",\n  \"Zlib\",\n  \"Unlicense\",\n  \"CC0-1.0\",\n  \"CDLA-Permissive-2.0\",\n  \"Unicode-3.0\",\n  \"bzip2-1.0.6\",\n]\nconfidence-threshold = 0.8\nexceptions = [\n  # cbindgen is a build-only tool for generating C headers, not linked into output\n  { allow = [\n    \"MPL-2.0\",\n  ], crate = \"cbindgen\" },\n]\n\n[licenses.private]\nignore = false\nregistries = []\n\n[bans]\nmultiple-versions = \"warn\"\nwildcards = \"allow\"\nhighlight = \"all\"\nworkspace-default-features = \"allow\"\nexternal-default-features = \"allow\"\nallow = []\ndeny = []\nskip = []\nskip-tree = []\n\n[sources]\nunknown-registry = \"warn\"\nunknown-git = \"warn\"\nallow-registry = [\"https://github.com/rust-lang/crates.io-index\"]\nallow-git = []\n\n[sources.allow-org]\ngithub = []\ngitlab = []\nbitbucket = []\n"
  },
  {
    "path": "docs/CNAME",
    "content": "docs.html-to-markdown.kreuzberg.dev\n"
  },
  {
    "path": "docs/api-reference.md",
    "content": "---\ntitle: API reference\ndescription: \"Generated, binding-accurate API pages from the Rust source (alef docs).\"\n---\n\n## API reference\n\nThese pages are **generated** from the public Rust API surface. You can run `task alef:generate` (or `alef docs`) after changing types, options, or `alef.toml` so the Markdown under `docs/reference/` stays in sync.\n\n| Topic | Link |\n|-------|------|\n| All types and result shapes | [Types](reference/types.md) |\n| Options, metadata, and field tables | [Configuration (generated)](reference/configuration.md) |\n| Error / enum reference | [Error types (generated)](reference/errors.md) |\n\n### Language APIs\n\n| Language | Page |\n|----------|------|\n| Rust | [api-rust](reference/api-rust.md) |\n| Python | [api-python](reference/api-python.md) |\n| TypeScript / Node | [api-typescript](reference/api-typescript.md) |\n| Go | [api-go](reference/api-go.md) |\n| Ruby | [api-ruby](reference/api-ruby.md) |\n| PHP | [api-php](reference/api-php.md) |\n| Java | [api-java](reference/api-java.md) |\n| C# | [api-csharp](reference/api-csharp.md) |\n| Elixir | [api-elixir](reference/api-elixir.md) |\n| R | [api-r](reference/api-r.md) |\n| C (FFI) | [api-c](reference/api-c.md) |\n| WebAssembly | [api-wasm](reference/api-wasm.md) |\n\nFor how to *use* the API in your language, start with [Language guides](language-guides.md) and the narrative [Configuration](configuration.md) and [Error handling](errors.md) pages; this section is the exhaustive field-level reference.\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# Architecture\n\nhtml-to-markdown has a Rust core with 12 language bindings on top: Python, TypeScript, Go, Ruby, PHP, Java, C#, Elixir, R, C, WASM, and a pre-built CLI. All 12 call the same `convert()` function. There is no separate conversion logic per language.\n\n## Crate Layout\n\nThe repository root is a **[Cargo workspace](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)**: the top-level `Cargo.toml` lists every Rust package that belongs to the project, they share one **`Cargo.lock`**, and you build or test them from the repo root with `cargo build`, `cargo test -p <crate-name>`, or `cargo build --workspace`. That keeps the core library and all first-party bindings on the same version line and guarantees they compile against each other.\n\nUnder **`crates/`** there are **eight packages** (each is its own crate with its own `Cargo.toml`):\n\n- **Core and shipping:** `html-to-markdown` (the library published as *html-to-markdown-rs*), `html-to-markdown-cli`, and `html-to-markdown-ffi` (`libhtml_to_markdown` for Go, Java, C#, C, and similar consumers).\n- **Native bindings compiled from this repo:** `html-to-markdown-py`, `html-to-markdown-node`, `html-to-markdown-php`, and `html-to-markdown-wasm` (each embeds or wraps the core differently—PyO3, NAPI-RS, ext-php-rs, wasm-bindgen).\n- **Shared glue:** `html-to-markdown-bindings-common` holds option marshalling and other bits reused by those bindings so they do not duplicate the same types.\n\nThe workspace also includes **`tools/`** crates (for example snippet generation and checks); they are not part of the public layout below but are built with the same workspace commands.\n\n```text\ncrates/\n├── html-to-markdown/          # Core library (html-to-markdown-rs on crates.io)\n├── html-to-markdown-cli/      # CLI binary\n├── html-to-markdown-ffi/      # C FFI shared library (libhtml_to_markdown)\n├── html-to-markdown-node/     # Node.js binding (NAPI-RS)\n├── html-to-markdown-py/       # Python binding (PyO3)\n├── html-to-markdown-php/      # PHP extension (ext-php-rs)\n├── html-to-markdown-wasm/     # WebAssembly (wasm-bindgen)\n└── html-to-markdown-bindings-common/  # Shared option marshalling\n```\n\nGo, Ruby, Java, C#, Elixir, and R all live under `packages/` and call into `libhtml_to_markdown` (the C FFI crate) via their language's native FFI mechanism.\n\n## Core Library\n\n`crates/html-to-markdown/` is the only place where conversion logic lives. Its public API is one function:\n\n```rust\npub fn convert(html: &str, options: Option<ConversionOptions>) -> Result<ConversionResult>\n```\n\nInternally, the conversion happens in five phases:\n\n1. **Preprocessing.** Scripts and styles are stripped with a fast byte-scanner pass. If `preprocess` is true, navigation elements are also removed before parsing.\n2. **Parsing.** The HTML is parsed by [astral-tl](https://crates.io/crates/astral-tl) (`tl` in the Cargo manifest), a high-performance single-pass HTML parser that targets modern HTML5.\n3. **DOM walk.** The library traverses the parse tree in pre-order. For every node it calls element-handler functions, accumulating Markdown into a string buffer. If a visitor is registered, each handler calls the corresponding `HtmlVisitor` method and uses the returned `VisitResult` to either proceed, substitute custom output, skip, or abort.\n4. **Post-processing.** Whitespace normalization, trailing newlines, reference-style link definitions, and Markdown dialect adjustments (Djot or plain text) are applied to the accumulated output.\n5. **Extraction.** Tables, inline images, document structure, and metadata are collected during the walk and assembled into `ConversionResult`.\n\n## Binding Mechanisms\n\nEach language reaches Rust via a different mechanism. All paths fall into one of two categories: direct embedding (the Rust code compiles into the host language's native extension module) or the C FFI shared library (the host language loads `libhtml_to_markdown` at runtime).\n\n### Direct embedding\n\n| Language | Mechanism | Crate |\n|----------|-----------|-------|\n| Python | [PyO3](https://pyo3.rs) extension module | `html-to-markdown-py` |\n| TypeScript / Node.js | [NAPI-RS](https://napi.rs) Node-API addon | `html-to-markdown-node` |\n| PHP | [ext-php-rs](https://davidcole1340.github.io/ext-php-rs/) PHP extension | `html-to-markdown-php` |\n| Ruby | [Magnus](https://github.com/matsadler/magnus) gem extension | `packages/ruby` |\n| Elixir | [Rustler](https://github.com/rusterlium/rustler) NIF | `packages/elixir` |\n| R | [extendr](https://extendr.github.io) R extension | `packages/r` |\n| WebAssembly | [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/) | `html-to-markdown-wasm` |\n\nFor these, the conversion function is compiled directly into the native extension. The host language never sees the C ABI.\n\n### Via libhtml_to_markdown\n\n| Language | Mechanism | Notes |\n|----------|-----------|-------|\n| Go | cgo | Bundles `libhtml_to_markdown.a` in the Go module. |\n| Java | Foreign Function & Memory API (Java 22+) | Extracts the shared library from the JAR at startup. |\n| C# | P/Invoke (`DllImport`) | Loads `libhtml_to_markdown` via `NativeLibrary`. |\n| C | Direct link | Consumer links against `html-to-markdown-ffi`. |\n\nThe C FFI crate exposes a minimal, null-safe C API and catches any Rust panics at the boundary, converting them to `ConversionError::Panic` rather than letting them unwind across the ABI.\n\n## Feature Flags\n\nThe core library compiles with six Cargo features. Bindings enable the subset they expose.\n\n| Feature | What it gates |\n|---------|---------------|\n| `metadata` | All metadata extraction code and `HtmlMetadata` types (on by default) |\n| `visitor` | `HtmlVisitor` trait and `ConversionError::Visitor` |\n| `inline-images` | Data-URI decoder and inline SVG extractor |\n| `serde` | `Serialize`/`Deserialize` on option and result types |\n| `full` | All four optional features combined |\n\nThe WASM build omits `inline-images` (no filesystem, no image decoder in the browser sandbox).\n\n## Thread Safety and Allocation\n\nThe core library is stateless and does not use global state. `convert()` is safe to call from multiple threads simultaneously. Each call allocates and frees its own buffers independently.\n\nBindings that cross the C FFI boundary use arena-managed allocation: the caller passes a pointer and the library writes into caller-controlled memory, minimising back-and-forth across the ABI. Memory returned across the FFI must be freed with `html_to_markdown_free_result`; the Go and Java bindings handle this internally.\n\n## HTML Parser\n\n[astral-tl](https://crates.io/crates/astral-tl) is a fork of the `tl` crate maintained by the Kreuzberg team. It is a streaming HTML tokenizer and DOM builder optimised for correctness on real-world HTML. It handles malformed HTML by following browser-compatible recovery rules, so conversion degrades gracefully on broken markup rather than aborting.\n\nThe parser produces an in-memory tree. The library does not stream: the entire HTML is parsed before the DOM walk starts. For very large documents (multi-MB) this is the dominant memory cost.\n\n## Single-Pass Traversal\n\nThe DOM walk is pre-order and single-pass. Metadata collection, visitor dispatch, table extraction, and Markdown serialisation all happen in the same traversal. There is no separate analysis phase and no second pass over the tree.\n\nThis design keeps memory low (the output buffer grows steadily; there is no intermediate AST) and makes adding a new output format a matter of adding a new handler, not restructuring the pipeline.\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/cli.md",
    "content": "# CLI\n\nThe `html-to-markdown` CLI converts HTML files or URLs to Markdown from the command line.\n\n## Installation\n\n```bash\ncargo install html-to-markdown-cli\n```\n\nOr via Homebrew:\n\n```bash\nbrew install kreuzberg-dev/tap/html-to-markdown\n```\n\n## Basic Usage\n\n```bash\n# Convert stdin\necho '<h1>Title</h1><p>Content</p>' | html-to-markdown\n\n# Convert a file\nhtml-to-markdown input.html\n\n# Convert a file and save output\nhtml-to-markdown input.html -o output.md\n\n# Fetch and convert a remote URL\nhtml-to-markdown --url https://example.com > output.md\n```\n\n## Input\n\n| Flag | Description |\n|------|-------------|\n| `FILE` | Input HTML file. Use `-` or omit for stdin. |\n| `--url URL` | Fetch HTML from a URL (alternative to file/stdin). |\n| `--user-agent UA` | Custom User-Agent header when using `--url`. |\n| `-e`, `--encoding ENCODING` | Input character encoding (default: `utf-8`). |\n\n## Output\n\n| Flag | Description |\n|------|-------------|\n| `-o`, `--output FILE` | Write output to file (default: stdout). |\n| `-f`, `--output-format FORMAT` | Output format: `markdown` (default) or `djot`. |\n\n## Heading Options\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--heading-style STYLE` | `atx`, `underlined`, `atx-closed` | `atx` | How headings are formatted. `atx` uses `#` prefixes; `underlined` uses `===`/`---` for h1/h2. |\n\n## List Options\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--list-indent-type TYPE` | `spaces`, `tab` | `spaces` | Indentation character for nested lists. |\n| `--list-indent-width N` | 1–8 | `2` | Spaces per nesting level. |\n| `-b`, `--bullets CHARS` | e.g. `*+-` | `-` | Characters to cycle through for unordered list markers. |\n\n## Text Formatting\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--strong-em-symbol CHAR` | `*`, `_` | `*` | Symbol for bold and italic. |\n| `--newline-style STYLE` | `backslash`, `spaces` | `backslash` | How `<br>` tags are rendered. |\n| `--sub-symbol SYMBOL` | any string | `\"\"` | Wrapper symbol for `<sub>` text. |\n| `--sup-symbol SYMBOL` | any string | `\"\"` | Wrapper symbol for `<sup>` text. |\n| `--highlight-style STYLE` | `double-equal`, `html`, `bold`, `none` | `double-equal` | Rendering of `<mark>` elements. |\n| `--escape-asterisks` | — | off | Escape `*` characters. |\n| `--escape-underscores` | — | off | Escape `_` characters. |\n| `--escape-misc` | — | off | Escape `[`, `]`, `<`, `>`, `#`, etc. |\n| `--escape-ascii` | — | off | Escape all ASCII punctuation (strict CommonMark). |\n\n## Code Blocks\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--code-block-style STYLE` | `indented`, `backticks`, `tildes` | `indented` | Format for multi-line code blocks. |\n| `-l`, `--code-language LANG` | any string | `\"\"` | Default language tag for fenced code blocks. |\n\n## Links\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--no-autolinks` | — | off | Disable autolink conversion. By default, when link text equals the href, the output is `<url>`. Pass this flag to emit `[url](url)` instead. |\n| `--default-title` | — | off | Use href as link title when no `title` attribute exists. |\n| `--link-style STYLE` | `inline`, `reference` | `inline` | `inline` emits `[text](url)`. `reference` emits `[text][1]` with numbered definitions at the end of the document. |\n\n## Images\n\n| Flag | Description |\n|------|-------------|\n| `--keep-inline-images-in ELEMENTS` | Comma-separated element names where images stay as `![alt](src)` (e.g. `a,strong`). |\n| `--skip-images` | Drop all `<img>` elements entirely. No `![alt](src)` output, no alt-text fallback. |\n\n## Tables\n\n| Flag | Description |\n|------|-------------|\n| `--br-in-tables` | Preserve line breaks in table cells as `<br>` rather than converting to spaces. |\n\n## Whitespace\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--whitespace-mode MODE` | `normalized`, `strict` | `normalized` | Whitespace handling strategy. |\n| `--strip-newlines` | — | off | Remove all newlines from input before processing. |\n\n## Wrapping\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `-w`, `--wrap` | — | off | Enable output line wrapping. |\n| `--wrap-width N` | 20–500 | `80` | Column width for wrapping. |\n\n## Element Handling\n\n| Flag | Description |\n|------|-------------|\n| `--convert-as-inline` | Treat block elements as inline (no paragraph breaks). |\n| `--strip-tags TAGS` | Comma-separated tags to strip (text content preserved, no Markdown conversion). |\n| `--preserve-tags TAGS` | Comma-separated tags to emit verbatim as raw HTML instead of converting. |\n| `--max-depth N` | Silently truncate subtrees beyond this DOM nesting depth. Omit for unlimited depth. |\n\n## Metadata\n\n| Flag | Description |\n|------|-------------|\n| `--extract-metadata` | Prepend a metadata comment block (title, description, Open Graph, links, images) to the Markdown output. In `--json` mode, populates the `metadata` field. |\n\n## JSON Output\n\n`--json` swaps the default Markdown output for a full `ConversionResult` object: `content`, `metadata`, `tables`, `document`, `images`, and `warnings` on a single JSON value. The flags in this section control which fields are populated.\n\n| Flag | Description |\n|------|-------------|\n| `--json` | Output a full `ConversionResult` as JSON instead of Markdown. |\n| `--include-structure` | Populate `document` with the parsed semantic tree. Requires `--json`. |\n| `--extract-inline-images` | Populate `images` with extracted data URIs and SVGs. Requires `--json`. |\n| `--no-content` | Skip Markdown rendering. `content` is empty, metadata and structure still populate. Requires `--json`. |\n| `--show-warnings` | Print each processing warning to stderr as `Warning [<kind>]: <message>`. Works with or without `--json`. |\n\n## Preprocessing\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `-p`, `--preprocess` | — | off | Clean up HTML before conversion. |\n| `--preset LEVEL` | `minimal`, `standard`, `aggressive` | `standard` | Preprocessing aggressiveness (requires `--preprocess`). |\n| `--keep-navigation` | — | off | Preserve `<nav>` elements during preprocessing (requires `--preprocess`). |\n| `--keep-forms` | — | off | Preserve form elements during preprocessing (requires `--preprocess`). |\n\n## Debugging\n\n| Flag | Description |\n|------|-------------|\n| `--debug` | Output diagnostic warnings and information. |\n\n## Shell Completions and Man Page\n\n```bash\n# Generate shell completions\nhtml-to-markdown --generate-completion bash > html-to-markdown.bash\nhtml-to-markdown --generate-completion zsh > _html-to-markdown\nhtml-to-markdown --generate-completion fish > html-to-markdown.fish\n\n# Generate man page\nhtml-to-markdown --generate-man > html-to-markdown.1\n```\n\n## Examples\n\n```bash\n# Web scraping with aggressive preprocessing\nhtml-to-markdown page.html --preprocess --preset aggressive\n\n# Extract full structured result as JSON\nhtml-to-markdown input.html --json \\\n    --extract-metadata --include-structure \\\n    -o output.json\n\n# Discord/Slack-friendly output (2-space list indents)\nhtml-to-markdown input.html --list-indent-width 2\n\n# Custom heading and list styles\nhtml-to-markdown input.html \\\n    --heading-style atx \\\n    --bullets '*' \\\n    --list-indent-width 2\n\n# Fetch and convert with Djot output\nhtml-to-markdown --url https://example.com --output-format djot\n```\n\n## CLI Flag ↔ ConversionOptions Mapping\n\nEach CLI flag has a corresponding `ConversionOptions` field. Library users can cross-reference here when translating a CLI invocation to code (or vice versa).\n\n| CLI Flag | `ConversionOptions` field | Notes |\n|----------|--------------------------|-------|\n| `--output-format FORMAT` | `output_format` | `\"markdown\"` \\| `\"djot\"` \\| `\"plain\"` \\| `\"none\"` |\n| `--heading-style STYLE` | `heading_style` | `\"atx\"` \\| `\"underlined\"` \\| `\"atx-closed\"` |\n| `--list-indent-type TYPE` | `list_indent_type` | `\"spaces\"` \\| `\"tab\"` |\n| `--list-indent-width N` | `list_indent_width` | integer |\n| `--bullets CHARS` | `bullets` | string |\n| `--strong-em-symbol CHAR` | `strong_em_symbol` | `\"*\"` \\| `\"_\"` |\n| `--newline-style STYLE` | `newline_style` | `\"backslash\"` \\| `\"spaces\"` |\n| `--sub-symbol SYMBOL` | `sub_symbol` | string |\n| `--sup-symbol SYMBOL` | `sup_symbol` | string |\n| `--highlight-style STYLE` | `highlight_style` | `\"double-equal\"` \\| `\"html\"` \\| `\"bold\"` \\| `\"none\"` |\n| `--escape-asterisks` | `escape_asterisks` | boolean flag |\n| `--escape-underscores` | `escape_underscores` | boolean flag |\n| `--escape-misc` | `escape_misc` | boolean flag |\n| `--escape-ascii` | `escape_ascii` | boolean flag |\n| `--code-block-style STYLE` | `code_block_style` | `\"indented\"` \\| `\"backticks\"` \\| `\"tildes\"` |\n| `-l, --code-language LANG` | `code_language` | string |\n| `--no-autolinks` | `autolinks` | inverted: flag sets `autolinks = false`; default is `true` |\n| `--default-title` | `default_title` | boolean flag |\n| `--link-style STYLE` | `link_style` | `\"inline\"` \\| `\"reference\"` |\n| `--keep-inline-images-in ELEMS` | `keep_inline_images_in` | comma-separated tag list |\n| `--skip-images` | `skip_images` | boolean flag |\n| `--br-in-tables` | `br_in_tables` | boolean flag |\n| `--whitespace-mode MODE` | `whitespace_mode` | `\"normalized\"` \\| `\"strict\"` |\n| `--strip-newlines` | `strip_newlines` | boolean flag |\n| `-w, --wrap` | `wrap` | boolean flag |\n| `--wrap-width N` | `wrap_width` | integer |\n| `--convert-as-inline` | `convert_as_inline` | boolean flag |\n| `--strip-tags TAGS` | `strip_tags` | comma-separated tag list |\n| `--preserve-tags TAGS` | `preserve_tags` | comma-separated tag list |\n| `--max-depth N` | `max_depth` | integer |\n| `--extract-metadata` | `extract_metadata` | boolean flag |\n| `--include-structure` | `include_document_structure` | boolean flag; `--json` only |\n| `--extract-inline-images` | `extract_images` | boolean flag; `--json` only |\n| `-p, --preprocess` | `preprocess` | boolean flag |\n| `--preset LEVEL` | `preset` | `\"minimal\"` \\| `\"standard\"` \\| `\"aggressive\"` |\n| `--keep-navigation` | `keep_navigation` | boolean flag |\n| `--keep-forms` | `keep_forms` | boolean flag |\n| `-e, --encoding ENCODING` | `encoding` | CLI only — decoded before `convert()` |\n| `--debug` | `debug` | CLI only — diagnostic output to stderr |\n\nFlags without a `ConversionOptions` counterpart: `FILE`, `--url`, `--user-agent`, `-o/--output`, `--json`, `--no-content`, `--show-warnings`, `--generate-completion`, `--generate-man`.\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# Configuration\n\nAll options are passed via `ConversionOptions` (builder pattern in Rust, keyword arguments in Python/Ruby/Elixir/R, object literal in TypeScript, struct in Go/Java/C#, constructor in PHP).\n\n## Options Reference\n\n### Output Format\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `output_format` | `\"markdown\"` \\| `\"djot\"` \\| `\"plain\"` \\| `\"none\"` | `\"markdown\"` | Target output format. Use `\"none\"` to skip conversion and only extract metadata/structure. |\n\n### Headings\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `heading_style` | `\"atx\"` \\| `\"underlined\"` \\| `\"atx_closed\"` | `\"atx\"` | ATX uses `#` prefixes (`# H1`). Underlined uses `===`/`---` for h1/h2. ATX closed adds trailing hashes (`# H1 #`). |\n\n### Lists\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `list_indent_type` | `\"spaces\"` \\| `\"tab\"` | `\"spaces\"` | Indentation character for nested lists. |\n| `list_indent_width` | `int` (1–8) | `2` | Number of spaces per nesting level (when using spaces). |\n| `bullets` | `string` | `\"-\"` | Characters to cycle through for unordered list markers. For example `\"*+-\"` uses `*` at level 1, `+` at level 2, `-` at level 3. |\n\n### Text Formatting\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `strong_em_symbol` | `\"*\"` \\| `\"_\"` | `\"*\"` | Symbol used for bold (`**text**`) and italic (`*text*`). |\n| `newline_style` | `\"backslash\"` \\| `\"spaces\"` | `\"backslash\"` | How to render `<br>` tags: backslash at end of line or two trailing spaces. |\n| `sub_symbol` | `string` | `\"\"` | Symbol to wrap `<sub>` content (e.g. `\"~\"` → `~text~`). |\n| `sup_symbol` | `string` | `\"\"` | Symbol to wrap `<sup>` content (e.g. `\"^\"` → `^text^`). |\n| `highlight_style` | `\"double-equal\"` \\| `\"html\"` \\| `\"bold\"` \\| `\"none\"` | `\"double-equal\"` | Rendering of `<mark>` elements. |\n\n### Escaping\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `escape_asterisks` | `bool` | `false` | Escape `*` characters in text. |\n| `escape_underscores` | `bool` | `false` | Escape `_` characters in text. |\n| `escape_misc` | `bool` | `false` | Escape characters like `[`, `]`, `<`, `>`, `#`, etc. |\n| `escape_ascii` | `bool` | `false` | Escape all ASCII punctuation (strict CommonMark compliance). |\n\n### Code Blocks\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `code_block_style` | `\"indented\"` \\| `\"backticks\"` \\| `\"tildes\"` | `\"indented\"` | How to format multi-line code blocks. |\n| `code_language` | `string` | `\"\"` | Default language tag for fenced code blocks without an explicit language. |\n\n### Links\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `autolinks` | `bool` | `false` | When link text equals the href, emit `<url>` instead of `[url](url)`. |\n| `default_title` | `bool` | `false` | Use the href as link title when no `title` attribute is present. |\n| `link_style` | `\"inline\"` \\| `\"reference\"` | `\"inline\"` | `inline` emits `[text](url)`. `reference` emits `[text][1]` with numbered definitions collected at the end of the document. |\n\n### Images\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `keep_inline_images_in` | `array` | `[]` | Element names where images should be kept as Markdown `![alt](src)` rather than converted to alt text. |\n| `extract_images` | `bool` | `false` | Extract data URIs and embedded SVGs into the `images` field of `ConversionResult`. |\n| `skip_images` | `bool` | `false` | Drop image elements entirely. No `![alt](src)` output, no alt-text fallback. |\n| `max_image_size` | `int` (bytes) | `5242880` | Maximum byte size for an extracted inline image. Larger images are skipped. 5 MB default. |\n| `capture_svg` | `bool` | `false` | Include inline `<svg>` elements in `result.images` when `extract_images` is enabled. |\n| `infer_dimensions` | `bool` | `true` | Infer missing `width` and `height` from decoded image bytes when extracting inline images. |\n\n### Tables\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `br_in_tables` | `bool` | `false` | Preserve line breaks in table cells as `<br>` rather than converting to spaces. |\n\n### Whitespace\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `whitespace_mode` | `\"normalized\"` \\| `\"strict\"` | `\"normalized\"` | `normalized` cleans excess whitespace; `strict` preserves whitespace as-is. |\n| `strip_newlines` | `bool` | `false` | Remove all newlines from input HTML before processing (useful for minified HTML). |\n\n### Wrapping\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `wrap` | `bool` | `false` | Enable line wrapping. |\n| `wrap_width` | `int` (20–500) | `80` | Column width for line wrapping when `wrap` is enabled. |\n\n### Element Handling\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `convert_as_inline` | `bool` | `false` | Treat block-level elements as inline (no paragraph breaks). |\n| `strip_tags` | `array` | `[]` | Tags to strip entirely (only text content is preserved, no Markdown conversion). |\n| `preserve_tags` | `array` | `[]` | Tags to emit verbatim as HTML instead of converting to Markdown. Counterpart to `strip_tags`. |\n\n### Parsing\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `encoding` | `string` | `\"utf-8\"` | **CLI only.** Character encoding of the input file or stdin. The value must be a label that the WHATWG Encoding Standard recognises (`\"windows-1252\"`, `\"shift_jis\"`, `\"iso-8859-1\"`, etc.). The core library stores but does not use this field; decoding happens in the CLI before the string reaches `convert()`. |\n\n### Debugging\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `debug` | `bool` | `false` | **CLI only.** When true, the CLI prints diagnostic lines to stderr after each conversion (e.g. `\"Generated 1234 bytes of markdown\"`). The core library stores but does not act on this field. |\n\n### Metadata Extraction\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `extract_metadata` | `bool` | `true` | Populate `result.metadata` (and `result.tables`) with extracted document metadata. |\n\n### Document Structure\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `include_document_structure` | `bool` | `false` | Populate `result.document` with a parsed tree of headings, paragraphs, lists, and tables. |\n\n### Preprocessing\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `preprocess` | `bool` | `false` | Clean up HTML before conversion. Required for any of the options below to have an effect. |\n| `preset` | `\"minimal\"` \\| `\"standard\"` \\| `\"aggressive\"` | `\"standard\"` | Preset level carried through for forward compatibility. Current releases honour the boolean flags below and do not branch on preset. |\n| `keep_navigation` | `bool` | `false` | Keep `<nav>`, and keep `<header>`/`<footer>`/`<aside>` that otherwise look like navigation. |\n| `keep_forms` | `bool` | `false` | Accepted and stored. Current releases do not drop form elements during preprocessing regardless of this flag. |\n\nWhen `preprocess` is `true` and `keep_navigation` is `false`, the preprocessor drops:\n\n- every `<nav>` element\n- `<header>` elements outside a semantic content ancestor (`<article>`, `<main>`, etc.)\n- `<header>`, `<footer>`, and `<aside>` that carry navigation hints in their class or id attributes (`menu`, `sidebar`, `breadcrumb`, and similar)\n\nScript and style tags are always stripped before the DOM walk starts, independent of `preprocess`.\n\n## Output Format Comparison\n\nGiven this HTML:\n\n```html\n<h1>Report</h1><p>See <a href=\"https://example.com\"><strong>example</strong></a>.</p>\n```\n\n=== \"markdown\"\n    ```markdown\n\n    # Report\n\n    See [**example**](https://example.com).\n    ```\n\n=== \"djot\"\n    ```djot\n\n    # Report\n\n    See [*example*](https://example.com).\n    ```\n\n=== \"plain\"\n    ```text\n    Report\n\n    See example.\n    ```\n\n`markdown` and `djot` both preserve structure and link targets. `djot` uses single-asterisk strong emphasis; `markdown` uses double asterisks. `plain` strips all formatting, link targets, and list markers, returning readable text only.\n\n## Builder Examples\n\n=== \"Rust\"\n    ```rust\n    use html_to_markdown_rs::{convert, ConversionOptions, HeadingStyle};\n\n    let options = ConversionOptions::builder()\n        .heading_style(HeadingStyle::Atx)\n        .code_block_style(\"backticks\")\n        .wrap(true)\n        .wrap_width(80)\n        .extract_metadata(true)\n        .build();\n\n    let result = convert(html, Some(options))?;\n    ```\n\n=== \"Python\"\n    ```python\n    from html_to_markdown import ConversionOptions, convert\n\n    options = ConversionOptions(\n        heading_style=\"atx\",\n        code_block_style=\"backticks\",\n        wrap=True,\n        wrap_width=80,\n        extract_metadata=True,\n    )\n    result = convert(html, options)\n    ```\n\n=== \"TypeScript\"\n    ```typescript\n    import { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\n    const options: ConversionOptions = {\n      headingStyle: 'atx',\n      codeBlockStyle: 'backticks',\n      wrap: true,\n      wrapWidth: 80,\n      extractMetadata: true,\n    };\n\n    const result = convert(html, options);\n    ```\n\n=== \"Go\"\n    ```go\n    opts := htmltomarkdown.ConversionOptions{\n        HeadingStyle:    \"atx\",\n        CodeBlockStyle:  \"backticks\",\n        Wrap:            true,\n        WrapWidth:       80,\n        ExtractMetadata: true,\n    }\n    result, err := htmltomarkdown.Convert(html, opts)```\n\n=== \"Ruby\"\n    ```ruby\n    result = HtmlToMarkdown.convert(\n      html,\n      heading_style: :atx,\n      code_block_style: :fenced,\n      wrap: true,\n      wrap_width: 80,\n      extract_metadata: true,\n    )```\n\n=== \"PHP\"\n    ```php\n    $options = new ConversionOptions(\n        headingStyle: 'Atx',\n        codeBlockStyle: 'Backticks',\n        wrap: true,\n        wrapWidth: 80,\n        extractMetadata: true,\n    );\n    $result = $converter->convert($html, $options);```\n\n=== \"Java\"\n    ```java\n    ConversionOptions options = ConversionOptions.builder()\n        .headingStyle(\"atx\")\n        .codeBlockStyle(\"backticks\")\n        .wrap(true)\n        .wrapWidth(80)\n        .extractMetadata(true)\n        .build();\n    ConversionResult result = HtmlToMarkdown.convert(html, options);```\n\n=== \"C#\"\n    ```csharp\n    var options = new ConversionOptions\n    {\n        HeadingStyle = \"atx\",\n        CodeBlockStyle = \"backticks\",\n        Wrap = true,\n        WrapWidth = 80,\n        ExtractMetadata = true,\n    };\n    var result = HtmlToMarkdownConverter.Convert(html, options);```\n\n=== \"Elixir\"\n    ```elixir\n    opts = %HtmlToMarkdown.Options{\n      heading_style: :atx,\n      code_block_style: :backticks,\n      wrap: true,\n      wrap_width: 80,\n      extract_metadata: true,\n    }\n    {:ok, result} = HtmlToMarkdown.convert(html, opts)```\n\n=== \"R\"\n    ```r\n    opts <- conversion_options(\n      heading_style = \"atx\",\n      code_block_style = \"backticks\",\n      wrap = TRUE,\n      wrap_width = 80L,\n      extract_metadata = TRUE\n    )\n    result <- convert(html, opts)```\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Contributing\n\nThanks for considering a contribution to html-to-markdown. Every fix, feature, and documentation improvement helps.\n\n## Ways to contribute\n\n- **Report a bug** — [open an issue](https://github.com/kreuzberg-dev/html-to-markdown/issues/new?labels=bug) with a minimal reproduction\n- **Fix a bug** — look for issues tagged [`good first issue`](https://github.com/kreuzberg-dev/html-to-markdown/issues?q=label%3A%22good+first+issue%22) or [`help wanted`](https://github.com/kreuzberg-dev/html-to-markdown/issues?q=label%3A%22help+wanted%22)\n- **Improve the docs** — edit any page directly on GitHub using the pencil icon, or clone and run the site locally\n- **Add a feature** — open an issue first to discuss scope before writing code; large changes without prior discussion may not be accepted\n\n## Getting started\n\n```bash\ngit clone https://github.com/kreuzberg-dev/html-to-markdown.git\ncd html-to-markdown\ntask setup          # installs deps, builds Rust extension, wires commit hooks\ntask test           # should pass before you make any changes\n```\n\nFull prerequisites and per-language build instructions are in [`CONTRIBUTING.md`](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) at the repo root.\n\n## Workflow\n\n1. Fork the repo and create a branch: `git checkout -b fix/your-change`\n2. Make your change, add tests if applicable\n3. Run `task test` (Rust + Python) and `pnpm test` (JS/TS) as needed\n4. Commit using [Conventional Commits](https://www.conventionalcommits.org/) — prek enforces this automatically\n5. Push and open a pull request against `main`\n\n## Documentation changes\n\nThe docs site lives in `docs/` and builds with Zensical:\n\n```bash\nuv sync --group doc\nuv run --no-sync zensical serve         # live preview at localhost:8000\nscripts/ci/docs/build.sh --strict       # what CI runs — fix all warnings before pushing\n```\n\nNew pages go in `docs/`, must be added to `nav:` in `mkdocs.yaml`, and should follow the style already on the page you're editing: terse, table-driven.\n\n## Getting help\n\n- **Have Any Questions** — [Join the GitHub Discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Join Our community** — [Discord](https://discord.gg/pXxagNK2zN)\n- **Report a Bugs** — [GitHub Issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/css/extra.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700&display=swap\");\n\nhtml {\n\tscroll-behavior: smooth;\n}\n\n.md-sidebar__scrollwrap {\n\tscroll-behavior: smooth;\n}\n\n.md-nav--secondary {\n\tscroll-behavior: smooth;\n}\n\n:root > * {\n\t--md-primary-fg-color: #323040;\n\t--md-primary-fg-color--light: #4a4858;\n\t--md-primary-fg-color--dark: #1a1926;\n\n\t--md-accent-fg-color: #da2ae0;\n\t--md-accent-fg-color-opposite: #58fbda;\n\n\t--md-default-fg-color: #323040;\n\t--md-default-fg-color--light: #323040;\n\t--md-default-fg-color--dark: #323040;\n\n\t--md-default-bg-color: #ffffff;\n\t--md-default-bg-color--light: #ffffff;\n\t--md-default-bg-color--dark: #f2f2f4;\n\n\t--md-primary-bg-color: #ffffff;\n\t--md-shadow-z1: none;\n}\n\n/* Slate palette: `data-md-color-scheme` is on `body`, not `html` — `:root[...]` never matched */\n[data-md-color-scheme=\"slate\"] {\n\tbackground-color: #23232c;\n\n\t--md-primary-fg-color: #323040;\n\t--md-primary-fg-color--light: #4a4858;\n\t--md-primary-fg-color--dark: #1a1926;\n\n\t--md-accent-fg-color: #58fbda;\n\t--md-accent-fg-color-opposite: #da2ae0;\n\n\t--md-default-fg-color: #ffffff;\n\t--md-default-fg-color--light: #ffffff;\n\t--md-default-fg-color--dark: #ffffff;\n\n\t--md-default-bg-color: #23232c;\n\t--md-default-bg-color--light: #2a2836;\n\t--md-default-bg-color--dark: #23232c;\n\n\t--md-primary-bg-color: #23232c;\n\t--md-shadow-z1: none;\n\n\t--md-banner-fg-color: #e8f7f3;\n\t--md-banner-bg-color: #1f2937;\n}\n\n[data-md-color-scheme=\"slate\"] .md-main {\n\tbackground-color: #23232c;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content {\n\tbackground-color: #23232c;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar {\n\tbackground-color: #23232c;\n}\n\n[data-md-color-scheme=\"slate\"] .md-container {\n\tbackground-color: #23232c;\n}\n\n[data-md-color-scheme=\"slate\"] body {\n\tbackground-color: #23232c;\n}\n\n.highlight {\n\tposition: relative;\n\tbackground: #282a36;\n\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\tborder-radius: 12px;\n\tpadding: 16px 16px 0 16px;\n\tmargin-bottom: 1.5rem;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-start;\n\tgap: 12px;\n}\n\n.highlight .filename,\n.highlight a.filename,\n.md-typeset .highlight .filename {\n\tcolor: rgba(255, 255, 255, 0.6);\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 22px;\n\tpadding: 0 0 0 24px;\n\tmargin: 0;\n\talign-self: flex-start;\n\tbackground: transparent url(\"/assets/icons/file-dark.svg\") no-repeat left center;\n\tbackground-size: 16px 16px;\n\tborder-bottom: none;\n}\n\n.highlight pre {\n\tbackground-color: transparent;\n\tcolor: #ffffff;\n\tmargin: 0;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 22.4px;\n\tposition: relative;\n\twidth: 100%;\n}\n\n.highlight code {\n\tbackground-color: transparent;\n\tcolor: #ffffff;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 22.4px;\n}\n\n.highlight .md-code__content {\n\tpadding-left: 0;\n\tmargin-left: 0;\n}\n\n.highlight .md-code__nav {\n\tposition: absolute;\n\tright: 0;\n\ttop: 0;\n\tbackground: transparent;\n}\n\n.highlight .md-code__button {\n\tcolor: #58fbda;\n}\n\n.highlight .md-code__button svg {\n\tfill: #58fbda;\n}\n\n[data-md-color-scheme=\"slate\"] code {\n\tbackground-color: #3c3a4a;\n\tcolor: #58fbda;\n\tpadding: 0.2em 0.4em;\n\tborder-radius: 4px;\n}\n\n[data-md-color-scheme=\"default\"] code {\n\tbackground-color: #f2f2f4;\n\tcolor: #da2ae0;\n\tpadding: 0.2em 0.4em;\n\tborder-radius: 4px;\n}\n\n.highlight code,\n.highlight pre code {\n\tbackground-color: transparent;\n\tcolor: #ffffff;\n}\n\n.highlight .k,\n.highlight .kn,\n.highlight .kd,\n.highlight .kp,\n.highlight .kr,\n.highlight .kt {\n\tcolor: #ff79c6;\n}\n\n.highlight .c,\n.highlight .c1,\n.highlight .cm,\n.highlight .ch,\n.highlight .cpf,\n.highlight .cs {\n\tcolor: #6272a4;\n\tfont-style: italic;\n}\n\n.highlight pre code .n,\n.highlight pre code .nv,\n.highlight pre code .vc,\n.highlight pre code .vg,\n.highlight pre code .vi,\n.highlight pre code .vm,\n.highlight .n,\n.highlight .nv,\n.highlight .vc,\n.highlight .vg,\n.highlight .vi,\n.highlight .vm {\n\tcolor: #f8f8f2;\n}\n\n.highlight pre code .nb,\n.highlight pre code .bp,\n.highlight .nb,\n.highlight .bp {\n\tcolor: #8be9fd;\n\tfont-style: italic;\n}\n\n.highlight pre code .nn,\n.highlight .nn {\n\tcolor: #8be9fd;\n\tfont-weight: bold;\n}\n\n.highlight pre code .no,\n.highlight .no {\n\tcolor: #bd93f9;\n}\n\n.highlight pre code .ni,\n.highlight .ni {\n\tcolor: #f8f8f2;\n}\n\n.highlight .o {\n\tcolor: #ff79c6;\n}\n\n.highlight .s,\n.highlight .s1,\n.highlight .s2,\n.highlight .sa,\n.highlight .sb,\n.highlight .sc,\n.highlight .dl,\n.highlight .se,\n.highlight .sh,\n.highlight .si,\n.highlight .sx,\n.highlight .sr,\n.highlight .ss,\n.highlight .sd {\n\tcolor: #f1fa8c;\n}\n\n.highlight .nf,\n.highlight .fm {\n\tcolor: #50fa7b;\n}\n\n.highlight .nc {\n\tcolor: #8be9fd;\n}\n\n.highlight .language-bash,\n.highlight .language-sh,\n.highlight .language-console {\n\tposition: relative;\n}\n\n.language-bash .filename,\n.language-sh .filename,\n.language-console .filename {\n\tbackground-image: url(\"/assets/icons/terminal-dark.svg\");\n\tbackground-repeat: no-repeat;\n\tbackground-position: left center;\n\tbackground-size: 16px 16px;\n}\n\n.highlight .language-bash:not(:has(.filename))::before,\n.highlight .language-sh:not(:has(.filename))::before,\n.highlight .language-console:not(:has(.filename))::before {\n\tcontent: \"Terminal\";\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tcolor: rgba(255, 255, 255, 0.6);\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-weight: 400;\n\tline-height: 22px;\n\tmargin-bottom: 0.75rem;\n\tbackground-image: url(\"/assets/icons/terminal-dark.svg\");\n\tbackground-repeat: no-repeat;\n\tbackground-position: left center;\n\tbackground-size: 16px 16px;\n\tpadding-left: 24px;\n}\n\n.md-button {\n\tborder-radius: 8px;\n\ttransition: all 0.2s ease;\n}\n\n.md-button--primary {\n\tbackground-color: var(--md-primary-fg-color);\n\tcolor: var(--md-default-fg-color);\n}\n\n.md-button--primary:hover {\n\tbackground-color: var(--md-accent-fg-color);\n\tcolor: #ffffff;\n}\n\n.md-button--primary:hover svg {\n\tfill: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-button--primary:hover {\n\tbackground-color: #58fbda;\n\tcolor: #000000;\n}\n\n[data-md-color-scheme=\"slate\"] .md-button--primary:hover svg {\n\tfill: #000000;\n}\n\n[data-md-color-scheme=\"slate\"] .md-top:hover {\n\tbackground-color: #58fbda;\n\tcolor: #000000;\n}\n\n[data-md-color-scheme=\"slate\"] .md-top:hover svg {\n\tfill: #000000 !important;\n}\n\n:root > * {\n\t--md-banner-fg-color: #0f172a;\n\t--md-banner-bg-color: #e8f7f3;\n}\n\n.md-typeset .admonition {\n\tborder-radius: 8px;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition li,\n[data-md-color-scheme=\"slate\"] .md-typeset details p,\n[data-md-color-scheme=\"slate\"] .md-typeset details li {\n\tcolor: var(--md-primary-fg-color);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details strong {\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note {\n\tborder: 2px solid #2558ff;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note > .admonition-title,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note > summary {\n\tbackground-color: rgba(37, 88, 255, 0.05);\n\tborder: none;\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note > .admonition-title::before,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note > summary::before {\n\tbackground-color: #ffffff;\n\tmask-image: url(\"/assets/icons/note-dark.svg\");\n\t-webkit-mask-image: url(\"/assets/icons/note-dark.svg\");\n\tmask-size: contain;\n\t-webkit-mask-size: contain;\n\tmask-repeat: no-repeat;\n\t-webkit-mask-repeat: no-repeat;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note li,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note p,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note li {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.note strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details.note strong {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question {\n\tborder: 2px solid #04d492;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question > .admonition-title,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question > summary {\n\tbackground-color: rgba(4, 212, 146, 0.05);\n\tborder: none;\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question > .admonition-title::before,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question > summary::before {\n\tbackground-color: #ffffff;\n\tmask-image: url(\"/assets/icons/question-dark.svg\");\n\t-webkit-mask-image: url(\"/assets/icons/question-dark.svg\");\n\tmask-size: contain;\n\t-webkit-mask-size: contain;\n\tmask-repeat: no-repeat;\n\t-webkit-mask-repeat: no-repeat;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question li,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question p,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question li {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.question strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details.question strong {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info {\n\tborder: 2px solid #43e5ff;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info > .admonition-title,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info > summary {\n\tbackground-color: rgba(67, 229, 255, 0.05);\n\tborder: none;\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info > .admonition-title::before,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info > summary::before {\n\tbackground-color: #ffffff;\n\tmask-image: url(\"/assets/icons/info-dark.svg\");\n\t-webkit-mask-image: url(\"/assets/icons/info-dark.svg\");\n\tmask-size: contain;\n\t-webkit-mask-size: contain;\n\tmask-repeat: no-repeat;\n\t-webkit-mask-repeat: no-repeat;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info li,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info p,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info li {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.info strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details.info strong {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning {\n\tborder: 2px solid #ffee00;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning > .admonition-title,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning > summary {\n\tbackground-color: rgba(255, 238, 0, 0.05);\n\tborder: none;\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning > .admonition-title::before,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning > summary::before {\n\tbackground-color: #ffffff;\n\tmask-image: url(\"/assets/icons/warning-dark.svg\");\n\t-webkit-mask-image: url(\"/assets/icons/warning-dark.svg\");\n\tmask-size: contain;\n\t-webkit-mask-size: contain;\n\tmask-repeat: no-repeat;\n\t-webkit-mask-repeat: no-repeat;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning li,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning p,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning li {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.warning strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details.warning strong {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip {\n\tborder: 2px solid #58fbda;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip > .admonition-title,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip > summary {\n\tbackground-color: rgba(88, 251, 218, 0.05);\n\tborder: none;\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip > .admonition-title::before,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip > summary::before {\n\tbackground-color: #ffffff;\n\tmask-image: url(\"/assets/icons/tip-dark.svg\");\n\t-webkit-mask-image: url(\"/assets/icons/tip-dark.svg\");\n\tmask-size: contain;\n\t-webkit-mask-size: contain;\n\tmask-repeat: no-repeat;\n\t-webkit-mask-repeat: no-repeat;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip p,\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip li,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip p,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip li {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition.tip strong,\n[data-md-color-scheme=\"slate\"] .md-typeset details.tip strong {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\ndetails .highlight {\n\tborder: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.md-typeset .tabbed-set {\n\tborder-radius: 8px;\n\toverflow: hidden;\n\tborder: none !important;\n\tbox-shadow: none !important;\n}\n\n.md-typeset .tabbed-set.tabbed-alternate {\n\tborder: none !important;\n\tbox-shadow: none !important;\n}\n\n.md-typeset .tabbed-labels,\n.md-typeset .tabbed-labels--linked {\n\tborder: none !important;\n\tborder-bottom: none !important;\n\tmargin-bottom: 4px !important;\n\tbox-shadow: none !important;\n}\n\n.md-typeset .tabbed-content {\n\tborder-top: none !important;\n\tbox-shadow: none !important;\n}\n\n.md-typeset .tabbed-labels::before {\n\tbackground-color: #da2ae0 !important;\n\theight: 2px !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels::before {\n\tbackground-color: #58fbda !important;\n}\n\n.md-typeset .tabbed-labels > label {\n\tcolor: var(--md-default-fg-color) !important;\n\ttransition: all 0.2s ease;\n\tborder-radius: 6px;\n\tpadding: 8px 16px;\n\tcursor: pointer;\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tfont-weight: 500;\n}\n\n.md-typeset .tabbed-labels > label a {\n\ttext-decoration: none;\n\tcolor: inherit;\n\tfont-size: 14px;\n\tfont-weight: inherit;\n\tline-height: normal;\n\ttransition: all 0.2s ease;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label {\n\tborder: none !important;\n\tcolor: #ffffff !important;\n\tbackground-color: transparent !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label a {\n\tcolor: #ffffff;\n\ttext-decoration: none;\n\ttransition: all 0.2s ease;\n}\n\n.md-typeset .tabbed-labels > label:hover {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #da2ae0 !important;\n}\n\n.md-typeset .tabbed-labels > label:hover a {\n\tcolor: #da2ae0 !important;\n\ttext-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label:hover {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label:hover a {\n\tcolor: #58fbda !important;\n\ttext-decoration: none;\n}\n\n.md-typeset .tabbed-labels > label:active {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #da2ae0 !important;\n}\n\n.md-typeset .tabbed-labels > label:active a {\n\tcolor: #da2ae0 !important;\n\ttext-decoration: none !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label:active {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label:active a {\n\tcolor: #58fbda !important;\n\ttext-decoration: none !important;\n}\n\n.md-typeset .tabbed-labels > label[for][aria-selected=\"true\"] {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #da2ae0 !important;\n\tfont-weight: 600;\n}\n\n.md-typeset .tabbed-labels > label[for][aria-selected=\"true\"] a {\n\tcolor: #da2ae0 !important;\n\ttext-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label[for][aria-selected=\"true\"] {\n\tbackground-color: transparent !important;\n\tborder: none !important;\n\tcolor: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .tabbed-labels > label[for][aria-selected=\"true\"] a {\n\tcolor: #58fbda !important;\n\ttext-decoration: none;\n}\n\n.md-search__input {\n\tdisplay: flex;\n\tpadding: 6px 16px 6px 40px;\n\talign-items: center;\n\tborder-radius: 4px;\n\tbackground: #3d3a4d;\n\tcolor: #99a1af;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: normal;\n}\n\n.md-search__input::placeholder {\n\tcolor: #99a1af;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: normal;\n\topacity: 1;\n}\n\n.md-search__icon {\n\tcolor: #99a1af;\n}\n\n.md-search__icon[for=\"__search\"] {\n\tcolor: #99a1af;\n}\n\n.md-search__icon svg {\n\ttransform: scale(0.75);\n}\n\n[data-md-toggle=\"search\"]:checked ~ .md-header .md-search__input {\n\tborder-radius: 0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-search__output .md-typeset h1,\n[data-md-color-scheme=\"slate\"] .md-search__output .md-typeset h2,\n[data-md-color-scheme=\"slate\"] .md-search__output .md-typeset p,\n[data-md-color-scheme=\"slate\"] .md-search__output .md-typeset li,\n[data-md-color-scheme=\"slate\"] .md-search__output .md-typeset ul {\n\tcolor: #323040;\n}\n\nlabel[for=\"__palette_0\"] svg,\nlabel[for=\"__palette_1\"] svg {\n\ttransform: scale(0.75);\n}\n\n.md-typeset h1 {\n\tcolor: #323040;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 32px;\n\tfont-style: normal;\n\tfont-weight: 600;\n\tline-height: 41.6px;\n}\n\n.md-typeset h2 {\n\tcolor: #323040;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 26px;\n\tfont-style: normal;\n\tfont-weight: 600;\n\tline-height: 33.8px;\n}\n\n.md-typeset h3 {\n\tcolor: #323040;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 16px;\n\tfont-style: normal;\n\tfont-weight: 700;\n\tline-height: 27.2px;\n}\n\n.md-typeset h4,\n.md-typeset h5,\n.md-typeset h6 {\n\tcolor: #323040;\n}\n\n.md-typeset p,\n.md-typeset li {\n\tcolor: #5b5966;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 16px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 26px;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset h1,\n[data-md-color-scheme=\"slate\"] .md-typeset h2,\n[data-md-color-scheme=\"slate\"] .md-typeset h3,\n[data-md-color-scheme=\"slate\"] .md-typeset h4,\n[data-md-color-scheme=\"slate\"] .md-typeset h5,\n[data-md-color-scheme=\"slate\"] .md-typeset h6 {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset p,\n[data-md-color-scheme=\"slate\"] .md-typeset li {\n\tcolor: rgba(255, 255, 255, 0.8);\n}\n\n.md-typeset a {\n\tcolor: #da2ae0;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 16px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 27.2px;\n\ttext-decoration-line: underline;\n\ttext-decoration-style: solid;\n\ttext-decoration-skip-ink: auto;\n\ttext-decoration-thickness: auto;\n\ttext-underline-offset: auto;\n\ttext-underline-position: from-font;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset a {\n\tcolor: #58fbda;\n}\n\n.md-content a,\n.md-typeset .md-content a {\n\tcolor: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content a,\n[data-md-color-scheme=\"slate\"] .md-typeset .md-content a {\n\tcolor: #58fbda;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset a code {\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset a code {\n\tcolor: #ffffff;\n}\n\nstrong {\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"slate\"] strong {\n\tcolor: #ffffff;\n}\n\n.md-header__title,\n.md-header__title *,\n.md-header__topic,\n.md-header__topic *,\n.md-header .md-ellipsis {\n\tfont-family: \"Exo 2\", sans-serif;\n\tfont-weight: 600;\n}\n\n.md-header__button.md-logo {\n\tmargin-right: 0.2rem;\n\tpadding: 0.2rem;\n}\n\n.md-header__button.md-logo img,\n.md-header__button.md-logo svg {\n\theight: 2.4rem;\n\twidth: 2.4rem;\n\tflex-shrink: 0;\n\tobject-fit: contain;\n}\n\n/* Teal mark reads clearly on both light and dark chrome */\n[data-md-color-scheme=\"default\"] .md-header__button.md-logo {\n\tfilter: drop-shadow(0 0 1px rgba(46, 45, 54, 0.35));\n}\n\n.md-header__title {\n\tmargin-left: 0;\n\tpadding-left: 0;\n}\n\n/* One continuous top bar in light mode (tabs are already this tone) */\n[data-md-color-scheme=\"default\"] .md-header {\n\tbackground-color: #3d3a4d;\n\tcolor: rgba(255, 255, 255, 0.95);\n}\n\n[data-md-color-scheme=\"default\"] .md-header__topic,\n[data-md-color-scheme=\"default\"] .md-header__title,\n[data-md-color-scheme=\"default\"] .md-header .md-ellipsis {\n\tcolor: rgba(255, 255, 255, 0.96);\n}\n\n[data-md-color-scheme=\"default\"] .md-source {\n\tcolor: rgba(255, 255, 255, 0.88);\n}\n\n[data-md-color-scheme=\"default\"] .md-source__repository {\n\tcolor: rgba(255, 255, 255, 0.92);\n}\n\n[data-md-color-scheme=\"default\"] .md-source__facts {\n\tcolor: rgba(255, 255, 255, 0.72);\n}\n\n.md-tabs {\n\tbackground-color: #3d3a4d;\n\twidth: 100%;\n}\n\n/* Inactive tab labels: must contrast with #3d3a4d (defaults were near #323040) */\n.md-tabs__link {\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 23.8px;\n\tcolor: rgba(255, 255, 255, 0.82) !important;\n}\n\n.md-tabs__item {\n\tposition: relative;\n}\n\n[data-md-color-scheme=\"default\"] .md-tabs__link--active,\n[data-md-color-scheme=\"default\"] .md-tabs__link:hover,\n[data-md-color-scheme=\"default\"] .md-tabs__link:focus,\n[data-md-color-scheme=\"default\"] .md-tabs__item--active .md-tabs__link {\n\tcolor: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-tabs__link--active,\n[data-md-color-scheme=\"slate\"] .md-tabs__link:hover,\n[data-md-color-scheme=\"slate\"] .md-tabs__link:focus,\n[data-md-color-scheme=\"slate\"] .md-tabs__item--active .md-tabs__link {\n\tcolor: #58fbda;\n}\n\n[data-md-color-scheme=\"default\"] .md-tabs__item--active::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\theight: 2px;\n\tbackground-color: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-tabs__item--active::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\theight: 2px;\n\tbackground-color: #58fbda;\n}\n\n[data-md-color-scheme=\"default\"] .md-tabs__item:hover::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\theight: 2px;\n\tbackground-color: #da2ae0;\n\topacity: 0.5;\n}\n\n[data-md-color-scheme=\"slate\"] .md-tabs__item:hover::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\theight: 2px;\n\tbackground-color: #58fbda;\n\topacity: 0.5;\n}\n\n.md-footer__inner.md-grid {\n\tdisplay: none;\n}\n\n.md-footer-meta {\n\tbackground-color: #323040;\n\tcolor: rgba(255, 255, 255, 0.82);\n}\n\n/* Light scheme: theme sets dark --md-footer-fg-color — copyright / “Made with …” vanish on #323040 */\n[data-md-color-scheme=\"default\"] .md-footer-meta {\n\t--md-footer-fg-color: rgba(255, 255, 255, 0.9);\n\t--md-footer-bg-color: #323040;\n\tcolor: rgba(255, 255, 255, 0.9) !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-copyright,\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-copyright__highlight {\n\tcolor: inherit !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta a:not(.md-social__link) {\n\tcolor: #7ee9d8 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta a:not(.md-social__link):hover {\n\tcolor: #ffffff !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta.md-typeset,\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-typeset,\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-typeset p {\n\tcolor: inherit !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta {\n\t--md-footer-fg-color: rgba(255, 255, 255, 0.9);\n\tcolor: rgba(255, 255, 255, 0.9) !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta a:not(.md-social__link) {\n\tcolor: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta.md-typeset,\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-typeset,\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-typeset p {\n\tcolor: inherit !important;\n}\n\n.md-social {\n\tpadding: 0.4rem 0;\n}\n\n.md-nav__link {\n\tcolor: #706e79;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tline-height: 23.8px;\n\tborder-radius: 4px;\n\tpadding: 4px 12px;\n}\n\n.md-nav__link.md-nav__link--active {\n\tbackground: linear-gradient(90deg, rgba(218, 42, 224, 0.15) 0%, rgba(218, 42, 224, 0.1) 100%);\n\tcolor: #323040;\n}\n\n.md-nav__link:not(.md-nav__link--active):hover {\n\tbackground: #fafafa;\n\tcolor: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link {\n\tcolor: rgba(255, 255, 255, 0.7);\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link.md-nav__link--active {\n\tbackground: linear-gradient(90deg, rgba(88, 251, 218, 0.2) 0%, rgba(88, 251, 218, 0.1) 100%);\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link:not(.md-nav__link--active):hover {\n\tbackground: #323040;\n\tcolor: #58fbda;\n}\n\n.md-nav--lifted > .md-nav__list > .md-nav__item--active {\n\tbackground: transparent;\n\tbox-shadow: none;\n}\n\n.md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link,\n.md-nav--lifted > .md-nav__list > .md-nav__item--active > label.md-nav__link,\n.md-nav--lifted > .md-nav__list > .md-nav__item--active > a.md-nav__link {\n\tbackground: linear-gradient(90deg, rgba(218, 42, 224, 0.15) 0%, rgba(218, 42, 224, 0.1) 100%);\n\tbox-shadow: none;\n\tborder-radius: 4px;\n\tpadding: 8px 60px 8px 12px;\n\talign-items: center;\n\tmargin-left: -12px;\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link,\n[data-md-color-scheme=\"slate\"] .md-nav--lifted > .md-nav__list > .md-nav__item--active > label.md-nav__link,\n[data-md-color-scheme=\"slate\"] .md-nav--lifted > .md-nav__list > .md-nav__item--active > a.md-nav__link {\n\tbackground: linear-gradient(90deg, rgba(88, 251, 218, 0.2) 0%, rgba(88, 251, 218, 0.1) 100%);\n\tcolor: #ffffff;\n}\n\n/* Integrated TOC: avoid large vertical rhythm (20px × every link read as a “hole” above siblings) */\n.md-nav--secondary .md-nav__list .md-nav__link {\n\tmargin-top: 0;\n\tmargin-right: 0;\n}\n\n.md-nav--secondary .md-nav__list .md-nav__item:not(:first-child) > .md-nav__link {\n\tmargin-top: 0.2rem;\n}\n\nnav.md-nav--secondary {\n\tborder: none;\n\tborder-left: none;\n\tborder-inline-start: none;\n\tbox-shadow: none;\n}\n\nnav.md-nav--secondary ul.md-nav__list {\n\tpadding-left: 0;\n\tpadding-inline-start: 0;\n}\n\nnav.md-nav--secondary > .md-nav__list > .md-nav__item > .md-nav__link {\n\tpadding-left: 0.5rem;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset__scrollwrap,\n[data-md-color-scheme=\"default\"] .md-typeset__table,\n[data-md-color-scheme=\"default\"] .md-typeset table {\n\tbackground: transparent;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset table thead,\n[data-md-color-scheme=\"default\"] .md-typeset table tbody,\n[data-md-color-scheme=\"default\"] .md-typeset table tr,\n[data-md-color-scheme=\"default\"] .md-typeset table th,\n[data-md-color-scheme=\"default\"] .md-typeset table td {\n\tbackground: transparent;\n\tbackground-color: transparent;\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset table tr:hover {\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset__scrollwrap,\n[data-md-color-scheme=\"slate\"] .md-typeset__table,\n[data-md-color-scheme=\"slate\"] .md-typeset table {\n\tbackground: transparent;\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset table thead,\n[data-md-color-scheme=\"slate\"] .md-typeset table tbody,\n[data-md-color-scheme=\"slate\"] .md-typeset table tr,\n[data-md-color-scheme=\"slate\"] .md-typeset table th,\n[data-md-color-scheme=\"slate\"] .md-typeset table td {\n\tbackground: transparent;\n\tbackground-color: transparent;\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset table tr:hover {\n\tbackground-color: transparent;\n}\n\n[data-md-color-scheme=\"default\"] .md-source-file {\n\tcolor: rgba(91, 89, 102, 0.6);\n}\n\n[data-md-color-scheme=\"default\"] .md-source-file__fact {\n\tcolor: rgba(91, 89, 102, 0.6);\n}\n\n[data-md-color-scheme=\"default\"] .md-source-file .md-icon svg {\n\tfill: rgba(91, 89, 102, 0.6);\n}\n\n[data-md-color-scheme=\"slate\"] .md-source-file {\n\tcolor: rgba(255, 255, 255, 0.4);\n}\n\n[data-md-color-scheme=\"slate\"] .md-source-file__fact {\n\tcolor: rgba(255, 255, 255, 0.4);\n}\n\n[data-md-color-scheme=\"slate\"] .md-source-file .md-icon svg {\n\tfill: rgba(255, 255, 255, 0.4);\n}\n\n[data-md-color-scheme=\"default\"] .md-sidebar__scrollwrap::-webkit-scrollbar {\n\twidth: 6px;\n}\n\n[data-md-color-scheme=\"default\"] .md-sidebar__scrollwrap::-webkit-scrollbar-track {\n\tbackground: transparent;\n}\n\n[data-md-color-scheme=\"default\"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb {\n\tbackground-color: #da2ae0;\n\tborder-radius: 3px;\n}\n\n[data-md-color-scheme=\"default\"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover {\n\tbackground-color: #b825c0;\n}\n\n[data-md-color-scheme=\"default\"] .md-sidebar__scrollwrap {\n\tscrollbar-width: thin;\n\tscrollbar-color: #da2ae0 transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar__scrollwrap::-webkit-scrollbar {\n\twidth: 6px;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar__scrollwrap::-webkit-scrollbar-track {\n\tbackground: transparent;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb {\n\tbackground-color: #58fbda;\n\tborder-radius: 3px;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover {\n\tbackground-color: #7ffce8;\n}\n\n[data-md-color-scheme=\"slate\"] .md-sidebar__scrollwrap {\n\tscrollbar-width: thin;\n\tscrollbar-color: #58fbda transparent;\n}\n\n.highlight .m,\n.highlight .mb,\n.highlight .mf,\n.highlight .mh,\n.highlight .mi,\n.highlight .il,\n.highlight .mo {\n\tcolor: #bd93f9;\n}\n\n.highlight .p {\n\tcolor: #f8f8f2;\n}\n\n.highlight .kc {\n\tcolor: #bd93f9;\n}\n\n.highlight .ow {\n\tcolor: #ff79c6;\n}\n\n.highlight .nf,\n.highlight .fm {\n\tcolor: #50fa7b;\n}\n\n.highlight .nc {\n\tcolor: #8be9fd;\n}\n\n.highlight .na,\n.highlight .nl {\n\tcolor: #50fa7b;\n}\n\n.highlight .nd {\n\tcolor: #50fa7b;\n}\n\n/* Generic tokens - Diff, Output, Errors */\n.highlight .err {\n\tcolor: #ff5555;\n\tbackground-color: rgba(255, 85, 85, 0.1);\n}\n\n.highlight .gd {\n\tcolor: #ff5555;\n\tbackground-color: rgba(255, 85, 85, 0.1);\n}\n\n.highlight .gi {\n\tcolor: #50fa7b;\n\tbackground-color: rgba(80, 250, 123, 0.1);\n}\n\n.highlight .gh {\n\tcolor: #8be9fd;\n\tfont-weight: bold;\n}\n\n.highlight .gu {\n\tcolor: #6272a4;\n\tfont-weight: bold;\n}\n\n.highlight .go {\n\tcolor: #44475a;\n}\n\nnav.md-nav--secondary > .md-nav__list > .md-nav__item > .md-nav > .md-nav__list > .md-nav__item > .md-nav__link {\n\tpadding-left: 1rem;\n}\n\n[data-md-color-scheme=\"slate\"] .md-path {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-path .md-path__link {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-path .md-path__list {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset .task-list-indicator::before {\n\tbackground-color: #da2ae0;\n}\n\n[data-md-color-scheme=\"default\"]\n\t.md-typeset\n\t.task-list-control\n\t[type=\"checkbox\"]:checked\n\t+ .task-list-indicator::before {\n\tbackground-color: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .task-list-indicator::before {\n\tbackground-color: #58fbda;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .task-list-control [type=\"checkbox\"]:checked + .task-list-indicator::before {\n\tbackground-color: #58fbda;\n}\n\n[data-md-color-scheme=\"slate\"] .mermaid {\n\t--md-mermaid-label-fg-color: #4a4a4a;\n\t--md-mermaid-node-bg-color: #e8e8e8;\n\t--md-mermaid-label-bg-color: rgba(40, 40, 45, 0.95);\n}\n\n[data-md-color-scheme=\"slate\"] {\n\t--md-default-fg-color--lightest: rgba(240, 240, 245, 0.5);\n\t--md-default-fg-color--lighter: rgba(220, 220, 230, 0.5);\n}\n\n[data-md-color-scheme=\"default\"] .md-icon svg {\n\tfill: #da2ae0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-icon svg {\n\tfill: #58fbda;\n}\n\n/*\n * Header (light scheme): dark bar + FA7 paths often use fill=\"currentColor\".\n * Set fill + color on svg and every descendant so GitHub / search / palette stay solid white.\n */\n[data-md-color-scheme=\"default\"] .md-header .md-icon svg,\n[data-md-color-scheme=\"default\"] .md-header .md-icon svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-header .md-source svg,\n[data-md-color-scheme=\"default\"] .md-header .md-source svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n/* Social icons can render in the header (e.g. Zensical); not only in .md-footer-meta */\n[data-md-color-scheme=\"default\"] .md-header .md-social__link {\n\tcolor: #ffffff !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-header .md-social__link svg,\n[data-md-color-scheme=\"default\"] .md-header .md-social__link svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-header .md-social__link:hover,\n[data-md-color-scheme=\"default\"] .md-header .md-social__link:hover svg,\n[data-md-color-scheme=\"default\"] .md-header .md-social__link:hover svg * {\n\tcolor: #58fbda !important;\n\tfill: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-header .md-social__link svg,\n[data-md-color-scheme=\"slate\"] .md-header .md-social__link svg * {\n\tfill: #58fbda !important;\n\tcolor: #58fbda !important;\n\topacity: 1 !important;\n}\n\n/*\n * Footer social (GitHub, Discord, npm, …): same FA path issue on dark #323040 bar.\n */\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-social__link {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-social__link:hover {\n\tcolor: #58fbda;\n}\n\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-social__link svg,\n[data-md-color-scheme=\"default\"] .md-footer-meta .md-social__link svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-social__link {\n\tcolor: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-social__link:hover {\n\tcolor: #58fbda;\n}\n\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-social__link svg,\n[data-md-color-scheme=\"slate\"] .md-footer-meta .md-social__link svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-header__button:not(.md-logo) svg,\n[data-md-color-scheme=\"default\"] .md-header__button:not(.md-logo) svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-header__button:not(.md-logo) svg,\n[data-md-color-scheme=\"slate\"] .md-header__button:not(.md-logo) svg * {\n\tfill: #58fbda !important;\n\tcolor: inherit !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-top {\n\tbackground-color: #ffffff;\n}\n\n[data-md-color-scheme=\"default\"] .md-top svg {\n\tfill: #323040;\n}\n\n[data-md-color-scheme=\"default\"] .md-top:hover {\n\tbackground-color: #da2ae0 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-top:hover svg {\n\tfill: #ffffff;\n}\n\n[data-md-color-scheme=\"slate\"] .md-top:hover {\n\tbackground-color: #58fbda !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-top svg {\n\tfill: #000000 !important;\n}\n\n.benchmark-dashboard {\n\tmargin: 2rem 0;\n}\n\n.benchmark-dashboard iframe {\n\tbox-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n\tborder-radius: 8px;\n}\n\n.full-width {\n\tmargin: 0 -2rem;\n\tpadding: 2rem;\n\tbackground: var(--md-default-bg-color);\n}\n\n/* Chart container responsive adjustments */\n@media (max-width: 768px) {\n\t.full-width {\n\t\tmargin: 0 -1rem;\n\t\tpadding: 1rem;\n\t}\n\n\t.benchmark-dashboard iframe {\n\t\tbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n\t}\n}\n\n.md-typeset h2 a {\n\tcolor: #da2ae0 !important;\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tfont-weight: inherit;\n\ttext-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset h2 a {\n\tcolor: #58fbda !important;\n}\n\n.md-typeset .headerlink {\n\tcolor: transparent;\n\ttransition: color 0.2s ease;\n}\n\n.md-typeset .headerlink:hover,\n.md-typeset :target > .headerlink {\n\tcolor: #da2ae0 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .headerlink:hover,\n[data-md-color-scheme=\"slate\"] .md-typeset :target > .headerlink {\n\tcolor: #58fbda !important;\n}\n\n/* Hide sidebar on benchmarks page and make content full-width */\n/* Target the page using the full-width-iframe class we already have in the markdown */\n.full-width-iframe {\n\twidth: 100vw !important;\n\tmargin-left: calc(-50vw + 50%) !important;\n\tmargin-right: calc(-50vw + 50%) !important;\n\tmax-width: 100vw !important;\n}\n\n/* Hide sidebars when full-width-iframe is present */\nbody:has(.full-width-iframe) .md-sidebar--primary,\nbody:has(.full-width-iframe) .md-sidebar--secondary {\n\tdisplay: none !important;\n}\n\n/* Make content area full width when full-width-iframe is present */\nbody:has(.full-width-iframe) .md-content {\n\tmax-width: none !important;\n\tmargin: 0 !important;\n}\n\nbody:has(.full-width-iframe) .md-content__inner {\n\tmax-width: none !important;\n\tmargin: 0 !important;\n\tpadding: 1rem !important;\n}\n\nbody:has(.full-width-iframe) .md-main__inner {\n\tmargin: 0 !important;\n\tmax-width: none !important;\n}\n\nbody:has(.full-width-iframe) .md-grid {\n\tmax-width: none !important;\n}\n\n/* Make the article itself full width */\nbody:has(.full-width-iframe) article {\n\tmax-width: none !important;\n\tmargin: 0 !important;\n}\n\n/* Version badges */\n.version-badge {\n\tdisplay: inline-block;\n\tfont-size: 0.75rem;\n\tfont-weight: 600;\n\tfont-family: Inter, sans-serif;\n\tline-height: 1;\n\tpadding: 0.2em 0.6em;\n\tborder-radius: 999px;\n\tvertical-align: middle;\n\tbackground-color: #e0e0e0;\n\tcolor: #323040;\n}\n\n[data-md-color-scheme=\"slate\"] .version-badge {\n\tbackground-color: #3c3a4a;\n\tcolor: #ffffff;\n}\n\n.version-badge.unreleased {\n\tbackground-color: #fff3cd;\n\tcolor: #856404;\n}\n\n[data-md-color-scheme=\"slate\"] .version-badge.unreleased {\n\tbackground-color: #5c4a1e;\n\tcolor: #ffc107;\n}\n\n.version-badge.deprecated {\n\tbackground-color: #f8d7da;\n\tcolor: #842029;\n}\n\n[data-md-color-scheme=\"slate\"] .version-badge.deprecated {\n\tbackground-color: #4a1e22;\n\tcolor: #f87171;\n}\n\n.version-badge.new {\n\tbackground-color: #d1e7dd;\n\tcolor: #0f5132;\n}\n\n[data-md-color-scheme=\"slate\"] .version-badge.new {\n\tbackground-color: #1e4a2e;\n\tcolor: #50fa7b;\n}\n\n/* Homepage hero: title + description read as one block */\n.md-typeset .home-hero {\n\tmargin: 0 0 2.25rem;\n\tmax-width: 44rem;\n}\n\n.md-typeset .home-hero h1 {\n\tfont-family: \"Exo 2\", Inter, sans-serif;\n\tfont-size: clamp(2rem, 4.2vw, 2.625rem);\n\tfont-weight: 700;\n\tline-height: 1.12;\n\tletter-spacing: -0.03em;\n\tmargin: 0 0 0.85rem;\n\tpadding: 0;\n\tcolor: #1e1d26;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero h1 {\n\tcolor: #ffffff;\n}\n\n.md-typeset .home-hero h1::after {\n\tcontent: \"\";\n\tdisplay: block;\n\twidth: 2.75rem;\n\theight: 0.22rem;\n\tmargin-top: 0.85rem;\n\tborder-radius: 2px;\n\tbackground: linear-gradient(90deg, #da2ae0 0%, #b825c0 40%, #58fbda 100%);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero h1::after {\n\tbackground: linear-gradient(90deg, #58fbda 0%, #7ffce8 50%, #da2ae0 100%);\n\topacity: 0.95;\n}\n\n.md-typeset .home-hero .home-lead {\n\tfont-family: Inter, sans-serif;\n\tfont-size: 1.1875rem;\n\tfont-weight: 400;\n\tline-height: 1.62;\n\tmax-width: 40rem;\n\tmargin: 0;\n\tcolor: #3f3d4d;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero .home-lead {\n\tcolor: rgba(255, 255, 255, 0.88);\n}\n\n/* CTA / instruction card under hero */\n.md-typeset .home-hero .home-instruction {\n\tmargin-top: 1.35rem;\n\tmax-width: 42rem;\n\tpadding: 1rem 1.15rem 1.05rem 1.25rem;\n\tborder-radius: 8px;\n\tborder: 1px solid rgba(50, 48, 64, 0.12);\n\tborder-left: 4px solid #da2ae0;\n\tbackground: linear-gradient(135deg, rgba(218, 42, 224, 0.06) 0%, rgba(50, 48, 64, 0.04) 100%);\n\tbox-shadow: 0 1px 2px rgba(50, 48, 64, 0.06);\n}\n\n.md-typeset .home-hero .home-instruction h3 {\n\tmargin: 0 0 0.65rem;\n\tpadding: 0;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 0.8125rem;\n\tfont-weight: 700;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n\tcolor: #5b5966;\n\tline-height: 1.3;\n\tborder: none;\n}\n\n.md-typeset .home-hero .home-instruction ol {\n\tmargin: 0;\n\tpadding-left: 1.35rem;\n}\n\n.md-typeset .home-hero .home-instruction li {\n\tmargin: 0.4rem 0;\n\tcolor: #3f3d4d;\n\tfont-size: 1rem;\n\tline-height: 1.55;\n}\n\n.md-typeset .home-hero .home-instruction li:first-child {\n\tmargin-top: 0;\n}\n\n.md-typeset .home-hero .home-instruction li:last-child {\n\tmargin-bottom: 0;\n}\n\n.md-typeset .home-hero .home-instruction a {\n\tfont-weight: 600;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero .home-instruction {\n\tborder-color: rgba(255, 255, 255, 0.12);\n\tborder-left-color: #58fbda;\n\tbackground: linear-gradient(135deg, rgba(88, 251, 218, 0.08) 0%, rgba(0, 0, 0, 0.2) 100%);\n\tbox-shadow: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero .home-instruction h3 {\n\tcolor: rgba(255, 255, 255, 0.65);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-hero .home-instruction li {\n\tcolor: rgba(255, 255, 255, 0.88);\n}\n\n.md-typeset > .home-hero + h2 {\n\tmargin-top: 0;\n\tpadding-bottom: 0.4rem;\n\tborder-bottom: 1px solid rgba(50, 48, 64, 0.12);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset > .home-hero + h2 {\n\tborder-bottom-color: rgba(255, 255, 255, 0.1);\n}\n\n/* Home: \"What it does\" — card grid (markup in index.html only; not Markdown list/code heuristics). */\n.md-typeset .home-feature-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));\n\tgap: 1rem 1.15rem;\n\tmargin: 1rem 0 0;\n}\n\n.md-typeset .home-feature-card {\n\tmargin: 0;\n\tpadding: 1rem 1.1rem 1.05rem;\n\tborder-radius: 0.4rem;\n\tborder: 1px solid rgba(50, 48, 64, 0.12);\n\tbackground: rgba(50, 48, 64, 0.04);\n\tbox-sizing: border-box;\n\tmin-width: 0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-feature-card {\n\tborder-color: rgba(255, 255, 255, 0.1);\n\tbackground: rgba(0, 0, 0, 0.28);\n}\n\n.md-typeset .home-feature-card__title {\n\tmargin: 0 0 0.65rem;\n\tpadding: 0 0 0.65rem;\n\tfont-size: 0.9rem;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\tcolor: #1a1926;\n\tborder: none;\n\tborder-bottom: 1px solid rgba(50, 48, 64, 0.15);\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 0.5rem;\n}\n\n.md-typeset .home-feature-card__title::before {\n\tcontent: \"\";\n\twidth: 0.2rem;\n\tmin-height: 1.1rem;\n\tborder-radius: 2px;\n\tbackground: #da2ae0;\n\tflex-shrink: 0;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-feature-card__title {\n\tcolor: rgba(255, 255, 255, 0.96);\n\tborder-bottom-color: rgba(255, 255, 255, 0.12);\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-feature-card__title::before {\n\tbackground: #58fbda;\n}\n\n.md-typeset .home-feature-card__desc {\n\tmargin: 0.75rem 0 0;\n\tfont-size: 0.8rem;\n\tline-height: 1.55;\n\tcolor: #5b5966;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-feature-card__desc {\n\tcolor: rgba(255, 255, 255, 0.72);\n}\n\n.md-typeset .home-feature-card__desc code {\n\tfont-size: 0.9em;\n}\n\n.md-typeset .home-kreuzberg {\n\tmargin-top: 2.5rem;\n\tpadding: 1.15rem 1.35rem;\n\tborder-radius: 8px;\n\tborder: 1px solid rgba(50, 48, 64, 0.15);\n\tbackground: rgba(50, 48, 64, 0.03);\n}\n\n.md-typeset .home-kreuzberg > h2:first-child {\n\tmargin-top: 0;\n\tmargin-bottom: 0.65rem;\n\tpadding-bottom: 0;\n\tborder-bottom: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .home-kreuzberg {\n\tborder-color: rgba(255, 255, 255, 0.12);\n\tbackground: rgba(0, 0, 0, 0.2);\n}\n\n/* Page feedback: compact footer strip (hidden on root index.md via template) */\n.doc-feedback {\n\tmargin-top: 2rem;\n\tpadding: 0.65rem 1rem;\n\tborder-radius: 8px;\n\tborder: 1px solid rgba(50, 48, 64, 0.14);\n\tbackground: rgba(50, 48, 64, 0.04);\n\tfont-size: 0.8125rem;\n\tline-height: 1.5;\n\tcolor: #5b5966;\n}\n\n.doc-feedback__text {\n\tmargin: 0;\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tgap: 0.35rem 0.5rem;\n}\n\n.doc-feedback__title {\n\tcolor: #323040;\n\tfont-weight: 600;\n}\n\n.doc-feedback__sep {\n\tcolor: rgba(91, 89, 102, 0.45);\n\tuser-select: none;\n}\n\n.doc-feedback a {\n\tcolor: #da2ae0;\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n\n.doc-feedback a:hover {\n\ttext-decoration: underline;\n}\n\n[data-md-color-scheme=\"slate\"] .doc-feedback {\n\tborder-color: rgba(255, 255, 255, 0.12);\n\tbackground: rgba(0, 0, 0, 0.22);\n\tcolor: rgba(255, 255, 255, 0.72);\n}\n\n[data-md-color-scheme=\"slate\"] .doc-feedback__title {\n\tcolor: rgba(255, 255, 255, 0.92);\n}\n\n[data-md-color-scheme=\"slate\"] .doc-feedback__sep {\n\tcolor: rgba(255, 255, 255, 0.35);\n}\n\n[data-md-color-scheme=\"slate\"] .doc-feedback a {\n\tcolor: #58fbda;\n}\n\n/*\n * Footer meta carries BOTH .md-footer-meta and .md-typeset. In light palette, Material sets dark\n * prose --md-default-fg-color on .md-typeset, so copyright / “Made with …” match the bar (#323040).\n * Social SVGs used color:inherit and picked up that dark value. Overrides below ignore palette.\n */\nfooter.md-footer .md-footer-meta.md-typeset {\n\tcolor: rgba(255, 255, 255, 0.94) !important;\n\t--md-default-fg-color: rgba(255, 255, 255, 0.94) !important;\n\t--md-typeset-color: rgba(255, 255, 255, 0.94) !important;\n}\n\nfooter.md-footer .md-footer-meta .md-footer-meta__inner {\n\tcolor: rgba(255, 255, 255, 0.94) !important;\n}\n\nfooter.md-footer .md-footer-meta .md-copyright,\nfooter.md-footer .md-footer-meta .md-copyright__highlight {\n\tcolor: rgba(255, 255, 255, 0.96) !important;\n}\n\nfooter.md-footer .md-footer-meta a:not(.md-social__link) {\n\tcolor: #7ee9d8 !important;\n}\n\nfooter.md-footer .md-footer-meta a:not(.md-social__link):hover {\n\tcolor: #ffffff !important;\n}\n\nfooter.md-footer .md-footer-meta .md-social__link {\n\tcolor: #ffffff !important;\n}\n\nfooter.md-footer .md-footer-meta .md-social__link:hover {\n\tcolor: #58fbda !important;\n}\n\nfooter.md-footer .md-footer-meta .md-social__link svg,\nfooter.md-footer .md-footer-meta .md-social__link svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n\nfooter.md-footer .md-footer-meta .md-social__link:hover svg,\nfooter.md-footer .md-footer-meta .md-social__link:hover svg * {\n\tfill: #58fbda !important;\n\tcolor: #58fbda !important;\n}\n\n/* Header repo icon: same .md-icon / FA currentColor fight in light palette */\n.md-header .md-source .md-source__icon svg,\n.md-header .md-source .md-source__icon svg * {\n\tfill: #ffffff !important;\n\tcolor: #ffffff !important;\n\topacity: 1 !important;\n}\n"
  },
  {
    "path": "docs/demo/html_to_markdown_wasm.js",
    "content": "let wasm;\n\nfunction addHeapObject(obj) {\n    if (heap_next === heap.length) heap.push(heap.length + 1);\n    const idx = heap_next;\n    heap_next = heap[idx];\n\n    heap[idx] = obj;\n    return idx;\n}\n\nfunction _assertClass(instance, klass) {\n    if (!(instance instanceof klass)) {\n        throw new Error(`expected instance of ${klass.name}`);\n    }\n}\n\nfunction debugString(val) {\n    // primitive types\n    const type = typeof val;\n    if (type == 'number' || type == 'boolean' || val == null) {\n        return  `${val}`;\n    }\n    if (type == 'string') {\n        return `\"${val}\"`;\n    }\n    if (type == 'symbol') {\n        const description = val.description;\n        if (description == null) {\n            return 'Symbol';\n        } else {\n            return `Symbol(${description})`;\n        }\n    }\n    if (type == 'function') {\n        const name = val.name;\n        if (typeof name == 'string' && name.length > 0) {\n            return `Function(${name})`;\n        } else {\n            return 'Function';\n        }\n    }\n    // objects\n    if (Array.isArray(val)) {\n        const length = val.length;\n        let debug = '[';\n        if (length > 0) {\n            debug += debugString(val[0]);\n        }\n        for(let i = 1; i < length; i++) {\n            debug += ', ' + debugString(val[i]);\n        }\n        debug += ']';\n        return debug;\n    }\n    // Test for built-in\n    const builtInMatches = /\\[object ([^\\]]+)\\]/.exec(toString.call(val));\n    let className;\n    if (builtInMatches && builtInMatches.length > 1) {\n        className = builtInMatches[1];\n    } else {\n        // Failed to match the standard '[object ClassName]'\n        return toString.call(val);\n    }\n    if (className == 'Object') {\n        // we're a user defined class or Object\n        // JSON.stringify avoids problems with cycles, and is generally much\n        // easier than looping through ownProperties of `val`.\n        try {\n            return 'Object(' + JSON.stringify(val) + ')';\n        } catch (_) {\n            return 'Object';\n        }\n    }\n    // errors\n    if (val instanceof Error) {\n        return `${val.name}: ${val.message}\\n${val.stack}`;\n    }\n    // TODO we could test for more things here, like `Set`s and `Map`s.\n    return className;\n}\n\nfunction dropObject(idx) {\n    if (idx < 132) return;\n    heap[idx] = heap_next;\n    heap_next = idx;\n}\n\nfunction getArrayJsValueFromWasm0(ptr, len) {\n    ptr = ptr >>> 0;\n    const mem = getDataViewMemory0();\n    const result = [];\n    for (let i = ptr; i < ptr + 4 * len; i += 4) {\n        result.push(takeObject(mem.getUint32(i, true)));\n    }\n    return result;\n}\n\nfunction getArrayU32FromWasm0(ptr, len) {\n    ptr = ptr >>> 0;\n    return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);\n}\n\nfunction getArrayU8FromWasm0(ptr, len) {\n    ptr = ptr >>> 0;\n    return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);\n}\n\nlet cachedDataViewMemory0 = null;\nfunction getDataViewMemory0() {\n    if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {\n        cachedDataViewMemory0 = new DataView(wasm.memory.buffer);\n    }\n    return cachedDataViewMemory0;\n}\n\nfunction getStringFromWasm0(ptr, len) {\n    ptr = ptr >>> 0;\n    return decodeText(ptr, len);\n}\n\nlet cachedUint32ArrayMemory0 = null;\nfunction getUint32ArrayMemory0() {\n    if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) {\n        cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer);\n    }\n    return cachedUint32ArrayMemory0;\n}\n\nlet cachedUint8ArrayMemory0 = null;\nfunction getUint8ArrayMemory0() {\n    if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {\n        cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);\n    }\n    return cachedUint8ArrayMemory0;\n}\n\nfunction getObject(idx) { return heap[idx]; }\n\nfunction handleError(f, args) {\n    try {\n        return f.apply(this, args);\n    } catch (e) {\n        wasm.__wbindgen_export3(addHeapObject(e));\n    }\n}\n\nlet heap = new Array(128).fill(undefined);\nheap.push(undefined, null, true, false);\n\nlet heap_next = heap.length;\n\nfunction isLikeNone(x) {\n    return x === undefined || x === null;\n}\n\nfunction passStringToWasm0(arg, malloc, realloc) {\n    if (realloc === undefined) {\n        const buf = cachedTextEncoder.encode(arg);\n        const ptr = malloc(buf.length, 1) >>> 0;\n        getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);\n        WASM_VECTOR_LEN = buf.length;\n        return ptr;\n    }\n\n    let len = arg.length;\n    let ptr = malloc(len, 1) >>> 0;\n\n    const mem = getUint8ArrayMemory0();\n\n    let offset = 0;\n\n    for (; offset < len; offset++) {\n        const code = arg.charCodeAt(offset);\n        if (code > 0x7F) break;\n        mem[ptr + offset] = code;\n    }\n    if (offset !== len) {\n        if (offset !== 0) {\n            arg = arg.slice(offset);\n        }\n        ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;\n        const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);\n        const ret = cachedTextEncoder.encodeInto(arg, view);\n\n        offset += ret.written;\n        ptr = realloc(ptr, len, offset, 1) >>> 0;\n    }\n\n    WASM_VECTOR_LEN = offset;\n    return ptr;\n}\n\nfunction takeObject(idx) {\n    const ret = getObject(idx);\n    dropObject(idx);\n    return ret;\n}\n\nlet cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });\ncachedTextDecoder.decode();\nconst MAX_SAFARI_DECODE_BYTES = 2146435072;\nlet numBytesDecoded = 0;\nfunction decodeText(ptr, len) {\n    numBytesDecoded += len;\n    if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {\n        cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });\n        cachedTextDecoder.decode();\n        numBytesDecoded = len;\n    }\n    return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));\n}\n\nconst cachedTextEncoder = new TextEncoder();\n\nif (!('encodeInto' in cachedTextEncoder)) {\n    cachedTextEncoder.encodeInto = function (arg, view) {\n        const buf = cachedTextEncoder.encode(arg);\n        view.set(buf);\n        return {\n            read: arg.length,\n            written: buf.length\n        };\n    }\n}\n\nlet WASM_VECTOR_LEN = 0;\n\nconst WasmConversionOptionsHandleFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasmconversionoptionshandle_free(ptr >>> 0, 1));\n\nconst WasmHtmlExtractionFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasmhtmlextraction_free(ptr >>> 0, 1));\n\nconst WasmInlineImageFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasminlineimage_free(ptr >>> 0, 1));\n\nconst WasmInlineImageConfigFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasminlineimageconfig_free(ptr >>> 0, 1));\n\nconst WasmInlineImageWarningFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasminlineimagewarning_free(ptr >>> 0, 1));\n\nconst WasmMetadataConfigFinalization = (typeof FinalizationRegistry === 'undefined')\n    ? { register: () => {}, unregister: () => {} }\n    : new FinalizationRegistry(ptr => wasm.__wbg_wasmmetadataconfig_free(ptr >>> 0, 1));\n\nexport class WasmConversionOptionsHandle {\n    static __wrap(ptr) {\n        ptr = ptr >>> 0;\n        const obj = Object.create(WasmConversionOptionsHandle.prototype);\n        obj.__wbg_ptr = ptr;\n        WasmConversionOptionsHandleFinalization.register(obj, obj.__wbg_ptr, obj);\n        return obj;\n    }\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmConversionOptionsHandleFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasmconversionoptionshandle_free(ptr, 0);\n    }\n    /**\n     * @param {any} options\n     */\n    constructor(options) {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasmconversionoptionshandle_new(retptr, addHeapObject(options));\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n            if (r2) {\n                throw takeObject(r1);\n            }\n            this.__wbg_ptr = r0 >>> 0;\n            WasmConversionOptionsHandleFinalization.register(this, this.__wbg_ptr, this);\n            return this;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n}\nif (Symbol.dispose) WasmConversionOptionsHandle.prototype[Symbol.dispose] = WasmConversionOptionsHandle.prototype.free;\n\n/**\n * Result of HTML extraction with inline images\n */\nexport class WasmHtmlExtraction {\n    static __wrap(ptr) {\n        ptr = ptr >>> 0;\n        const obj = Object.create(WasmHtmlExtraction.prototype);\n        obj.__wbg_ptr = ptr;\n        WasmHtmlExtractionFinalization.register(obj, obj.__wbg_ptr, obj);\n        return obj;\n    }\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmHtmlExtractionFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasmhtmlextraction_free(ptr, 0);\n    }\n    /**\n     * @returns {WasmInlineImage[]}\n     */\n    get inlineImages() {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasmhtmlextraction_inlineImages(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            var v1 = getArrayJsValueFromWasm0(r0, r1).slice();\n            wasm.__wbindgen_export4(r0, r1 * 4, 4);\n            return v1;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n    /**\n     * @returns {string}\n     */\n    get markdown() {\n        let deferred1_0;\n        let deferred1_1;\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasmhtmlextraction_markdown(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            deferred1_0 = r0;\n            deferred1_1 = r1;\n            return getStringFromWasm0(r0, r1);\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n            wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);\n        }\n    }\n    /**\n     * @returns {WasmInlineImageWarning[]}\n     */\n    get warnings() {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasmhtmlextraction_warnings(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            var v1 = getArrayJsValueFromWasm0(r0, r1).slice();\n            wasm.__wbindgen_export4(r0, r1 * 4, 4);\n            return v1;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n}\nif (Symbol.dispose) WasmHtmlExtraction.prototype[Symbol.dispose] = WasmHtmlExtraction.prototype.free;\n\n/**\n * Inline image data\n */\nexport class WasmInlineImage {\n    static __wrap(ptr) {\n        ptr = ptr >>> 0;\n        const obj = Object.create(WasmInlineImage.prototype);\n        obj.__wbg_ptr = ptr;\n        WasmInlineImageFinalization.register(obj, obj.__wbg_ptr, obj);\n        return obj;\n    }\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmInlineImageFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasminlineimage_free(ptr, 0);\n    }\n    /**\n     * @returns {any}\n     */\n    get attributes() {\n        const ret = wasm.wasminlineimage_attributes(this.__wbg_ptr);\n        return takeObject(ret);\n    }\n    /**\n     * @returns {Uint32Array | undefined}\n     */\n    get dimensions() {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimage_dimensions(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            let v1;\n            if (r0 !== 0) {\n                v1 = getArrayU32FromWasm0(r0, r1).slice();\n                wasm.__wbindgen_export4(r0, r1 * 4, 4);\n            }\n            return v1;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n    /**\n     * @returns {string | undefined}\n     */\n    get description() {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimage_description(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            let v1;\n            if (r0 !== 0) {\n                v1 = getStringFromWasm0(r0, r1).slice();\n                wasm.__wbindgen_export4(r0, r1 * 1, 1);\n            }\n            return v1;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n    /**\n     * @returns {Uint8Array}\n     */\n    get data() {\n        const ret = wasm.wasminlineimage_data(this.__wbg_ptr);\n        return takeObject(ret);\n    }\n    /**\n     * @returns {string}\n     */\n    get format() {\n        let deferred1_0;\n        let deferred1_1;\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimage_format(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            deferred1_0 = r0;\n            deferred1_1 = r1;\n            return getStringFromWasm0(r0, r1);\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n            wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);\n        }\n    }\n    /**\n     * @returns {string}\n     */\n    get source() {\n        let deferred1_0;\n        let deferred1_1;\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimage_source(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            deferred1_0 = r0;\n            deferred1_1 = r1;\n            return getStringFromWasm0(r0, r1);\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n            wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);\n        }\n    }\n    /**\n     * @returns {string | undefined}\n     */\n    get filename() {\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimage_filename(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            let v1;\n            if (r0 !== 0) {\n                v1 = getStringFromWasm0(r0, r1).slice();\n                wasm.__wbindgen_export4(r0, r1 * 1, 1);\n            }\n            return v1;\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n        }\n    }\n}\nif (Symbol.dispose) WasmInlineImage.prototype[Symbol.dispose] = WasmInlineImage.prototype.free;\n\n/**\n * Inline image configuration\n */\nexport class WasmInlineImageConfig {\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmInlineImageConfigFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasminlineimageconfig_free(ptr, 0);\n    }\n    /**\n     * @param {boolean} capture\n     */\n    set captureSvg(capture) {\n        wasm.wasminlineimageconfig_set_captureSvg(this.__wbg_ptr, capture);\n    }\n    /**\n     * @param {string | null} [prefix]\n     */\n    set filenamePrefix(prefix) {\n        var ptr0 = isLikeNone(prefix) ? 0 : passStringToWasm0(prefix, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        var len0 = WASM_VECTOR_LEN;\n        wasm.wasminlineimageconfig_set_filenamePrefix(this.__wbg_ptr, ptr0, len0);\n    }\n    /**\n     * @param {boolean} infer\n     */\n    set inferDimensions(infer) {\n        wasm.wasminlineimageconfig_set_inferDimensions(this.__wbg_ptr, infer);\n    }\n    /**\n     * @param {number | null} [max_decoded_size_bytes]\n     */\n    constructor(max_decoded_size_bytes) {\n        const ret = wasm.wasminlineimageconfig_new(!isLikeNone(max_decoded_size_bytes), isLikeNone(max_decoded_size_bytes) ? 0 : max_decoded_size_bytes);\n        this.__wbg_ptr = ret >>> 0;\n        WasmInlineImageConfigFinalization.register(this, this.__wbg_ptr, this);\n        return this;\n    }\n}\nif (Symbol.dispose) WasmInlineImageConfig.prototype[Symbol.dispose] = WasmInlineImageConfig.prototype.free;\n\n/**\n * Warning about inline image processing\n */\nexport class WasmInlineImageWarning {\n    static __wrap(ptr) {\n        ptr = ptr >>> 0;\n        const obj = Object.create(WasmInlineImageWarning.prototype);\n        obj.__wbg_ptr = ptr;\n        WasmInlineImageWarningFinalization.register(obj, obj.__wbg_ptr, obj);\n        return obj;\n    }\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmInlineImageWarningFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasminlineimagewarning_free(ptr, 0);\n    }\n    /**\n     * @returns {number}\n     */\n    get index() {\n        const ret = wasm.wasminlineimagewarning_index(this.__wbg_ptr);\n        return ret >>> 0;\n    }\n    /**\n     * @returns {string}\n     */\n    get message() {\n        let deferred1_0;\n        let deferred1_1;\n        try {\n            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n            wasm.wasminlineimagewarning_message(retptr, this.__wbg_ptr);\n            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n            deferred1_0 = r0;\n            deferred1_1 = r1;\n            return getStringFromWasm0(r0, r1);\n        } finally {\n            wasm.__wbindgen_add_to_stack_pointer(16);\n            wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);\n        }\n    }\n}\nif (Symbol.dispose) WasmInlineImageWarning.prototype[Symbol.dispose] = WasmInlineImageWarning.prototype.free;\n\n/**\n * Metadata extraction configuration\n */\nexport class WasmMetadataConfig {\n    __destroy_into_raw() {\n        const ptr = this.__wbg_ptr;\n        this.__wbg_ptr = 0;\n        WasmMetadataConfigFinalization.unregister(this);\n        return ptr;\n    }\n    free() {\n        const ptr = this.__destroy_into_raw();\n        wasm.__wbg_wasmmetadataconfig_free(ptr, 0);\n    }\n    /**\n     * @returns {boolean}\n     */\n    get extract_links() {\n        const ret = wasm.wasmmetadataconfig_extract_links(this.__wbg_ptr);\n        return ret !== 0;\n    }\n    /**\n     * @returns {boolean}\n     */\n    get extract_images() {\n        const ret = wasm.wasmmetadataconfig_extract_images(this.__wbg_ptr);\n        return ret !== 0;\n    }\n    /**\n     * @returns {boolean}\n     */\n    get extract_headers() {\n        const ret = wasm.wasmmetadataconfig_extract_headers(this.__wbg_ptr);\n        return ret !== 0;\n    }\n    /**\n     * @returns {boolean}\n     */\n    get extract_document() {\n        const ret = wasm.wasmmetadataconfig_extract_document(this.__wbg_ptr);\n        return ret !== 0;\n    }\n    /**\n     * @param {boolean} value\n     */\n    set extract_links(value) {\n        wasm.wasmmetadataconfig_set_extract_links(this.__wbg_ptr, value);\n    }\n    /**\n     * @param {boolean} value\n     */\n    set extract_images(value) {\n        wasm.wasmmetadataconfig_set_extract_images(this.__wbg_ptr, value);\n    }\n    /**\n     * @param {boolean} value\n     */\n    set extract_headers(value) {\n        wasm.wasmmetadataconfig_set_extract_headers(this.__wbg_ptr, value);\n    }\n    /**\n     * @param {boolean} value\n     */\n    set extract_document(value) {\n        wasm.wasmmetadataconfig_set_extract_document(this.__wbg_ptr, value);\n    }\n    /**\n     * @returns {boolean}\n     */\n    get extract_structured_data() {\n        const ret = wasm.wasmmetadataconfig_extract_structured_data(this.__wbg_ptr);\n        return ret !== 0;\n    }\n    /**\n     * @returns {number}\n     */\n    get max_structured_data_size() {\n        const ret = wasm.wasmmetadataconfig_max_structured_data_size(this.__wbg_ptr);\n        return ret >>> 0;\n    }\n    /**\n     * @param {boolean} value\n     */\n    set extract_structured_data(value) {\n        wasm.wasmmetadataconfig_set_extract_structured_data(this.__wbg_ptr, value);\n    }\n    /**\n     * @param {number} value\n     */\n    set max_structured_data_size(value) {\n        wasm.wasmmetadataconfig_set_max_structured_data_size(this.__wbg_ptr, value);\n    }\n    /**\n     * Create a new metadata configuration with defaults\n     *\n     * All extraction types enabled by default with 1MB structured data limit\n     */\n    constructor() {\n        const ret = wasm.wasmmetadataconfig_new();\n        this.__wbg_ptr = ret >>> 0;\n        WasmMetadataConfigFinalization.register(this, this.__wbg_ptr, this);\n        return this;\n    }\n}\nif (Symbol.dispose) WasmMetadataConfig.prototype[Symbol.dispose] = WasmMetadataConfig.prototype.free;\n\n/**\n * Convert HTML to Markdown\n *\n * # Arguments\n *\n * * `html` - The HTML string to convert\n * * `options` - Optional conversion options (as a JavaScript object)\n *\n * # Example\n *\n * ```javascript\n * import { convert } from 'html-to-markdown-wasm';\n *\n * const html = '<h1>Hello World</h1>';\n * const markdown = convert(html);\n * console.log(markdown); // # Hello World\n * ```\n * @param {string} html\n * @param {any} options\n * @returns {string}\n */\nexport function convert(html, options) {\n    let deferred3_0;\n    let deferred3_1;\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        const ptr0 = passStringToWasm0(html, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len0 = WASM_VECTOR_LEN;\n        wasm.convert(retptr, ptr0, len0, addHeapObject(options));\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);\n        var ptr2 = r0;\n        var len2 = r1;\n        if (r3) {\n            ptr2 = 0; len2 = 0;\n            throw takeObject(r2);\n        }\n        deferred3_0 = ptr2;\n        deferred3_1 = len2;\n        return getStringFromWasm0(ptr2, len2);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n        wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);\n    }\n}\n\n/**\n * @param {Uint8Array} html\n * @param {any} options\n * @returns {string}\n */\nexport function convertBytes(html, options) {\n    let deferred2_0;\n    let deferred2_1;\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        wasm.convertBytes(retptr, addHeapObject(html), addHeapObject(options));\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);\n        var ptr1 = r0;\n        var len1 = r1;\n        if (r3) {\n            ptr1 = 0; len1 = 0;\n            throw takeObject(r2);\n        }\n        deferred2_0 = ptr1;\n        deferred2_1 = len1;\n        return getStringFromWasm0(ptr1, len1);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n        wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1);\n    }\n}\n\n/**\n * @param {Uint8Array} html\n * @param {any} options\n * @param {WasmInlineImageConfig | null} [image_config]\n * @returns {WasmHtmlExtraction}\n */\nexport function convertBytesWithInlineImages(html, options, image_config) {\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        let ptr0 = 0;\n        if (!isLikeNone(image_config)) {\n            _assertClass(image_config, WasmInlineImageConfig);\n            ptr0 = image_config.__destroy_into_raw();\n        }\n        wasm.convertBytesWithInlineImages(retptr, addHeapObject(html), addHeapObject(options), ptr0);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        if (r2) {\n            throw takeObject(r1);\n        }\n        return WasmHtmlExtraction.__wrap(r0);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n    }\n}\n\n/**\n * Convert HTML bytes to Markdown with metadata extraction\n *\n * # Arguments\n *\n * * `html` - The HTML bytes to convert\n * * `options` - Optional conversion options (as a JavaScript object)\n * * `metadata_config` - Metadata extraction configuration\n *\n * # Returns\n *\n * JavaScript object with `markdown` (string) and `metadata` (object) fields\n * @param {Uint8Array} html\n * @param {any} options\n * @param {WasmMetadataConfig | null} [metadata_config]\n * @returns {any}\n */\nexport function convertBytesWithMetadata(html, options, metadata_config) {\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        let ptr0 = 0;\n        if (!isLikeNone(metadata_config)) {\n            _assertClass(metadata_config, WasmMetadataConfig);\n            ptr0 = metadata_config.__destroy_into_raw();\n        }\n        wasm.convertBytesWithMetadata(retptr, addHeapObject(html), addHeapObject(options), ptr0);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        if (r2) {\n            throw takeObject(r1);\n        }\n        return takeObject(r0);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n    }\n}\n\n/**\n * @param {Uint8Array} html\n * @param {WasmConversionOptionsHandle} handle\n * @returns {string}\n */\nexport function convertBytesWithOptionsHandle(html, handle) {\n    let deferred2_0;\n    let deferred2_1;\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        _assertClass(handle, WasmConversionOptionsHandle);\n        wasm.convertBytesWithOptionsHandle(retptr, addHeapObject(html), handle.__wbg_ptr);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);\n        var ptr1 = r0;\n        var len1 = r1;\n        if (r3) {\n            ptr1 = 0; len1 = 0;\n            throw takeObject(r2);\n        }\n        deferred2_0 = ptr1;\n        deferred2_1 = len1;\n        return getStringFromWasm0(ptr1, len1);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n        wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1);\n    }\n}\n\n/**\n * @param {string} html\n * @param {any} options\n * @param {WasmInlineImageConfig | null} [image_config]\n * @returns {WasmHtmlExtraction}\n */\nexport function convertWithInlineImages(html, options, image_config) {\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        const ptr0 = passStringToWasm0(html, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len0 = WASM_VECTOR_LEN;\n        let ptr1 = 0;\n        if (!isLikeNone(image_config)) {\n            _assertClass(image_config, WasmInlineImageConfig);\n            ptr1 = image_config.__destroy_into_raw();\n        }\n        wasm.convertWithInlineImages(retptr, ptr0, len0, addHeapObject(options), ptr1);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        if (r2) {\n            throw takeObject(r1);\n        }\n        return WasmHtmlExtraction.__wrap(r0);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n    }\n}\n\n/**\n * Convert HTML to Markdown with metadata extraction\n *\n * # Arguments\n *\n * * `html` - The HTML string to convert\n * * `options` - Optional conversion options (as a JavaScript object)\n * * `metadata_config` - Metadata extraction configuration\n *\n * # Returns\n *\n * JavaScript object with `markdown` (string) and `metadata` (object) fields\n *\n * # Example\n *\n * ```javascript\n * import { convertWithMetadata, WasmMetadataConfig } from 'html-to-markdown-wasm';\n *\n * const html = '<h1>Hello World</h1><a href=\"https://example.com\">Link</a>';\n * const config = new WasmMetadataConfig();\n * config.extractHeaders = true;\n * config.extractLinks = true;\n *\n * const result = convertWithMetadata(html, null, config);\n * console.log(result.markdown); // # Hello World\\n\\n[Link](https://example.com)\n * console.log(result.metadata.headers); // [{ level: 1, text: \"Hello World\", ... }]\n * console.log(result.metadata.links); // [{ href: \"https://example.com\", text: \"Link\", ... }]\n * ```\n * @param {string} html\n * @param {any} options\n * @param {WasmMetadataConfig | null} [metadata_config]\n * @returns {any}\n */\nexport function convertWithMetadata(html, options, metadata_config) {\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        const ptr0 = passStringToWasm0(html, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len0 = WASM_VECTOR_LEN;\n        let ptr1 = 0;\n        if (!isLikeNone(metadata_config)) {\n            _assertClass(metadata_config, WasmMetadataConfig);\n            ptr1 = metadata_config.__destroy_into_raw();\n        }\n        wasm.convertWithMetadata(retptr, ptr0, len0, addHeapObject(options), ptr1);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        if (r2) {\n            throw takeObject(r1);\n        }\n        return takeObject(r0);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n    }\n}\n\n/**\n * @param {string} html\n * @param {WasmConversionOptionsHandle} handle\n * @returns {string}\n */\nexport function convertWithOptionsHandle(html, handle) {\n    let deferred3_0;\n    let deferred3_1;\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        const ptr0 = passStringToWasm0(html, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len0 = WASM_VECTOR_LEN;\n        _assertClass(handle, WasmConversionOptionsHandle);\n        wasm.convertWithOptionsHandle(retptr, ptr0, len0, handle.__wbg_ptr);\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);\n        var ptr2 = r0;\n        var len2 = r1;\n        if (r3) {\n            ptr2 = 0; len2 = 0;\n            throw takeObject(r2);\n        }\n        deferred3_0 = ptr2;\n        deferred3_1 = len2;\n        return getStringFromWasm0(ptr2, len2);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n        wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);\n    }\n}\n\n/**\n * @param {any} options\n * @returns {WasmConversionOptionsHandle}\n */\nexport function createConversionOptionsHandle(options) {\n    try {\n        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);\n        wasm.createConversionOptionsHandle(retptr, addHeapObject(options));\n        var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);\n        var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);\n        var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);\n        if (r2) {\n            throw takeObject(r1);\n        }\n        return WasmConversionOptionsHandle.__wrap(r0);\n    } finally {\n        wasm.__wbindgen_add_to_stack_pointer(16);\n    }\n}\n\n/**\n * Initialize panic hook for better error messages in the browser\n */\nexport function init() {\n    wasm.init();\n}\n\nconst EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);\n\nasync function __wbg_load(module, imports) {\n    if (typeof Response === 'function' && module instanceof Response) {\n        if (typeof WebAssembly.instantiateStreaming === 'function') {\n            try {\n                return await WebAssembly.instantiateStreaming(module, imports);\n            } catch (e) {\n                const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type);\n\n                if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {\n                    console.warn(\"`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\\n\", e);\n\n                } else {\n                    throw e;\n                }\n            }\n        }\n\n        const bytes = await module.arrayBuffer();\n        return await WebAssembly.instantiate(bytes, imports);\n    } else {\n        const instance = await WebAssembly.instantiate(module, imports);\n\n        if (instance instanceof WebAssembly.Instance) {\n            return { instance, module };\n        } else {\n            return instance;\n        }\n    }\n}\n\nfunction __wbg_get_imports() {\n    const imports = {};\n    imports.wbg = {};\n    imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) {\n        const ret = Error(getStringFromWasm0(arg0, arg1));\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) {\n        const ret = Number(getObject(arg0));\n        return ret;\n    };\n    imports.wbg.__wbg_String_8f0eb39a4a4c2f66 = function(arg0, arg1) {\n        const ret = String(getObject(arg1));\n        const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len1 = WASM_VECTOR_LEN;\n        getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);\n    };\n    imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) {\n        const v = getObject(arg1);\n        const ret = typeof(v) === 'bigint' ? v : undefined;\n        getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);\n    };\n    imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) {\n        const v = getObject(arg0);\n        const ret = typeof(v) === 'boolean' ? v : undefined;\n        return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;\n    };\n    imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) {\n        const ret = debugString(getObject(arg1));\n        const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len1 = WASM_VECTOR_LEN;\n        getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);\n    };\n    imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) {\n        const ret = getObject(arg0) in getObject(arg1);\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) {\n        const ret = typeof(getObject(arg0)) === 'bigint';\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) {\n        const ret = typeof(getObject(arg0)) === 'function';\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_null_dfda7d66506c95b5 = function(arg0) {\n        const ret = getObject(arg0) === null;\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) {\n        const val = getObject(arg0);\n        const ret = typeof(val) === 'object' && val !== null;\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_string_704ef9c8fc131030 = function(arg0) {\n        const ret = typeof(getObject(arg0)) === 'string';\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) {\n        const ret = getObject(arg0) === undefined;\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) {\n        const ret = getObject(arg0) === getObject(arg1);\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) {\n        const ret = getObject(arg0) == getObject(arg1);\n        return ret;\n    };\n    imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) {\n        const obj = getObject(arg1);\n        const ret = typeof(obj) === 'number' ? obj : undefined;\n        getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);\n    };\n    imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) {\n        const obj = getObject(arg1);\n        const ret = typeof(obj) === 'string' ? obj : undefined;\n        var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        var len1 = WASM_VECTOR_LEN;\n        getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);\n    };\n    imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) {\n        throw new Error(getStringFromWasm0(arg0, arg1));\n    };\n    imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) {\n        const ret = getObject(arg0).call(getObject(arg1));\n        return addHeapObject(ret);\n    }, arguments) };\n    imports.wbg.__wbg_codePointAt_6fd4439a1e465afd = function(arg0, arg1) {\n        const ret = getObject(arg0).codePointAt(arg1 >>> 0);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) {\n        const ret = getObject(arg0).done;\n        return ret;\n    };\n    imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {\n        let deferred0_0;\n        let deferred0_1;\n        try {\n            deferred0_0 = arg0;\n            deferred0_1 = arg1;\n            console.error(getStringFromWasm0(arg0, arg1));\n        } finally {\n            wasm.__wbindgen_export4(deferred0_0, deferred0_1, 1);\n        }\n    };\n    imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) {\n        const ret = getObject(arg0)[arg1 >>> 0];\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) {\n        const ret = Reflect.get(getObject(arg0), getObject(arg1));\n        return addHeapObject(ret);\n    }, arguments) };\n    imports.wbg.__wbg_get_with_ref_key_1dc361bd10053bfe = function(arg0, arg1) {\n        const ret = getObject(arg0)[getObject(arg1)];\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) {\n        let result;\n        try {\n            result = getObject(arg0) instanceof ArrayBuffer;\n        } catch (_) {\n            result = false;\n        }\n        const ret = result;\n        return ret;\n    };\n    imports.wbg.__wbg_instanceof_Object_577e21051f7bcb79 = function(arg0) {\n        let result;\n        try {\n            result = getObject(arg0) instanceof Object;\n        } catch (_) {\n            result = false;\n        }\n        const ret = result;\n        return ret;\n    };\n    imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) {\n        let result;\n        try {\n            result = getObject(arg0) instanceof Uint8Array;\n        } catch (_) {\n            result = false;\n        }\n        const ret = result;\n        return ret;\n    };\n    imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) {\n        const ret = Array.isArray(getObject(arg0));\n        return ret;\n    };\n    imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) {\n        const ret = Number.isSafeInteger(getObject(arg0));\n        return ret;\n    };\n    imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() {\n        const ret = Symbol.iterator;\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_keys_f5c6002ff150fc6c = function(arg0) {\n        const ret = Object.keys(getObject(arg0));\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_length_1f83b8e5895c84aa = function(arg0) {\n        const ret = getObject(arg0).length;\n        return ret;\n    };\n    imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) {\n        const ret = getObject(arg0).length;\n        return ret;\n    };\n    imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) {\n        const ret = getObject(arg0).length;\n        return ret;\n    };\n    imports.wbg.__wbg_new_1ba21ce319a06297 = function() {\n        const ret = new Object();\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_new_25f239778d6112b9 = function() {\n        const ret = new Array();\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) {\n        const ret = new Uint8Array(getObject(arg0));\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {\n        const ret = new Error();\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_new_b546ae120718850e = function() {\n        const ret = new Map();\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) {\n        const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1));\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) {\n        const ret = getObject(arg0).next;\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) {\n        const ret = getObject(arg0).next();\n        return addHeapObject(ret);\n    }, arguments) };\n    imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) {\n        Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));\n    };\n    imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) {\n        getObject(arg0)[takeObject(arg1)] = takeObject(arg2);\n    };\n    imports.wbg.__wbg_set_781438a03c0c3c81 = function() { return handleError(function (arg0, arg1, arg2) {\n        const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));\n        return ret;\n    }, arguments) };\n    imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) {\n        getObject(arg0)[arg1 >>> 0] = takeObject(arg2);\n    };\n    imports.wbg.__wbg_set_efaaf145b9377369 = function(arg0, arg1, arg2) {\n        const ret = getObject(arg0).set(getObject(arg1), getObject(arg2));\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {\n        const ret = getObject(arg1).stack;\n        const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);\n        const len1 = WASM_VECTOR_LEN;\n        getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);\n        getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);\n    };\n    imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) {\n        const ret = getObject(arg0).value;\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_wasminlineimage_new = function(arg0) {\n        const ret = WasmInlineImage.__wrap(arg0);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbg_wasminlineimagewarning_new = function(arg0) {\n        const ret = WasmInlineImageWarning.__wrap(arg0);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {\n        // Cast intrinsic for `Ref(String) -> Externref`.\n        const ret = getStringFromWasm0(arg0, arg1);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) {\n        // Cast intrinsic for `U64 -> Externref`.\n        const ret = BigInt.asUintN(64, arg0);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {\n        // Cast intrinsic for `F64 -> Externref`.\n        const ret = arg0;\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbindgen_object_clone_ref = function(arg0) {\n        const ret = getObject(arg0);\n        return addHeapObject(ret);\n    };\n    imports.wbg.__wbindgen_object_drop_ref = function(arg0) {\n        takeObject(arg0);\n    };\n\n    return imports;\n}\n\nfunction __wbg_finalize_init(instance, module) {\n    wasm = instance.exports;\n    __wbg_init.__wbindgen_wasm_module = module;\n    cachedDataViewMemory0 = null;\n    cachedUint32ArrayMemory0 = null;\n    cachedUint8ArrayMemory0 = null;\n\n\n    wasm.__wbindgen_start();\n    return wasm;\n}\n\nfunction initSync(module) {\n    if (wasm !== undefined) return wasm;\n\n\n    if (typeof module !== 'undefined') {\n        if (Object.getPrototypeOf(module) === Object.prototype) {\n            ({module} = module)\n        } else {\n            console.warn('using deprecated parameters for `initSync()`; pass a single object instead')\n        }\n    }\n\n    const imports = __wbg_get_imports();\n    if (!(module instanceof WebAssembly.Module)) {\n        module = new WebAssembly.Module(module);\n    }\n    const instance = new WebAssembly.Instance(module, imports);\n    return __wbg_finalize_init(instance, module);\n}\n\nasync function __wbg_init(module_or_path) {\n    if (wasm !== undefined) return wasm;\n\n\n    if (typeof module_or_path !== 'undefined') {\n        if (Object.getPrototypeOf(module_or_path) === Object.prototype) {\n            ({module_or_path} = module_or_path)\n        } else {\n            console.warn('using deprecated parameters for the initialization function; pass a single object instead')\n        }\n    }\n\n    if (typeof module_or_path === 'undefined') {\n        module_or_path = new URL('html_to_markdown_wasm_bg.wasm', import.meta.url);\n    }\n    const imports = __wbg_get_imports();\n\n    if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {\n        module_or_path = fetch(module_or_path);\n    }\n\n    const { instance, module } = await __wbg_load(await module_or_path, imports);\n\n    return __wbg_finalize_init(instance, module);\n}\n\nexport { initSync };\nexport default __wbg_init;\n"
  },
  {
    "path": "docs/demo/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>HTML to Markdown | Live Demo</title>\n    <meta name=\"description\" content=\"High-performance HTML to Markdown converter powered by Rust and WebAssembly. Try it live in your browser.\">\n    <meta property=\"og:title\" content=\"HTML to Markdown | Live Demo\">\n    <meta property=\"og:description\" content=\"Convert HTML to Markdown in your browser. Rust core compiled to WebAssembly.\">\n    <meta property=\"og:url\" content=\"https://docs.html-to-markdown.kreuzberg.dev/demo/\">\n    <meta property=\"og:type\" content=\"website\">\n\n    <!-- Google Fonts -->\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link href=\"https://fonts.googleapis.com/css2?family=Sora:wght@100..800&family=Exo+2:ital,wght@0,100..900;1,100..900&family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n\n    <style>\n        *, *::before, *::after {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        :root {\n            --bg: #323040;\n            --surface: #26203a;\n            --surface-alt: #302D3F;\n            --cyan: #58FBDA;\n            --purple: #da2ae0;\n            --text: #e6f6f3;\n            --text-dim: #a8a3b5;\n            --red: #ff456d;\n            --success: #58FBDA;\n        }\n\n        body {\n            font-family: \"Exo 2\", sans-serif;\n            background-color: var(--bg);\n            color: var(--text);\n            min-height: 100vh;\n            display: flex;\n            flex-direction: column;\n            position: relative;\n            overflow-x: hidden;\n            font-optical-sizing: auto;\n        }\n\n        /* Animated background blobs */\n        .blob-container {\n            pointer-events: none;\n            position: fixed;\n            inset: 0;\n            z-index: -1;\n        }\n        .blob {\n            position: absolute;\n            border-radius: 50%;\n            opacity: 0.4;\n            mix-blend-mode: screen;\n            filter: blur(80px);\n            animation: blob 7s infinite;\n        }\n        .blob-cyan {\n            width: 500px;\n            height: 500px;\n            background: var(--cyan);\n            top: -100px;\n            right: -100px;\n            animation-delay: 2s;\n        }\n        .blob-purple {\n            width: 500px;\n            height: 500px;\n            background: var(--purple);\n            top: 80px;\n            right: -60px;\n        }\n        @keyframes blob {\n            0% { transform: translate(0, 0) scale(1); }\n            33% { transform: translate(30px, -50px) scale(1.1); }\n            66% { transform: translate(-20px, 20px) scale(0.9); }\n            100% { transform: translate(0, 0) scale(1); }\n        }\n\n        /* Header */\n        header {\n            width: 100%;\n            padding: 16px 24px;\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n        }\n        .header-brand {\n            font-family: \"Sora\", sans-serif;\n            font-size: 1.1rem;\n            font-weight: 600;\n            color: var(--text);\n            text-decoration: none;\n        }\n        .header-links {\n            display: flex;\n            align-items: center;\n            gap: 12px;\n        }\n        .header-links a {\n            display: flex;\n            align-items: center;\n            gap: 8px;\n            padding: 8px 16px;\n            border: 1px solid var(--cyan);\n            border-radius: 8px;\n            background: var(--surface);\n            color: white;\n            text-decoration: none;\n            font-family: \"Sora\", sans-serif;\n            font-size: 0.875rem;\n            transition: background 0.2s;\n        }\n        .header-links a:hover {\n            background: var(--bg);\n        }\n        .header-links a svg {\n            width: 20px;\n            height: 20px;\n            fill: currentColor;\n        }\n\n        /* Main */\n        main {\n            flex: 1;\n            width: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            padding: 20px 16px 40px;\n        }\n\n        .hero-title {\n            font-family: \"Sora\", sans-serif;\n            font-size: clamp(2rem, 6vw, 4rem);\n            font-weight: 700;\n            color: var(--text);\n            text-align: center;\n            line-height: 1.2;\n            margin-bottom: 12px;\n        }\n        .hero-subtitle {\n            font-size: clamp(0.95rem, 2vw, 1.25rem);\n            font-weight: 300;\n            color: var(--text);\n            text-align: center;\n            margin-bottom: 40px;\n            max-width: 680px;\n        }\n\n        /* Converter card with conic gradient border */\n        .converter-card {\n            position: relative;\n            width: 100%;\n            max-width: 1100px;\n            background: var(--bg);\n            border-radius: 22px;\n            padding: 24px;\n            display: flex;\n            flex-direction: column;\n            gap: 20px;\n        }\n        .converter-card::before {\n            content: \"\";\n            pointer-events: none;\n            position: absolute;\n            inset: 0;\n            z-index: 1;\n            border-radius: 22px;\n            padding: 2px;\n            background-image: conic-gradient(from 90deg, #58FBDA, #da2ae0, #58FBDA);\n            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);\n            mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);\n            -webkit-mask-composite: xor;\n            mask-composite: exclude;\n        }\n\n        /* Panels grid */\n        .panels {\n            display: grid;\n            grid-template-columns: 1fr;\n            gap: 20px;\n            position: relative;\n            z-index: 2;\n        }\n        @media (min-width: 768px) {\n            .panels {\n                grid-template-columns: 1fr 1fr;\n            }\n        }\n\n        .panel {\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n        }\n        .panel-label {\n            font-family: \"Sora\", sans-serif;\n            font-size: 0.875rem;\n            font-weight: 600;\n            color: var(--text-dim);\n            text-transform: uppercase;\n            letter-spacing: 0.05em;\n        }\n\n        textarea {\n            width: 100%;\n            min-height: 380px;\n            flex: 1;\n            padding: 16px;\n            border: 1px solid rgba(88, 251, 218, 0.15);\n            border-radius: 12px;\n            background: var(--surface);\n            color: var(--text);\n            font-family: \"JetBrains Mono\", monospace;\n            font-size: 0.8125rem;\n            line-height: 1.7;\n            resize: vertical;\n            transition: border-color 0.2s;\n        }\n        textarea:focus {\n            outline: none;\n            border-color: var(--cyan);\n        }\n        textarea::placeholder {\n            color: var(--text-dim);\n        }\n\n        /* Output area */\n        .output-wrapper {\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            flex: 1;\n        }\n        .output-header {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n        }\n\n        /* Tab switcher */\n        .tab-switcher {\n            position: relative;\n            display: inline-flex;\n            width: 200px;\n            height: 34px;\n            align-items: center;\n            border-radius: 16px;\n            border: 1px solid var(--cyan);\n            background: var(--surface);\n            padding: 3px;\n        }\n        .tab-indicator {\n            position: absolute;\n            left: 3px;\n            top: 3px;\n            bottom: 3px;\n            width: calc(50% - 3px);\n            border-radius: 12px;\n            background: var(--cyan);\n            transition: transform 0.3s ease-in-out;\n        }\n        .tab-indicator.json {\n            transform: translateX(100%);\n        }\n        .tab-btn {\n            position: relative;\n            z-index: 1;\n            flex: 1;\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            border: none;\n            background: none;\n            font-family: \"Sora\", sans-serif;\n            font-size: 0.8125rem;\n            font-weight: 500;\n            cursor: pointer;\n            transition: color 0.2s;\n        }\n        .tab-btn.active {\n            color: #323040;\n        }\n        .tab-btn:not(.active) {\n            color: var(--cyan);\n        }\n\n        /* Copy button */\n        .btn-copy {\n            padding: 4px 12px;\n            border: 1px solid rgba(88, 251, 218, 0.3);\n            border-radius: 6px;\n            background: transparent;\n            color: var(--text-dim);\n            font-family: \"Sora\", sans-serif;\n            font-size: 0.75rem;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n        .btn-copy:hover {\n            border-color: var(--cyan);\n            color: var(--cyan);\n        }\n        .btn-copy.copied {\n            background: var(--cyan);\n            color: var(--bg);\n            border-color: var(--cyan);\n        }\n\n        .output-content {\n            width: 100%;\n            min-height: 380px;\n            flex: 1;\n            padding: 16px;\n            border-radius: 12px;\n            background: var(--surface);\n            font-family: \"JetBrains Mono\", monospace;\n            font-size: 0.8125rem;\n            line-height: 1.7;\n            overflow: auto;\n            white-space: pre-wrap;\n            word-wrap: break-word;\n            border: 1px solid rgba(88, 251, 218, 0.15);\n        }\n        .output-content.json {\n            color: var(--cyan);\n        }\n        .output-content.hidden {\n            display: none;\n        }\n\n        /* JSON syntax highlighting */\n        .json-key { color: var(--cyan); }\n        .json-string { color: var(--text); }\n        .json-number { color: var(--purple); }\n        .json-bool { color: var(--red); }\n        .json-null { color: var(--text-dim); }\n\n        /* Status bar */\n        .status-bar {\n            position: relative;\n            z-index: 2;\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            gap: 12px;\n            padding: 0 4px;\n        }\n        .status-text {\n            font-size: 0.8125rem;\n            color: var(--text-dim);\n        }\n        .status-text.success {\n            color: var(--success);\n        }\n        .status-text.error {\n            color: var(--red);\n        }\n        .btn-clear {\n            padding: 6px 14px;\n            border: 1px solid rgba(88, 251, 218, 0.2);\n            border-radius: 8px;\n            background: transparent;\n            color: var(--text-dim);\n            font-family: \"Sora\", sans-serif;\n            font-size: 0.8125rem;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n        .btn-clear:hover {\n            border-color: var(--cyan);\n            color: var(--text);\n        }\n\n        /* Badges row */\n        .badges {\n            display: flex;\n            gap: 8px;\n            flex-wrap: wrap;\n            justify-content: center;\n            margin-top: 32px;\n            max-width: 1100px;\n        }\n        .badges a {\n            display: inline-flex;\n            align-items: center;\n            padding: 4px 8px;\n            border-radius: 6px;\n            background: rgba(88, 251, 218, 0.06);\n            transition: background 0.2s;\n        }\n        .badges a:hover {\n            background: rgba(88, 251, 218, 0.12);\n        }\n        .badges img {\n            height: 22px;\n            display: block;\n        }\n\n        /* Footer */\n        footer {\n            padding: 10px 24px;\n            display: flex;\n            align-items: center;\n        }\n        footer a {\n            font-size: 0.875rem;\n            color: var(--cyan);\n            text-decoration: none;\n            font-weight: 400;\n            transition: opacity 0.2s;\n        }\n        footer a:hover {\n            text-decoration: underline;\n        }\n\n        /* Scrollbar */\n        textarea::-webkit-scrollbar,\n        .output-content::-webkit-scrollbar {\n            width: 6px;\n            height: 6px;\n        }\n        textarea::-webkit-scrollbar-track,\n        .output-content::-webkit-scrollbar-track {\n            background: transparent;\n        }\n        textarea::-webkit-scrollbar-thumb,\n        .output-content::-webkit-scrollbar-thumb {\n            background: rgba(88, 251, 218, 0.2);\n            border-radius: 3px;\n        }\n        textarea::-webkit-scrollbar-thumb:hover,\n        .output-content::-webkit-scrollbar-thumb:hover {\n            background: rgba(88, 251, 218, 0.4);\n        }\n    </style>\n</head>\n<body>\n    <!-- Animated Background Blobs -->\n    <div class=\"blob-container\">\n        <div class=\"blob blob-cyan\"></div>\n        <div class=\"blob blob-purple\"></div>\n    </div>\n\n    <!-- Header -->\n    <header>\n        <a href=\"https://docs.html-to-markdown.kreuzberg.dev\" class=\"header-brand\">html-to-markdown</a>\n        <div class=\"header-links\">\n            <a href=\"https://github.com/kreuzberg-dev/html-to-markdown\" target=\"_blank\" rel=\"noopener\" aria-label=\"View on GitHub\">\n                <svg viewBox=\"0 0 16 16\">\n                    <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"/>\n                </svg>\n                <span>GitHub</span>\n            </a>\n        </div>\n    </header>\n\n    <!-- Main -->\n    <main>\n        <h1 class=\"hero-title\">Live Demo</h1>\n        <p class=\"hero-subtitle\">\n            Paste HTML, get Markdown. Everything runs in your browser via WebAssembly -- no server, no upload.\n        </p>\n\n        <div class=\"converter-card\">\n            <div class=\"panels\">\n                <!-- Input panel -->\n                <div class=\"panel\">\n                    <label class=\"panel-label\" for=\"htmlInput\">HTML Input</label>\n                    <textarea\n                        id=\"htmlInput\"\n                        placeholder=\"Paste your HTML here...\"\n                        spellcheck=\"false\"\n                    ><h1>Welcome to HTML to Markdown</h1>\n<p>This is a <strong>powerful</strong> and <em>fast</em> converter built with <a href=\"https://www.rust-lang.org/\">Rust</a> and compiled to WebAssembly.</p>\n\n<h2>Features</h2>\n<ul>\n    <li>High-performance conversion</li>\n    <li>Supports complex HTML structures</li>\n    <li>Tables, lists, and more</li>\n    <li>Runs entirely in your browser</li>\n</ul>\n\n<h3>Code Example</h3>\n<pre><code>function hello() {\n    console.log(\"Hello, World!\");\n}</code></pre>\n\n<blockquote>\n    <p>This converter handles blockquotes, images, and various HTML elements with ease.</p>\n</blockquote>\n\n<table>\n    <thead>\n        <tr>\n            <th>Feature</th>\n            <th>Status</th>\n        </tr>\n    </thead>\n    <tbody>\n        <tr>\n            <td>Fast</td>\n            <td>Yes</td>\n        </tr>\n        <tr>\n            <td>Reliable</td>\n            <td>Yes</td>\n        </tr>\n    </tbody>\n</table></textarea>\n                </div>\n\n                <!-- Output panel -->\n                <div class=\"panel\">\n                    <div class=\"output-header\">\n                        <div class=\"tab-switcher\">\n                            <div id=\"tabIndicator\" class=\"tab-indicator\"></div>\n                            <button id=\"tabMarkdown\" class=\"tab-btn active\" type=\"button\">Markdown</button>\n                            <button id=\"tabJson\" class=\"tab-btn\" type=\"button\">JSON</button>\n                        </div>\n                        <button id=\"copyBtn\" class=\"btn-copy\" type=\"button\">Copy</button>\n                    </div>\n                    <div class=\"output-wrapper\">\n                        <pre id=\"outputMarkdown\" class=\"output-content\"></pre>\n                        <pre id=\"outputJson\" class=\"output-content json hidden\"></pre>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Status bar -->\n            <div class=\"status-bar\">\n                <span id=\"status\" class=\"status-text\">Loading WASM module...</span>\n                <button id=\"clearBtn\" class=\"btn-clear\" type=\"button\">Clear</button>\n            </div>\n        </div>\n\n        <!-- Badges -->\n        <div class=\"badges\">\n            <a href=\"https://crates.io/crates/html-to-markdown-rs\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?logo=rust&label=Crates.io\" alt=\"Crates.io\">\n            </a>\n            <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?logo=npm&label=npm%20(Node)\" alt=\"npm (Node)\">\n            </a>\n            <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?logo=npm&label=npm%20(WASM)\" alt=\"npm (WASM)\">\n            </a>\n            <a href=\"https://pypi.org/project/html-to-markdown/\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/pypi/v/html-to-markdown?logo=pypi&label=PyPI\" alt=\"PyPI\">\n            </a>\n            <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?logo=packagist&label=Packagist\" alt=\"Packagist\">\n            </a>\n            <a href=\"https://rubygems.org/gems/html-to-markdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/gem/v/html-to-markdown?logo=rubygems&label=RubyGems\" alt=\"RubyGems\">\n            </a>\n            <a href=\"https://hex.pm/packages/html_to_markdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?logo=elixir&label=Hex.pm\" alt=\"Hex.pm\">\n            </a>\n            <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?logo=nuget&label=NuGet\" alt=\"NuGet\">\n            </a>\n            <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?logo=apache-maven&label=Maven%20Central\" alt=\"Maven Central\">\n            </a>\n            <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\" target=\"_blank\" rel=\"noopener\">\n                <img src=\"https://img.shields.io/badge/Go-pkg.dev-blue?logo=go&label=Go%20pkg\" alt=\"Go pkg\">\n            </a>\n        </div>\n    </main>\n\n    <footer>\n        <a href=\"https://kreuzberg.dev\">kreuzberg.dev</a>\n    </footer>\n\n    <script type=\"module\" src=\"script.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "docs/demo/script.js",
    "content": "import init, { convert } from \"https://cdn.jsdelivr.net/npm/@kreuzberg/html-to-markdown-wasm@latest/dist-web/html_to_markdown_wasm.js\";\n\nlet wasmInitialized = false;\n\nconst htmlInput = document.getElementById(\"htmlInput\");\nconst outputMarkdown = document.getElementById(\"outputMarkdown\");\nconst outputJson = document.getElementById(\"outputJson\");\nconst copyBtn = document.getElementById(\"copyBtn\");\nconst clearBtn = document.getElementById(\"clearBtn\");\nconst statusEl = document.getElementById(\"status\");\nconst tabMarkdown = document.getElementById(\"tabMarkdown\");\nconst tabJson = document.getElementById(\"tabJson\");\nconst tabIndicator = document.getElementById(\"tabIndicator\");\n\nlet activeTab = \"markdown\";\n\nasync function initWasm() {\n  try {\n    await init();\n    wasmInitialized = true;\n    statusEl.textContent = \"Ready\";\n    statusEl.className = \"status-text success\";\n    performConversion();\n  } catch (error) {\n    console.error(\"Failed to initialize WASM:\", error);\n    statusEl.textContent = \"Failed to load WASM module\";\n    statusEl.className = \"status-text error\";\n  }\n}\n\nfunction highlightJson(obj) {\n  const raw = JSON.stringify(obj, null, 2);\n  return raw.replace(\n    /(\"[^\"]*\"\\s*:)|(\"[^\"]*\")|(-?\\d+\\.?\\d*(?:[eE][+-]?\\d+)?)|(true|false)|(null)/g,\n    (match, key, str, num, bool, nil) => {\n      if (key) return `<span class=\"json-key\">${key}</span>`;\n      if (str) return `<span class=\"json-string\">${str}</span>`;\n      if (num) return `<span class=\"json-number\">${num}</span>`;\n      if (bool) return `<span class=\"json-bool\">${bool}</span>`;\n      if (nil) return `<span class=\"json-null\">${nil}</span>`;\n      return match;\n    }\n  );\n}\n\nfunction performConversion() {\n  if (!wasmInitialized) {\n    statusEl.textContent = \"WASM module not initialized yet...\";\n    statusEl.className = \"status-text\";\n    return;\n  }\n\n  const html = htmlInput.value.trim();\n\n  if (!html) {\n    outputMarkdown.textContent = \"\";\n    outputJson.innerHTML = \"\";\n    statusEl.textContent = \"Enter some HTML to convert\";\n    statusEl.className = \"status-text\";\n    return;\n  }\n\n  try {\n    const startTime = performance.now();\n    const result = convert(html, null);\n    const duration = (performance.now() - startTime).toFixed(2);\n\n    outputMarkdown.textContent = result.content ?? \"\";\n\n    const jsonData = {\n      content: result.content ?? null,\n      metadata: result.metadata ?? null,\n      tables: result.tables ?? [],\n      images: (result.images ?? []).map((img) => ({\n        format: img.format,\n        filename: img.filename ?? null,\n        description: img.description ?? null,\n        width: img.width ?? null,\n        height: img.height ?? null,\n        source: img.source,\n      })),\n      warnings: result.warnings ?? [],\n    };\n    outputJson.innerHTML = highlightJson(jsonData);\n\n    statusEl.textContent = `Converted in ${duration}ms`;\n    statusEl.className = \"status-text success\";\n    copyBtn.classList.remove(\"copied\");\n    copyBtn.textContent = \"Copy\";\n  } catch (error) {\n    console.error(\"Conversion error:\", error);\n    outputMarkdown.textContent = \"\";\n    outputJson.innerHTML = \"\";\n    statusEl.textContent = `Error: ${error.message || error}`;\n    statusEl.className = \"status-text error\";\n  }\n}\n\nfunction switchTab(tab) {\n  activeTab = tab;\n  const isMd = tab === \"markdown\";\n  tabIndicator.classList.toggle(\"json\", !isMd);\n  tabMarkdown.classList.toggle(\"active\", isMd);\n  tabJson.classList.toggle(\"active\", !isMd);\n  outputMarkdown.classList.toggle(\"hidden\", !isMd);\n  outputJson.classList.toggle(\"hidden\", isMd);\n}\n\nasync function copyToClipboard() {\n  const text =\n    activeTab === \"markdown\"\n      ? outputMarkdown.textContent\n      : outputJson.innerText;\n\n  if (!text) {\n    statusEl.textContent = \"Nothing to copy\";\n    statusEl.className = \"status-text\";\n    return;\n  }\n\n  try {\n    await navigator.clipboard.writeText(text);\n    copyBtn.classList.add(\"copied\");\n    copyBtn.textContent = \"Copied!\";\n    statusEl.textContent = \"Copied to clipboard\";\n    statusEl.className = \"status-text success\";\n    setTimeout(() => {\n      copyBtn.classList.remove(\"copied\");\n      copyBtn.textContent = \"Copy\";\n    }, 2000);\n  } catch (error) {\n    console.error(\"Failed to copy:\", error);\n    statusEl.textContent = \"Failed to copy to clipboard\";\n    statusEl.className = \"status-text error\";\n  }\n}\n\nfunction clearInput() {\n  htmlInput.value = \"\";\n  outputMarkdown.textContent = \"\";\n  outputJson.innerHTML = \"\";\n  statusEl.textContent = \"Input cleared\";\n  statusEl.className = \"status-text\";\n  htmlInput.focus();\n}\n\ntabMarkdown.addEventListener(\"click\", () => switchTab(\"markdown\"));\ntabJson.addEventListener(\"click\", () => switchTab(\"json\"));\ncopyBtn.addEventListener(\"click\", copyToClipboard);\nclearBtn.addEventListener(\"click\", clearInput);\n\nhtmlInput.addEventListener(\"keydown\", (e) => {\n  if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n    e.preventDefault();\n    performConversion();\n  }\n});\n\nlet debounceTimer;\nhtmlInput.addEventListener(\"input\", () => {\n  clearTimeout(debounceTimer);\n  debounceTimer = setTimeout(() => {\n    if (wasmInitialized && htmlInput.value.trim()) {\n      performConversion();\n    }\n  }, 300);\n});\n\ninitWasm();\n"
  },
  {
    "path": "docs/demo/style.css",
    "content": ":root {\n    --primary-color: #2563eb;\n    --primary-hover: #1d4ed8;\n    --secondary-color: #64748b;\n    --success-color: #10b981;\n    --background: #ffffff;\n    --surface: #f8fafc;\n    --border: #e2e8f0;\n    --text-primary: #0f172a;\n    --text-secondary: #475569;\n    --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n}\n\n* {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n}\n\nbody {\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n    min-height: 100vh;\n    padding: 2rem 1rem;\n    color: var(--text-primary);\n}\n\n.container {\n    max-width: 1400px;\n    margin: 0 auto;\n    background: var(--background);\n    border-radius: 1rem;\n    box-shadow: var(--shadow-lg);\n    overflow: hidden;\n}\n\nheader {\n    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n    color: white;\n    padding: 3rem 2rem;\n    text-align: center;\n}\n\nheader h1 {\n    font-size: 2.5rem;\n    font-weight: 700;\n    margin-bottom: 0.5rem;\n}\n\n.subtitle {\n    font-size: 1.125rem;\n    opacity: 0.95;\n    margin-bottom: 1.5rem;\n}\n\n.links {\n    display: flex;\n    gap: 1rem;\n    justify-content: center;\n    flex-wrap: wrap;\n}\n\n.links a {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.35rem 0.65rem;\n    background: rgba(255, 255, 255, 0.15);\n    color: white;\n    text-decoration: none;\n    border-radius: 0.5rem;\n    font-weight: 500;\n    transition: all 0.2s;\n    backdrop-filter: blur(10px);\n}\n\n.links a:hover {\n    background: rgba(255, 255, 255, 0.3);\n    transform: translateY(-2px);\n}\n\n.links a img {\n    height: 26px;\n    display: block;\n}\n\nmain {\n    padding: 2rem;\n}\n\n.converter {\n    display: grid;\n    gap: 1.5rem;\n    grid-auto-rows: minmax(0, 1fr);\n}\n\n.section-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 1rem;\n}\n\n.section-header h2 {\n    font-size: 1.25rem;\n    font-weight: 600;\n    color: var(--text-primary);\n}\n\n.input-section,\n.output-section {\n    background: var(--surface);\n    padding: 1.5rem;\n    border-radius: 0.75rem;\n    border: 1px solid var(--border);\n    display: flex;\n    flex-direction: column;\n    min-height: 360px;\n}\n\ntextarea,\n.output {\n    width: 100%;\n    min-height: 320px;\n    flex: 1;\n    padding: 1rem;\n    border: 1px solid var(--border);\n    border-radius: 0.5rem;\n    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;\n    font-size: 0.875rem;\n    line-height: 1.6;\n    resize: vertical;\n    background: var(--background);\n    color: var(--text-primary);\n    transition: border-color 0.2s;\n}\n\ntextarea:focus {\n    outline: none;\n    border-color: var(--primary-color);\n    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);\n}\n\n.output {\n    overflow-x: auto;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n\n.controls {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 1rem;\n    padding: 1rem 0;\n}\n\n.primary-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.75rem;\n    padding: 1rem 2rem;\n    background: var(--primary-color);\n    color: white;\n    border: none;\n    border-radius: 0.5rem;\n    font-size: 1.125rem;\n    font-weight: 600;\n    cursor: pointer;\n    transition: all 0.2s;\n    box-shadow: var(--shadow);\n}\n\n.primary-btn:hover {\n    background: var(--primary-hover);\n    transform: translateY(-2px);\n    box-shadow: var(--shadow-lg);\n}\n\n.primary-btn:active {\n    transform: translateY(0);\n}\n\n.primary-btn:disabled {\n    opacity: 0.6;\n    cursor: not-allowed;\n    transform: none;\n}\n\n.btn-icon {\n    font-size: 1.5rem;\n    transition: transform 0.2s;\n}\n\n.primary-btn:hover .btn-icon {\n    transform: translateX(4px);\n}\n\n.secondary-btn {\n    padding: 0.5rem 1rem;\n    background: var(--background);\n    color: var(--text-secondary);\n    border: 1px solid var(--border);\n    border-radius: 0.375rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    cursor: pointer;\n    transition: all 0.2s;\n    position: relative;\n}\n\n.secondary-btn:hover {\n    background: var(--surface);\n    border-color: var(--secondary-color);\n    color: var(--text-primary);\n}\n\n#copyBtn .copied-text {\n    display: none;\n}\n\n#copyBtn.copied .copy-text {\n    display: none;\n}\n\n#copyBtn.copied .copied-text {\n    display: inline;\n}\n\n#copyBtn.copied {\n    background: var(--success-color);\n    color: white;\n    border-color: var(--success-color);\n}\n\n.status {\n    font-size: 0.875rem;\n    color: var(--text-secondary);\n    min-height: 1.5rem;\n}\n\n.status.success {\n    color: var(--success-color);\n    font-weight: 500;\n}\n\n.status.error {\n    color: #ef4444;\n    font-weight: 500;\n}\n\nfooter {\n    background: var(--surface);\n    padding: 2rem;\n    text-align: center;\n    border-top: 1px solid var(--border);\n    color: var(--text-secondary);\n}\n\nfooter p {\n    margin-bottom: 0.5rem;\n}\n\nfooter a {\n    color: var(--primary-color);\n    text-decoration: none;\n    font-weight: 500;\n}\n\nfooter a:hover {\n    text-decoration: underline;\n}\n\n.version {\n    font-size: 0.875rem;\n    opacity: 0.8;\n}\n\n#wasmStatus {\n    display: inline-block;\n    padding: 0.25rem 0.75rem;\n    background: var(--background);\n    border-radius: 0.25rem;\n    font-family: 'Monaco', 'Menlo', monospace;\n    font-size: 0.75rem;\n}\n\n#wasmStatus.loaded {\n    color: var(--success-color);\n}\n\n#wasmStatus.error {\n    color: #ef4444;\n}\n\n/* Responsive Design */\n@media (min-width: 768px) {\n    body {\n        padding: 3rem 2rem;\n    }\n\n    header h1 {\n        font-size: 3rem;\n    }\n\n    main {\n        padding: 3rem;\n    }\n\n    .converter {\n        grid-template-columns: repeat(2, minmax(0, 1fr));\n        grid-template-rows: minmax(0, 1fr) auto;\n        align-items: stretch;\n    }\n\n    .input-section {\n        grid-column: 1;\n        grid-row: 1;\n    }\n\n    .controls {\n        grid-column: 1 / -1;\n        grid-row: 2;\n        flex-direction: row;\n        justify-content: center;\n    }\n\n    .output-section {\n        grid-column: 2;\n        grid-row: 1;\n    }\n}\n\n/* Loading Animation */\n@keyframes pulse {\n    0%, 100% {\n        opacity: 1;\n    }\n    50% {\n        opacity: 0.5;\n    }\n}\n\n.primary-btn:disabled {\n    animation: pulse 1.5s ease-in-out infinite;\n}\n\n/* Scrollbar Styling */\ntextarea::-webkit-scrollbar,\n.output::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n}\n\ntextarea::-webkit-scrollbar-track,\n.output::-webkit-scrollbar-track {\n    background: var(--surface);\n    border-radius: 4px;\n}\n\ntextarea::-webkit-scrollbar-thumb,\n.output::-webkit-scrollbar-thumb {\n    background: var(--border);\n    border-radius: 4px;\n}\n\ntextarea::-webkit-scrollbar-thumb:hover,\n.output::-webkit-scrollbar-thumb:hover {\n    background: var(--secondary-color);\n}\n"
  },
  {
    "path": "docs/errors.md",
    "content": "# Error Handling\n\n`convert()` returns `Result<ConversionResult, ConversionError>` in Rust. Every other binding maps the error to its native idiom: Python raises an exception, Go returns `(result, error)`, Java throws a checked exception, and so on.\n\nNon-fatal issues never produce an error. They accumulate in `result.warnings` and the call still succeeds.\n\n## ConversionError Variants\n\nEight variants. All carry a `String` message except `IoError`, which wraps `std::io::Error` via `#[from]`.\n\n| Variant | Payload | Cause |\n|---------|---------|-------|\n| `ParseError` | `String` | Malformed HTML the parser could not recover from. |\n| `SanitizationError` | `String` | The sanitizer rejected the input outright. |\n| `ConfigError` | `String` | `ConversionOptions` contains an invalid combination (unknown format string, out-of-range width, etc.). |\n| `IoError` | `std::io::Error` | Reading a file, reading stdin, or writing output failed. |\n| `Panic` | `String` | A panic was caught inside the conversion core. The FFI boundaries catch unwinds so other bindings see a normal error instead of a crash. |\n| `InvalidInput` | `String` | Empty input, input exceeding the configured size cap, or decoding failure for a wrong `encoding` setting. |\n| `Visitor` | `String` | A visitor callback returned `VisitResult::Error(...)`. Only compiled with `features = [\"visitor\"]`. Rust users on default features never see this variant. |\n| `Other` | `String` | Catch-all for anything that does not fit above. |\n\n## Warnings\n\n`result.warnings` is a `Vec<ProcessingWarning>`. Each warning has a kind and a message. The CLI prints them to stderr with `--show-warnings`; in library code, iterate and log them yourself.\n\nWarnings are the right place to surface \"this was weird but the conversion worked\" signals: skipped oversized images, unknown class attributes on code blocks, malformed table rows that were repaired. None of these halt the call.\n\n## Handling Patterns\n\n=== \"Rust\"\n    ```rust\n    use html_to_markdown_rs::{convert, ConversionError};\n\n    match convert(html, None) {\n        Ok(result) => println!(\"{}\", result.content.unwrap_or_default()),\n        Err(ConversionError::InvalidInput(msg)) => eprintln!(\"bad input: {msg}\"),\n        Err(ConversionError::ParseError(msg)) => eprintln!(\"parse failed: {msg}\"),\n        Err(e) => eprintln!(\"conversion failed: {e}\"),\n    }\n    ```\n\n=== \"Python\"\n    ```python\n    from html_to_markdown import convert, ConversionError\n\n    try:\n        result = convert(html)\n    except ConversionError as e:\n        print(f\"conversion failed: {e}\")\n    ```\n\n=== \"TypeScript\"\n    ```typescript\n    import { convert, ConversionError } from '@kreuzberg/html-to-markdown';\n\n    try {\n      const result = convert(html);\n    } catch (e) {\n      if (e instanceof ConversionError) {\n        console.error(`conversion failed: ${e.message}`);\n      } else {\n        throw e;\n      }\n    }\n    ```\n\n=== \"Go\"\n    ```go\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatalf(\"conversion failed: %v\", err)\n    }```\n\n=== \"Ruby\"\n    ```ruby\n    begin\n      result = HtmlToMarkdown.convert(html)\n    rescue HtmlToMarkdown::ConversionError => e\n      warn \"conversion failed: #{e.message}\"\n    end```\n\n=== \"PHP\"\n    ```php\n    try {\n        $result = $converter->convert($html);\n    } catch (ConversionException $e) {\n        error_log(\"conversion failed: \" . $e->getMessage());\n    }```\n\n=== \"Java\"\n    ```java\n    try {\n        ConversionResult result = HtmlToMarkdown.convert(html);\n    } catch (ConversionException e) {\n        System.err.println(\"conversion failed: \" + e.getMessage());\n    }```\n\n=== \"C#\"\n    ```csharp\n    try\n    {\n        var result = HtmlToMarkdownConverter.Convert(html);\n    }\n    catch (ConversionException e)\n    {\n        Console.Error.WriteLine($\"conversion failed: {e.Message}\");\n    }```\n\n=== \"Elixir\"\n    ```elixir\n    case HtmlToMarkdown.convert(html) do\n      {:ok, result} -> IO.puts(result.content)\n      {:error, reason} -> IO.warn(\"conversion failed: #{reason}\")\n    end```\n\n=== \"R\"\n    ```r\n    result <- tryCatch(\n      htmltomarkdown::convert(html),\n      error = function(e) {\n        message(\"conversion failed: \", conditionMessage(e))\n        NULL\n      }\n    )```\n\n## CLI Warnings\n\nThe CLI hides warnings by default. Pass `--show-warnings` to print each one to stderr in the format `Warning [<kind>]: <message>`. The flag works with or without `--json`. See [CLI: JSON Output](cli.md#json-output).\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\ntitle: html-to-markdown\ndescription: \"html-to-markdown — Convert HTML to Markdown, Djot, or plain text. Rust core, 12 native language bindings, 150–280 MB/s.\"\n---\n\n<div class=\"home-hero\" markdown=\"1\">\n\n\n<p class=\"home-lead\" markdown=\"1\">\nConvert HTML to Markdown, Djot, or plain text. One Rust core, 12 native language bindings, identical output on every runtime.\n</p>\n\n<div class=\"home-instruction\" markdown=\"1\">\n\n## Start here\n\n1. **[Install a binding](installation.md)** — add the package to your project (versions, feature flags, and verify steps are on that page).\n2. **[Run a minimal `convert()`](usage.md#basic-conversion)** — open *Usage → Basic conversion*, choose your language tab, and copy the hello-world snippet.\n\n</div>\n\n</div>\n\n---\n\n## What It Does\n\n<div class=\"home-feature-grid\">\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">150–280 MB/s throughput</p>\n<p class=\"home-feature-card__desc\">Rust-native parsing and a single-pass DOM walk. 10–80x faster than pure-language alternatives. No JVM, no interpreter overhead.</p>\n</article>\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">12 language bindings</p>\n<p class=\"home-feature-card__desc\">Rust, Python, TypeScript, Go, Ruby, PHP, Java, C#, Elixir, R, C, and WebAssembly. One core — no per-language conversion logic.</p>\n</article>\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">Three output formats</p>\n<p class=\"home-feature-card__desc\">CommonMark, Djot, and plain text via <code>output_format</code>. The same options apply to every format.</p>\n</article>\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">Metadata extraction</p>\n<p class=\"home-feature-card__desc\">Open Graph, Twitter Card, JSON-LD, links, and images in one pass. Enable with <code>extract_metadata: true</code>.</p>\n</article>\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">Table extraction</p>\n<p class=\"home-feature-card__desc\">HTML tables into <code>result.tables</code> (cells, spans, headers) plus rendered Markdown when applicable.</p>\n</article>\n<article class=\"home-feature-card\">\n<p class=\"home-feature-card__title\">Visitor pattern</p>\n<p class=\"home-feature-card__desc\">40 callbacks to filter nodes or emit custom output. Zero cost when you do not register a visitor.</p>\n</article>\n</div>\n\n---\n\n## Getting Help\n\n- **Bugs and feature requests** — [Open an issue on GitHub](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Contributing** — [Read the contributor guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md)\n\n<div class=\"home-kreuzberg\" markdown=\"1\">\n\n### Part of Kreuzberg\n\nhtml-to-markdown powers the HTML conversion pipeline in [Kreuzberg](https://docs.kreuzberg.dev), a document intelligence library for extracting text and structured data from PDFs, DOCX, images, and other document formats.\n\n</div>\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\nInstall **html-to-markdown** for your language using the commands below. Each binding wraps the same Rust core, so behavior matches across runtimes. Pick a tab under **Language bindings** for copy-paste snippets, feature flags, and a quick verification step where applicable.\n\n## Requirements\n\n| Language | Min version | Package |\n|----------|-------------|---------|\n| Rust | 1.70 | `html-to-markdown-rs` on crates.io |\n| Python | 3.10 | `html-to-markdown` on PyPI |\n| TypeScript / Node.js | Node.js 18 | `@kreuzberg/html-to-markdown` on npm |\n| Go | 1.26 | `github.com/kreuzberg-dev/html-to-markdown/packages/go/v3` |\n| Ruby | 3.2 | `html-to-markdown` on RubyGems |\n| PHP | 8.2 | `kreuzberg-dev/html-to-markdown` on Packagist |\n| Java | 22 | `dev.kreuzberg:html-to-markdown` on Maven Central |\n| C# | .NET Standard 2.0 | `KreuzbergDev.HtmlToMarkdown` on NuGet |\n| Elixir | 1.19 | `html_to_markdown` on Hex |\n| R | 4.0 | `htmltomarkdown` on CRAN |\n| C | — | `libhtml_to_markdown` (GitHub Releases) |\n| WebAssembly | — | `@kreuzberg/html-to-markdown-wasm` on npm |\n\n---\n\n## Language bindings\n\n=== \"Rust\"\n\n    Add to `Cargo.toml`:\n\n    ```toml\n    [dependencies]\n    html-to-markdown-rs = \"3.1\"\n    ```\n\n    Or via `cargo add`:\n\n    ```bash\n    cargo add html-to-markdown-rs\n    ```\n\n    **Verify:**\n\n    ```rust\n    use html_to_markdown_rs::convert;\n\n    fn main() {\n        let result = convert(\"<h1>Hello</h1>\", None).unwrap();\n        println!(\"{}\", result.content.unwrap());\n        // # Hello\n    }\n    ```\n\n    ### Feature flags\n\n    By default only the `metadata` feature is enabled. Opt in to additional features as needed:\n\n    | Feature | Enables | Default |\n    |---------|---------|---------|\n    | `metadata` | `HtmlMetadata` extraction and all `extract_*` options | yes |\n    | `visitor` | `HtmlVisitor` trait and `ConversionError::Visitor` | no |\n    | `inline-images` | data-URI decoding and inline SVG extraction | no |\n    | `serde` | `Serialize`/`Deserialize` on all public option and result types | no |\n    | `full` | all of the above | no |\n\n    ```toml\n    # With visitor support\n    html-to-markdown-rs = { version = \"3.1\", features = [\"visitor\"] }\n\n    # Everything\n    html-to-markdown-rs = { version = \"3.1\", features = [\"full\"] }\n    ```\n\n=== \"Python\"\n\n    ```bash\n    pip install html-to-markdown\n    ```\n\n    Using `uv`:\n\n    ```bash\n    uv add html-to-markdown\n    ```\n\n    **Verify:**\n\n    ```bash\n    python -c \"from html_to_markdown import convert; print(convert('<h1>Hello</h1>').content)\"\n    # # Hello\n    ```\n\n    Requires Python 3.10+. The package ships precompiled wheels for Linux (x86_64, aarch64), macOS (Apple Silicon, x86_64), and Windows (x64). A source distribution is also available if no wheel matches your platform.\n\n=== \"TypeScript\"\n\n    ```bash\n    npm install @kreuzberg/html-to-markdown\n    ```\n\n    Using `pnpm` or `yarn`:\n\n    ```bash\n    pnpm add @kreuzberg/html-to-markdown\n    yarn add @kreuzberg/html-to-markdown\n    ```\n\n    **Verify:**\n\n    ```typescript\n    import { convert } from '@kreuzberg/html-to-markdown';\n\n    const result = convert('<h1>Hello</h1>');\n    console.log(result.content);\n    // # Hello\n    ```\n\n    Requires Node.js 18+. The package ships both ESM and CJS outputs with bundled TypeScript types. No separate `@types/` package needed.\n\n=== \"Go\"\n\n    ```bash\n    go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\n    ```\n\n    **Verify:**\n\n    ```go\n    package main\n\n    import (\n        \"fmt\"\n        htmltomarkdown \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n    )\n\n    func main() {\n        result, err := htmltomarkdown.Convert(\"<h1>Hello</h1>\", nil)\n        if err != nil {\n            panic(err)\n        }\n        fmt.Println(*result.Content)\n        // # Hello\n    }\n    ```\n\n    Requires Go 1.26+. The module bundles a precompiled `libhtml_to_markdown.a` static library for each supported platform — no separate Rust toolchain needed.\n\n=== \"Ruby\"\n\n    ```bash\n    gem install html-to-markdown\n    ```\n\n    Or add to your `Gemfile`:\n\n    ```ruby\n    gem 'html-to-markdown', '~> 3.1'\n    ```\n\n    **Verify:**\n\n    ```bash\n    ruby -e \"require 'html_to_markdown'; puts HtmlToMarkdown.convert('<h1>Hello</h1>')[:content]\"\n    # # Hello\n    ```\n\n    Requires Ruby 3.2+. Precompiled native gems are available for Linux and macOS. On unsupported platforms `bundle install` will compile the extension from source, which requires a Rust toolchain.\n\n=== \"PHP\"\n\n    ```bash\n    composer require kreuzberg-dev/html-to-markdown\n    ```\n\n    **Verify:**\n\n    ```php\n    <?php\n    require 'vendor/autoload.php';\n\n    use function HtmlToMarkdown\\convert;\n\n    $result = convert('<h1>Hello</h1>');\n    echo $result['content'];\n    // # Hello\n    ```\n\n    Requires PHP 8.2+. The package ships precompiled extensions for common PHP versions. If no prebuilt extension matches, Composer will compile from source via `cargo`.\n\n=== \"Java\"\n\n    **Maven** — add to `pom.xml`:\n\n    ```xml\n    <dependency>\n        <groupId>dev.kreuzberg</groupId>\n        <artifactId>html-to-markdown</artifactId>\n        <version>3.1.0</version>\n    </dependency>\n    ```\n\n    **Gradle** — add to `build.gradle`:\n\n    ```groovy\n    implementation 'dev.kreuzberg:html-to-markdown:3.1.0'\n    ```\n\n    Or Kotlin DSL (`build.gradle.kts`):\n\n    ```kotlin\n    implementation(\"dev.kreuzberg:html-to-markdown:3.1.0\")\n    ```\n\n    **Verify:**\n\n    ```java\n    import dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\n\n    public class Main {\n        public static void main(String[] args) {\n            var result = HtmlToMarkdown.convert(\"<h1>Hello</h1>\");\n            System.out.println(result.content());\n            // # Hello\n        }\n    }\n    ```\n\n    Requires Java 22+. The JAR extracts the native `libhtml_to_markdown` shared library at startup. No separate install step is needed — the library is bundled for Linux x86_64, macOS arm64/x86_64, and Windows x64.\n\n=== \"C#\"\n\n    ```bash\n    dotnet add package KreuzbergDev.HtmlToMarkdown\n    ```\n\n    Or via the NuGet Package Manager in Visual Studio — search for `KreuzbergDev.HtmlToMarkdown`.\n\n    **Verify:**\n\n    ```csharp\n    using HtmlToMarkdown;\n\n    var result = HtmlToMarkdownConverter.Convert(\"<h1>Hello</h1>\");\n    Console.WriteLine(result.Content);\n    // # Hello\n    ```\n\n    Targets .NET Standard 2.0 and above (.NET 6+, .NET Framework 4.6.1+). The package bundles native binaries for Linux, macOS, and Windows via `NativeLibrary` P/Invoke.\n\n=== \"Elixir\"\n\n    Add to `mix.exs`:\n\n    ```elixir\n    def deps do\n      [\n        {:html_to_markdown, \"~> 3.1\"}\n      ]\n    end\n    ```\n\n    Then fetch:\n\n    ```bash\n    mix deps.get\n    ```\n\n    **Verify:**\n\n    ```elixir\n    {:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1>\")\n    IO.puts(result.content)\n    # # Hello\n    ```\n\n    Requires Elixir 1.19+. Uses Rustler NIFs — precompiled NIF binaries are fetched automatically via `RustlerPrecompiled`. Set `RUSTLER_PRECOMPILED_GLOBAL_CACHE_PATH` to control cache location.\n\n=== \"R\"\n\n    ```r\n    install.packages(\"htmltomarkdown\")\n    ```\n\n    **Verify:**\n\n    ```r\n    library(htmltomarkdown)\n    result <- htmltomarkdown::convert(\"<h1>Hello</h1>\")\n    cat(result$content)\n    # # Hello\n    ```\n\n    Requires R 4.0+. Available on CRAN for Linux (x86_64), macOS (arm64, x86_64), and Windows.\n\n=== \"C\"\n\n    Download a prebuilt release archive for your platform from the [GitHub Releases page](https://github.com/kreuzberg-dev/html-to-markdown/releases). Each archive contains:\n\n    - `libhtml_to_markdown.so` / `.dylib` / `.dll` — shared library\n    - `libhtml_to_markdown.a` — static library\n    - `html_to_markdown.h` — C header\n\n    **Build from source** (requires Rust toolchain):\n\n    ```bash\n    git clone https://github.com/kreuzberg-dev/html-to-markdown.git\n    cd html-to-markdown\n    cargo build --release -p html-to-markdown-ffi\n    # output: target/release/libhtml_to_markdown.{so,dylib,dll}\n    ```\n\n    **Link and verify:**\n\n    ```c\n    #include \"html_to_markdown.h\"\n    #include <stdio.h>\n\n    int main(void) {\n        HtmlToMarkdownResult r = html_to_markdown_convert(\"<h1>Hello</h1>\", NULL);\n        if (r.error == NULL) {\n            printf(\"%s\\n\", r.content);\n            // # Hello\n        }\n        html_to_markdown_free_result(r);\n        return 0;\n    }\n    ```\n\n    Compile with:\n\n    ```bash\n    gcc main.c -lhtml_to_markdown -L./target/release -o main\n    ```\n\n    Always call `html_to_markdown_free_result` after every call. Memory returned across the FFI boundary is owned by the Rust allocator and must not be freed with `free()`.\n\n=== \"WASM\"\n\n    ```bash\n    npm install @kreuzberg/html-to-markdown-wasm\n    ```\n\n    **Verify:**\n\n    ```javascript\n    import init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\n    await init();  // load and instantiate the .wasm file — call once\n\n    const result = convert('<h1>Hello</h1>');\n    console.log(result.content);\n    // # Hello\n    ```\n\n    `init()` must complete before any `convert()` call. After that, `convert` is synchronous. The WASM build omits the `inline-images` feature (no filesystem access in the browser sandbox). Works in browsers, Cloudflare Workers, Deno, and Bun.\n\n---\n\n## CLI\n\nInstall via Cargo:\n\n```bash\ncargo install html-to-markdown-cli\n```\n\nOr via Homebrew:\n\n```bash\nbrew install kreuzberg-dev/tap/html-to-markdown\n```\n\n**Verify:**\n\n```bash\necho \"<h1>Hello</h1>\" | html-to-markdown\n# # Hello\n```\n\nSee [CLI reference](cli.md) for all flags and options.\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/language-guides.md",
    "content": "# Language Binding Guides\n\nEvery binding wraps the same Rust core. The option names and return shapes are identical across languages; only the syntax differs. This page covers per-language install notes and naming conventions.\n\n## Rust\n\n**Package:** `html-to-markdown-rs` on [crates.io](https://crates.io/crates/html-to-markdown-rs)\n\n```toml\nhtml-to-markdown-rs = \"3.1\"\n```\n\nOption structs follow Rust naming conventions (`snake_case`). Use the builder API via `ConversionOptions::builder()` or construct `ConversionOptions` directly.\n\nSee [Installation: Feature Flags](installation.md#feature-flags) for the six Cargo features.\n\n## Python\n\n**Package:** `html-to-markdown` on PyPI\n**Requires:** Python ≥ 3.10\n\n```bash\npip install html-to-markdown\n```\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nresult = convert(\"<h1>Title</h1>\", ConversionOptions(heading_style=\"atx\"))\nprint(result.content)\n```\n\nOption keys match the Rust field names (`snake_case`). `ConversionOptions` accepts keyword arguments. `ConversionResult` is a class with attributes — access fields as `result.content`, `result.metadata`, `result.tables`, `result.images`, `result.document`, `result.warnings`.\n\n## TypeScript\n\n**Package:** `@kreuzberg/html-to-markdown` on npm\n**Requires:** Node.js ≥ 18\n\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst result = convert('<h1>Title</h1>', { headingStyle: 'atx' });\nconsole.log(result.content);\n```\n\nOption keys are `camelCase` (`headingStyle`, `linkStyle`, `outputFormat`). The package ships both ESM and CJS outputs. TypeScript types are bundled.\n\n## Go\n\n**Module:** `github.com/kreuzberg-dev/html-to-markdown/packages/go/v3`\n**Requires:** Go ≥ 1.26\n\n```bash\ngo get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\n```\n\n```go\nimport htmltomarkdown \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n\nresult, err := htmltomarkdown.Convert(\"<h1>Title</h1>\", nil)\nif err != nil {\n    log.Fatal(err)\n}\nfmt.Println(result.Content)\n```\n\nOptions use Go struct field names (`PascalCase`). `Convert` returns `(*ConversionResult, error)`. Errors are standard Go errors. Use `errors.Is`/`errors.As` to inspect them.\n\n## Ruby\n\n**Gem:** `html-to-markdown` on RubyGems\n**Requires:** Ruby ≥ 3.2\n\n```bash\ngem install html-to-markdown\n```\n\n```ruby\nrequire 'html_to_markdown'\n\nresult = HtmlToMarkdown.convert('<h1>Title</h1>', heading_style: :atx)\nputs result[:content]\n```\n\nOptions are keyword arguments with `snake_case` symbols. `result` is a hash. Errors raise `HtmlToMarkdown::ConversionError`.\n\n## PHP\n\n**Package:** `kreuzberg/html-to-markdown` on Packagist\n\n```bash\ncomposer require kreuzberg/html-to-markdown\n```\n\n```php\n$converter = new \\Kreuzberg\\HtmlToMarkdown\\Converter();\n$result = $converter->convert('<h1>Title</h1>', ['headingStyle' => 'atx']);\necho $result->content;\n```\n\nOptions are a plain associative array with `camelCase` keys. `$result` is a value object. Errors throw `\\Kreuzberg\\HtmlToMarkdown\\ConversionException`.\n\n## Java\n\n**Maven:** `dev.kreuzberg:html-to-markdown`\n\n```xml\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.1.0</version>\n</dependency>\n```\n\n```java\nimport dev.kreuzberg.HtmlToMarkdown;\nimport dev.kreuzberg.ConversionOptions;\n\nConversionOptions options = ConversionOptions.builder()\n    .headingStyle(\"atx\")\n    .build();\nConversionResult result = HtmlToMarkdown.convert(\"<h1>Title</h1>\", options);\nSystem.out.println(result.getContent());\n```\n\nUses a builder for options. Errors throw `dev.kreuzberg.ConversionException` (checked). The library ships with native binaries for Linux x86_64, macOS arm64/x86_64, and Windows x86_64.\n\n## C\n\n**NuGet:** `KreuzbergDev.HtmlToMarkdown`\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n\n```csharp\nusing KreuzbergDev.HtmlToMarkdown;\n\nvar options = new ConversionOptions { HeadingStyle = \"atx\" };\nvar result = HtmlToMarkdownConverter.Convert(\"<h1>Title</h1>\", options);\nConsole.WriteLine(result.Content);\n```\n\nOption properties are `PascalCase`. Errors throw `ConversionException`. The package targets `netstandard2.0` and above.\n\n## Elixir\n\n**Hex:** `html_to_markdown`\n**Requires:** Elixir ~> 1.19\n\n```elixir\n{:html_to_markdown, \"~> 3.1\"}\n```\n\n```elixir\ncase HtmlToMarkdown.convert(\"<h1>Title</h1>\", heading_style: :atx) do\n  {:ok, result} -> IO.puts(result.content)\n  {:error, reason} -> IO.warn(\"failed: #{reason}\")\nend\n```\n\n`convert/2` returns `{:ok, result}` or `{:error, reason}`. Options are a keyword list. The struct fields match the Rust names (`snake_case`).\n\n## R\n\n**CRAN:** `htmltomarkdown`\n\n```r\ninstall.packages(\"htmltomarkdown\")\n```\n\n```r\nlibrary(htmltomarkdown)\n\nresult <- htmltomarkdown::convert(\"<h1>Title</h1>\", heading_style = \"atx\")\ncat(result$content)\n```\n\nOptions are named function arguments. The returned list matches the `ConversionResult` shape with `snake_case` names. Errors stop execution with a message; wrap in `tryCatch` if you need to handle them.\n\n## C\n\n**Link against:** `libhtml_to_markdown`\n**Header:** `html_to_markdown.h`\n\nDownload a pre-built release archive for your platform from the [GitHub releases page](https://github.com/kreuzberg-dev/html-to-markdown/releases), or build from source with `cargo build --release -p html-to-markdown-ffi`.\n\n```c\n#include \"html_to_markdown.h\"\n\nHtmlToMarkdownResult result = html_to_markdown_convert(\"<h1>Title</h1>\", NULL);\nif (result.error == NULL) {\n    printf(\"%s\\n\", result.content);\n}\nhtml_to_markdown_free_result(result);\n```\n\nAlways call `html_to_markdown_free_result` to release memory owned by the Rust allocator. The C API is a thin synchronous FFI layer. No async mode, no thread-local state.\n\n## WASM\n\n**npm:** `@kreuzberg/html-to-markdown-wasm`\n\n```bash\nnpm install @kreuzberg/html-to-markdown-wasm\n```\n\n```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init();\nconst result = convert('<h1>Title</h1>', { headingStyle: 'atx' });\nconsole.log(result.content);\n```\n\n`init()` loads and instantiates the `.wasm` file. Call it once before any conversion. After that, `convert` is synchronous. Options use `camelCase` and have the same shape as the TypeScript binding. The WASM build omits the `inline-images` feature (no file-system access in the browser sandbox).\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/llms.txt",
    "content": "# html-to-markdown\n\n> High-performance HTML to Markdown conversion library with 12 native language bindings.\n\n## Links\n\n- GitHub: https://github.com/kreuzberg-dev/html-to-markdown\n- Organization: https://github.com/kreuzberg-dev\n- Kreuzberg (document extraction): https://github.com/kreuzberg-dev/kreuzberg\n- Kreuzberg Docs: https://docs.kreuzberg.dev\n- PyPI: https://pypi.org/project/html-to-markdown/\n- npm: https://www.npmjs.com/package/@kreuzberg/html-to-markdown\n- crates.io: https://crates.io/crates/html-to-markdown-rs\n- Docs: https://docs.html-to-markdown.kreuzberg.dev\n\n## Capabilities\n\n- Convert HTML to Markdown, Djot, or plain text\n- Extract structured document tree (DocumentStructure with headings, paragraphs, lists, tables, etc.)\n- Extract HTML metadata (title, description, Open Graph, Twitter Card, JSON-LD, links, images)\n- Extract inline images from data URIs and SVGs\n- Table extraction with cell-level data (colspan, rowspan, headers)\n- Custom visitor pattern for content filtering and transformation\n- 12 native language bindings: Rust, Python, TypeScript, Go, Ruby, PHP, Java, C#, Elixir, R, C (FFI), WebAssembly\n- CLI tool for command-line conversion\n- Processing speed: 150-280 MB/s\n\n## API (v3)\n\nSingle entry point: `convert(html, options?) -> ConversionResult`\n\nConversionResult contains:\n- content: Optional<String> — the converted text (markdown/djot/plain); null/None when output_format is \"none\"\n- document: Optional<DocumentStructure> — structured document tree; only populated when include_document_structure is true\n- metadata: HtmlMetadata — extracted HTML metadata (title, description, Open Graph, Twitter Card, JSON-LD, links, images)\n- tables: Vec<TableData> — extracted tables with grid data (headers, rows, colspan/rowspan)\n- images: Vec<ExtractedImage> — extracted inline images (data URIs, embedded SVGs); only populated when extract_images is true\n- warnings: Vec<ProcessingWarning> — non-fatal processing warnings\n\n## Usage Examples\n\n### Rust\n```rust\nuse html_to_markdown_rs::convert;\n\nlet result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\nlet markdown = result.content.unwrap_or_default();\n// \"# Hello\\n\\nWorld\"\n```\n\n### Python\n```python\nfrom html_to_markdown import convert\n\nresult = convert(\"<h1>Hello</h1><p>World</p>\")\nmarkdown = result.content\n# \"# Hello\\n\\nWorld\"\n```\n\n### TypeScript\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst result = convert('<h1>Hello</h1><p>World</p>');\nconst markdown = result.content;\n// \"# Hello\\n\\nWorld\"\n```\n\n### Go\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n\nresult, err := htmltomarkdown.Convert(\"<h1>Hello</h1><p>World</p>\")\nif result.Content != nil {\n    fmt.Println(*result.Content)\n}\n```\n\n### Ruby\n```ruby\nrequire 'html_to_markdown'\nresult = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\")\nmarkdown = result[:content]\n```\n\n### PHP\n```php\nuse function HtmlToMarkdown\\convert;\n$result = convert('<h1>Hello</h1><p>World</p>');\n$markdown = $result['content'];\n```\n\n### Java\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nConversionResult result = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\");\nSystem.out.println(result.content());\n```\n\n### C#\n```csharp\nusing HtmlToMarkdown;\nvar result = HtmlToMarkdownConverter.Convert(\"<h1>Hello</h1><p>World</p>\");\nConsole.WriteLine(result.Content);\n```\n\n### Elixir\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\")\nIO.puts(result.content)\n```\n\n### R\n```r\nlibrary(htmltomarkdown)\nresult <- convert(\"<h1>Hello</h1><p>World</p>\")\ncat(result$content)\n```\n\n### C (FFI)\n```c\n#include \"html_to_markdown.h\"\nchar *json = html_to_markdown_convert(\"<h1>Hello</h1>\", NULL);\n// json: {\"content\":\"# Hello\",\"metadata\":null,\"tables\":null}\nhtml_to_markdown_free_string(json);\n```\n\n### WASM\n```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\nawait init();\nconst result = convert('<h1>Hello</h1><p>World</p>');\nconsole.log(result.content);\n```\n\n## Configuration Options\n\nAll options are passed to `convert()` as the second argument via a `ConversionOptions` struct/object/dict.\n\n- output_format: \"markdown\" | \"djot\" | \"plain\" | \"none\" (default: \"markdown\")\n- heading_style: \"atx\" | \"underlined\" | \"atx_closed\" (default: \"atx\")\n- list_indent_type: \"spaces\" | \"tab\" (default: \"spaces\")\n- list_indent_width: int 1-8 (default: 2)\n- bullets: string cycling through list marker chars (default: \"-\")\n- strong_em_symbol: \"*\" | \"_\" (default: \"*\")\n- newline_style: \"backslash\" | \"spaces\" (default: \"backslash\")\n- code_block_style: \"indented\" | \"backticks\" | \"tildes\" (default: \"indented\")\n- code_language: string default language for fenced code blocks (default: \"\")\n- autolinks: bool — use <url> when link text equals href (default: false)\n- default_title: bool — use href as title when none present (default: false)\n- keep_inline_images_in: list of element names to keep images as markdown (default: [])\n- br_in_tables: bool — preserve <br> in table cells (default: false)\n- highlight_style: \"double-equal\" | \"html\" | \"bold\" | \"none\" (default: \"double-equal\")\n- escape_asterisks: bool (default: false)\n- escape_underscores: bool (default: false)\n- escape_misc: bool (default: false)\n- escape_ascii: bool (default: false)\n- sub_symbol: string wrapper for <sub> text (default: \"\")\n- sup_symbol: string wrapper for <sup> text (default: \"\")\n- whitespace_mode: \"normalized\" | \"strict\" (default: \"normalized\")\n- strip_newlines: bool (default: false)\n- wrap: bool — enable line wrapping (default: false)\n- wrap_width: int 20-500 (default: 80)\n- convert_as_inline: bool (default: false)\n- strip_tags: list of tag names to strip (default: [])\n- preserve_tags: list of tag names to emit verbatim as HTML (default: [])\n- link_style: \"inline\" | \"reference\" — inline emits [text](url); reference emits [text][1] with definitions at end (default: \"inline\")\n- skip_images: bool — drop images entirely (default: false)\n- max_image_size: int bytes — skip inline images larger than this (default: 5242880)\n- capture_svg: bool — include inline SVGs in result.images when extract_images is true (default: false)\n- infer_dimensions: bool — decode image bytes to infer width/height (default: true)\n- encoding: string — CLI only; input file character encoding (default: \"utf-8\"); ignored by the core library\n- debug: bool — CLI only; prints diagnostic lines to stderr after each conversion (default: false)\n- extract_metadata: bool — populate result.metadata and result.tables (default: true)\n- extract_images: bool — extract img elements and populate result.images (default: false)\n- include_document_structure: bool — populate result.document (default: false)\n- preprocess: bool — clean HTML before conversion (default: false)\n- preset: \"minimal\" | \"standard\" | \"aggressive\" preprocessing aggressiveness (default: \"standard\")\n- keep_navigation: bool — preserve <nav> during preprocessing (default: false)\n- keep_forms: bool — preserve forms during preprocessing (default: false)\n\n## CLI\n\nInstall: `cargo install html-to-markdown-cli`\n\n```\nhtml-to-markdown [FILE] [OPTIONS]\n\nInput:\n  FILE                     Input file (omit or use - for stdin)\n  --url URL                Fetch from URL\n  -e, --encoding ENC       Input encoding (default: utf-8)\n\nOutput:\n  -o, --output FILE        Output file (default: stdout)\n  -f, --output-format FMT  markdown | djot (default: markdown)\n\nFormatting:\n  --heading-style STYLE    atx | underlined | atx-closed\n  --list-indent-width N    Spaces per nesting level (default: 2)\n  --bullets CHARS          List marker characters\n  --code-block-style STYLE indented | backticks | tildes\n  -l, --code-language LANG Default language for fenced code blocks\n  --no-autolinks           Disable autolink conversion (autolinks on by default)\n  -w, --wrap               Enable line wrapping\n  --wrap-width N           Wrap column (default: 80)\n\nLinks:\n  --link-style STYLE       inline | reference (default: inline)\n\nImages:\n  --skip-images            Drop all <img> elements entirely\n\nElements:\n  --strip-tags TAGS        Comma-separated tags to strip\n  --preserve-tags TAGS     Comma-separated tags to emit as raw HTML\n  --max-depth N            Truncate subtrees beyond this nesting depth\n\nMetadata:\n  --extract-metadata       Prepend metadata comment block to output\n  --json                   Output full ConversionResult as JSON\n  --include-structure      Populate document tree (requires --json)\n  --extract-inline-images  Populate images field (requires --json)\n  --no-content             Skip markdown rendering (requires --json)\n  --show-warnings          Print warnings to stderr\n\nPreprocessing:\n  -p, --preprocess         Clean HTML before conversion\n  --preset LEVEL           minimal | standard | aggressive\n```\n\n## Installation\n\n- Rust: `cargo add html-to-markdown-rs`\n- Python: `pip install html-to-markdown`\n- TypeScript/Node.js: `npm install @kreuzberg/html-to-markdown`\n- Go: `go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3`\n- Ruby: `gem install html_to_markdown`\n- PHP: `composer require kreuzberg/html-to-markdown`\n- Java: Maven artifact `dev.kreuzberg:html-to-markdown:3.1.0`\n- C#: `dotnet add package KreuzbergDev.HtmlToMarkdown`\n- Elixir: `{:html_to_markdown, \"~> 3.1\"}` in mix.exs\n- R: `install.packages(\"htmltomarkdown\")`\n- C: link against `libhtml_to_markdown` (see GitHub releases)\n- WASM: `npm install @kreuzberg/html-to-markdown-wasm`\n\n## Part of kreuzberg.dev\n\nhtml-to-markdown is part of the kreuzberg.dev ecosystem — a suite of high-performance document processing tools built with Rust and exposed to multiple programming languages. It powers the HTML conversion pipeline in [kreuzberg](https://docs.kreuzberg.dev), a document intelligence library for extracting text and structured data from PDFs, DOCX, images, and other formats.\n"
  },
  {
    "path": "docs/migration.md",
    "content": "# Migrating to v3\n\n## Background\n\nhtml-to-markdown began as a Python library — a fork of\n[markdownify](https://github.com/matthewwithanm/python-markdownify) —\nfocused on converting HTML to Markdown. Over time, the core was\nrewritten in Rust for performance, and native bindings were added for\n12 programming languages: Python, TypeScript, Ruby, Go, PHP, Java,\nC#, Elixir, R, C, and WebAssembly.\n\nv3.0.0 rationalises the library's API surface. Instead of multiple\nspecialised functions, there is now a single `convert()` function that\nreturns a structured `ConversionResult`. This simplifies the API while\nmaking all extraction capabilities available through one call.\n\n### hOCR support moved to Kreuzberg\n\nhOCR (OCR-generated HTML) processing was removed from html-to-markdown\nand moved into [Kreuzberg](https://github.com/kreuzberg-dev/kreuzberg),\nthe document extraction library in the Kreuzberg.dev ecosystem. hOCR is\na document-level concern — it belongs in the extraction pipeline, not\nin an HTML-to-Markdown converter.\n\n## API changes\n\n### Before (v2)\n\nv2 had multiple functions for different extraction needs:\n\n- `convert(html)` → `String`\n- `convert_with_metadata(html, options, config)` → `(String, Metadata)`\n- `convert_with_inline_images(html, options, config)` → `Extraction`\n- `convert_with_tables(html, options)` → `TablesResult`\n- `convert_with_visitor(html, options, visitor)` → `String`\n\n### After (v3)\n\nv3 has one function:\n\n- `convert(html, options?, visitor?)` → `ConversionResult`\n\n`ConversionResult` contains all data:\n\n- `content` — the converted text (Markdown, Djot, or plain text)\n- `metadata` — HTML metadata (title, OG, links, images, structured data)\n- `tables` — extracted tables with cell data\n- `document` — structured document tree (when enabled)\n- `images` — extracted inline images (when enabled)\n- `warnings` — non-fatal processing warnings\n\n### Migration table\n\n| v2 | v3 |\n|----|-----|\n| `convert(html)` | `convert(html).content` |\n| `convert_with_metadata(html, opts, cfg)` | `convert(html, opts).metadata` |\n| `convert_with_inline_images(html, opts, cfg)` | `convert(html, opts).images` (set `extract_images: true`) |\n| `convert_with_tables(html, opts)` | `convert(html, opts).tables` |\n| `convert_with_visitor(html, opts, v)` | `convert(html, opts, v).content` |\n| `convert_to_string(html)` | `convert(html).content` |\n| `hocr_spatial_tables: true` | Use [Kreuzberg](https://github.com/kreuzberg-dev/kreuzberg) |\n| `ExtendedMetadata` | `HtmlMetadata` |\n| `start_profiling()` | Removed |\n| `markdownify(html)` (Python) | `convert(html).content` |\n\n## Examples by language\n\n### Rust\n\n```rust\n// v2\nlet markdown = convert(html, None)?;\nlet (md, meta) = convert_with_metadata(html, None, cfg, None)?;\n\n// v3\nlet result = convert(html, None)?;\nlet markdown = result.content.unwrap_or_default();\nlet metadata = result.metadata; // always available\n```\n\n### Python\n\n```python\n# v2\nmarkdown = convert(html)\nmarkdown, metadata = convert_with_metadata(html)\n\n# v3\nresult = convert(html)\nmarkdown = result.content\nmetadata = result.metadata\n```\n\n### TypeScript\n\n```typescript\n// v2\nconst markdown = convert(html);\nconst { markdown, metadata } = convertWithMetadata(html);\n\n// v3\nconst result = convert(html);\nconst markdown = result.content;\nconst metadata = result.metadata;\n```\n\n### Go\n\n```go\n// v2\nmarkdown, err := htmltomarkdown.Convert(html, nil)\nmarkdown, meta, err := htmltomarkdown.ConvertWithMetadata(html, nil)\n\n// v3\nresult, err := htmltomarkdown.Convert(html, nil)\nmarkdown := result.Content\nmetadata := result.Metadata\n```\n\n### Ruby\n\n```ruby\n# v2\nmarkdown = HtmlToMarkdown.convert(html)\nmarkdown, metadata = HtmlToMarkdown.convert_with_metadata(html)\n\n# v3\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]\nmetadata = result[:metadata]\n```\n\n### PHP\n\n```php\n// v2\n$markdown = HtmlToMarkdown\\convert($html);\n[$markdown, $metadata] = HtmlToMarkdown\\convert_with_metadata($html);\n\n// v3\n$result = HtmlToMarkdown\\convert($html);\n$markdown = $result->content;\n$metadata = $result->metadata;\n```\n\n### Java\n\n```java\n// v2\nString markdown = HtmlToMarkdown.convert(html);\nConversionPair pair = HtmlToMarkdown.convertWithMetadata(html);\n\n// v3\nConversionResult result = HtmlToMarkdown.convert(html);\nString markdown = result.getContent();\nHtmlMetadata metadata = result.getMetadata();\n```\n\n### C\n\n```csharp\n// v2\nstring markdown = HtmlToMarkdown.Convert(html);\n(string md, Metadata meta) = HtmlToMarkdown.ConvertWithMetadata(html);\n\n// v3\nConversionResult result = HtmlToMarkdown.Convert(html);\nstring markdown = result.Content;\nHtmlMetadata metadata = result.Metadata;\n```\n\n### Elixir\n\n```elixir\n# v2\n{:ok, markdown} = HtmlToMarkdown.convert(html)\n{:ok, markdown, metadata} = HtmlToMarkdown.convert_with_metadata(html)\n\n# v3\n{:ok, result} = HtmlToMarkdown.convert(html)\nmarkdown = result.content\nmetadata = result.metadata\n```\n\n### R\n\n```r\n# v2\nmarkdown <- html_to_markdown(html)\nresult   <- html_to_markdown_with_metadata(html)\n\n# v3\nresult   <- convert(html)\nmarkdown <- result$content\nmetadata <- result$metadata\n```\n\n### C (FFI)\n\n```c\n/* v2 */\nchar *markdown = htm_convert(html, NULL);\nhtm_free(markdown);\n\n/* v3 */\nHtmResult *result = htm_convert(html, NULL);\nconst char *markdown = htm_result_content(result);\nhtm_result_free(result);\n```\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/overrides/main.html",
    "content": "{% extends \"base.html\" %}\n\n{% block extrahead %}\n  <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-8G4NQW55PF\"></script>\n  <script>\n    window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n\n    gtag('config', 'G-8G4NQW55PF');\n    gtag('config', 'AW-17853694443');\n  </script>\n\n  <meta property=\"og:type\" content=\"website\" />\n  <meta property=\"og:title\" content=\"{{ config.site_name }}{% if page and page.title and not page.is_homepage %} - {{ page.title | striptags }}{% endif %}\" />\n  <meta property=\"og:description\" content=\"{% if page and page.meta and page.meta.description %}{{ page.meta.description }}{% else %}{{ config.site_description }}{% endif %}\" />\n  <meta property=\"og:url\" content=\"{{ page.canonical_url }}\" />\n  <meta property=\"og:site_name\" content=\"{{ config.site_name }}\" />\n  <meta property=\"og:image\" content=\"{{ config.site_url }}assets/og-image.png\" />\n  <meta property=\"og:image:width\" content=\"1200\" />\n  <meta property=\"og:image:height\" content=\"630\" />\n\n  <meta name=\"twitter:card\" content=\"summary_large_image\" />\n  <meta name=\"twitter:title\" content=\"{{ config.site_name }}{% if page and page.title and not page.is_homepage %} - {{ page.title | striptags }}{% endif %}\" />\n  <meta name=\"twitter:description\" content=\"{% if page and page.meta and page.meta.description %}{{ page.meta.description }}{% else %}{{ config.site_description }}{% endif %}\" />\n  <meta name=\"twitter:image\" content=\"{{ config.site_url }}assets/og-image.png\" />\n{% endblock %}\n"
  },
  {
    "path": "docs/reference/api-c.md",
    "content": "---\ntitle: \"C API Reference\"\n---\n\n## C API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### htm_convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```c\nHtmConversionResult* htm_convert(const char* html, HtmConversionOptions options);\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `const char*` | Yes | The html |\n| `options` | `HtmConversionOptions*` | No | The options to use |\n\n**Returns:** `HtmConversionResult`\n\n**Errors:** Returns `NULL` on error.\n\n\n---\n\n### Types\n\n#### HtmConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HtmHeadingStyle` | `HTM_HTM_ATX` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `HtmListIndentType` | `HTM_HTM_SPACES` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `uintptr_t` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `const char*` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `const char*` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `const char*` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HtmHighlightStyle` | `HTM_HTM_DOUBLE_EQUAL` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `HtmWhitespaceMode` | `HTM_HTM_NORMALIZED` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `uintptr_t` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `const char*` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `const char*` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `HtmNewlineStyle` | `HTM_HTM_SPACES` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `HtmCodeBlockStyle` | `HTM_HTM_BACKTICKS` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `const char**` | `NULL` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `HtmPreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `const char*` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `const char**` | `NULL` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `const char**` | `NULL` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `HtmLinkStyle` | `HTM_HTM_INLINE` | Link rendering style (inline or reference). |\n| `output_format` | `HtmOutputFormat` | `HTM_HTM_MARKDOWN` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `bool` | `false` | Include structured document tree in result. |\n| `extract_images` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `uint64_t` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `bool` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `max_depth` | `uintptr_t*` | `NULL` | Maximum DOM traversal depth. `NULL` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `const char**` | `NULL` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor*` | `NULL` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### htm_default()\n\n**Signature:**\n\n```c\nHtmConversionOptions htm_default();\n```\n\n###### htm_builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_builder();\n```\n\n###### htm_apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```c\nvoid htm_apply_update(HtmConversionOptionsUpdate update);\n```\n\n###### htm_from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```c\nHtmConversionOptions htm_from_update(HtmConversionOptionsUpdate update);\n```\n\n###### htm_from()\n\n**Signature:**\n\n```c\nHtmConversionOptions htm_from(HtmConversionOptionsUpdate update);\n```\n\n\n---\n\n#### HtmConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `const char**` | `NULL` | Converted text output (markdown, djot, or plain text). `NULL` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `HtmDocumentStructure*` | `NULL` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmHtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `HtmTableData*` | `NULL` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `const char**` | `NULL` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `HtmProcessingWarning*` | `NULL` | Non-fatal processing warnings. |\n\n\n---\n\n#### HtmConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### htm_strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_strip_tags(const char** tags);\n```\n\n###### htm_preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_preserve_tags(const char** tags);\n```\n\n###### htm_keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_keep_inline_images_in(const char** tags);\n```\n\n###### htm_exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_exclude_selectors(const char** selectors);\n```\n\n###### htm_visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_visitor(HtmVisitorHandle visitor);\n```\n\n###### htm_preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```c\nHtmConversionOptionsBuilder htm_preprocessing(HtmPreprocessingOptions preprocessing);\n```\n\n###### htm_build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```c\nHtmConversionOptions htm_build();\n```\n\n\n---\n\n#### HtmDocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `const char**` | `NULL` | Document title from `<title>` tag |\n| `description` | `const char**` | `NULL` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `const char**` | `NULL` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `const char**` | `NULL` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `const char**` | `NULL` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `const char**` | `NULL` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `const char**` | `NULL` | Document language from `lang` attribute |\n| `text_direction` | `HtmTextDirection*` | `NULL` | Document text direction from `dir` attribute |\n| `open_graph` | `void*` | `NULL` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `void*` | `NULL` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `void*` | `NULL` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### HtmDocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `const char*` | — | Deterministic node identifier. |\n| `content` | `HtmNodeContent` | — | The semantic content of this node. |\n| `parent` | `uint32_t*` | `NULL` | Index of the parent node (None for root nodes). |\n| `children` | `uint32_t*` | — | Indices of child nodes in reading order. |\n| `annotations` | `HtmTextAnnotation*` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `void**` | `NULL` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### HtmDocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `HtmDocumentNode*` | — | All nodes in document reading order. |\n| `source_format` | `const char**` | `NULL` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### HtmGridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `const char*` | — | The text content of the cell. |\n| `row` | `uint32_t` | — | 0-indexed row position. |\n| `col` | `uint32_t` | — | 0-indexed column position. |\n| `row_span` | `uint32_t` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `uint32_t` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HtmHeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `uint8_t` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `const char*` | — | Normalized text content of the header |\n| `id` | `const char**` | `NULL` | HTML id attribute if present |\n| `depth` | `uintptr_t` | — | Document tree depth at the header element |\n| `html_offset` | `uintptr_t` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### htm_is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```c\nbool htm_is_valid();\n```\n\n\n---\n\n#### HtmHtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `HtmDocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `HtmHeaderMetadata*` | `NULL` | Extracted header elements with hierarchy |\n| `links` | `HtmLinkMetadata*` | `NULL` | Extracted hyperlinks with type classification |\n| `images` | `HtmImageMetadata*` | `NULL` | Extracted images with source and dimensions |\n| `structured_data` | `HtmStructuredData*` | `NULL` | Extracted structured data blocks |\n\n\n---\n\n#### HtmHtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### htm_visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_element_start(HtmNodeContext ctx);\n```\n\n##### htm_visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_element_end(HtmNodeContext ctx, const char* output);\n```\n\n###### htm_visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_text(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_link(HtmNodeContext ctx, const char* href, const char* text, const char* title);\n```\n\n###### htm_visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_image(HtmNodeContext ctx, const char* src, const char* alt, const char* title);\n```\n\n###### htm_visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_heading(HtmNodeContext ctx, uint32_t level, const char* text, const char* id);\n```\n\n###### htm_visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_code_block(HtmNodeContext ctx, const char* lang, const char* code);\n```\n\n###### htm_visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_code_inline(HtmNodeContext ctx, const char* code);\n```\n\n###### htm_visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_list_item(HtmNodeContext ctx, bool ordered, const char* marker, const char* text);\n```\n\n###### htm_visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_list_start(HtmNodeContext ctx, bool ordered);\n```\n\n###### htm_visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_list_end(HtmNodeContext ctx, bool ordered, const char* output);\n```\n\n###### htm_visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_table_start(HtmNodeContext ctx);\n```\n\n###### htm_visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_table_row(HtmNodeContext ctx, const char** cells, bool is_header);\n```\n\n###### htm_visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_table_end(HtmNodeContext ctx, const char* output);\n```\n\n###### htm_visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_blockquote(HtmNodeContext ctx, const char* content, uintptr_t depth);\n```\n\n###### htm_visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_strong(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_emphasis(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_strikethrough(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_underline(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_subscript(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_superscript(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_mark(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_line_break(HtmNodeContext ctx);\n```\n\n###### htm_visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_horizontal_rule(HtmNodeContext ctx);\n```\n\n###### htm_visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_custom_element(HtmNodeContext ctx, const char* tag_name, const char* html);\n```\n\n###### htm_visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_definition_list_start(HtmNodeContext ctx);\n```\n\n###### htm_visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_definition_term(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_definition_description(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_definition_list_end(HtmNodeContext ctx, const char* output);\n```\n\n###### htm_visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_form(HtmNodeContext ctx, const char* action, const char* method);\n```\n\n###### htm_visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_input(HtmNodeContext ctx, const char* input_type, const char* name, const char* value);\n```\n\n###### htm_visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_button(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_audio(HtmNodeContext ctx, const char* src);\n```\n\n###### htm_visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_video(HtmNodeContext ctx, const char* src);\n```\n\n###### htm_visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_iframe(HtmNodeContext ctx, const char* src);\n```\n\n###### htm_visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_details(HtmNodeContext ctx, bool open);\n```\n\n###### htm_visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_summary(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_figure_start(HtmNodeContext ctx);\n```\n\n###### htm_visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_figcaption(HtmNodeContext ctx, const char* text);\n```\n\n###### htm_visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```c\nHtmVisitResult htm_visit_figure_end(HtmNodeContext ctx, const char* output);\n```\n\n\n---\n\n##### HtmImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `const char*` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `const char**` | `NULL` | Alternative text from alt attribute (for accessibility) |\n| `title` | `const char**` | `NULL` | Title attribute (often shown as tooltip) |\n| `dimensions` | `uint32_t**` | `NULL` | Image dimensions as (width, height) if available |\n| `image_type` | `HtmImageType` | — | Image type classification |\n| `attributes` | `void*` | — | Additional HTML attributes |\n\n\n---\n\n##### HtmLinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `const char*` | — | The href URL value |\n| `text` | `const char*` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `const char**` | `NULL` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `HtmLinkType` | — | Link type classification |\n| `rel` | `const char**` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `void*` | — | Additional HTML attributes |\n\n###### Methods\n\n###### htm_classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```c\nHtmLinkType htm_classify_link(const char* href);\n```\n\n\n---\n\n##### HtmNodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `HtmNodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `const char*` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `void*` | — | All HTML attributes as key-value pairs |\n| `depth` | `uintptr_t` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `uintptr_t` | — | Index among siblings (0-based) |\n| `parent_tag` | `const char**` | `NULL` | Parent element's tag name (None if root) |\n| `is_inline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### HtmPreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `preset` | `HtmPreprocessingPreset` | `HTM_HTM_STANDARD` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### htm_default()\n\n**Signature:**\n\n```c\nHtmPreprocessingOptions htm_default();\n```\n\n###### htm_apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```c\nvoid htm_apply_update(HtmPreprocessingOptionsUpdate update);\n```\n\n###### htm_from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```c\nHtmPreprocessingOptions htm_from_update(HtmPreprocessingOptionsUpdate update);\n```\n\n###### htm_from()\n\n**Signature:**\n\n```c\nHtmPreprocessingOptions htm_from(HtmPreprocessingOptionsUpdate update);\n```\n\n\n---\n\n##### HtmProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `const char*` | — | Human-readable warning message. |\n| `kind` | `HtmWarningKind` | — | The category of warning. |\n\n\n---\n\n##### HtmStructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `HtmStructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `const char*` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `const char**` | `NULL` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### HtmTableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `HtmTableGrid` | — | The structured table grid. |\n| `markdown` | `const char*` | — | The markdown rendering of this table. |\n\n\n---\n\n##### HtmTableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `uint32_t` | — | Number of rows. |\n| `cols` | `uint32_t` | — | Number of columns. |\n| `cells` | `HtmGridCell*` | `NULL` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### HtmTextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `uint32_t` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `uint32_t` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `HtmAnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### HtmVisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### HtmTextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_LEFT_TO_RIGHT` | Left-to-right text flow (default for Latin scripts) |\n| `HTM_RIGHT_TO_LEFT` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `HTM_AUTO` | Automatic directionality detection |\n\n\n---\n\n##### HtmLinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_ANCHOR` | Anchor link within same document (href starts with #) |\n| `HTM_INTERNAL` | Internal link within same domain |\n| `HTM_EXTERNAL` | External link to different domain |\n| `HTM_EMAIL` | Email link (mailto:) |\n| `HTM_PHONE` | Phone link (tel:) |\n| `HTM_OTHER` | Other protocol or unclassifiable |\n\n\n---\n\n##### HtmImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_DATA_URI` | Data URI embedded image (base64 or other encoding) |\n| `HTM_INLINE_SVG` | Inline SVG element |\n| `HTM_EXTERNAL` | External image URL (http/https) |\n| `HTM_RELATIVE` | Relative image path |\n\n\n---\n\n##### HtmStructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_JSON_LD` | JSON-LD (JSON for Linking Data) script blocks |\n| `HTM_MICRODATA` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `HTM_RDFA` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### HtmPreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_MINIMAL` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `HTM_STANDARD` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `HTM_AGGRESSIVE` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HtmHeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_UNDERLINED` | Underlined style (=== for h1, --- for h2). |\n| `HTM_ATX` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `HTM_ATX_CLOSED` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### HtmListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_SPACES` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `HTM_TABS` | Use tabs for indentation. |\n\n\n---\n\n##### HtmWhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_NORMALIZED` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `HTM_STRICT` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### HtmNewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_SPACES` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `HTM_BACKSLASH` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### HtmCodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_INDENTED` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `HTM_BACKTICKS` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `HTM_TILDES` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HtmHighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_DOUBLE_EQUAL` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `HTM_HTML` | Preserve as HTML (==text==). Original HTML tag. |\n| `HTM_BOLD` | Render as bold (**text**). Uses strong emphasis. |\n| `HTM_NONE` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### HtmLinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_INLINE` | Inline links: `[text](url)`. Default. |\n| `HTM_REFERENCE` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### HtmOutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_MARKDOWN` | Standard Markdown (CommonMark compatible). Default. |\n| `HTM_DJOT` | Djot lightweight markup language. |\n| `HTM_PLAIN` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### HtmNodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_HEADING` | A heading element (h1-h6). — Fields: `level`: `uint8_t`, `text`: `const char*` |\n| `HTM_PARAGRAPH` | A paragraph of text. — Fields: `text`: `const char*` |\n| `HTM_LIST` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `bool` |\n| `HTM_LIST_ITEM` | A single list item. — Fields: `text`: `const char*` |\n| `HTM_TABLE` | A table with structured cell data. — Fields: `grid`: `HtmTableGrid` |\n| `HTM_IMAGE` | An image element. — Fields: `description`: `const char*`, `src`: `const char*`, `image_index`: `uint32_t` |\n| `HTM_CODE` | A code block or inline code. — Fields: `text`: `const char*`, `language`: `const char*` |\n| `HTM_QUOTE` | A block quote container. |\n| `HTM_DEFINITION_LIST` | A definition list container. |\n| `HTM_DEFINITION_ITEM` | A definition list entry with term and description. — Fields: `term`: `const char*`, `definition`: `const char*` |\n| `HTM_RAW_BLOCK` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `const char*`, `content`: `const char*` |\n| `HTM_METADATA_BLOCK` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `const char**` |\n| `HTM_GROUP` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `const char*`, `heading_level`: `uint8_t`, `heading_text`: `const char*` |\n\n\n---\n\n##### HtmAnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_BOLD` | Bold / strong emphasis. |\n| `HTM_ITALIC` | Italic / emphasis. |\n| `HTM_UNDERLINE` | Underline. |\n| `HTM_STRIKETHROUGH` | Strikethrough / deleted text. |\n| `HTM_CODE` | Inline code. |\n| `HTM_SUBSCRIPT` | Subscript text. |\n| `HTM_SUPERSCRIPT` | Superscript text. |\n| `HTM_HIGHLIGHT` | Highlighted / marked text. |\n| `HTM_LINK` | A hyperlink. — Fields: `url`: `const char*`, `title`: `const char*` |\n\n\n---\n\n##### HtmWarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_IMAGE_EXTRACTION_FAILED` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `HTM_ENCODING_FALLBACK` | The input encoding was not recognized; fell back to UTF-8. |\n| `HTM_TRUNCATED_INPUT` | The input was truncated due to size limits. |\n| `HTM_MALFORMED_HTML` | The HTML was malformed but processing continued with best effort. |\n| `HTM_SANITIZATION_APPLIED` | Sanitization was applied to remove potentially unsafe content. |\n| `HTM_DEPTH_LIMIT_EXCEEDED` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### HtmNodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_TEXT` | Text node (most frequent - 100+ per document) |\n| `HTM_ELEMENT` | Generic element node |\n| `HTM_HEADING` | Heading elements (h1-h6) |\n| `HTM_PARAGRAPH` | Paragraph element |\n| `HTM_DIV` | Generic div container |\n| `HTM_BLOCKQUOTE` | Blockquote element |\n| `HTM_PRE` | Preformatted text block |\n| `HTM_HR` | Horizontal rule |\n| `HTM_LIST` | Ordered or unordered list (ul, ol) |\n| `HTM_LIST_ITEM` | List item (li) |\n| `HTM_DEFINITION_LIST` | Definition list (dl) |\n| `HTM_DEFINITION_TERM` | Definition term (dt) |\n| `HTM_DEFINITION_DESCRIPTION` | Definition description (dd) |\n| `HTM_TABLE` | Table element |\n| `HTM_TABLE_ROW` | Table row (tr) |\n| `HTM_TABLE_CELL` | Table cell (td, th) |\n| `HTM_TABLE_HEADER` | Table header cell (th) |\n| `HTM_TABLE_BODY` | Table body (tbody) |\n| `HTM_TABLE_HEAD` | Table head (thead) |\n| `HTM_TABLE_FOOT` | Table foot (tfoot) |\n| `HTM_LINK` | Anchor link (a) |\n| `HTM_IMAGE` | Image (img) |\n| `HTM_STRONG` | Strong/bold (strong, b) |\n| `HTM_EM` | Emphasis/italic (em, i) |\n| `HTM_CODE` | Inline code (code) |\n| `HTM_STRIKETHROUGH` | Strikethrough (s, del, strike) |\n| `HTM_UNDERLINE` | Underline (u, ins) |\n| `HTM_SUBSCRIPT` | Subscript (sub) |\n| `HTM_SUPERSCRIPT` | Superscript (sup) |\n| `HTM_MARK` | Mark/highlight (mark) |\n| `HTM_SMALL` | Small text (small) |\n| `HTM_BR` | Line break (br) |\n| `HTM_SPAN` | Span element |\n| `HTM_ARTICLE` | Article element |\n| `HTM_SECTION` | Section element |\n| `HTM_NAV` | Navigation element |\n| `HTM_ASIDE` | Aside element |\n| `HTM_HEADER` | Header element |\n| `HTM_FOOTER` | Footer element |\n| `HTM_MAIN` | Main element |\n| `HTM_FIGURE` | Figure element |\n| `HTM_FIGCAPTION` | Figure caption |\n| `HTM_TIME` | Time element |\n| `HTM_DETAILS` | Details element |\n| `HTM_SUMMARY` | Summary element |\n| `HTM_FORM` | Form element |\n| `HTM_INPUT` | Input element |\n| `HTM_SELECT` | Select element |\n| `HTM_OPTION` | Option element |\n| `HTM_BUTTON` | Button element |\n| `HTM_TEXTAREA` | Textarea element |\n| `HTM_LABEL` | Label element |\n| `HTM_FIELDSET` | Fieldset element |\n| `HTM_LEGEND` | Legend element |\n| `HTM_AUDIO` | Audio element |\n| `HTM_VIDEO` | Video element |\n| `HTM_PICTURE` | Picture element |\n| `HTM_SOURCE` | Source element |\n| `HTM_IFRAME` | Iframe element |\n| `HTM_SVG` | SVG element |\n| `HTM_CANVAS` | Canvas element |\n| `HTM_RUBY` | Ruby annotation |\n| `HTM_RT` | Ruby text |\n| `HTM_RP` | Ruby parenthesis |\n| `HTM_ABBR` | Abbreviation |\n| `HTM_KBD` | Keyboard input |\n| `HTM_SAMP` | Sample output |\n| `HTM_VAR` | Variable |\n| `HTM_CITE` | Citation |\n| `HTM_Q` | Quote |\n| `HTM_DEL` | Deleted text |\n| `HTM_INS` | Inserted text |\n| `HTM_DATA` | Data element |\n| `HTM_METER` | Meter element |\n| `HTM_PROGRESS` | Progress element |\n| `HTM_OUTPUT` | Output element |\n| `HTM_TEMPLATE` | Template element |\n| `HTM_SLOT` | Slot element |\n| `HTM_HTML` | HTML root element |\n| `HTM_HEAD` | Head element |\n| `HTM_BODY` | Body element |\n| `HTM_TITLE` | Title element |\n| `HTM_META` | Meta element |\n| `HTM_LINK_TAG` | Link element (not anchor) |\n| `HTM_STYLE` | Style element |\n| `HTM_SCRIPT` | Script element |\n| `HTM_BASE` | Base element |\n| `HTM_CUSTOM` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### HtmVisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `HTM_CONTINUE` | Continue with default conversion behavior |\n| `HTM_CUSTOM` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `const char*` |\n| `HTM_SKIP` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `HTM_PRESERVE_HTML` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `HTM_ERROR` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `const char*` |\n\n\n---\n\n#### Errors\n\n##### HtmConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `HTM_PARSE_ERROR` | HTML parsing error |\n| `HTM_SANITIZATION_ERROR` | HTML sanitization error |\n| `HTM_CONFIG_ERROR` | Invalid configuration |\n| `HTM_IO_ERROR` | I/O error |\n| `HTM_PANIC` | Internal error caught during conversion |\n| `HTM_INVALID_INPUT` | Invalid input data |\n| `HTM_OTHER` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-csharp.md",
    "content": "---\ntitle: \"C# API Reference\"\n---\n\n## C# API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### Convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```csharp\npublic static ConversionResult Convert(string html, ConversionOptions? options = null)\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `Html` | `string` | Yes | The html |\n| `Options` | `ConversionOptions?` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Throws `Error`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `HeadingStyle` | `HeadingStyle` | `HeadingStyle.Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `ListIndentType` | `ListIndentType` | `ListIndentType.Spaces` | How to indent nested list items (spaces or tab). |\n| `ListIndentWidth` | `nuint` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `Bullets` | `string` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `StrongEmSymbol` | `string` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `EscapeAsterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `EscapeUnderscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `EscapeMisc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `EscapeAscii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `CodeLanguage` | `string` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `Autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `DefaultTitle` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `BrInTables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `HighlightStyle` | `HighlightStyle` | `HighlightStyle.DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `ExtractMetadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `WhitespaceMode` | `WhitespaceMode` | `WhitespaceMode.Normalized` | Controls how whitespace is normalised during conversion. |\n| `StripNewlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `Wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `WrapWidth` | `nuint` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `ConvertAsInline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `SubSymbol` | `string` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `SupSymbol` | `string` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `NewlineStyle` | `NewlineStyle` | `NewlineStyle.Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `CodeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle.Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `KeepInlineImagesIn` | `List<string>` | `new List<string>()` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `Preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `Encoding` | `string` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `Debug` | `bool` | `false` | Emit debug information during conversion. |\n| `StripTags` | `List<string>` | `new List<string>()` | HTML tag names whose content is stripped from the output entirely. |\n| `PreserveTags` | `List<string>` | `new List<string>()` | HTML tag names that are preserved verbatim in the output. |\n| `SkipImages` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `LinkStyle` | `LinkStyle` | `LinkStyle.Inline` | Link rendering style (inline or reference). |\n| `OutputFormat` | `OutputFormat` | `OutputFormat.Markdown` | Target output format (Markdown, plain text, etc.). |\n| `IncludeDocumentStructure` | `bool` | `false` | Include structured document tree in result. |\n| `ExtractImages` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `MaxImageSize` | `ulong` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `CaptureSvg` | `bool` | `false` | Capture SVG elements as images. |\n| `InferDimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `MaxDepth` | `nuint?` | `null` | Maximum DOM traversal depth. `null` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `ExcludeSelectors` | `List<string>` | `new List<string>()` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `Visitor` | `HtmlVisitor (interface)` | `null` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### CreateDefault()\n\n**Signature:**\n\n```csharp\npublic ConversionOptions CreateDefault()\n```\n\n###### Builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder Builder()\n```\n\n###### ApplyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```csharp\npublic void ApplyUpdate(ConversionOptionsUpdate update)\n```\n\n###### FromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```csharp\npublic ConversionOptions FromUpdate(ConversionOptionsUpdate update)\n```\n\n###### From()\n\n**Signature:**\n\n```csharp\npublic ConversionOptions From(ConversionOptionsUpdate update)\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Content` | `string?` | `null` | Converted text output (markdown, djot, or plain text). `null` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `Document` | `DocumentStructure?` | `null` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `Metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `Tables` | `List<TableData>` | `new List<TableData>()` | Extracted tables with structured cell data and markdown representation. |\n| `Images` | `List<string>` | `new List<string>()` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `Warnings` | `List<ProcessingWarning>` | `new List<ProcessingWarning>()` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### StripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder StripTags(List<string> tags)\n```\n\n###### PreserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder PreserveTags(List<string> tags)\n```\n\n###### KeepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder KeepInlineImagesIn(List<string> tags)\n```\n\n###### ExcludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder ExcludeSelectors(List<string> selectors)\n```\n\n###### Visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder Visitor(VisitorHandle visitor)\n```\n\n###### Preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```csharp\npublic ConversionOptionsBuilder Preprocessing(PreprocessingOptions preprocessing)\n```\n\n###### Build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```csharp\npublic ConversionOptions Build()\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Title` | `string?` | `null` | Document title from `<title>` tag |\n| `Description` | `string?` | `null` | Document description from `<meta name=\"description\">` tag |\n| `Keywords` | `List<string>` | `new List<string>()` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `Author` | `string?` | `null` | Document author from `<meta name=\"author\">` tag |\n| `CanonicalUrl` | `string?` | `null` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `BaseHref` | `string?` | `null` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `Language` | `string?` | `null` | Document language from `lang` attribute |\n| `TextDirection` | `TextDirection?` | `null` | Document text direction from `dir` attribute |\n| `OpenGraph` | `Dictionary<string, string>` | `new Dictionary<string, string>()` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `TwitterCard` | `Dictionary<string, string>` | `new Dictionary<string, string>()` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `MetaTags` | `Dictionary<string, string>` | `new Dictionary<string, string>()` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Id` | `string` | — | Deterministic node identifier. |\n| `Content` | `NodeContent` | — | The semantic content of this node. |\n| `Parent` | `uint?` | `null` | Index of the parent node (None for root nodes). |\n| `Children` | `List<uint>` | — | Indices of child nodes in reading order. |\n| `Annotations` | `List<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `Attributes` | `Dictionary<string, string>?` | `null` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Nodes` | `List<DocumentNode>` | — | All nodes in document reading order. |\n| `SourceFormat` | `string?` | `null` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Content` | `string` | — | The text content of the cell. |\n| `Row` | `uint` | — | 0-indexed row position. |\n| `Col` | `uint` | — | 0-indexed column position. |\n| `RowSpan` | `uint` | — | Number of rows this cell spans (default 1). |\n| `ColSpan` | `uint` | — | Number of columns this cell spans (default 1). |\n| `IsHeader` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Level` | `byte` | — | Header level: 1 (h1) through 6 (h6) |\n| `Text` | `string` | — | Normalized text content of the header |\n| `Id` | `string?` | `null` | HTML id attribute if present |\n| `Depth` | `nuint` | — | Document tree depth at the header element |\n| `HtmlOffset` | `nuint` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### IsValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```csharp\npublic bool IsValid()\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `Headers` | `List<HeaderMetadata>` | `new List<HeaderMetadata>()` | Extracted header elements with hierarchy |\n| `Links` | `List<LinkMetadata>` | `new List<LinkMetadata>()` | Extracted hyperlinks with type classification |\n| `Images` | `List<ImageMetadata>` | `new List<ImageMetadata>()` | Extracted images with source and dimensions |\n| `StructuredData` | `List<StructuredData>` | `new List<StructuredData>()` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### VisitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitElementStart(NodeContext ctx)\n```\n\n##### VisitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitElementEnd(NodeContext ctx, string output)\n```\n\n###### VisitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitText(NodeContext ctx, string text)\n```\n\n###### VisitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitLink(NodeContext ctx, string href, string text, string title)\n```\n\n###### VisitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitImage(NodeContext ctx, string src, string alt, string title)\n```\n\n###### VisitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitHeading(NodeContext ctx, uint level, string text, string id)\n```\n\n###### VisitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitCodeBlock(NodeContext ctx, string lang, string code)\n```\n\n###### VisitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitCodeInline(NodeContext ctx, string code)\n```\n\n###### VisitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitListItem(NodeContext ctx, bool ordered, string marker, string text)\n```\n\n###### VisitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitListStart(NodeContext ctx, bool ordered)\n```\n\n###### VisitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitListEnd(NodeContext ctx, bool ordered, string output)\n```\n\n###### VisitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitTableStart(NodeContext ctx)\n```\n\n###### VisitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitTableRow(NodeContext ctx, List<string> cells, bool isHeader)\n```\n\n###### VisitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitTableEnd(NodeContext ctx, string output)\n```\n\n###### VisitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitBlockquote(NodeContext ctx, string content, nuint depth)\n```\n\n###### VisitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitStrong(NodeContext ctx, string text)\n```\n\n###### VisitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitEmphasis(NodeContext ctx, string text)\n```\n\n###### VisitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitStrikethrough(NodeContext ctx, string text)\n```\n\n###### VisitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitUnderline(NodeContext ctx, string text)\n```\n\n###### VisitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitSubscript(NodeContext ctx, string text)\n```\n\n###### VisitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitSuperscript(NodeContext ctx, string text)\n```\n\n###### VisitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitMark(NodeContext ctx, string text)\n```\n\n###### VisitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitLineBreak(NodeContext ctx)\n```\n\n###### VisitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitHorizontalRule(NodeContext ctx)\n```\n\n###### VisitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitCustomElement(NodeContext ctx, string tagName, string html)\n```\n\n###### VisitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitDefinitionListStart(NodeContext ctx)\n```\n\n###### VisitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitDefinitionTerm(NodeContext ctx, string text)\n```\n\n###### VisitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitDefinitionDescription(NodeContext ctx, string text)\n```\n\n###### VisitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitDefinitionListEnd(NodeContext ctx, string output)\n```\n\n###### VisitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitForm(NodeContext ctx, string action, string method)\n```\n\n###### VisitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitInput(NodeContext ctx, string inputType, string name, string value)\n```\n\n###### VisitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitButton(NodeContext ctx, string text)\n```\n\n###### VisitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitAudio(NodeContext ctx, string src)\n```\n\n###### VisitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitVideo(NodeContext ctx, string src)\n```\n\n###### VisitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitIframe(NodeContext ctx, string src)\n```\n\n###### VisitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitDetails(NodeContext ctx, bool open)\n```\n\n###### VisitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitSummary(NodeContext ctx, string text)\n```\n\n###### VisitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitFigureStart(NodeContext ctx)\n```\n\n###### VisitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitFigcaption(NodeContext ctx, string text)\n```\n\n###### VisitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```csharp\npublic VisitResult VisitFigureEnd(NodeContext ctx, string output)\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Src` | `string` | — | Image source (URL, data URI, or SVG content identifier) |\n| `Alt` | `string?` | `null` | Alternative text from alt attribute (for accessibility) |\n| `Title` | `string?` | `null` | Title attribute (often shown as tooltip) |\n| `Dimensions` | `List<uint>?` | `null` | Image dimensions as (width, height) if available |\n| `ImageType` | `ImageType` | — | Image type classification |\n| `Attributes` | `Dictionary<string, string>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Href` | `string` | — | The href URL value |\n| `Text` | `string` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `Title` | `string?` | `null` | Optional title attribute (often shown as tooltip) |\n| `LinkType` | `LinkType` | — | Link type classification |\n| `Rel` | `List<string>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `Attributes` | `Dictionary<string, string>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### ClassifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```csharp\npublic LinkType ClassifyLink(string href)\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `NodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `TagName` | `string` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `Attributes` | `Dictionary<string, string>` | — | All HTML attributes as key-value pairs |\n| `Depth` | `nuint` | — | Depth in the DOM tree (0 = root) |\n| `IndexInParent` | `nuint` | — | Index among siblings (0-based) |\n| `ParentTag` | `string?` | `null` | Parent element's tag name (None if root) |\n| `IsInline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `Preset` | `PreprocessingPreset` | `PreprocessingPreset.Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `RemoveNavigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `RemoveForms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### CreateDefault()\n\n**Signature:**\n\n```csharp\npublic PreprocessingOptions CreateDefault()\n```\n\n###### ApplyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```csharp\npublic void ApplyUpdate(PreprocessingOptionsUpdate update)\n```\n\n###### FromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```csharp\npublic PreprocessingOptions FromUpdate(PreprocessingOptionsUpdate update)\n```\n\n###### From()\n\n**Signature:**\n\n```csharp\npublic PreprocessingOptions From(PreprocessingOptionsUpdate update)\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Message` | `string` | — | Human-readable warning message. |\n| `Kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `DataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `RawJson` | `string` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `SchemaType` | `string?` | `null` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Grid` | `TableGrid` | — | The structured table grid. |\n| `Markdown` | `string` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Rows` | `uint` | — | Number of rows. |\n| `Cols` | `uint` | — | Number of columns. |\n| `Cells` | `List<GridCell>` | `new List<GridCell>()` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Start` | `uint` | — | Start byte offset (inclusive) into the parent node's text. |\n| `End` | `uint` | — | End byte offset (exclusive) into the parent node's text. |\n| `Kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `Level`: `byte`, `Text`: `string` |\n| `Paragraph` | A paragraph of text. — Fields: `Text`: `string` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `Ordered`: `bool` |\n| `ListItem` | A single list item. — Fields: `Text`: `string` |\n| `Table` | A table with structured cell data. — Fields: `Grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `Description`: `string`, `Src`: `string`, `ImageIndex`: `uint` |\n| `Code` | A code block or inline code. — Fields: `Text`: `string`, `Language`: `string` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `Term`: `string`, `Definition`: `string` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `Format`: `string`, `Content`: `string` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `Entries`: `List<string>` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `Label`: `string`, `HeadingLevel`: `byte`, `HeadingText`: `string` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `Url`: `string`, `Title`: `string` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `string` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `string` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-elixir.md",
    "content": "---\ntitle: \"Elixir API Reference\"\n---\n\n## Elixir API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```elixir\n@spec convert(html, options) :: {:ok, term()} | {:error, term()}\ndef convert(html, options)\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `String.t()` | Yes | The html |\n| `options` | `ConversionOptions | nil` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Returns `{:error, reason}`\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `:atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `:spaces` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `integer()` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `String.t()` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `String.t()` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `boolean()` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `boolean()` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `boolean()` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `boolean()` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `String.t()` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `boolean()` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `boolean()` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `boolean()` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `:double_equal` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `boolean()` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `:normalized` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `boolean()` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `boolean()` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `integer()` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `boolean()` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `String.t()` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `String.t()` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `:spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `:backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `list(String.t())` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `String.t()` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `boolean()` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `list(String.t())` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `list(String.t())` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `boolean()` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `:inline` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `:markdown` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `boolean()` | `false` | Include structured document tree in result. |\n| `extract_images` | `boolean()` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `integer()` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `boolean()` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `boolean()` | `true` | Infer image dimensions from data. |\n| `max_depth` | `integer() | nil` | `nil` | Maximum DOM traversal depth. `nil` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `list(String.t())` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (map)` | `nil` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Functions\n\n###### default()\n\n**Signature:**\n\n```elixir\ndef default()\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```elixir\ndef builder()\n```\n\n###### apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```elixir\ndef apply_update(update)\n```\n\n###### from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```elixir\ndef from_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```elixir\ndef from(update)\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String.t() | nil` | `nil` | Converted text output (markdown, djot, or plain text). `nil` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure | nil` | `nil` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `list(TableData)` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `list(String.t())` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `list(ProcessingWarning)` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Functions\n\n###### strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```elixir\ndef strip_tags(tags)\n```\n\n###### preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```elixir\ndef preserve_tags(tags)\n```\n\n###### keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```elixir\ndef keep_inline_images_in(tags)\n```\n\n###### exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```elixir\ndef exclude_selectors(selectors)\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```elixir\ndef visitor(visitor)\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```elixir\ndef preprocessing(preprocessing)\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```elixir\ndef build()\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `String.t() | nil` | `nil` | Document title from `<title>` tag |\n| `description` | `String.t() | nil` | `nil` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `list(String.t())` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `String.t() | nil` | `nil` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `String.t() | nil` | `nil` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `String.t() | nil` | `nil` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `String.t() | nil` | `nil` | Document language from `lang` attribute |\n| `text_direction` | `TextDirection | nil` | `nil` | Document text direction from `dir` attribute |\n| `open_graph` | `map()` | `%{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `map()` | `%{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `map()` | `%{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `String.t()` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `integer() | nil` | `nil` | Index of the parent node (None for root nodes). |\n| `children` | `list(integer())` | — | Indices of child nodes in reading order. |\n| `annotations` | `list(TextAnnotation)` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `map() | nil` | `nil` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `list(DocumentNode)` | — | All nodes in document reading order. |\n| `source_format` | `String.t() | nil` | `nil` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String.t()` | — | The text content of the cell. |\n| `row` | `integer()` | — | 0-indexed row position. |\n| `col` | `integer()` | — | 0-indexed column position. |\n| `row_span` | `integer()` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `integer()` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `boolean()` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `integer()` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `String.t()` | — | Normalized text content of the header |\n| `id` | `String.t() | nil` | `nil` | HTML id attribute if present |\n| `depth` | `integer()` | — | Document tree depth at the header element |\n| `html_offset` | `integer()` | — | Byte offset in original HTML document |\n\n##### Functions\n\n###### is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```elixir\ndef is_valid()\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `list(HeaderMetadata)` | `[]` | Extracted header elements with hierarchy |\n| `links` | `list(LinkMetadata)` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `list(ImageMetadata)` | `[]` | Extracted images with source and dimensions |\n| `structured_data` | `list(StructuredData)` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Functions\n\n#### visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```elixir\ndef visit_element_start(ctx)\n```\n\n##### visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```elixir\ndef visit_element_end(ctx, output)\n```\n\n###### visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```elixir\ndef visit_text(ctx, text)\n```\n\n###### visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```elixir\ndef visit_link(ctx, href, text, title)\n```\n\n###### visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```elixir\ndef visit_image(ctx, src, alt, title)\n```\n\n###### visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```elixir\ndef visit_heading(ctx, level, text, id)\n```\n\n###### visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```elixir\ndef visit_code_block(ctx, lang, code)\n```\n\n###### visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```elixir\ndef visit_code_inline(ctx, code)\n```\n\n###### visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```elixir\ndef visit_list_item(ctx, ordered, marker, text)\n```\n\n###### visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```elixir\ndef visit_list_start(ctx, ordered)\n```\n\n###### visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```elixir\ndef visit_list_end(ctx, ordered, output)\n```\n\n###### visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```elixir\ndef visit_table_start(ctx)\n```\n\n###### visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```elixir\ndef visit_table_row(ctx, cells, is_header)\n```\n\n###### visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```elixir\ndef visit_table_end(ctx, output)\n```\n\n###### visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```elixir\ndef visit_blockquote(ctx, content, depth)\n```\n\n###### visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```elixir\ndef visit_strong(ctx, text)\n```\n\n###### visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```elixir\ndef visit_emphasis(ctx, text)\n```\n\n###### visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```elixir\ndef visit_strikethrough(ctx, text)\n```\n\n###### visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```elixir\ndef visit_underline(ctx, text)\n```\n\n###### visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```elixir\ndef visit_subscript(ctx, text)\n```\n\n###### visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```elixir\ndef visit_superscript(ctx, text)\n```\n\n###### visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```elixir\ndef visit_mark(ctx, text)\n```\n\n###### visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```elixir\ndef visit_line_break(ctx)\n```\n\n###### visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```elixir\ndef visit_horizontal_rule(ctx)\n```\n\n###### visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```elixir\ndef visit_custom_element(ctx, tag_name, html)\n```\n\n###### visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```elixir\ndef visit_definition_list_start(ctx)\n```\n\n###### visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```elixir\ndef visit_definition_term(ctx, text)\n```\n\n###### visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```elixir\ndef visit_definition_description(ctx, text)\n```\n\n###### visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```elixir\ndef visit_definition_list_end(ctx, output)\n```\n\n###### visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```elixir\ndef visit_form(ctx, action, method)\n```\n\n###### visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```elixir\ndef visit_input(ctx, input_type, name, value)\n```\n\n###### visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```elixir\ndef visit_button(ctx, text)\n```\n\n###### visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```elixir\ndef visit_audio(ctx, src)\n```\n\n###### visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```elixir\ndef visit_video(ctx, src)\n```\n\n###### visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```elixir\ndef visit_iframe(ctx, src)\n```\n\n###### visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```elixir\ndef visit_details(ctx, open)\n```\n\n###### visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```elixir\ndef visit_summary(ctx, text)\n```\n\n###### visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```elixir\ndef visit_figure_start(ctx)\n```\n\n###### visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```elixir\ndef visit_figcaption(ctx, text)\n```\n\n###### visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```elixir\ndef visit_figure_end(ctx, output)\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `String.t()` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `String.t() | nil` | `nil` | Alternative text from alt attribute (for accessibility) |\n| `title` | `String.t() | nil` | `nil` | Title attribute (often shown as tooltip) |\n| `dimensions` | `list(integer()) | nil` | `nil` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `map()` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `String.t()` | — | The href URL value |\n| `text` | `String.t()` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `String.t() | nil` | `nil` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `list(String.t())` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `map()` | — | Additional HTML attributes |\n\n###### Functions\n\n###### classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```elixir\ndef classify_link(href)\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `String.t()` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `map()` | — | All HTML attributes as key-value pairs |\n| `depth` | `integer()` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `integer()` | — | Index among siblings (0-based) |\n| `parent_tag` | `String.t() | nil` | `nil` | Parent element's tag name (None if root) |\n| `is_inline` | `boolean()` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `boolean()` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `:standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `boolean()` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `boolean()` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Functions\n\n###### default()\n\n**Signature:**\n\n```elixir\ndef default()\n```\n\n###### apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```elixir\ndef apply_update(update)\n```\n\n###### from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```elixir\ndef from_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```elixir\ndef from(update)\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `String.t()` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `String.t()` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `String.t() | nil` | `nil` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `String.t()` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `integer()` | — | Number of rows. |\n| `cols` | `integer()` | — | Number of columns. |\n| `cells` | `list(GridCell)` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `integer()` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `integer()` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `left_to_right` | Left-to-right text flow (default for Latin scripts) |\n| `right_to_left` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `anchor` | Anchor link within same document (href starts with #) |\n| `internal` | Internal link within same domain |\n| `external` | External link to different domain |\n| `email` | Email link (mailto:) |\n| `phone` | Phone link (tel:) |\n| `other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `data_uri` | Data URI embedded image (base64 or other encoding) |\n| `inline_svg` | Inline SVG element |\n| `external` | External image URL (http/https) |\n| `relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `json_ld` | JSON-LD (JSON for Linking Data) script blocks |\n| `microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `rdfa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `underlined` | Underlined style (=== for h1, --- for h2). |\n| `atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `atx_closed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `double_equal` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `html` | Preserve as HTML (==text==). Original HTML tag. |\n| `bold` | Render as bold (**text**). Uses strong emphasis. |\n| `none` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `inline` | Inline links: `[text](url)`. Default. |\n| `reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `djot` | Djot lightweight markup language. |\n| `plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `heading` | A heading element (h1-h6). — Fields: `level`: `integer()`, `text`: `String.t()` |\n| `paragraph` | A paragraph of text. — Fields: `text`: `String.t()` |\n| `list` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `boolean()` |\n| `list_item` | A single list item. — Fields: `text`: `String.t()` |\n| `table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `image` | An image element. — Fields: `description`: `String.t()`, `src`: `String.t()`, `image_index`: `integer()` |\n| `code` | A code block or inline code. — Fields: `text`: `String.t()`, `language`: `String.t()` |\n| `quote` | A block quote container. |\n| `definition_list` | A definition list container. |\n| `definition_item` | A definition list entry with term and description. — Fields: `term`: `String.t()`, `definition`: `String.t()` |\n| `raw_block` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `String.t()`, `content`: `String.t()` |\n| `metadata_block` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `list(String.t())` |\n| `group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `String.t()`, `heading_level`: `integer()`, `heading_text`: `String.t()` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `bold` | Bold / strong emphasis. |\n| `italic` | Italic / emphasis. |\n| `underline` | Underline. |\n| `strikethrough` | Strikethrough / deleted text. |\n| `code` | Inline code. |\n| `subscript` | Subscript text. |\n| `superscript` | Superscript text. |\n| `highlight` | Highlighted / marked text. |\n| `link` | A hyperlink. — Fields: `url`: `String.t()`, `title`: `String.t()` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `image_extraction_failed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `encoding_fallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `truncated_input` | The input was truncated due to size limits. |\n| `malformed_html` | The HTML was malformed but processing continued with best effort. |\n| `sanitization_applied` | Sanitization was applied to remove potentially unsafe content. |\n| `depth_limit_exceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `text` | Text node (most frequent - 100+ per document) |\n| `element` | Generic element node |\n| `heading` | Heading elements (h1-h6) |\n| `paragraph` | Paragraph element |\n| `div` | Generic div container |\n| `blockquote` | Blockquote element |\n| `pre` | Preformatted text block |\n| `hr` | Horizontal rule |\n| `list` | Ordered or unordered list (ul, ol) |\n| `list_item` | List item (li) |\n| `definition_list` | Definition list (dl) |\n| `definition_term` | Definition term (dt) |\n| `definition_description` | Definition description (dd) |\n| `table` | Table element |\n| `table_row` | Table row (tr) |\n| `table_cell` | Table cell (td, th) |\n| `table_header` | Table header cell (th) |\n| `table_body` | Table body (tbody) |\n| `table_head` | Table head (thead) |\n| `table_foot` | Table foot (tfoot) |\n| `link` | Anchor link (a) |\n| `image` | Image (img) |\n| `strong` | Strong/bold (strong, b) |\n| `em` | Emphasis/italic (em, i) |\n| `code` | Inline code (code) |\n| `strikethrough` | Strikethrough (s, del, strike) |\n| `underline` | Underline (u, ins) |\n| `subscript` | Subscript (sub) |\n| `superscript` | Superscript (sup) |\n| `mark` | Mark/highlight (mark) |\n| `small` | Small text (small) |\n| `br` | Line break (br) |\n| `span` | Span element |\n| `article` | Article element |\n| `section` | Section element |\n| `nav` | Navigation element |\n| `aside` | Aside element |\n| `header` | Header element |\n| `footer` | Footer element |\n| `main` | Main element |\n| `figure` | Figure element |\n| `figcaption` | Figure caption |\n| `time` | Time element |\n| `details` | Details element |\n| `summary` | Summary element |\n| `form` | Form element |\n| `input` | Input element |\n| `select` | Select element |\n| `option` | Option element |\n| `button` | Button element |\n| `textarea` | Textarea element |\n| `label` | Label element |\n| `fieldset` | Fieldset element |\n| `legend` | Legend element |\n| `audio` | Audio element |\n| `video` | Video element |\n| `picture` | Picture element |\n| `source` | Source element |\n| `iframe` | Iframe element |\n| `svg` | SVG element |\n| `canvas` | Canvas element |\n| `ruby` | Ruby annotation |\n| `rt` | Ruby text |\n| `rp` | Ruby parenthesis |\n| `abbr` | Abbreviation |\n| `kbd` | Keyboard input |\n| `samp` | Sample output |\n| `var` | Variable |\n| `cite` | Citation |\n| `q` | Quote |\n| `del` | Deleted text |\n| `ins` | Inserted text |\n| `data` | Data element |\n| `meter` | Meter element |\n| `progress` | Progress element |\n| `output` | Output element |\n| `template` | Template element |\n| `slot` | Slot element |\n| `html` | HTML root element |\n| `head` | Head element |\n| `body` | Body element |\n| `title` | Title element |\n| `meta` | Meta element |\n| `link_tag` | Link element (not anchor) |\n| `style` | Style element |\n| `script` | Script element |\n| `base` | Base element |\n| `custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `continue` | Continue with default conversion behavior |\n| `custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `String.t()` |\n| `skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `preserve_html` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `String.t()` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `parse_error` | HTML parsing error |\n| `sanitization_error` | HTML sanitization error |\n| `config_error` | Invalid configuration |\n| `io_error` | I/O error |\n| `panic` | Internal error caught during conversion |\n| `invalid_input` | Invalid input data |\n| `other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-go.md",
    "content": "---\ntitle: \"Go API Reference\"\n---\n\n## Go API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### Convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```go\nfunc Convert(html string, options ConversionOptions) (ConversionResult, error)\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `Html` | `string` | Yes | The html |\n| `Options` | `*ConversionOptions` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Returns `error`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `HeadingStyle` | `HeadingStyle` | `HeadingStyle.Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `ListIndentType` | `ListIndentType` | `ListIndentType.Spaces` | How to indent nested list items (spaces or tab). |\n| `ListIndentWidth` | `int` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `Bullets` | `string` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `StrongEmSymbol` | `string` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `EscapeAsterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `EscapeUnderscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `EscapeMisc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `EscapeAscii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `CodeLanguage` | `string` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `Autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `DefaultTitle` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `BrInTables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `HighlightStyle` | `HighlightStyle` | `HighlightStyle.DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `ExtractMetadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `WhitespaceMode` | `WhitespaceMode` | `WhitespaceMode.Normalized` | Controls how whitespace is normalised during conversion. |\n| `StripNewlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `Wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `WrapWidth` | `int` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `ConvertAsInline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `SubSymbol` | `string` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `SupSymbol` | `string` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `NewlineStyle` | `NewlineStyle` | `NewlineStyle.Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `CodeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle.Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `KeepInlineImagesIn` | `[]string` | `nil` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `Preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `Encoding` | `string` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `Debug` | `bool` | `false` | Emit debug information during conversion. |\n| `StripTags` | `[]string` | `nil` | HTML tag names whose content is stripped from the output entirely. |\n| `PreserveTags` | `[]string` | `nil` | HTML tag names that are preserved verbatim in the output. |\n| `SkipImages` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `LinkStyle` | `LinkStyle` | `LinkStyle.Inline` | Link rendering style (inline or reference). |\n| `OutputFormat` | `OutputFormat` | `OutputFormat.Markdown` | Target output format (Markdown, plain text, etc.). |\n| `IncludeDocumentStructure` | `bool` | `false` | Include structured document tree in result. |\n| `ExtractImages` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `MaxImageSize` | `uint64` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `CaptureSvg` | `bool` | `false` | Capture SVG elements as images. |\n| `InferDimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `MaxDepth` | `*int` | `nil` | Maximum DOM traversal depth. `nil` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `ExcludeSelectors` | `[]string` | `nil` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `Visitor` | `HtmlVisitor (interface)` | `nil` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### Default()\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptions) Default() ConversionOptions\n```\n\n###### Builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptions) Builder() ConversionOptionsBuilder\n```\n\n###### ApplyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptions) ApplyUpdate(update ConversionOptionsUpdate)\n```\n\n###### FromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptions) FromUpdate(update ConversionOptionsUpdate) ConversionOptions\n```\n\n###### From()\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptions) From(update ConversionOptionsUpdate) ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Content` | `*string` | `nil` | Converted text output (markdown, djot, or plain text). `nil` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `Document` | `*DocumentStructure` | `nil` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `Metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `Tables` | `[]TableData` | `nil` | Extracted tables with structured cell data and markdown representation. |\n| `Images` | `[]string` | `nil` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `Warnings` | `[]ProcessingWarning` | `nil` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### StripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) StripTags(tags []string) ConversionOptionsBuilder\n```\n\n###### PreserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) PreserveTags(tags []string) ConversionOptionsBuilder\n```\n\n###### KeepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) KeepInlineImagesIn(tags []string) ConversionOptionsBuilder\n```\n\n###### ExcludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) ExcludeSelectors(selectors []string) ConversionOptionsBuilder\n```\n\n###### Visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) Visitor(visitor VisitorHandle) ConversionOptionsBuilder\n```\n\n###### Preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) Preprocessing(preprocessing PreprocessingOptions) ConversionOptionsBuilder\n```\n\n###### Build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```go\nfunc (o *ConversionOptionsBuilder) Build() ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Title` | `*string` | `nil` | Document title from `<title>` tag |\n| `Description` | `*string` | `nil` | Document description from `<meta name=\"description\">` tag |\n| `Keywords` | `[]string` | `nil` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `Author` | `*string` | `nil` | Document author from `<meta name=\"author\">` tag |\n| `CanonicalUrl` | `*string` | `nil` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `BaseHref` | `*string` | `nil` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `Language` | `*string` | `nil` | Document language from `lang` attribute |\n| `TextDirection` | `*TextDirection` | `nil` | Document text direction from `dir` attribute |\n| `OpenGraph` | `map[string]string` | `nil` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `TwitterCard` | `map[string]string` | `nil` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `MetaTags` | `map[string]string` | `nil` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Id` | `string` | — | Deterministic node identifier. |\n| `Content` | `NodeContent` | — | The semantic content of this node. |\n| `Parent` | `*uint32` | `nil` | Index of the parent node (None for root nodes). |\n| `Children` | `[]uint32` | — | Indices of child nodes in reading order. |\n| `Annotations` | `[]TextAnnotation` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `Attributes` | `*map[string]string` | `nil` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Nodes` | `[]DocumentNode` | — | All nodes in document reading order. |\n| `SourceFormat` | `*string` | `nil` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Content` | `string` | — | The text content of the cell. |\n| `Row` | `uint32` | — | 0-indexed row position. |\n| `Col` | `uint32` | — | 0-indexed column position. |\n| `RowSpan` | `uint32` | — | Number of rows this cell spans (default 1). |\n| `ColSpan` | `uint32` | — | Number of columns this cell spans (default 1). |\n| `IsHeader` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Level` | `uint8` | — | Header level: 1 (h1) through 6 (h6) |\n| `Text` | `string` | — | Normalized text content of the header |\n| `Id` | `*string` | `nil` | HTML id attribute if present |\n| `Depth` | `int` | — | Document tree depth at the header element |\n| `HtmlOffset` | `int` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### IsValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```go\nfunc (o *HeaderMetadata) IsValid() bool\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `Headers` | `[]HeaderMetadata` | `nil` | Extracted header elements with hierarchy |\n| `Links` | `[]LinkMetadata` | `nil` | Extracted hyperlinks with type classification |\n| `Images` | `[]ImageMetadata` | `nil` | Extracted images with source and dimensions |\n| `StructuredData` | `[]StructuredData` | `nil` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### VisitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitElementStart(ctx NodeContext) VisitResult\n```\n\n##### VisitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitElementEnd(ctx NodeContext, output string) VisitResult\n```\n\n###### VisitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitText(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitLink(ctx NodeContext, href string, text string, title string) VisitResult\n```\n\n###### VisitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitImage(ctx NodeContext, src string, alt string, title string) VisitResult\n```\n\n###### VisitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitHeading(ctx NodeContext, level uint32, text string, id string) VisitResult\n```\n\n###### VisitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitCodeBlock(ctx NodeContext, lang string, code string) VisitResult\n```\n\n###### VisitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitCodeInline(ctx NodeContext, code string) VisitResult\n```\n\n###### VisitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitListItem(ctx NodeContext, ordered bool, marker string, text string) VisitResult\n```\n\n###### VisitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitListStart(ctx NodeContext, ordered bool) VisitResult\n```\n\n###### VisitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitListEnd(ctx NodeContext, ordered bool, output string) VisitResult\n```\n\n###### VisitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitTableStart(ctx NodeContext) VisitResult\n```\n\n###### VisitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitTableRow(ctx NodeContext, cells []string, isHeader bool) VisitResult\n```\n\n###### VisitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitTableEnd(ctx NodeContext, output string) VisitResult\n```\n\n###### VisitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitBlockquote(ctx NodeContext, content string, depth int) VisitResult\n```\n\n###### VisitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitStrong(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitEmphasis(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitStrikethrough(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitUnderline(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitSubscript(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitSuperscript(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitMark(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitLineBreak(ctx NodeContext) VisitResult\n```\n\n###### VisitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitHorizontalRule(ctx NodeContext) VisitResult\n```\n\n###### VisitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitCustomElement(ctx NodeContext, tagName string, html string) VisitResult\n```\n\n###### VisitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitDefinitionListStart(ctx NodeContext) VisitResult\n```\n\n###### VisitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitDefinitionTerm(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitDefinitionDescription(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitDefinitionListEnd(ctx NodeContext, output string) VisitResult\n```\n\n###### VisitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitForm(ctx NodeContext, action string, method string) VisitResult\n```\n\n###### VisitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitInput(ctx NodeContext, inputType string, name string, value string) VisitResult\n```\n\n###### VisitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitButton(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitAudio(ctx NodeContext, src string) VisitResult\n```\n\n###### VisitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitVideo(ctx NodeContext, src string) VisitResult\n```\n\n###### VisitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitIframe(ctx NodeContext, src string) VisitResult\n```\n\n###### VisitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitDetails(ctx NodeContext, open bool) VisitResult\n```\n\n###### VisitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitSummary(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitFigureStart(ctx NodeContext) VisitResult\n```\n\n###### VisitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitFigcaption(ctx NodeContext, text string) VisitResult\n```\n\n###### VisitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```go\nfunc (o *HtmlVisitor) VisitFigureEnd(ctx NodeContext, output string) VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Src` | `string` | — | Image source (URL, data URI, or SVG content identifier) |\n| `Alt` | `*string` | `nil` | Alternative text from alt attribute (for accessibility) |\n| `Title` | `*string` | `nil` | Title attribute (often shown as tooltip) |\n| `Dimensions` | `*[]uint32` | `nil` | Image dimensions as (width, height) if available |\n| `ImageType` | `ImageType` | — | Image type classification |\n| `Attributes` | `map[string]string` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Href` | `string` | — | The href URL value |\n| `Text` | `string` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `Title` | `*string` | `nil` | Optional title attribute (often shown as tooltip) |\n| `LinkType` | `LinkType` | — | Link type classification |\n| `Rel` | `[]string` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `Attributes` | `map[string]string` | — | Additional HTML attributes |\n\n###### Methods\n\n###### ClassifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```go\nfunc (o *LinkMetadata) ClassifyLink(href string) LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `NodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `TagName` | `string` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `Attributes` | `map[string]string` | — | All HTML attributes as key-value pairs |\n| `Depth` | `int` | — | Depth in the DOM tree (0 = root) |\n| `IndexInParent` | `int` | — | Index among siblings (0-based) |\n| `ParentTag` | `*string` | `nil` | Parent element's tag name (None if root) |\n| `IsInline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `Preset` | `PreprocessingPreset` | `PreprocessingPreset.Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `RemoveNavigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `RemoveForms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### Default()\n\n**Signature:**\n\n```go\nfunc (o *PreprocessingOptions) Default() PreprocessingOptions\n```\n\n###### ApplyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```go\nfunc (o *PreprocessingOptions) ApplyUpdate(update PreprocessingOptionsUpdate)\n```\n\n###### FromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```go\nfunc (o *PreprocessingOptions) FromUpdate(update PreprocessingOptionsUpdate) PreprocessingOptions\n```\n\n###### From()\n\n**Signature:**\n\n```go\nfunc (o *PreprocessingOptions) From(update PreprocessingOptionsUpdate) PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Message` | `string` | — | Human-readable warning message. |\n| `Kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `DataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `RawJson` | `string` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `SchemaType` | `*string` | `nil` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Grid` | `TableGrid` | — | The structured table grid. |\n| `Markdown` | `string` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Rows` | `uint32` | — | Number of rows. |\n| `Cols` | `uint32` | — | Number of columns. |\n| `Cells` | `[]GridCell` | `nil` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `Start` | `uint32` | — | Start byte offset (inclusive) into the parent node's text. |\n| `End` | `uint32` | — | End byte offset (exclusive) into the parent node's text. |\n| `Kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `Level`: `uint8`, `Text`: `string` |\n| `Paragraph` | A paragraph of text. — Fields: `Text`: `string` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `Ordered`: `bool` |\n| `ListItem` | A single list item. — Fields: `Text`: `string` |\n| `Table` | A table with structured cell data. — Fields: `Grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `Description`: `string`, `Src`: `string`, `ImageIndex`: `uint32` |\n| `Code` | A code block or inline code. — Fields: `Text`: `string`, `Language`: `string` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `Term`: `string`, `Definition`: `string` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `Format`: `string`, `Content`: `string` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `Entries`: `[]string` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `Label`: `string`, `HeadingLevel`: `uint8`, `HeadingText`: `string` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `Url`: `string`, `Title`: `string` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `string` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `string` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-java.md",
    "content": "---\ntitle: \"Java API Reference\"\n---\n\n## Java API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```java\npublic static ConversionResult convert(String html, ConversionOptions options) throws Error\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `String` | Yes | The html |\n| `options` | `Optional<ConversionOptions>` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Throws `ErrorException`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `headingStyle` | `HeadingStyle` | `HeadingStyle.ATX` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `listIndentType` | `ListIndentType` | `ListIndentType.SPACES` | How to indent nested list items (spaces or tab). |\n| `listIndentWidth` | `long` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `String` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strongEmSymbol` | `String` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escapeAsterisks` | `boolean` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escapeUnderscores` | `boolean` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escapeMisc` | `boolean` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escapeAscii` | `boolean` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `codeLanguage` | `String` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `boolean` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `defaultTitle` | `boolean` | `false` | Emit a default title when no `<title>` tag is present. |\n| `brInTables` | `boolean` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlightStyle` | `HighlightStyle` | `HighlightStyle.DOUBLE_EQUAL` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extractMetadata` | `boolean` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespaceMode` | `WhitespaceMode` | `WhitespaceMode.NORMALIZED` | Controls how whitespace is normalised during conversion. |\n| `stripNewlines` | `boolean` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `boolean` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrapWidth` | `long` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convertAsInline` | `boolean` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `subSymbol` | `String` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `supSymbol` | `String` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newlineStyle` | `NewlineStyle` | `NewlineStyle.SPACES` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `codeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle.BACKTICKS` | Style used for fenced code blocks (backticks or tilde). |\n| `keepInlineImagesIn` | `List<String>` | `Collections.emptyList()` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `String` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `boolean` | `false` | Emit debug information during conversion. |\n| `stripTags` | `List<String>` | `Collections.emptyList()` | HTML tag names whose content is stripped from the output entirely. |\n| `preserveTags` | `List<String>` | `Collections.emptyList()` | HTML tag names that are preserved verbatim in the output. |\n| `skipImages` | `boolean` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `linkStyle` | `LinkStyle` | `LinkStyle.INLINE` | Link rendering style (inline or reference). |\n| `outputFormat` | `OutputFormat` | `OutputFormat.MARKDOWN` | Target output format (Markdown, plain text, etc.). |\n| `includeDocumentStructure` | `boolean` | `false` | Include structured document tree in result. |\n| `extractImages` | `boolean` | `false` | Extract inline images from data URIs and SVGs. |\n| `maxImageSize` | `long` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `captureSvg` | `boolean` | `false` | Capture SVG elements as images. |\n| `inferDimensions` | `boolean` | `true` | Infer image dimensions from data. |\n| `maxDepth` | `Optional<Long>` | `null` | Maximum DOM traversal depth. `null` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `excludeSelectors` | `List<String>` | `Collections.emptyList()` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (interface)` | `null` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### defaultOptions()\n\n**Signature:**\n\n```java\npublic static ConversionOptions defaultOptions()\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```java\npublic static ConversionOptionsBuilder builder()\n```\n\n###### applyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```java\npublic void applyUpdate(ConversionOptionsUpdate update)\n```\n\n###### fromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```java\npublic static ConversionOptions fromUpdate(ConversionOptionsUpdate update)\n```\n\n###### from()\n\n**Signature:**\n\n```java\npublic static ConversionOptions from(ConversionOptionsUpdate update)\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `Optional<String>` | `null` | Converted text output (markdown, djot, or plain text). `null` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `Optional<DocumentStructure>` | `null` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `List<TableData>` | `Collections.emptyList()` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `List<String>` | `Collections.emptyList()` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `List<ProcessingWarning>` | `Collections.emptyList()` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### stripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder stripTags(List<String> tags)\n```\n\n###### preserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder preserveTags(List<String> tags)\n```\n\n###### keepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder keepInlineImagesIn(List<String> tags)\n```\n\n###### excludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder excludeSelectors(List<String> selectors)\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder visitor(VisitorHandle visitor)\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```java\npublic ConversionOptionsBuilder preprocessing(PreprocessingOptions preprocessing)\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```java\npublic ConversionOptions build()\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `Optional<String>` | `null` | Document title from `<title>` tag |\n| `description` | `Optional<String>` | `null` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `List<String>` | `Collections.emptyList()` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `Optional<String>` | `null` | Document author from `<meta name=\"author\">` tag |\n| `canonicalUrl` | `Optional<String>` | `null` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `baseHref` | `Optional<String>` | `null` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `Optional<String>` | `null` | Document language from `lang` attribute |\n| `textDirection` | `Optional<TextDirection>` | `null` | Document text direction from `dir` attribute |\n| `openGraph` | `Map<String, String>` | `Collections.emptyMap()` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitterCard` | `Map<String, String>` | `Collections.emptyMap()` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `metaTags` | `Map<String, String>` | `Collections.emptyMap()` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `String` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `Optional<Integer>` | `null` | Index of the parent node (None for root nodes). |\n| `children` | `List<Integer>` | — | Indices of child nodes in reading order. |\n| `annotations` | `List<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `Optional<Map<String, String>>` | `null` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `List<DocumentNode>` | — | All nodes in document reading order. |\n| `sourceFormat` | `Optional<String>` | `null` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String` | — | The text content of the cell. |\n| `row` | `int` | — | 0-indexed row position. |\n| `col` | `int` | — | 0-indexed column position. |\n| `rowSpan` | `int` | — | Number of rows this cell spans (default 1). |\n| `colSpan` | `int` | — | Number of columns this cell spans (default 1). |\n| `isHeader` | `boolean` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `byte` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `String` | — | Normalized text content of the header |\n| `id` | `Optional<String>` | `null` | HTML id attribute if present |\n| `depth` | `long` | — | Document tree depth at the header element |\n| `htmlOffset` | `long` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### isValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```java\npublic boolean isValid()\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `List<HeaderMetadata>` | `Collections.emptyList()` | Extracted header elements with hierarchy |\n| `links` | `List<LinkMetadata>` | `Collections.emptyList()` | Extracted hyperlinks with type classification |\n| `images` | `List<ImageMetadata>` | `Collections.emptyList()` | Extracted images with source and dimensions |\n| `structuredData` | `List<StructuredData>` | `Collections.emptyList()` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```java\npublic VisitResult visitElementStart(NodeContext ctx)\n```\n\n##### visitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```java\npublic VisitResult visitElementEnd(NodeContext ctx, String output)\n```\n\n###### visitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```java\npublic VisitResult visitText(NodeContext ctx, String text)\n```\n\n###### visitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```java\npublic VisitResult visitLink(NodeContext ctx, String href, String text, String title)\n```\n\n###### visitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```java\npublic VisitResult visitImage(NodeContext ctx, String src, String alt, String title)\n```\n\n###### visitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitHeading(NodeContext ctx, int level, String text, String id)\n```\n\n###### visitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitCodeBlock(NodeContext ctx, String lang, String code)\n```\n\n###### visitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitCodeInline(NodeContext ctx, String code)\n```\n\n###### visitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitListItem(NodeContext ctx, boolean ordered, String marker, String text)\n```\n\n###### visitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitListStart(NodeContext ctx, boolean ordered)\n```\n\n###### visitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitListEnd(NodeContext ctx, boolean ordered, String output)\n```\n\n###### visitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitTableStart(NodeContext ctx)\n```\n\n###### visitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitTableRow(NodeContext ctx, List<String> cells, boolean isHeader)\n```\n\n###### visitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitTableEnd(NodeContext ctx, String output)\n```\n\n###### visitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitBlockquote(NodeContext ctx, String content, long depth)\n```\n\n###### visitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitStrong(NodeContext ctx, String text)\n```\n\n###### visitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitEmphasis(NodeContext ctx, String text)\n```\n\n###### visitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitStrikethrough(NodeContext ctx, String text)\n```\n\n###### visitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitUnderline(NodeContext ctx, String text)\n```\n\n###### visitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitSubscript(NodeContext ctx, String text)\n```\n\n###### visitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitSuperscript(NodeContext ctx, String text)\n```\n\n###### visitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitMark(NodeContext ctx, String text)\n```\n\n###### visitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitLineBreak(NodeContext ctx)\n```\n\n###### visitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitHorizontalRule(NodeContext ctx)\n```\n\n###### visitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```java\npublic VisitResult visitCustomElement(NodeContext ctx, String tagName, String html)\n```\n\n###### visitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitDefinitionListStart(NodeContext ctx)\n```\n\n###### visitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitDefinitionTerm(NodeContext ctx, String text)\n```\n\n###### visitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitDefinitionDescription(NodeContext ctx, String text)\n```\n\n###### visitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitDefinitionListEnd(NodeContext ctx, String output)\n```\n\n###### visitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitForm(NodeContext ctx, String action, String method)\n```\n\n###### visitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitInput(NodeContext ctx, String inputType, String name, String value)\n```\n\n###### visitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitButton(NodeContext ctx, String text)\n```\n\n###### visitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitAudio(NodeContext ctx, String src)\n```\n\n###### visitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitVideo(NodeContext ctx, String src)\n```\n\n###### visitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitIframe(NodeContext ctx, String src)\n```\n\n###### visitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitDetails(NodeContext ctx, boolean open)\n```\n\n###### visitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitSummary(NodeContext ctx, String text)\n```\n\n###### visitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitFigureStart(NodeContext ctx)\n```\n\n###### visitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitFigcaption(NodeContext ctx, String text)\n```\n\n###### visitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```java\npublic VisitResult visitFigureEnd(NodeContext ctx, String output)\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `String` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `Optional<String>` | `null` | Alternative text from alt attribute (for accessibility) |\n| `title` | `Optional<String>` | `null` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Optional<List<Integer>>` | `null` | Image dimensions as (width, height) if available |\n| `imageType` | `ImageType` | — | Image type classification |\n| `attributes` | `Map<String, String>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `String` | — | The href URL value |\n| `text` | `String` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `Optional<String>` | `null` | Optional title attribute (often shown as tooltip) |\n| `linkType` | `LinkType` | — | Link type classification |\n| `rel` | `List<String>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `Map<String, String>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```java\npublic static LinkType classifyLink(String href)\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `tagName` | `String` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `Map<String, String>` | — | All HTML attributes as key-value pairs |\n| `depth` | `long` | — | Depth in the DOM tree (0 = root) |\n| `indexInParent` | `long` | — | Index among siblings (0-based) |\n| `parentTag` | `Optional<String>` | `null` | Parent element's tag name (None if root) |\n| `isInline` | `boolean` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `boolean` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset.STANDARD` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `removeNavigation` | `boolean` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `removeForms` | `boolean` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### defaultOptions()\n\n**Signature:**\n\n```java\npublic static PreprocessingOptions defaultOptions()\n```\n\n###### applyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```java\npublic void applyUpdate(PreprocessingOptionsUpdate update)\n```\n\n###### fromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```java\npublic static PreprocessingOptions fromUpdate(PreprocessingOptionsUpdate update)\n```\n\n###### from()\n\n**Signature:**\n\n```java\npublic static PreprocessingOptions from(PreprocessingOptionsUpdate update)\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `String` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `dataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `rawJson` | `String` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schemaType` | `Optional<String>` | `null` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `String` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `int` | — | Number of rows. |\n| `cols` | `int` | — | Number of columns. |\n| `cells` | `List<GridCell>` | `Collections.emptyList()` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `int` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `int` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LEFT_TO_RIGHT` | Left-to-right text flow (default for Latin scripts) |\n| `RIGHT_TO_LEFT` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `AUTO` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `ANCHOR` | Anchor link within same document (href starts with #) |\n| `INTERNAL` | Internal link within same domain |\n| `EXTERNAL` | External link to different domain |\n| `EMAIL` | Email link (mailto:) |\n| `PHONE` | Phone link (tel:) |\n| `OTHER` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DATA_URI` | Data URI embedded image (base64 or other encoding) |\n| `INLINE_SVG` | Inline SVG element |\n| `EXTERNAL` | External image URL (http/https) |\n| `RELATIVE` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JSON_LD` | JSON-LD (JSON for Linking Data) script blocks |\n| `MICRODATA` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFA` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `MINIMAL` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `STANDARD` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `AGGRESSIVE` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `UNDERLINED` | Underlined style (=== for h1, --- for h2). |\n| `ATX` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `ATX_CLOSED` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `SPACES` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `TABS` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `NORMALIZED` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `STRICT` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `SPACES` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `BACKSLASH` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `INDENTED` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `BACKTICKS` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `TILDES` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DOUBLE_EQUAL` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `HTML` | Preserve as HTML (==text==). Original HTML tag. |\n| `BOLD` | Render as bold (**text**). Uses strong emphasis. |\n| `NONE` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `INLINE` | Inline links: `[text](url)`. Default. |\n| `REFERENCE` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `MARKDOWN` | Standard Markdown (CommonMark compatible). Default. |\n| `DJOT` | Djot lightweight markup language. |\n| `PLAIN` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `HEADING` | A heading element (h1-h6). — Fields: `level`: `byte`, `text`: `String` |\n| `PARAGRAPH` | A paragraph of text. — Fields: `text`: `String` |\n| `LIST` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `boolean` |\n| `LIST_ITEM` | A single list item. — Fields: `text`: `String` |\n| `TABLE` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `IMAGE` | An image element. — Fields: `description`: `String`, `src`: `String`, `imageIndex`: `int` |\n| `CODE` | A code block or inline code. — Fields: `text`: `String`, `language`: `String` |\n| `QUOTE` | A block quote container. |\n| `DEFINITION_LIST` | A definition list container. |\n| `DEFINITION_ITEM` | A definition list entry with term and description. — Fields: `term`: `String`, `definition`: `String` |\n| `RAW_BLOCK` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `String`, `content`: `String` |\n| `METADATA_BLOCK` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `List<String>` |\n| `GROUP` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `String`, `headingLevel`: `byte`, `headingText`: `String` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `BOLD` | Bold / strong emphasis. |\n| `ITALIC` | Italic / emphasis. |\n| `UNDERLINE` | Underline. |\n| `STRIKETHROUGH` | Strikethrough / deleted text. |\n| `CODE` | Inline code. |\n| `SUBSCRIPT` | Subscript text. |\n| `SUPERSCRIPT` | Superscript text. |\n| `HIGHLIGHT` | Highlighted / marked text. |\n| `LINK` | A hyperlink. — Fields: `url`: `String`, `title`: `String` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `IMAGE_EXTRACTION_FAILED` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `ENCODING_FALLBACK` | The input encoding was not recognized; fell back to UTF-8. |\n| `TRUNCATED_INPUT` | The input was truncated due to size limits. |\n| `MALFORMED_HTML` | The HTML was malformed but processing continued with best effort. |\n| `SANITIZATION_APPLIED` | Sanitization was applied to remove potentially unsafe content. |\n| `DEPTH_LIMIT_EXCEEDED` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `TEXT` | Text node (most frequent - 100+ per document) |\n| `ELEMENT` | Generic element node |\n| `HEADING` | Heading elements (h1-h6) |\n| `PARAGRAPH` | Paragraph element |\n| `DIV` | Generic div container |\n| `BLOCKQUOTE` | Blockquote element |\n| `PRE` | Preformatted text block |\n| `HR` | Horizontal rule |\n| `LIST` | Ordered or unordered list (ul, ol) |\n| `LIST_ITEM` | List item (li) |\n| `DEFINITION_LIST` | Definition list (dl) |\n| `DEFINITION_TERM` | Definition term (dt) |\n| `DEFINITION_DESCRIPTION` | Definition description (dd) |\n| `TABLE` | Table element |\n| `TABLE_ROW` | Table row (tr) |\n| `TABLE_CELL` | Table cell (td, th) |\n| `TABLE_HEADER` | Table header cell (th) |\n| `TABLE_BODY` | Table body (tbody) |\n| `TABLE_HEAD` | Table head (thead) |\n| `TABLE_FOOT` | Table foot (tfoot) |\n| `LINK` | Anchor link (a) |\n| `IMAGE` | Image (img) |\n| `STRONG` | Strong/bold (strong, b) |\n| `EM` | Emphasis/italic (em, i) |\n| `CODE` | Inline code (code) |\n| `STRIKETHROUGH` | Strikethrough (s, del, strike) |\n| `UNDERLINE` | Underline (u, ins) |\n| `SUBSCRIPT` | Subscript (sub) |\n| `SUPERSCRIPT` | Superscript (sup) |\n| `MARK` | Mark/highlight (mark) |\n| `SMALL` | Small text (small) |\n| `BR` | Line break (br) |\n| `SPAN` | Span element |\n| `ARTICLE` | Article element |\n| `SECTION` | Section element |\n| `NAV` | Navigation element |\n| `ASIDE` | Aside element |\n| `HEADER` | Header element |\n| `FOOTER` | Footer element |\n| `MAIN` | Main element |\n| `FIGURE` | Figure element |\n| `FIGCAPTION` | Figure caption |\n| `TIME` | Time element |\n| `DETAILS` | Details element |\n| `SUMMARY` | Summary element |\n| `FORM` | Form element |\n| `INPUT` | Input element |\n| `SELECT` | Select element |\n| `OPTION` | Option element |\n| `BUTTON` | Button element |\n| `TEXTAREA` | Textarea element |\n| `LABEL` | Label element |\n| `FIELDSET` | Fieldset element |\n| `LEGEND` | Legend element |\n| `AUDIO` | Audio element |\n| `VIDEO` | Video element |\n| `PICTURE` | Picture element |\n| `SOURCE` | Source element |\n| `IFRAME` | Iframe element |\n| `SVG` | SVG element |\n| `CANVAS` | Canvas element |\n| `RUBY` | Ruby annotation |\n| `RT` | Ruby text |\n| `RP` | Ruby parenthesis |\n| `ABBR` | Abbreviation |\n| `KBD` | Keyboard input |\n| `SAMP` | Sample output |\n| `VAR` | Variable |\n| `CITE` | Citation |\n| `Q` | Quote |\n| `DEL` | Deleted text |\n| `INS` | Inserted text |\n| `DATA` | Data element |\n| `METER` | Meter element |\n| `PROGRESS` | Progress element |\n| `OUTPUT` | Output element |\n| `TEMPLATE` | Template element |\n| `SLOT` | Slot element |\n| `HTML` | HTML root element |\n| `HEAD` | Head element |\n| `BODY` | Body element |\n| `TITLE` | Title element |\n| `META` | Meta element |\n| `LINK_TAG` | Link element (not anchor) |\n| `STYLE` | Style element |\n| `SCRIPT` | Script element |\n| `BASE` | Base element |\n| `CUSTOM` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `CONTINUE` | Continue with default conversion behavior |\n| `CUSTOM` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `String` |\n| `SKIP` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PRESERVE_HTML` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `ERROR` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `String` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `PARSE_ERROR` | HTML parsing error |\n| `SANITIZATION_ERROR` | HTML sanitization error |\n| `CONFIG_ERROR` | Invalid configuration |\n| `IO_ERROR` | I/O error |\n| `PANIC` | Internal error caught during conversion |\n| `INVALID_INPUT` | Invalid input data |\n| `OTHER` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-php.md",
    "content": "---\ntitle: \"PHP API Reference\"\n---\n\n## PHP API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate::visitor::HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```php\npublic static function convert(string $html, ?ConversionOptions $options = null): ConversionResult\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `string` | Yes | The html |\n| `options` | `?ConversionOptions` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Throws `Error`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions::builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `headingStyle` | `HeadingStyle` | `HeadingStyle::Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `listIndentType` | `ListIndentType` | `ListIndentType::Spaces` | How to indent nested list items (spaces or tab). |\n| `listIndentWidth` | `int` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `string` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strongEmSymbol` | `string` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escapeAsterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escapeUnderscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escapeMisc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escapeAscii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `codeLanguage` | `string` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `defaultTitle` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `brInTables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlightStyle` | `HighlightStyle` | `HighlightStyle::DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extractMetadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespaceMode` | `WhitespaceMode` | `WhitespaceMode::Normalized` | Controls how whitespace is normalised during conversion. |\n| `stripNewlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrapWidth` | `int` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convertAsInline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `subSymbol` | `string` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `supSymbol` | `string` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newlineStyle` | `NewlineStyle` | `NewlineStyle::Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `codeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle::Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keepInlineImagesIn` | `array<string>` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `string` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `false` | Emit debug information during conversion. |\n| `stripTags` | `array<string>` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserveTags` | `array<string>` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skipImages` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `linkStyle` | `LinkStyle` | `LinkStyle::Inline` | Link rendering style (inline or reference). |\n| `outputFormat` | `OutputFormat` | `OutputFormat::Markdown` | Target output format (Markdown, plain text, etc.). |\n| `includeDocumentStructure` | `bool` | `false` | Include structured document tree in result. |\n| `extractImages` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `maxImageSize` | `int` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `captureSvg` | `bool` | `false` | Capture SVG elements as images. |\n| `inferDimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `maxDepth` | `?int` | `null` | Maximum DOM traversal depth. `null` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `excludeSelectors` | `array<string>` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (object)` | `null` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate::visitor::HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```php\npublic static function default(): ConversionOptions\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```php\npublic static function builder(): ConversionOptionsBuilder\n```\n\n###### applyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```php\npublic function applyUpdate(ConversionOptionsUpdate $update): void\n```\n\n###### fromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```php\npublic static function fromUpdate(ConversionOptionsUpdate $update): ConversionOptions\n```\n\n###### from()\n\n**Signature:**\n\n```php\npublic static function from(ConversionOptionsUpdate $update): ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `?string` | `null` | Converted text output (markdown, djot, or plain text). `null` when `output_format` is set to `OutputFormat::None`, indicating extraction-only mode. |\n| `document` | `?DocumentStructure` | `null` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `array<TableData>` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `array<string>` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `array<ProcessingWarning>` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### stripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```php\npublic function stripTags(array<string> $tags): ConversionOptionsBuilder\n```\n\n###### preserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```php\npublic function preserveTags(array<string> $tags): ConversionOptionsBuilder\n```\n\n###### keepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```php\npublic function keepInlineImagesIn(array<string> $tags): ConversionOptionsBuilder\n```\n\n###### excludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```php\npublic function excludeSelectors(array<string> $selectors): ConversionOptionsBuilder\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```php\npublic function visitor(VisitorHandle $visitor): ConversionOptionsBuilder\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```php\npublic function preprocessing(PreprocessingOptions $preprocessing): ConversionOptionsBuilder\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```php\npublic function build(): ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `?string` | `null` | Document title from `<title>` tag |\n| `description` | `?string` | `null` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `array<string>` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `?string` | `null` | Document author from `<meta name=\"author\">` tag |\n| `canonicalUrl` | `?string` | `null` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `baseHref` | `?string` | `null` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `?string` | `null` | Document language from `lang` attribute |\n| `textDirection` | `?TextDirection` | `null` | Document text direction from `dir` attribute |\n| `openGraph` | `array<string, string>` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitterCard` | `array<string, string>` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `metaTags` | `array<string, string>` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `string` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `?int` | `null` | Index of the parent node (None for root nodes). |\n| `children` | `array<int>` | — | Indices of child nodes in reading order. |\n| `annotations` | `array<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `?array<string, string>` | `null` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `array<DocumentNode>` | — | All nodes in document reading order. |\n| `sourceFormat` | `?string` | `null` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `string` | — | The text content of the cell. |\n| `row` | `int` | — | 0-indexed row position. |\n| `col` | `int` | — | 0-indexed column position. |\n| `rowSpan` | `int` | — | Number of rows this cell spans (default 1). |\n| `colSpan` | `int` | — | Number of columns this cell spans (default 1). |\n| `isHeader` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `int` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `string` | — | Normalized text content of the header |\n| `id` | `?string` | `null` | HTML id attribute if present |\n| `depth` | `int` | — | Document tree depth at the header element |\n| `htmlOffset` | `int` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### isValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```php\npublic function isValid(): bool\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `array<HeaderMetadata>` | `[]` | Extracted header elements with hierarchy |\n| `links` | `array<LinkMetadata>` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `array<ImageMetadata>` | `[]` | Extracted images with source and dimensions |\n| `structuredData` | `array<StructuredData>` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult::Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult::Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```php\npublic function visitElementStart(NodeContext $ctx): VisitResult\n```\n\n##### visitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```php\npublic function visitElementEnd(NodeContext $ctx, string $output): VisitResult\n```\n\n###### visitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```php\npublic function visitText(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```php\npublic function visitLink(NodeContext $ctx, string $href, string $text, string $title): VisitResult\n```\n\n###### visitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```php\npublic function visitImage(NodeContext $ctx, string $src, string $alt, string $title): VisitResult\n```\n\n###### visitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```php\npublic function visitHeading(NodeContext $ctx, int $level, string $text, string $id): VisitResult\n```\n\n###### visitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```php\npublic function visitCodeBlock(NodeContext $ctx, string $lang, string $code): VisitResult\n```\n\n###### visitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```php\npublic function visitCodeInline(NodeContext $ctx, string $code): VisitResult\n```\n\n###### visitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```php\npublic function visitListItem(NodeContext $ctx, bool $ordered, string $marker, string $text): VisitResult\n```\n\n###### visitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```php\npublic function visitListStart(NodeContext $ctx, bool $ordered): VisitResult\n```\n\n###### visitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```php\npublic function visitListEnd(NodeContext $ctx, bool $ordered, string $output): VisitResult\n```\n\n###### visitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```php\npublic function visitTableStart(NodeContext $ctx): VisitResult\n```\n\n###### visitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```php\npublic function visitTableRow(NodeContext $ctx, array<string> $cells, bool $isHeader): VisitResult\n```\n\n###### visitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```php\npublic function visitTableEnd(NodeContext $ctx, string $output): VisitResult\n```\n\n###### visitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```php\npublic function visitBlockquote(NodeContext $ctx, string $content, int $depth): VisitResult\n```\n\n###### visitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```php\npublic function visitStrong(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```php\npublic function visitEmphasis(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```php\npublic function visitStrikethrough(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```php\npublic function visitUnderline(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```php\npublic function visitSubscript(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```php\npublic function visitSuperscript(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```php\npublic function visitMark(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```php\npublic function visitLineBreak(NodeContext $ctx): VisitResult\n```\n\n###### visitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```php\npublic function visitHorizontalRule(NodeContext $ctx): VisitResult\n```\n\n###### visitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```php\npublic function visitCustomElement(NodeContext $ctx, string $tagName, string $html): VisitResult\n```\n\n###### visitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```php\npublic function visitDefinitionListStart(NodeContext $ctx): VisitResult\n```\n\n###### visitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```php\npublic function visitDefinitionTerm(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```php\npublic function visitDefinitionDescription(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```php\npublic function visitDefinitionListEnd(NodeContext $ctx, string $output): VisitResult\n```\n\n###### visitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```php\npublic function visitForm(NodeContext $ctx, string $action, string $method): VisitResult\n```\n\n###### visitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```php\npublic function visitInput(NodeContext $ctx, string $inputType, string $name, string $value): VisitResult\n```\n\n###### visitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```php\npublic function visitButton(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```php\npublic function visitAudio(NodeContext $ctx, string $src): VisitResult\n```\n\n###### visitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```php\npublic function visitVideo(NodeContext $ctx, string $src): VisitResult\n```\n\n###### visitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```php\npublic function visitIframe(NodeContext $ctx, string $src): VisitResult\n```\n\n###### visitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```php\npublic function visitDetails(NodeContext $ctx, bool $open): VisitResult\n```\n\n###### visitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```php\npublic function visitSummary(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```php\npublic function visitFigureStart(NodeContext $ctx): VisitResult\n```\n\n###### visitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```php\npublic function visitFigcaption(NodeContext $ctx, string $text): VisitResult\n```\n\n###### visitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```php\npublic function visitFigureEnd(NodeContext $ctx, string $output): VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `string` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `?string` | `null` | Alternative text from alt attribute (for accessibility) |\n| `title` | `?string` | `null` | Title attribute (often shown as tooltip) |\n| `dimensions` | `?array<int>` | `null` | Image dimensions as (width, height) if available |\n| `imageType` | `ImageType` | — | Image type classification |\n| `attributes` | `array<string, string>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `string` | — | The href URL value |\n| `text` | `string` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `?string` | `null` | Optional title attribute (often shown as tooltip) |\n| `linkType` | `LinkType` | — | Link type classification |\n| `rel` | `array<string>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `array<string, string>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```php\npublic static function classifyLink(string $href): LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `tagName` | `string` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `array<string, string>` | — | All HTML attributes as key-value pairs |\n| `depth` | `int` | — | Depth in the DOM tree (0 = root) |\n| `indexInParent` | `int` | — | Index among siblings (0-based) |\n| `parentTag` | `?string` | `null` | Parent element's tag name (None if root) |\n| `isInline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset::Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `removeNavigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `removeForms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```php\npublic static function default(): PreprocessingOptions\n```\n\n###### applyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```php\npublic function applyUpdate(PreprocessingOptionsUpdate $update): void\n```\n\n###### fromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```php\npublic static function fromUpdate(PreprocessingOptionsUpdate $update): PreprocessingOptions\n```\n\n###### from()\n\n**Signature:**\n\n```php\npublic static function from(PreprocessingOptionsUpdate $update): PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `string` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `dataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `rawJson` | `string` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schemaType` | `?string` | `null` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `string` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `int` | — | Number of rows. |\n| `cols` | `int` | — | Number of columns. |\n| `cells` | `array<GridCell>` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `int` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `int` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `level`: `int`, `text`: `string` |\n| `Paragraph` | A paragraph of text. — Fields: `text`: `string` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `bool` |\n| `ListItem` | A single list item. — Fields: `text`: `string` |\n| `Table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `description`: `string`, `src`: `string`, `imageIndex`: `int` |\n| `Code` | A code block or inline code. — Fields: `text`: `string`, `language`: `string` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `term`: `string`, `definition`: `string` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `string`, `content`: `string` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `array<string>` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `string`, `headingLevel`: `int`, `headingText`: `string` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `url`: `string`, `title`: `string` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `string` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `string` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-python.md",
    "content": "---\ntitle: \"Python API Reference\"\n---\n\n## Python API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```python\ndef convert(html: str, options: ConversionOptions = None) -> ConversionResult\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `str` | Yes | The html |\n| `options` | `ConversionOptions | None` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Raises `Error`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `HeadingStyle.ATX` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `ListIndentType.SPACES` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `int` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `str` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `str` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `bool` | `False` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `bool` | `False` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `bool` | `False` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `bool` | `False` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `str` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `True` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `bool` | `False` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `bool` | `False` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `HighlightStyle.DOUBLE_EQUAL` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `bool` | `True` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `WhitespaceMode.NORMALIZED` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `bool` | `False` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `False` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `int` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `bool` | `False` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `str` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `str` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `NewlineStyle.SPACES` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `CodeBlockStyle.BACKTICKS` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `list[str]` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `str` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `False` | Emit debug information during conversion. |\n| `strip_tags` | `list[str]` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `list[str]` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `bool` | `False` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `LinkStyle.INLINE` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `OutputFormat.MARKDOWN` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `bool` | `False` | Include structured document tree in result. |\n| `extract_images` | `bool` | `False` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `int` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `bool` | `False` | Capture SVG elements as images. |\n| `infer_dimensions` | `bool` | `True` | Infer image dimensions from data. |\n| `max_depth` | `int | None` | `None` | Maximum DOM traversal depth. `None` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `list[str]` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor(Protocol)` | `None` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```python\n@staticmethod\ndef default() -> ConversionOptions\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```python\n@staticmethod\ndef builder() -> ConversionOptionsBuilder\n```\n\n###### apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```python\ndef apply_update(self, update: ConversionOptionsUpdate) -> None\n```\n\n###### from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```python\n@staticmethod\ndef from_update(update: ConversionOptionsUpdate) -> ConversionOptions\n```\n\n###### from()\n\n**Signature:**\n\n```python\n@staticmethod\ndef from(update: ConversionOptionsUpdate) -> ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `str | None` | `None` | Converted text output (markdown, djot, or plain text). `None` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure | None` | `None` | Structured document tree with semantic elements. Populated when `include_document_structure` is `True` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `list[TableData]` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `list[str]` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `True` in options. |\n| `warnings` | `list[ProcessingWarning]` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```python\ndef strip_tags(self, tags: list[str]) -> ConversionOptionsBuilder\n```\n\n###### preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```python\ndef preserve_tags(self, tags: list[str]) -> ConversionOptionsBuilder\n```\n\n###### keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```python\ndef keep_inline_images_in(self, tags: list[str]) -> ConversionOptionsBuilder\n```\n\n###### exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```python\ndef exclude_selectors(self, selectors: list[str]) -> ConversionOptionsBuilder\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```python\ndef visitor(self, visitor: VisitorHandle) -> ConversionOptionsBuilder\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```python\ndef preprocessing(self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```python\ndef build(self) -> ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `str | None` | `None` | Document title from `<title>` tag |\n| `description` | `str | None` | `None` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `list[str]` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `str | None` | `None` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `str | None` | `None` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `str | None` | `None` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `str | None` | `None` | Document language from `lang` attribute |\n| `text_direction` | `TextDirection | None` | `None` | Document text direction from `dir` attribute |\n| `open_graph` | `dict[str, str]` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `dict[str, str]` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `dict[str, str]` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `str` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `int | None` | `None` | Index of the parent node (None for root nodes). |\n| `children` | `list[int]` | — | Indices of child nodes in reading order. |\n| `annotations` | `list[TextAnnotation]` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `dict[str, str] | None` | `None` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `list[DocumentNode]` | — | All nodes in document reading order. |\n| `source_format` | `str | None` | `None` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `str` | — | The text content of the cell. |\n| `row` | `int` | — | 0-indexed row position. |\n| `col` | `int` | — | 0-indexed column position. |\n| `row_span` | `int` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `int` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `int` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `str` | — | Normalized text content of the header |\n| `id` | `str | None` | `None` | HTML id attribute if present |\n| `depth` | `int` | — | Document tree depth at the header element |\n| `html_offset` | `int` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`True` if level is 1-6, `False` otherwise.\n\n**Signature:**\n\n```python\ndef is_valid(self) -> bool\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `list[HeaderMetadata]` | `[]` | Extracted header elements with hierarchy |\n| `links` | `list[LinkMetadata]` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `list[ImageMetadata]` | `[]` | Extracted images with source and dimensions |\n| `structured_data` | `list[StructuredData]` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```python\ndef visit_element_start(self, ctx: NodeContext) -> VisitResult\n```\n\n##### visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```python\ndef visit_element_end(self, ctx: NodeContext, output: str) -> VisitResult\n```\n\n###### visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```python\ndef visit_text(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```python\ndef visit_link(self, ctx: NodeContext, href: str, text: str, title: str) -> VisitResult\n```\n\n###### visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```python\ndef visit_image(self, ctx: NodeContext, src: str, alt: str, title: str) -> VisitResult\n```\n\n###### visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```python\ndef visit_heading(self, ctx: NodeContext, level: int, text: str, id: str) -> VisitResult\n```\n\n###### visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```python\ndef visit_code_block(self, ctx: NodeContext, lang: str, code: str) -> VisitResult\n```\n\n###### visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```python\ndef visit_code_inline(self, ctx: NodeContext, code: str) -> VisitResult\n```\n\n###### visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```python\ndef visit_list_item(self, ctx: NodeContext, ordered: bool, marker: str, text: str) -> VisitResult\n```\n\n###### visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```python\ndef visit_list_start(self, ctx: NodeContext, ordered: bool) -> VisitResult\n```\n\n###### visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```python\ndef visit_list_end(self, ctx: NodeContext, ordered: bool, output: str) -> VisitResult\n```\n\n###### visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```python\ndef visit_table_start(self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```python\ndef visit_table_row(self, ctx: NodeContext, cells: list[str], is_header: bool) -> VisitResult\n```\n\n###### visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```python\ndef visit_table_end(self, ctx: NodeContext, output: str) -> VisitResult\n```\n\n###### visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```python\ndef visit_blockquote(self, ctx: NodeContext, content: str, depth: int) -> VisitResult\n```\n\n###### visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```python\ndef visit_strong(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```python\ndef visit_emphasis(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```python\ndef visit_strikethrough(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```python\ndef visit_underline(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```python\ndef visit_subscript(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```python\ndef visit_superscript(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```python\ndef visit_mark(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```python\ndef visit_line_break(self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```python\ndef visit_horizontal_rule(self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```python\ndef visit_custom_element(self, ctx: NodeContext, tag_name: str, html: str) -> VisitResult\n```\n\n###### visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```python\ndef visit_definition_list_start(self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```python\ndef visit_definition_term(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```python\ndef visit_definition_description(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```python\ndef visit_definition_list_end(self, ctx: NodeContext, output: str) -> VisitResult\n```\n\n###### visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```python\ndef visit_form(self, ctx: NodeContext, action: str, method: str) -> VisitResult\n```\n\n###### visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```python\ndef visit_input(self, ctx: NodeContext, input_type: str, name: str, value: str) -> VisitResult\n```\n\n###### visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```python\ndef visit_button(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```python\ndef visit_audio(self, ctx: NodeContext, src: str) -> VisitResult\n```\n\n###### visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```python\ndef visit_video(self, ctx: NodeContext, src: str) -> VisitResult\n```\n\n###### visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```python\ndef visit_iframe(self, ctx: NodeContext, src: str) -> VisitResult\n```\n\n###### visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```python\ndef visit_details(self, ctx: NodeContext, open: bool) -> VisitResult\n```\n\n###### visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```python\ndef visit_summary(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```python\ndef visit_figure_start(self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```python\ndef visit_figcaption(self, ctx: NodeContext, text: str) -> VisitResult\n```\n\n###### visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```python\ndef visit_figure_end(self, ctx: NodeContext, output: str) -> VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `str` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `str | None` | `None` | Alternative text from alt attribute (for accessibility) |\n| `title` | `str | None` | `None` | Title attribute (often shown as tooltip) |\n| `dimensions` | `list[int] | None` | `None` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `dict[str, str]` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `str` | — | The href URL value |\n| `text` | `str` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `str | None` | `None` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `list[str]` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `dict[str, str]` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```python\n@staticmethod\ndef classify_link(href: str) -> LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `str` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `dict[str, str]` | — | All HTML attributes as key-value pairs |\n| `depth` | `int` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `int` | — | Index among siblings (0-based) |\n| `parent_tag` | `str | None` | `None` | Parent element's tag name (None if root) |\n| `is_inline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `True` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset.STANDARD` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `bool` | `True` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `bool` | `True` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```python\n@staticmethod\ndef default() -> PreprocessingOptions\n```\n\n###### apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```python\ndef apply_update(self, update: PreprocessingOptionsUpdate) -> None\n```\n\n###### from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```python\n@staticmethod\ndef from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions\n```\n\n###### from()\n\n**Signature:**\n\n```python\n@staticmethod\ndef from(update: PreprocessingOptionsUpdate) -> PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `str` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `str` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `str | None` | `None` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `str` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `int` | — | Number of rows. |\n| `cols` | `int` | — | Number of columns. |\n| `cells` | `list[GridCell]` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `int` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `int` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LEFT_TO_RIGHT` | Left-to-right text flow (default for Latin scripts) |\n| `RIGHT_TO_LEFT` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `AUTO` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `ANCHOR` | Anchor link within same document (href starts with #) |\n| `INTERNAL` | Internal link within same domain |\n| `EXTERNAL` | External link to different domain |\n| `EMAIL` | Email link (mailto:) |\n| `PHONE` | Phone link (tel:) |\n| `OTHER` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DATA_URI` | Data URI embedded image (base64 or other encoding) |\n| `INLINE_SVG` | Inline SVG element |\n| `EXTERNAL` | External image URL (http/https) |\n| `RELATIVE` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JSON_LD` | JSON-LD (JSON for Linking Data) script blocks |\n| `MICRODATA` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFA` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `MINIMAL` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `STANDARD` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `AGGRESSIVE` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `UNDERLINED` | Underlined style (=== for h1, --- for h2). |\n| `ATX` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `ATX_CLOSED` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `SPACES` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `TABS` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `NORMALIZED` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `STRICT` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `SPACES` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `BACKSLASH` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `INDENTED` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `BACKTICKS` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `TILDES` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DOUBLE_EQUAL` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `HTML` | Preserve as HTML (==text==). Original HTML tag. |\n| `BOLD` | Render as bold (**text**). Uses strong emphasis. |\n| `NONE` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `INLINE` | Inline links: `[text](url)`. Default. |\n| `REFERENCE` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `MARKDOWN` | Standard Markdown (CommonMark compatible). Default. |\n| `DJOT` | Djot lightweight markup language. |\n| `PLAIN` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `HEADING` | A heading element (h1-h6). — Fields: `level`: `int`, `text`: `str` |\n| `PARAGRAPH` | A paragraph of text. — Fields: `text`: `str` |\n| `LIST` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `bool` |\n| `LIST_ITEM` | A single list item. — Fields: `text`: `str` |\n| `TABLE` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `IMAGE` | An image element. — Fields: `description`: `str`, `src`: `str`, `image_index`: `int` |\n| `CODE` | A code block or inline code. — Fields: `text`: `str`, `language`: `str` |\n| `QUOTE` | A block quote container. |\n| `DEFINITION_LIST` | A definition list container. |\n| `DEFINITION_ITEM` | A definition list entry with term and description. — Fields: `term`: `str`, `definition`: `str` |\n| `RAW_BLOCK` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `str`, `content`: `str` |\n| `METADATA_BLOCK` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `list[str]` |\n| `GROUP` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `str`, `heading_level`: `int`, `heading_text`: `str` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `BOLD` | Bold / strong emphasis. |\n| `ITALIC` | Italic / emphasis. |\n| `UNDERLINE` | Underline. |\n| `STRIKETHROUGH` | Strikethrough / deleted text. |\n| `CODE` | Inline code. |\n| `SUBSCRIPT` | Subscript text. |\n| `SUPERSCRIPT` | Superscript text. |\n| `HIGHLIGHT` | Highlighted / marked text. |\n| `LINK` | A hyperlink. — Fields: `url`: `str`, `title`: `str` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `IMAGE_EXTRACTION_FAILED` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `ENCODING_FALLBACK` | The input encoding was not recognized; fell back to UTF-8. |\n| `TRUNCATED_INPUT` | The input was truncated due to size limits. |\n| `MALFORMED_HTML` | The HTML was malformed but processing continued with best effort. |\n| `SANITIZATION_APPLIED` | Sanitization was applied to remove potentially unsafe content. |\n| `DEPTH_LIMIT_EXCEEDED` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `TEXT` | Text node (most frequent - 100+ per document) |\n| `ELEMENT` | Generic element node |\n| `HEADING` | Heading elements (h1-h6) |\n| `PARAGRAPH` | Paragraph element |\n| `DIV` | Generic div container |\n| `BLOCKQUOTE` | Blockquote element |\n| `PRE` | Preformatted text block |\n| `HR` | Horizontal rule |\n| `LIST` | Ordered or unordered list (ul, ol) |\n| `LIST_ITEM` | List item (li) |\n| `DEFINITION_LIST` | Definition list (dl) |\n| `DEFINITION_TERM` | Definition term (dt) |\n| `DEFINITION_DESCRIPTION` | Definition description (dd) |\n| `TABLE` | Table element |\n| `TABLE_ROW` | Table row (tr) |\n| `TABLE_CELL` | Table cell (td, th) |\n| `TABLE_HEADER` | Table header cell (th) |\n| `TABLE_BODY` | Table body (tbody) |\n| `TABLE_HEAD` | Table head (thead) |\n| `TABLE_FOOT` | Table foot (tfoot) |\n| `LINK` | Anchor link (a) |\n| `IMAGE` | Image (img) |\n| `STRONG` | Strong/bold (strong, b) |\n| `EM` | Emphasis/italic (em, i) |\n| `CODE` | Inline code (code) |\n| `STRIKETHROUGH` | Strikethrough (s, del, strike) |\n| `UNDERLINE` | Underline (u, ins) |\n| `SUBSCRIPT` | Subscript (sub) |\n| `SUPERSCRIPT` | Superscript (sup) |\n| `MARK` | Mark/highlight (mark) |\n| `SMALL` | Small text (small) |\n| `BR` | Line break (br) |\n| `SPAN` | Span element |\n| `ARTICLE` | Article element |\n| `SECTION` | Section element |\n| `NAV` | Navigation element |\n| `ASIDE` | Aside element |\n| `HEADER` | Header element |\n| `FOOTER` | Footer element |\n| `MAIN` | Main element |\n| `FIGURE` | Figure element |\n| `FIGCAPTION` | Figure caption |\n| `TIME` | Time element |\n| `DETAILS` | Details element |\n| `SUMMARY` | Summary element |\n| `FORM` | Form element |\n| `INPUT` | Input element |\n| `SELECT` | Select element |\n| `OPTION` | Option element |\n| `BUTTON` | Button element |\n| `TEXTAREA` | Textarea element |\n| `LABEL` | Label element |\n| `FIELDSET` | Fieldset element |\n| `LEGEND` | Legend element |\n| `AUDIO` | Audio element |\n| `VIDEO` | Video element |\n| `PICTURE` | Picture element |\n| `SOURCE` | Source element |\n| `IFRAME` | Iframe element |\n| `SVG` | SVG element |\n| `CANVAS` | Canvas element |\n| `RUBY` | Ruby annotation |\n| `RT` | Ruby text |\n| `RP` | Ruby parenthesis |\n| `ABBR` | Abbreviation |\n| `KBD` | Keyboard input |\n| `SAMP` | Sample output |\n| `VAR` | Variable |\n| `CITE` | Citation |\n| `Q` | Quote |\n| `DEL` | Deleted text |\n| `INS` | Inserted text |\n| `DATA` | Data element |\n| `METER` | Meter element |\n| `PROGRESS` | Progress element |\n| `OUTPUT` | Output element |\n| `TEMPLATE` | Template element |\n| `SLOT` | Slot element |\n| `HTML` | HTML root element |\n| `HEAD` | Head element |\n| `BODY` | Body element |\n| `TITLE` | Title element |\n| `META` | Meta element |\n| `LINK_TAG` | Link element (not anchor) |\n| `STYLE` | Style element |\n| `SCRIPT` | Script element |\n| `BASE` | Base element |\n| `CUSTOM` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `CONTINUE` | Continue with default conversion behavior |\n| `CUSTOM` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `str` |\n| `SKIP` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PRESERVE_HTML` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `ERROR` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `str` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n**Base class:** `ConversionError(Exception)`\n\n| Exception | Description |\n|-----------|-------------|\n| `ParseError(ConversionError)` | HTML parsing error |\n| `SanitizationError(ConversionError)` | HTML sanitization error |\n| `ConfigError(ConversionError)` | Invalid configuration |\n| `IoError(ConversionError)` | I/O error |\n| `Panic(ConversionError)` | Internal error caught during conversion |\n| `InvalidInput(ConversionError)` | Invalid input data |\n| `Other(ConversionError)` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-r.md",
    "content": "---\ntitle: \"R API Reference\"\n---\n\n## R API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```r\nconvert(html, options = NULL)\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `character` | Yes | The html |\n| `options` | `ConversionOptions or NULL` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Stops with error message.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `\"atx\"` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `\"spaces\"` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `integer` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `character` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `character` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `logical` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `logical` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `logical` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `logical` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `character` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `logical` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `logical` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `logical` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `\"double_equal\"` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `logical` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `\"normalized\"` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `logical` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `logical` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `integer` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `logical` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `character` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `character` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `\"spaces\"` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `\"backticks\"` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `list` | `list()` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `character` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `logical` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `list` | `list()` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `list` | `list()` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `logical` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `\"inline\"` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `\"markdown\"` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `logical` | `false` | Include structured document tree in result. |\n| `extract_images` | `logical` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `integer` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `logical` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `logical` | `true` | Infer image dimensions from data. |\n| `max_depth` | `integer or NULL` | `NULL` | Maximum DOM traversal depth. `NULL` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `list` | `list()` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (function)` | `NULL` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```r\ndefault()\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```r\nbuilder()\n```\n\n###### apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```r\napply_update(update)\n```\n\n###### from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```r\nfrom_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```r\nfrom(update)\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `character or NULL` | `NULL` | Converted text output (markdown, djot, or plain text). `NULL` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure or NULL` | `NULL` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `list` | `list()` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `list` | `list()` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `list` | `list()` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```r\nstrip_tags(tags)\n```\n\n###### preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```r\npreserve_tags(tags)\n```\n\n###### keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```r\nkeep_inline_images_in(tags)\n```\n\n###### exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```r\nexclude_selectors(selectors)\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```r\nvisitor(visitor)\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```r\npreprocessing(preprocessing)\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```r\nbuild()\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `character or NULL` | `NULL` | Document title from `<title>` tag |\n| `description` | `character or NULL` | `NULL` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `list` | `list()` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `character or NULL` | `NULL` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `character or NULL` | `NULL` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `character or NULL` | `NULL` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `character or NULL` | `NULL` | Document language from `lang` attribute |\n| `text_direction` | `TextDirection or NULL` | `NULL` | Document text direction from `dir` attribute |\n| `open_graph` | `list` | `list()` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `list` | `list()` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `list` | `list()` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `character` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `integer or NULL` | `NULL` | Index of the parent node (None for root nodes). |\n| `children` | `list` | — | Indices of child nodes in reading order. |\n| `annotations` | `list` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `list or NULL` | `NULL` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `list` | — | All nodes in document reading order. |\n| `source_format` | `character or NULL` | `NULL` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `character` | — | The text content of the cell. |\n| `row` | `integer` | — | 0-indexed row position. |\n| `col` | `integer` | — | 0-indexed column position. |\n| `row_span` | `integer` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `integer` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `logical` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `integer` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `character` | — | Normalized text content of the header |\n| `id` | `character or NULL` | `NULL` | HTML id attribute if present |\n| `depth` | `integer` | — | Document tree depth at the header element |\n| `html_offset` | `integer` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```r\nis_valid()\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `list` | `list()` | Extracted header elements with hierarchy |\n| `links` | `list` | `list()` | Extracted hyperlinks with type classification |\n| `images` | `list` | `list()` | Extracted images with source and dimensions |\n| `structured_data` | `list` | `list()` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```r\nvisit_element_start(ctx)\n```\n\n##### visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```r\nvisit_element_end(ctx, output)\n```\n\n###### visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```r\nvisit_text(ctx, text)\n```\n\n###### visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```r\nvisit_link(ctx, href, text, title)\n```\n\n###### visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```r\nvisit_image(ctx, src, alt, title)\n```\n\n###### visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```r\nvisit_heading(ctx, level, text, id)\n```\n\n###### visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```r\nvisit_code_block(ctx, lang, code)\n```\n\n###### visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```r\nvisit_code_inline(ctx, code)\n```\n\n###### visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```r\nvisit_list_item(ctx, ordered, marker, text)\n```\n\n###### visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```r\nvisit_list_start(ctx, ordered)\n```\n\n###### visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```r\nvisit_list_end(ctx, ordered, output)\n```\n\n###### visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```r\nvisit_table_start(ctx)\n```\n\n###### visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```r\nvisit_table_row(ctx, cells, is_header)\n```\n\n###### visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```r\nvisit_table_end(ctx, output)\n```\n\n###### visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```r\nvisit_blockquote(ctx, content, depth)\n```\n\n###### visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```r\nvisit_strong(ctx, text)\n```\n\n###### visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```r\nvisit_emphasis(ctx, text)\n```\n\n###### visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```r\nvisit_strikethrough(ctx, text)\n```\n\n###### visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```r\nvisit_underline(ctx, text)\n```\n\n###### visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```r\nvisit_subscript(ctx, text)\n```\n\n###### visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```r\nvisit_superscript(ctx, text)\n```\n\n###### visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```r\nvisit_mark(ctx, text)\n```\n\n###### visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```r\nvisit_line_break(ctx)\n```\n\n###### visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```r\nvisit_horizontal_rule(ctx)\n```\n\n###### visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```r\nvisit_custom_element(ctx, tag_name, html)\n```\n\n###### visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```r\nvisit_definition_list_start(ctx)\n```\n\n###### visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```r\nvisit_definition_term(ctx, text)\n```\n\n###### visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```r\nvisit_definition_description(ctx, text)\n```\n\n###### visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```r\nvisit_definition_list_end(ctx, output)\n```\n\n###### visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```r\nvisit_form(ctx, action, method)\n```\n\n###### visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```r\nvisit_input(ctx, input_type, name, value)\n```\n\n###### visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```r\nvisit_button(ctx, text)\n```\n\n###### visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```r\nvisit_audio(ctx, src)\n```\n\n###### visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```r\nvisit_video(ctx, src)\n```\n\n###### visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```r\nvisit_iframe(ctx, src)\n```\n\n###### visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```r\nvisit_details(ctx, open)\n```\n\n###### visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```r\nvisit_summary(ctx, text)\n```\n\n###### visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```r\nvisit_figure_start(ctx)\n```\n\n###### visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```r\nvisit_figcaption(ctx, text)\n```\n\n###### visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```r\nvisit_figure_end(ctx, output)\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `character` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `character or NULL` | `NULL` | Alternative text from alt attribute (for accessibility) |\n| `title` | `character or NULL` | `NULL` | Title attribute (often shown as tooltip) |\n| `dimensions` | `list or NULL` | `NULL` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `list` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `character` | — | The href URL value |\n| `text` | `character` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `character or NULL` | `NULL` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `list` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `list` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```r\nclassify_link(href)\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `character` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `list` | — | All HTML attributes as key-value pairs |\n| `depth` | `integer` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `integer` | — | Index among siblings (0-based) |\n| `parent_tag` | `character or NULL` | `NULL` | Parent element's tag name (None if root) |\n| `is_inline` | `logical` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `logical` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `\"standard\"` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `logical` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `logical` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```r\ndefault()\n```\n\n###### apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```r\napply_update(update)\n```\n\n###### from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```r\nfrom_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```r\nfrom(update)\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `character` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `character` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `character or NULL` | `NULL` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `character` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `integer` | — | Number of rows. |\n| `cols` | `integer` | — | Number of columns. |\n| `cells` | `list` | `list()` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `integer` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `integer` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `left_to_right` | Left-to-right text flow (default for Latin scripts) |\n| `right_to_left` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `anchor` | Anchor link within same document (href starts with #) |\n| `internal` | Internal link within same domain |\n| `external` | External link to different domain |\n| `email` | Email link (mailto:) |\n| `phone` | Phone link (tel:) |\n| `other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `data_uri` | Data URI embedded image (base64 or other encoding) |\n| `inline_svg` | Inline SVG element |\n| `external` | External image URL (http/https) |\n| `relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `json_ld` | JSON-LD (JSON for Linking Data) script blocks |\n| `microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `rdfa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `underlined` | Underlined style (=== for h1, --- for h2). |\n| `atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `atx_closed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `double_equal` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `html` | Preserve as HTML (==text==). Original HTML tag. |\n| `bold` | Render as bold (**text**). Uses strong emphasis. |\n| `none` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `inline` | Inline links: `[text](url)`. Default. |\n| `reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `djot` | Djot lightweight markup language. |\n| `plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `heading` | A heading element (h1-h6). — Fields: `level`: `integer`, `text`: `character` |\n| `paragraph` | A paragraph of text. — Fields: `text`: `character` |\n| `list` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `logical` |\n| `list_item` | A single list item. — Fields: `text`: `character` |\n| `table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `image` | An image element. — Fields: `description`: `character`, `src`: `character`, `image_index`: `integer` |\n| `code` | A code block or inline code. — Fields: `text`: `character`, `language`: `character` |\n| `quote` | A block quote container. |\n| `definition_list` | A definition list container. |\n| `definition_item` | A definition list entry with term and description. — Fields: `term`: `character`, `definition`: `character` |\n| `raw_block` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `character`, `content`: `character` |\n| `metadata_block` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `list` |\n| `group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `character`, `heading_level`: `integer`, `heading_text`: `character` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `bold` | Bold / strong emphasis. |\n| `italic` | Italic / emphasis. |\n| `underline` | Underline. |\n| `strikethrough` | Strikethrough / deleted text. |\n| `code` | Inline code. |\n| `subscript` | Subscript text. |\n| `superscript` | Superscript text. |\n| `highlight` | Highlighted / marked text. |\n| `link` | A hyperlink. — Fields: `url`: `character`, `title`: `character` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `image_extraction_failed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `encoding_fallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `truncated_input` | The input was truncated due to size limits. |\n| `malformed_html` | The HTML was malformed but processing continued with best effort. |\n| `sanitization_applied` | Sanitization was applied to remove potentially unsafe content. |\n| `depth_limit_exceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `text` | Text node (most frequent - 100+ per document) |\n| `element` | Generic element node |\n| `heading` | Heading elements (h1-h6) |\n| `paragraph` | Paragraph element |\n| `div` | Generic div container |\n| `blockquote` | Blockquote element |\n| `pre` | Preformatted text block |\n| `hr` | Horizontal rule |\n| `list` | Ordered or unordered list (ul, ol) |\n| `list_item` | List item (li) |\n| `definition_list` | Definition list (dl) |\n| `definition_term` | Definition term (dt) |\n| `definition_description` | Definition description (dd) |\n| `table` | Table element |\n| `table_row` | Table row (tr) |\n| `table_cell` | Table cell (td, th) |\n| `table_header` | Table header cell (th) |\n| `table_body` | Table body (tbody) |\n| `table_head` | Table head (thead) |\n| `table_foot` | Table foot (tfoot) |\n| `link` | Anchor link (a) |\n| `image` | Image (img) |\n| `strong` | Strong/bold (strong, b) |\n| `em` | Emphasis/italic (em, i) |\n| `code` | Inline code (code) |\n| `strikethrough` | Strikethrough (s, del, strike) |\n| `underline` | Underline (u, ins) |\n| `subscript` | Subscript (sub) |\n| `superscript` | Superscript (sup) |\n| `mark` | Mark/highlight (mark) |\n| `small` | Small text (small) |\n| `br` | Line break (br) |\n| `span` | Span element |\n| `article` | Article element |\n| `section` | Section element |\n| `nav` | Navigation element |\n| `aside` | Aside element |\n| `header` | Header element |\n| `footer` | Footer element |\n| `main` | Main element |\n| `figure` | Figure element |\n| `figcaption` | Figure caption |\n| `time` | Time element |\n| `details` | Details element |\n| `summary` | Summary element |\n| `form` | Form element |\n| `input` | Input element |\n| `select` | Select element |\n| `option` | Option element |\n| `button` | Button element |\n| `textarea` | Textarea element |\n| `label` | Label element |\n| `fieldset` | Fieldset element |\n| `legend` | Legend element |\n| `audio` | Audio element |\n| `video` | Video element |\n| `picture` | Picture element |\n| `source` | Source element |\n| `iframe` | Iframe element |\n| `svg` | SVG element |\n| `canvas` | Canvas element |\n| `ruby` | Ruby annotation |\n| `rt` | Ruby text |\n| `rp` | Ruby parenthesis |\n| `abbr` | Abbreviation |\n| `kbd` | Keyboard input |\n| `samp` | Sample output |\n| `var` | Variable |\n| `cite` | Citation |\n| `q` | Quote |\n| `del` | Deleted text |\n| `ins` | Inserted text |\n| `data` | Data element |\n| `meter` | Meter element |\n| `progress` | Progress element |\n| `output` | Output element |\n| `template` | Template element |\n| `slot` | Slot element |\n| `html` | HTML root element |\n| `head` | Head element |\n| `body` | Body element |\n| `title` | Title element |\n| `meta` | Meta element |\n| `link_tag` | Link element (not anchor) |\n| `style` | Style element |\n| `script` | Script element |\n| `base` | Base element |\n| `custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `continue` | Continue with default conversion behavior |\n| `custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `character` |\n| `skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `preserve_html` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `character` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `parse_error` | HTML parsing error |\n| `sanitization_error` | HTML sanitization error |\n| `config_error` | Invalid configuration |\n| `io_error` | I/O error |\n| `panic` | Internal error caught during conversion |\n| `invalid_input` | Invalid input data |\n| `other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-ruby.md",
    "content": "---\ntitle: \"Ruby API Reference\"\n---\n\n## Ruby API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```ruby\ndef self.convert(html, options: nil)\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `String` | Yes | The html |\n| `options` | `ConversionOptions?` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Raises `Error`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `:atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `:spaces` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `Integer` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `String` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `String` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `Boolean` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `Boolean` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `Boolean` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `Boolean` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `String` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `Boolean` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `Boolean` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `Boolean` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `:double_equal` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `Boolean` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `:normalized` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `Boolean` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `Boolean` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `Integer` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `Boolean` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `String` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `String` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `:spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `:backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `Array<String>` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `String` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `Boolean` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `Array<String>` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `Array<String>` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `Boolean` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `:inline` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `:markdown` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `Boolean` | `false` | Include structured document tree in result. |\n| `extract_images` | `Boolean` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `Integer` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `Boolean` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `Boolean` | `true` | Infer image dimensions from data. |\n| `max_depth` | `Integer?` | `nil` | Maximum DOM traversal depth. `nil` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `Array<String>` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (duck-type)` | `nil` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```ruby\ndef self.default()\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```ruby\ndef self.builder()\n```\n\n###### apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```ruby\ndef apply_update(update)\n```\n\n###### from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```ruby\ndef self.from_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```ruby\ndef self.from(update)\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String?` | `nil` | Converted text output (markdown, djot, or plain text). `nil` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure?` | `nil` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `Array<TableData>` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `Array<String>` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `Array<ProcessingWarning>` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```ruby\ndef strip_tags(tags)\n```\n\n###### preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```ruby\ndef preserve_tags(tags)\n```\n\n###### keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```ruby\ndef keep_inline_images_in(tags)\n```\n\n###### exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```ruby\ndef exclude_selectors(selectors)\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```ruby\ndef visitor(visitor)\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```ruby\ndef preprocessing(preprocessing)\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```ruby\ndef build()\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `String?` | `nil` | Document title from `<title>` tag |\n| `description` | `String?` | `nil` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `Array<String>` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `String?` | `nil` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `String?` | `nil` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `String?` | `nil` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `String?` | `nil` | Document language from `lang` attribute |\n| `text_direction` | `TextDirection?` | `nil` | Document text direction from `dir` attribute |\n| `open_graph` | `Hash{String=>String}` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `Hash{String=>String}` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `Hash{String=>String}` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `String` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `Integer?` | `nil` | Index of the parent node (None for root nodes). |\n| `children` | `Array<Integer>` | — | Indices of child nodes in reading order. |\n| `annotations` | `Array<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `Hash{String=>String}?` | `nil` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `Array<DocumentNode>` | — | All nodes in document reading order. |\n| `source_format` | `String?` | `nil` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String` | — | The text content of the cell. |\n| `row` | `Integer` | — | 0-indexed row position. |\n| `col` | `Integer` | — | 0-indexed column position. |\n| `row_span` | `Integer` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `Integer` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `Boolean` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `Integer` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `String` | — | Normalized text content of the header |\n| `id` | `String?` | `nil` | HTML id attribute if present |\n| `depth` | `Integer` | — | Document tree depth at the header element |\n| `html_offset` | `Integer` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```ruby\ndef is_valid()\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `Array<HeaderMetadata>` | `[]` | Extracted header elements with hierarchy |\n| `links` | `Array<LinkMetadata>` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `Array<ImageMetadata>` | `[]` | Extracted images with source and dimensions |\n| `structured_data` | `Array<StructuredData>` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```ruby\ndef visit_element_start(ctx)\n```\n\n##### visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```ruby\ndef visit_element_end(ctx, output)\n```\n\n###### visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```ruby\ndef visit_text(ctx, text)\n```\n\n###### visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```ruby\ndef visit_link(ctx, href, text, title)\n```\n\n###### visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```ruby\ndef visit_image(ctx, src, alt, title)\n```\n\n###### visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```ruby\ndef visit_heading(ctx, level, text, id)\n```\n\n###### visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```ruby\ndef visit_code_block(ctx, lang, code)\n```\n\n###### visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```ruby\ndef visit_code_inline(ctx, code)\n```\n\n###### visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```ruby\ndef visit_list_item(ctx, ordered, marker, text)\n```\n\n###### visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```ruby\ndef visit_list_start(ctx, ordered)\n```\n\n###### visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```ruby\ndef visit_list_end(ctx, ordered, output)\n```\n\n###### visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```ruby\ndef visit_table_start(ctx)\n```\n\n###### visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```ruby\ndef visit_table_row(ctx, cells, is_header)\n```\n\n###### visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```ruby\ndef visit_table_end(ctx, output)\n```\n\n###### visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```ruby\ndef visit_blockquote(ctx, content, depth)\n```\n\n###### visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```ruby\ndef visit_strong(ctx, text)\n```\n\n###### visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```ruby\ndef visit_emphasis(ctx, text)\n```\n\n###### visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```ruby\ndef visit_strikethrough(ctx, text)\n```\n\n###### visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```ruby\ndef visit_underline(ctx, text)\n```\n\n###### visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```ruby\ndef visit_subscript(ctx, text)\n```\n\n###### visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```ruby\ndef visit_superscript(ctx, text)\n```\n\n###### visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```ruby\ndef visit_mark(ctx, text)\n```\n\n###### visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```ruby\ndef visit_line_break(ctx)\n```\n\n###### visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```ruby\ndef visit_horizontal_rule(ctx)\n```\n\n###### visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```ruby\ndef visit_custom_element(ctx, tag_name, html)\n```\n\n###### visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```ruby\ndef visit_definition_list_start(ctx)\n```\n\n###### visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```ruby\ndef visit_definition_term(ctx, text)\n```\n\n###### visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```ruby\ndef visit_definition_description(ctx, text)\n```\n\n###### visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```ruby\ndef visit_definition_list_end(ctx, output)\n```\n\n###### visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```ruby\ndef visit_form(ctx, action, method)\n```\n\n###### visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```ruby\ndef visit_input(ctx, input_type, name, value)\n```\n\n###### visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```ruby\ndef visit_button(ctx, text)\n```\n\n###### visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```ruby\ndef visit_audio(ctx, src)\n```\n\n###### visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```ruby\ndef visit_video(ctx, src)\n```\n\n###### visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```ruby\ndef visit_iframe(ctx, src)\n```\n\n###### visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```ruby\ndef visit_details(ctx, open)\n```\n\n###### visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```ruby\ndef visit_summary(ctx, text)\n```\n\n###### visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```ruby\ndef visit_figure_start(ctx)\n```\n\n###### visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```ruby\ndef visit_figcaption(ctx, text)\n```\n\n###### visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```ruby\ndef visit_figure_end(ctx, output)\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `String` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `String?` | `nil` | Alternative text from alt attribute (for accessibility) |\n| `title` | `String?` | `nil` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Array<Integer>?` | `nil` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `Hash{String=>String}` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `String` | — | The href URL value |\n| `text` | `String` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `String?` | `nil` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `Array<String>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `Hash{String=>String}` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```ruby\ndef self.classify_link(href)\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `String` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `Hash{String=>String}` | — | All HTML attributes as key-value pairs |\n| `depth` | `Integer` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `Integer` | — | Index among siblings (0-based) |\n| `parent_tag` | `String?` | `nil` | Parent element's tag name (None if root) |\n| `is_inline` | `Boolean` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `Boolean` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `:standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `Boolean` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `Boolean` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```ruby\ndef self.default()\n```\n\n###### apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```ruby\ndef apply_update(update)\n```\n\n###### from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```ruby\ndef self.from_update(update)\n```\n\n###### from()\n\n**Signature:**\n\n```ruby\ndef self.from(update)\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `String` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `String` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `String?` | `nil` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `String` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `Integer` | — | Number of rows. |\n| `cols` | `Integer` | — | Number of columns. |\n| `cells` | `Array<GridCell>` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `Integer` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `Integer` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `left_to_right` | Left-to-right text flow (default for Latin scripts) |\n| `right_to_left` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `anchor` | Anchor link within same document (href starts with #) |\n| `internal` | Internal link within same domain |\n| `external` | External link to different domain |\n| `email` | Email link (mailto:) |\n| `phone` | Phone link (tel:) |\n| `other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `data_uri` | Data URI embedded image (base64 or other encoding) |\n| `inline_svg` | Inline SVG element |\n| `external` | External image URL (http/https) |\n| `relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `json_ld` | JSON-LD (JSON for Linking Data) script blocks |\n| `microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `rdfa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `underlined` | Underlined style (=== for h1, --- for h2). |\n| `atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `atx_closed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `double_equal` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `html` | Preserve as HTML (==text==). Original HTML tag. |\n| `bold` | Render as bold (**text**). Uses strong emphasis. |\n| `none` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `inline` | Inline links: `[text](url)`. Default. |\n| `reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `djot` | Djot lightweight markup language. |\n| `plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `heading` | A heading element (h1-h6). — Fields: `level`: `Integer`, `text`: `String` |\n| `paragraph` | A paragraph of text. — Fields: `text`: `String` |\n| `list` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `Boolean` |\n| `list_item` | A single list item. — Fields: `text`: `String` |\n| `table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `image` | An image element. — Fields: `description`: `String`, `src`: `String`, `image_index`: `Integer` |\n| `code` | A code block or inline code. — Fields: `text`: `String`, `language`: `String` |\n| `quote` | A block quote container. |\n| `definition_list` | A definition list container. |\n| `definition_item` | A definition list entry with term and description. — Fields: `term`: `String`, `definition`: `String` |\n| `raw_block` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `String`, `content`: `String` |\n| `metadata_block` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `Array<String>` |\n| `group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `String`, `heading_level`: `Integer`, `heading_text`: `String` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `bold` | Bold / strong emphasis. |\n| `italic` | Italic / emphasis. |\n| `underline` | Underline. |\n| `strikethrough` | Strikethrough / deleted text. |\n| `code` | Inline code. |\n| `subscript` | Subscript text. |\n| `superscript` | Superscript text. |\n| `highlight` | Highlighted / marked text. |\n| `link` | A hyperlink. — Fields: `url`: `String`, `title`: `String` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `image_extraction_failed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `encoding_fallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `truncated_input` | The input was truncated due to size limits. |\n| `malformed_html` | The HTML was malformed but processing continued with best effort. |\n| `sanitization_applied` | Sanitization was applied to remove potentially unsafe content. |\n| `depth_limit_exceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `text` | Text node (most frequent - 100+ per document) |\n| `element` | Generic element node |\n| `heading` | Heading elements (h1-h6) |\n| `paragraph` | Paragraph element |\n| `div` | Generic div container |\n| `blockquote` | Blockquote element |\n| `pre` | Preformatted text block |\n| `hr` | Horizontal rule |\n| `list` | Ordered or unordered list (ul, ol) |\n| `list_item` | List item (li) |\n| `definition_list` | Definition list (dl) |\n| `definition_term` | Definition term (dt) |\n| `definition_description` | Definition description (dd) |\n| `table` | Table element |\n| `table_row` | Table row (tr) |\n| `table_cell` | Table cell (td, th) |\n| `table_header` | Table header cell (th) |\n| `table_body` | Table body (tbody) |\n| `table_head` | Table head (thead) |\n| `table_foot` | Table foot (tfoot) |\n| `link` | Anchor link (a) |\n| `image` | Image (img) |\n| `strong` | Strong/bold (strong, b) |\n| `em` | Emphasis/italic (em, i) |\n| `code` | Inline code (code) |\n| `strikethrough` | Strikethrough (s, del, strike) |\n| `underline` | Underline (u, ins) |\n| `subscript` | Subscript (sub) |\n| `superscript` | Superscript (sup) |\n| `mark` | Mark/highlight (mark) |\n| `small` | Small text (small) |\n| `br` | Line break (br) |\n| `span` | Span element |\n| `article` | Article element |\n| `section` | Section element |\n| `nav` | Navigation element |\n| `aside` | Aside element |\n| `header` | Header element |\n| `footer` | Footer element |\n| `main` | Main element |\n| `figure` | Figure element |\n| `figcaption` | Figure caption |\n| `time` | Time element |\n| `details` | Details element |\n| `summary` | Summary element |\n| `form` | Form element |\n| `input` | Input element |\n| `select` | Select element |\n| `option` | Option element |\n| `button` | Button element |\n| `textarea` | Textarea element |\n| `label` | Label element |\n| `fieldset` | Fieldset element |\n| `legend` | Legend element |\n| `audio` | Audio element |\n| `video` | Video element |\n| `picture` | Picture element |\n| `source` | Source element |\n| `iframe` | Iframe element |\n| `svg` | SVG element |\n| `canvas` | Canvas element |\n| `ruby` | Ruby annotation |\n| `rt` | Ruby text |\n| `rp` | Ruby parenthesis |\n| `abbr` | Abbreviation |\n| `kbd` | Keyboard input |\n| `samp` | Sample output |\n| `var` | Variable |\n| `cite` | Citation |\n| `q` | Quote |\n| `del` | Deleted text |\n| `ins` | Inserted text |\n| `data` | Data element |\n| `meter` | Meter element |\n| `progress` | Progress element |\n| `output` | Output element |\n| `template` | Template element |\n| `slot` | Slot element |\n| `html` | HTML root element |\n| `head` | Head element |\n| `body` | Body element |\n| `title` | Title element |\n| `meta` | Meta element |\n| `link_tag` | Link element (not anchor) |\n| `style` | Style element |\n| `script` | Script element |\n| `base` | Base element |\n| `custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `continue` | Continue with default conversion behavior |\n| `custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `String` |\n| `skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `preserve_html` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `String` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `parse_error` | HTML parsing error |\n| `sanitization_error` | HTML sanitization error |\n| `config_error` | Invalid configuration |\n| `io_error` | I/O error |\n| `panic` | Internal error caught during conversion |\n| `invalid_input` | Invalid input data |\n| `other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-rust.md",
    "content": "---\ntitle: \"Rust API Reference\"\n---\n\n## Rust API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```rust\npub fn convert(html: &str, options: Option<ConversionOptions>) -> Result<ConversionResult, Error>\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `String` | Yes | The html |\n| `options` | `Option<ConversionOptions>` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Returns `Err(Error)`.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `HeadingStyle::Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `ListIndentType::Spaces` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `usize` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `String` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `String` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `String` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `HighlightStyle::DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `WhitespaceMode::Normalized` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `usize` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `String` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `String` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `NewlineStyle::Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `CodeBlockStyle::Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `Vec<String>` | `vec![]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `String` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `Vec<String>` | `vec![]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `Vec<String>` | `vec![]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `LinkStyle::Inline` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `OutputFormat::Markdown` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `bool` | `false` | Include structured document tree in result. |\n| `extract_images` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `u64` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `bool` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `max_depth` | `Option<usize>` | `None` | Maximum DOM traversal depth. `None` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `Vec<String>` | `vec![]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `Box<dyn HtmlVisitor>` | `None` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```rust\npub fn default() -> ConversionOptions\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```rust\npub fn builder() -> ConversionOptionsBuilder\n```\n\n###### apply_update()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```rust\npub fn apply_update(&self, update: ConversionOptionsUpdate)\n```\n\n###### from_update()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```rust\npub fn from_update(update: ConversionOptionsUpdate) -> ConversionOptions\n```\n\n###### from()\n\n**Signature:**\n\n```rust\npub fn from(update: ConversionOptionsUpdate) -> ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `Option<String>` | `Default::default()` | Converted text output (markdown, djot, or plain text). `None` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `Option<DocumentStructure>` | `Default::default()` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `Vec<TableData>` | `vec![]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `Vec<String>` | `vec![]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `Vec<ProcessingWarning>` | `vec![]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### strip_tags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```rust\npub fn strip_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder\n```\n\n###### preserve_tags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```rust\npub fn preserve_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder\n```\n\n###### keep_inline_images_in()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```rust\npub fn keep_inline_images_in(&self, tags: Vec<String>) -> ConversionOptionsBuilder\n```\n\n###### exclude_selectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```rust\npub fn exclude_selectors(&self, selectors: Vec<String>) -> ConversionOptionsBuilder\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```rust\npub fn visitor(&self, visitor: Option<VisitorHandle>) -> ConversionOptionsBuilder\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```rust\npub fn preprocessing(&self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```rust\npub fn build(&self) -> ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `Option<String>` | `Default::default()` | Document title from `<title>` tag |\n| `description` | `Option<String>` | `Default::default()` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `Vec<String>` | `vec![]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `Option<String>` | `Default::default()` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `Option<String>` | `Default::default()` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `Option<String>` | `Default::default()` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `Option<String>` | `Default::default()` | Document language from `lang` attribute |\n| `text_direction` | `Option<TextDirection>` | `Default::default()` | Document text direction from `dir` attribute |\n| `open_graph` | `HashMap<String, String>` | `HashMap::new()` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `HashMap<String, String>` | `HashMap::new()` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `HashMap<String, String>` | `HashMap::new()` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `String` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `Option<u32>` | `None` | Index of the parent node (None for root nodes). |\n| `children` | `Vec<u32>` | — | Indices of child nodes in reading order. |\n| `annotations` | `Vec<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `Option<HashMap<String, String>>` | `None` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `Vec<DocumentNode>` | — | All nodes in document reading order. |\n| `source_format` | `Option<String>` | `None` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String` | — | The text content of the cell. |\n| `row` | `u32` | — | 0-indexed row position. |\n| `col` | `u32` | — | 0-indexed column position. |\n| `row_span` | `u32` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `u32` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `u8` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `String` | — | Normalized text content of the header |\n| `id` | `Option<String>` | `None` | HTML id attribute if present |\n| `depth` | `usize` | — | Document tree depth at the header element |\n| `html_offset` | `usize` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### is_valid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```rust\npub fn is_valid(&self) -> bool\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `Vec<HeaderMetadata>` | `vec![]` | Extracted header elements with hierarchy |\n| `links` | `Vec<LinkMetadata>` | `vec![]` | Extracted hyperlinks with type classification |\n| `images` | `Vec<ImageMetadata>` | `vec![]` | Extracted images with source and dimensions |\n| `structured_data` | `Vec<StructuredData>` | `vec![]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visit_element_start()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```rust\npub fn visit_element_start(&self, ctx: NodeContext) -> VisitResult\n```\n\n##### visit_element_end()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```rust\npub fn visit_element_end(&self, ctx: NodeContext, output: &str) -> VisitResult\n```\n\n###### visit_text()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```rust\npub fn visit_text(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_link()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```rust\npub fn visit_link(&self, ctx: NodeContext, href: &str, text: &str, title: Option<String>) -> VisitResult\n```\n\n###### visit_image()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```rust\npub fn visit_image(&self, ctx: NodeContext, src: &str, alt: &str, title: Option<String>) -> VisitResult\n```\n\n###### visit_heading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```rust\npub fn visit_heading(&self, ctx: NodeContext, level: u32, text: &str, id: Option<String>) -> VisitResult\n```\n\n###### visit_code_block()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```rust\npub fn visit_code_block(&self, ctx: NodeContext, lang: Option<String>, code: &str) -> VisitResult\n```\n\n###### visit_code_inline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```rust\npub fn visit_code_inline(&self, ctx: NodeContext, code: &str) -> VisitResult\n```\n\n###### visit_list_item()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```rust\npub fn visit_list_item(&self, ctx: NodeContext, ordered: bool, marker: &str, text: &str) -> VisitResult\n```\n\n###### visit_list_start()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```rust\npub fn visit_list_start(&self, ctx: NodeContext, ordered: bool) -> VisitResult\n```\n\n###### visit_list_end()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```rust\npub fn visit_list_end(&self, ctx: NodeContext, ordered: bool, output: &str) -> VisitResult\n```\n\n###### visit_table_start()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```rust\npub fn visit_table_start(&self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_table_row()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```rust\npub fn visit_table_row(&self, ctx: NodeContext, cells: Vec<String>, is_header: bool) -> VisitResult\n```\n\n###### visit_table_end()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```rust\npub fn visit_table_end(&self, ctx: NodeContext, output: &str) -> VisitResult\n```\n\n###### visit_blockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```rust\npub fn visit_blockquote(&self, ctx: NodeContext, content: &str, depth: usize) -> VisitResult\n```\n\n###### visit_strong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```rust\npub fn visit_strong(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_emphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```rust\npub fn visit_emphasis(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_strikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```rust\npub fn visit_strikethrough(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_underline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```rust\npub fn visit_underline(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_subscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```rust\npub fn visit_subscript(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_superscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```rust\npub fn visit_superscript(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_mark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```rust\npub fn visit_mark(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_line_break()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```rust\npub fn visit_line_break(&self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_horizontal_rule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```rust\npub fn visit_horizontal_rule(&self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_custom_element()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```rust\npub fn visit_custom_element(&self, ctx: NodeContext, tag_name: &str, html: &str) -> VisitResult\n```\n\n###### visit_definition_list_start()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```rust\npub fn visit_definition_list_start(&self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_definition_term()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```rust\npub fn visit_definition_term(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_definition_description()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```rust\npub fn visit_definition_description(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_definition_list_end()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```rust\npub fn visit_definition_list_end(&self, ctx: NodeContext, output: &str) -> VisitResult\n```\n\n###### visit_form()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```rust\npub fn visit_form(&self, ctx: NodeContext, action: Option<String>, method: Option<String>) -> VisitResult\n```\n\n###### visit_input()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```rust\npub fn visit_input(&self, ctx: NodeContext, input_type: &str, name: Option<String>, value: Option<String>) -> VisitResult\n```\n\n###### visit_button()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```rust\npub fn visit_button(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_audio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```rust\npub fn visit_audio(&self, ctx: NodeContext, src: Option<String>) -> VisitResult\n```\n\n###### visit_video()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```rust\npub fn visit_video(&self, ctx: NodeContext, src: Option<String>) -> VisitResult\n```\n\n###### visit_iframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```rust\npub fn visit_iframe(&self, ctx: NodeContext, src: Option<String>) -> VisitResult\n```\n\n###### visit_details()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```rust\npub fn visit_details(&self, ctx: NodeContext, open: bool) -> VisitResult\n```\n\n###### visit_summary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```rust\npub fn visit_summary(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_figure_start()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```rust\npub fn visit_figure_start(&self, ctx: NodeContext) -> VisitResult\n```\n\n###### visit_figcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```rust\npub fn visit_figcaption(&self, ctx: NodeContext, text: &str) -> VisitResult\n```\n\n###### visit_figure_end()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```rust\npub fn visit_figure_end(&self, ctx: NodeContext, output: &str) -> VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `String` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `Option<String>` | `None` | Alternative text from alt attribute (for accessibility) |\n| `title` | `Option<String>` | `None` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Option<Vec<u32>>` | `None` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `HashMap<String, String>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `String` | — | The href URL value |\n| `text` | `String` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `Option<String>` | `None` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `Vec<String>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `HashMap<String, String>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classify_link()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```rust\npub fn classify_link(href: &str) -> LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `String` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `HashMap<String, String>` | — | All HTML attributes as key-value pairs |\n| `depth` | `usize` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `usize` | — | Index among siblings (0-based) |\n| `parent_tag` | `Option<String>` | `None` | Parent element's tag name (None if root) |\n| `is_inline` | `bool` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset::Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```rust\npub fn default() -> PreprocessingOptions\n```\n\n###### apply_update()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```rust\npub fn apply_update(&self, update: PreprocessingOptionsUpdate)\n```\n\n###### from_update()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```rust\npub fn from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions\n```\n\n###### from()\n\n**Signature:**\n\n```rust\npub fn from(update: PreprocessingOptionsUpdate) -> PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `String` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `String` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `Option<String>` | `None` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `String` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `u32` | — | Number of rows. |\n| `cols` | `u32` | — | Number of columns. |\n| `cells` | `Vec<GridCell>` | `vec![]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `u32` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `u32` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `level`: `u8`, `text`: `String` |\n| `Paragraph` | A paragraph of text. — Fields: `text`: `String` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `bool` |\n| `ListItem` | A single list item. — Fields: `text`: `String` |\n| `Table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `description`: `String`, `src`: `String`, `image_index`: `u32` |\n| `Code` | A code block or inline code. — Fields: `text`: `String`, `language`: `String` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `term`: `String`, `definition`: `String` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `String`, `content`: `String` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `Vec<String>` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `String`, `heading_level`: `u8`, `heading_text`: `String` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `url`: `String`, `title`: `String` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `String` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `String` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-typescript.md",
    "content": "---\ntitle: \"TypeScript API Reference\"\n---\n\n## TypeScript API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```typescript\nfunction convert(html: string, options?: ConversionOptions): ConversionResult\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `string` | Yes | The html |\n| `options` | `ConversionOptions | null` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Throws `Error` with a descriptive message.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `headingStyle` | `HeadingStyle` | `HeadingStyle.Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `listIndentType` | `ListIndentType` | `ListIndentType.Spaces` | How to indent nested list items (spaces or tab). |\n| `listIndentWidth` | `number` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `string` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strongEmSymbol` | `string` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escapeAsterisks` | `boolean` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escapeUnderscores` | `boolean` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escapeMisc` | `boolean` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escapeAscii` | `boolean` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `codeLanguage` | `string` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `boolean` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `defaultTitle` | `boolean` | `false` | Emit a default title when no `<title>` tag is present. |\n| `brInTables` | `boolean` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlightStyle` | `HighlightStyle` | `HighlightStyle.DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extractMetadata` | `boolean` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespaceMode` | `WhitespaceMode` | `WhitespaceMode.Normalized` | Controls how whitespace is normalised during conversion. |\n| `stripNewlines` | `boolean` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `boolean` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrapWidth` | `number` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convertAsInline` | `boolean` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `subSymbol` | `string` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `supSymbol` | `string` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newlineStyle` | `NewlineStyle` | `NewlineStyle.Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `codeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle.Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keepInlineImagesIn` | `Array<string>` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `string` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `boolean` | `false` | Emit debug information during conversion. |\n| `stripTags` | `Array<string>` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserveTags` | `Array<string>` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skipImages` | `boolean` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `linkStyle` | `LinkStyle` | `LinkStyle.Inline` | Link rendering style (inline or reference). |\n| `outputFormat` | `OutputFormat` | `OutputFormat.Markdown` | Target output format (Markdown, plain text, etc.). |\n| `includeDocumentStructure` | `boolean` | `false` | Include structured document tree in result. |\n| `extractImages` | `boolean` | `false` | Extract inline images from data URIs and SVGs. |\n| `maxImageSize` | `number` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `captureSvg` | `boolean` | `false` | Capture SVG elements as images. |\n| `inferDimensions` | `boolean` | `true` | Infer image dimensions from data. |\n| `maxDepth` | `number | null` | `null` | Maximum DOM traversal depth. `null` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `excludeSelectors` | `Array<string>` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (interface)` | `null` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```typescript\nstatic default(): ConversionOptions\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```typescript\nstatic builder(): ConversionOptionsBuilder\n```\n\n###### applyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```typescript\napplyUpdate(update: ConversionOptionsUpdate): void\n```\n\n###### fromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```typescript\nstatic fromUpdate(update: ConversionOptionsUpdate): ConversionOptions\n```\n\n###### from()\n\n**Signature:**\n\n```typescript\nstatic from(update: ConversionOptionsUpdate): ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `string | null` | `null` | Converted text output (markdown, djot, or plain text). `null` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure | null` | `null` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `Array<TableData>` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `Array<string>` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `Array<ProcessingWarning>` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### stripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```typescript\nstripTags(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### preserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```typescript\npreserveTags(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### keepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```typescript\nkeepInlineImagesIn(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### excludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```typescript\nexcludeSelectors(selectors: Array<string>): ConversionOptionsBuilder\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```typescript\nvisitor(visitor: VisitorHandle): ConversionOptionsBuilder\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```typescript\npreprocessing(preprocessing: PreprocessingOptions): ConversionOptionsBuilder\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```typescript\nbuild(): ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `string | null` | `null` | Document title from `<title>` tag |\n| `description` | `string | null` | `null` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `Array<string>` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `string | null` | `null` | Document author from `<meta name=\"author\">` tag |\n| `canonicalUrl` | `string | null` | `null` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `baseHref` | `string | null` | `null` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `string | null` | `null` | Document language from `lang` attribute |\n| `textDirection` | `TextDirection | null` | `null` | Document text direction from `dir` attribute |\n| `openGraph` | `Record<string, string>` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitterCard` | `Record<string, string>` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `metaTags` | `Record<string, string>` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `string` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `number | null` | `null` | Index of the parent node (None for root nodes). |\n| `children` | `Array<number>` | — | Indices of child nodes in reading order. |\n| `annotations` | `Array<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `Record<string, string> | null` | `null` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `Array<DocumentNode>` | — | All nodes in document reading order. |\n| `sourceFormat` | `string | null` | `null` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `string` | — | The text content of the cell. |\n| `row` | `number` | — | 0-indexed row position. |\n| `col` | `number` | — | 0-indexed column position. |\n| `rowSpan` | `number` | — | Number of rows this cell spans (default 1). |\n| `colSpan` | `number` | — | Number of columns this cell spans (default 1). |\n| `isHeader` | `boolean` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `number` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `string` | — | Normalized text content of the header |\n| `id` | `string | null` | `null` | HTML id attribute if present |\n| `depth` | `number` | — | Document tree depth at the header element |\n| `htmlOffset` | `number` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### isValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```typescript\nisValid(): boolean\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `Array<HeaderMetadata>` | `[]` | Extracted header elements with hierarchy |\n| `links` | `Array<LinkMetadata>` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `Array<ImageMetadata>` | `[]` | Extracted images with source and dimensions |\n| `structuredData` | `Array<StructuredData>` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```typescript\nvisitElementStart(ctx: NodeContext): VisitResult\n```\n\n##### visitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```typescript\nvisitElementEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```typescript\nvisitText(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```typescript\nvisitLink(ctx: NodeContext, href: string, text: string, title: string): VisitResult\n```\n\n###### visitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```typescript\nvisitImage(ctx: NodeContext, src: string, alt: string, title: string): VisitResult\n```\n\n###### visitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```typescript\nvisitHeading(ctx: NodeContext, level: number, text: string, id: string): VisitResult\n```\n\n###### visitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```typescript\nvisitCodeBlock(ctx: NodeContext, lang: string, code: string): VisitResult\n```\n\n###### visitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```typescript\nvisitCodeInline(ctx: NodeContext, code: string): VisitResult\n```\n\n###### visitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```typescript\nvisitListItem(ctx: NodeContext, ordered: boolean, marker: string, text: string): VisitResult\n```\n\n###### visitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```typescript\nvisitListStart(ctx: NodeContext, ordered: boolean): VisitResult\n```\n\n###### visitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```typescript\nvisitListEnd(ctx: NodeContext, ordered: boolean, output: string): VisitResult\n```\n\n###### visitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```typescript\nvisitTableStart(ctx: NodeContext): VisitResult\n```\n\n###### visitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```typescript\nvisitTableRow(ctx: NodeContext, cells: Array<string>, isHeader: boolean): VisitResult\n```\n\n###### visitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```typescript\nvisitTableEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```typescript\nvisitBlockquote(ctx: NodeContext, content: string, depth: number): VisitResult\n```\n\n###### visitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```typescript\nvisitStrong(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```typescript\nvisitEmphasis(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```typescript\nvisitStrikethrough(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```typescript\nvisitUnderline(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```typescript\nvisitSubscript(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```typescript\nvisitSuperscript(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```typescript\nvisitMark(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```typescript\nvisitLineBreak(ctx: NodeContext): VisitResult\n```\n\n###### visitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```typescript\nvisitHorizontalRule(ctx: NodeContext): VisitResult\n```\n\n###### visitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```typescript\nvisitCustomElement(ctx: NodeContext, tagName: string, html: string): VisitResult\n```\n\n###### visitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionListStart(ctx: NodeContext): VisitResult\n```\n\n###### visitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionTerm(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionDescription(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionListEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```typescript\nvisitForm(ctx: NodeContext, action: string, method: string): VisitResult\n```\n\n###### visitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```typescript\nvisitInput(ctx: NodeContext, inputType: string, name: string, value: string): VisitResult\n```\n\n###### visitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```typescript\nvisitButton(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```typescript\nvisitAudio(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```typescript\nvisitVideo(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```typescript\nvisitIframe(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```typescript\nvisitDetails(ctx: NodeContext, open: boolean): VisitResult\n```\n\n###### visitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```typescript\nvisitSummary(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```typescript\nvisitFigureStart(ctx: NodeContext): VisitResult\n```\n\n###### visitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```typescript\nvisitFigcaption(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```typescript\nvisitFigureEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `string` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `string | null` | `null` | Alternative text from alt attribute (for accessibility) |\n| `title` | `string | null` | `null` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Array<number> | null` | `null` | Image dimensions as (width, height) if available |\n| `imageType` | `ImageType` | — | Image type classification |\n| `attributes` | `Record<string, string>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `string` | — | The href URL value |\n| `text` | `string` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `string | null` | `null` | Optional title attribute (often shown as tooltip) |\n| `linkType` | `LinkType` | — | Link type classification |\n| `rel` | `Array<string>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `Record<string, string>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```typescript\nstatic classifyLink(href: string): LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `tagName` | `string` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `Record<string, string>` | — | All HTML attributes as key-value pairs |\n| `depth` | `number` | — | Depth in the DOM tree (0 = root) |\n| `indexInParent` | `number` | — | Index among siblings (0-based) |\n| `parentTag` | `string | null` | `null` | Parent element's tag name (None if root) |\n| `isInline` | `boolean` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `boolean` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset.Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `removeNavigation` | `boolean` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `removeForms` | `boolean` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```typescript\nstatic default(): PreprocessingOptions\n```\n\n###### applyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```typescript\napplyUpdate(update: PreprocessingOptionsUpdate): void\n```\n\n###### fromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```typescript\nstatic fromUpdate(update: PreprocessingOptionsUpdate): PreprocessingOptions\n```\n\n###### from()\n\n**Signature:**\n\n```typescript\nstatic from(update: PreprocessingOptionsUpdate): PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `string` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `dataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `rawJson` | `string` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schemaType` | `string | null` | `null` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `string` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `number` | — | Number of rows. |\n| `cols` | `number` | — | Number of columns. |\n| `cells` | `Array<GridCell>` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `number` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `number` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `level`: `number`, `text`: `string` |\n| `Paragraph` | A paragraph of text. — Fields: `text`: `string` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `boolean` |\n| `ListItem` | A single list item. — Fields: `text`: `string` |\n| `Table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `description`: `string`, `src`: `string`, `imageIndex`: `number` |\n| `Code` | A code block or inline code. — Fields: `text`: `string`, `language`: `string` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `term`: `string`, `definition`: `string` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `string`, `content`: `string` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `Array<string>` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `string`, `headingLevel`: `number`, `headingText`: `string` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `url`: `string`, `title`: `string` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `string` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `string` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\nErrors are thrown as plain `Error` objects with descriptive messages.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/api-wasm.md",
    "content": "---\ntitle: \"WebAssembly API Reference\"\n---\n\n## WebAssembly API Reference <span class=\"version-badge\">v3.4.0-rc.25</span>\n\n### Functions\n\n#### convert()\n\nConvert HTML to Markdown, returning a `ConversionResult` with content, metadata, images,\nand warnings.\n\n  When the `visitor` feature is enabled, a custom `crate.visitor.HtmlVisitor` can be\n  attached via the `visitor` field on `ConversionOptions`.\n\n**Errors:**\n\nReturns an error if HTML parsing fails or if the input contains invalid UTF-8.\n\n**Signature:**\n\n```typescript\nfunction convert(html: string, options?: ConversionOptions): ConversionResult\n```\n\n**Parameters:**\n\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n| `html` | `string` | Yes | The html |\n| `options` | `ConversionOptions | null` | No | The options to use |\n\n**Returns:** `ConversionResult`\n\n**Errors:** Throws `Error` with a descriptive message.\n\n\n---\n\n### Types\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `headingStyle` | `HeadingStyle` | `HeadingStyle.Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `listIndentType` | `ListIndentType` | `ListIndentType.Spaces` | How to indent nested list items (spaces or tab). |\n| `listIndentWidth` | `number` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `string` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strongEmSymbol` | `string` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escapeAsterisks` | `boolean` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escapeUnderscores` | `boolean` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escapeMisc` | `boolean` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escapeAscii` | `boolean` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `codeLanguage` | `string` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `boolean` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `defaultTitle` | `boolean` | `false` | Emit a default title when no `<title>` tag is present. |\n| `brInTables` | `boolean` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlightStyle` | `HighlightStyle` | `HighlightStyle.DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extractMetadata` | `boolean` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespaceMode` | `WhitespaceMode` | `WhitespaceMode.Normalized` | Controls how whitespace is normalised during conversion. |\n| `stripNewlines` | `boolean` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `boolean` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrapWidth` | `number` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convertAsInline` | `boolean` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `subSymbol` | `string` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `supSymbol` | `string` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newlineStyle` | `NewlineStyle` | `NewlineStyle.Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `codeBlockStyle` | `CodeBlockStyle` | `CodeBlockStyle.Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keepInlineImagesIn` | `Array<string>` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `string` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `boolean` | `false` | Emit debug information during conversion. |\n| `stripTags` | `Array<string>` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserveTags` | `Array<string>` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skipImages` | `boolean` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `linkStyle` | `LinkStyle` | `LinkStyle.Inline` | Link rendering style (inline or reference). |\n| `outputFormat` | `OutputFormat` | `OutputFormat.Markdown` | Target output format (Markdown, plain text, etc.). |\n| `includeDocumentStructure` | `boolean` | `false` | Include structured document tree in result. |\n| `extractImages` | `boolean` | `false` | Extract inline images from data URIs and SVGs. |\n| `maxImageSize` | `number` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `captureSvg` | `boolean` | `false` | Capture SVG elements as images. |\n| `inferDimensions` | `boolean` | `true` | Infer image dimensions from data. |\n| `maxDepth` | `number | null` | `null` | Maximum DOM traversal depth. `null` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `excludeSelectors` | `Array<string>` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor (interface)` | `null` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n##### Methods\n\n###### default()\n\n**Signature:**\n\n```typescript\nstatic default(): ConversionOptions\n```\n\n###### builder()\n\nCreate a new builder with default values.\n\n**Signature:**\n\n```typescript\nstatic builder(): ConversionOptionsBuilder\n```\n\n###### applyUpdate()\n\nApply a partial update to these conversion options.\n\n**Signature:**\n\n```typescript\napplyUpdate(update: ConversionOptionsUpdate): void\n```\n\n###### fromUpdate()\n\nCreate from a partial update, applying to defaults.\n\n**Signature:**\n\n```typescript\nstatic fromUpdate(update: ConversionOptionsUpdate): ConversionOptions\n```\n\n###### from()\n\n**Signature:**\n\n```typescript\nstatic from(update: ConversionOptionsUpdate): ConversionOptions\n```\n\n\n---\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `string | null` | `null` | Converted text output (markdown, djot, or plain text). `null` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure | null` | `null` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `Array<TableData>` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `Array<string>` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `Array<ProcessingWarning>` | `[]` | Non-fatal processing warnings. |\n\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n##### Methods\n\n###### stripTags()\n\nSet the list of HTML tag names whose content is stripped from output.\n\n**Signature:**\n\n```typescript\nstripTags(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### preserveTags()\n\nSet the list of HTML tag names that are preserved verbatim in output.\n\n**Signature:**\n\n```typescript\npreserveTags(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### keepInlineImagesIn()\n\nSet the list of HTML tag names whose `<img>` children are kept inline.\n\n**Signature:**\n\n```typescript\nkeepInlineImagesIn(tags: Array<string>): ConversionOptionsBuilder\n```\n\n###### excludeSelectors()\n\nSet the list of CSS selectors for elements to exclude entirely from output.\n\n**Signature:**\n\n```typescript\nexcludeSelectors(selectors: Array<string>): ConversionOptionsBuilder\n```\n\n###### visitor()\n\nSet the visitor used during conversion.\n\n**Signature:**\n\n```typescript\nvisitor(visitor: VisitorHandle): ConversionOptionsBuilder\n```\n\n###### preprocessing()\n\nSet the pre-processing options applied to the HTML before conversion.\n\n**Signature:**\n\n```typescript\npreprocessing(preprocessing: PreprocessingOptions): ConversionOptionsBuilder\n```\n\n###### build()\n\nBuild the final `ConversionOptions`.\n\n**Signature:**\n\n```typescript\nbuild(): ConversionOptions\n```\n\n\n---\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `string | null` | `null` | Document title from `<title>` tag |\n| `description` | `string | null` | `null` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `Array<string>` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `string | null` | `null` | Document author from `<meta name=\"author\">` tag |\n| `canonicalUrl` | `string | null` | `null` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `baseHref` | `string | null` | `null` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `string | null` | `null` | Document language from `lang` attribute |\n| `textDirection` | `TextDirection | null` | `null` | Document text direction from `dir` attribute |\n| `openGraph` | `Record<string, string>` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitterCard` | `Record<string, string>` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `metaTags` | `Record<string, string>` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `string` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `number | null` | `null` | Index of the parent node (None for root nodes). |\n| `children` | `Array<number>` | — | Indices of child nodes in reading order. |\n| `annotations` | `Array<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `Record<string, string> | null` | `null` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n\n---\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `Array<DocumentNode>` | — | All nodes in document reading order. |\n| `sourceFormat` | `string | null` | `null` | The source format (always \"html\" for this library). |\n\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `string` | — | The text content of the cell. |\n| `row` | `number` | — | 0-indexed row position. |\n| `col` | `number` | — | 0-indexed column position. |\n| `rowSpan` | `number` | — | Number of rows this cell spans (default 1). |\n| `colSpan` | `number` | — | Number of columns this cell spans (default 1). |\n| `isHeader` | `boolean` | — | Whether this is a header cell (`<th>`). |\n\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `number` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `string` | — | Normalized text content of the header |\n| `id` | `string | null` | `null` | HTML id attribute if present |\n| `depth` | `number` | — | Document tree depth at the header element |\n| `htmlOffset` | `number` | — | Byte offset in original HTML document |\n\n##### Methods\n\n###### isValid()\n\nValidate that the header level is within valid range (1-6).\n\n**Returns:**\n\n`true` if level is 1-6, `false` otherwise.\n\n**Signature:**\n\n```typescript\nisValid(): boolean\n```\n\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `Array<HeaderMetadata>` | `[]` | Extracted header elements with hierarchy |\n| `links` | `Array<LinkMetadata>` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `Array<ImageMetadata>` | `[]` | Extracted images with source and dimensions |\n| `structuredData` | `Array<StructuredData>` | `[]` | Extracted structured data blocks |\n\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n### Methods\n\n#### visitElementStart()\n\nCalled before entering any element.\n\nThis is the first callback invoked for every HTML element, allowing\nvisitors to implement generic element handling before tag-specific logic.\n\n**Signature:**\n\n```typescript\nvisitElementStart(ctx: NodeContext): VisitResult\n```\n\n##### visitElementEnd()\n\nCalled after exiting any element.\n\nReceives the default markdown output that would be generated.\nVisitors can inspect or replace this output.\n\n**Signature:**\n\n```typescript\nvisitElementEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitText()\n\nVisit text nodes (most frequent callback - ~100+ per document).\n\n**Signature:**\n\n```typescript\nvisitText(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitLink()\n\nVisit anchor links `<a href=\"...\">`.\n\n**Signature:**\n\n```typescript\nvisitLink(ctx: NodeContext, href: string, text: string, title: string): VisitResult\n```\n\n###### visitImage()\n\nVisit images `<img src=\"...\">`.\n\n**Signature:**\n\n```typescript\nvisitImage(ctx: NodeContext, src: string, alt: string, title: string): VisitResult\n```\n\n###### visitHeading()\n\nVisit heading elements `<h1>` through `<h6>`.\n\n**Signature:**\n\n```typescript\nvisitHeading(ctx: NodeContext, level: number, text: string, id: string): VisitResult\n```\n\n###### visitCodeBlock()\n\nVisit code blocks `<pre><code>`.\n\n**Signature:**\n\n```typescript\nvisitCodeBlock(ctx: NodeContext, lang: string, code: string): VisitResult\n```\n\n###### visitCodeInline()\n\nVisit inline code `<code>`.\n\n**Signature:**\n\n```typescript\nvisitCodeInline(ctx: NodeContext, code: string): VisitResult\n```\n\n###### visitListItem()\n\nVisit list items `<li>`.\n\n**Signature:**\n\n```typescript\nvisitListItem(ctx: NodeContext, ordered: boolean, marker: string, text: string): VisitResult\n```\n\n###### visitListStart()\n\nCalled before processing a list `<ul>` or `<ol>`.\n\n**Signature:**\n\n```typescript\nvisitListStart(ctx: NodeContext, ordered: boolean): VisitResult\n```\n\n###### visitListEnd()\n\nCalled after processing a list `</ul>` or `</ol>`.\n\n**Signature:**\n\n```typescript\nvisitListEnd(ctx: NodeContext, ordered: boolean, output: string): VisitResult\n```\n\n###### visitTableStart()\n\nCalled before processing a table `<table>`.\n\n**Signature:**\n\n```typescript\nvisitTableStart(ctx: NodeContext): VisitResult\n```\n\n###### visitTableRow()\n\nVisit table rows `<tr>`.\n\n**Signature:**\n\n```typescript\nvisitTableRow(ctx: NodeContext, cells: Array<string>, isHeader: boolean): VisitResult\n```\n\n###### visitTableEnd()\n\nCalled after processing a table `</table>`.\n\n**Signature:**\n\n```typescript\nvisitTableEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitBlockquote()\n\nVisit blockquote elements `<blockquote>`.\n\n**Signature:**\n\n```typescript\nvisitBlockquote(ctx: NodeContext, content: string, depth: number): VisitResult\n```\n\n###### visitStrong()\n\nVisit strong/bold elements `<strong>`, `<b>`.\n\n**Signature:**\n\n```typescript\nvisitStrong(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitEmphasis()\n\nVisit emphasis/italic elements `<em>`, `<i>`.\n\n**Signature:**\n\n```typescript\nvisitEmphasis(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitStrikethrough()\n\nVisit strikethrough elements `<s>`, `<del>`, `<strike>`.\n\n**Signature:**\n\n```typescript\nvisitStrikethrough(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitUnderline()\n\nVisit underline elements `<u>`, `<ins>`.\n\n**Signature:**\n\n```typescript\nvisitUnderline(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitSubscript()\n\nVisit subscript elements `<sub>`.\n\n**Signature:**\n\n```typescript\nvisitSubscript(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitSuperscript()\n\nVisit superscript elements `<sup>`.\n\n**Signature:**\n\n```typescript\nvisitSuperscript(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitMark()\n\nVisit mark/highlight elements `<mark>`.\n\n**Signature:**\n\n```typescript\nvisitMark(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitLineBreak()\n\nVisit line break elements `<br>`.\n\n**Signature:**\n\n```typescript\nvisitLineBreak(ctx: NodeContext): VisitResult\n```\n\n###### visitHorizontalRule()\n\nVisit horizontal rule elements `<hr>`.\n\n**Signature:**\n\n```typescript\nvisitHorizontalRule(ctx: NodeContext): VisitResult\n```\n\n###### visitCustomElement()\n\nVisit custom elements (web components) or unknown tags.\n\n**Signature:**\n\n```typescript\nvisitCustomElement(ctx: NodeContext, tagName: string, html: string): VisitResult\n```\n\n###### visitDefinitionListStart()\n\nVisit definition list `<dl>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionListStart(ctx: NodeContext): VisitResult\n```\n\n###### visitDefinitionTerm()\n\nVisit definition term `<dt>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionTerm(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitDefinitionDescription()\n\nVisit definition description `<dd>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionDescription(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitDefinitionListEnd()\n\nCalled after processing a definition list `</dl>`.\n\n**Signature:**\n\n```typescript\nvisitDefinitionListEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n###### visitForm()\n\nVisit form elements `<form>`.\n\n**Signature:**\n\n```typescript\nvisitForm(ctx: NodeContext, action: string, method: string): VisitResult\n```\n\n###### visitInput()\n\nVisit input elements `<input>`.\n\n**Signature:**\n\n```typescript\nvisitInput(ctx: NodeContext, inputType: string, name: string, value: string): VisitResult\n```\n\n###### visitButton()\n\nVisit button elements `<button>`.\n\n**Signature:**\n\n```typescript\nvisitButton(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitAudio()\n\nVisit audio elements `<audio>`.\n\n**Signature:**\n\n```typescript\nvisitAudio(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitVideo()\n\nVisit video elements `<video>`.\n\n**Signature:**\n\n```typescript\nvisitVideo(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitIframe()\n\nVisit iframe elements `<iframe>`.\n\n**Signature:**\n\n```typescript\nvisitIframe(ctx: NodeContext, src: string): VisitResult\n```\n\n###### visitDetails()\n\nVisit details elements `<details>`.\n\n**Signature:**\n\n```typescript\nvisitDetails(ctx: NodeContext, open: boolean): VisitResult\n```\n\n###### visitSummary()\n\nVisit summary elements `<summary>`.\n\n**Signature:**\n\n```typescript\nvisitSummary(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitFigureStart()\n\nVisit figure elements `<figure>`.\n\n**Signature:**\n\n```typescript\nvisitFigureStart(ctx: NodeContext): VisitResult\n```\n\n###### visitFigcaption()\n\nVisit figcaption elements `<figcaption>`.\n\n**Signature:**\n\n```typescript\nvisitFigcaption(ctx: NodeContext, text: string): VisitResult\n```\n\n###### visitFigureEnd()\n\nCalled after processing a figure `</figure>`.\n\n**Signature:**\n\n```typescript\nvisitFigureEnd(ctx: NodeContext, output: string): VisitResult\n```\n\n\n---\n\n##### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `string` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `string | null` | `null` | Alternative text from alt attribute (for accessibility) |\n| `title` | `string | null` | `null` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Array<number> | null` | `null` | Image dimensions as (width, height) if available |\n| `imageType` | `ImageType` | — | Image type classification |\n| `attributes` | `Record<string, string>` | — | Additional HTML attributes |\n\n\n---\n\n##### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `string` | — | The href URL value |\n| `text` | `string` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `string | null` | `null` | Optional title attribute (often shown as tooltip) |\n| `linkType` | `LinkType` | — | Link type classification |\n| `rel` | `Array<string>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `Record<string, string>` | — | Additional HTML attributes |\n\n###### Methods\n\n###### classifyLink()\n\nClassify a link based on href value.\n\n**Returns:**\n\nAppropriate `LinkType` based on protocol and content.\n\n**Signature:**\n\n```typescript\nstatic classifyLink(href: string): LinkType\n```\n\n\n---\n\n##### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodeType` | `NodeType` | — | Coarse-grained node type classification |\n| `tagName` | `string` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `Record<string, string>` | — | All HTML attributes as key-value pairs |\n| `depth` | `number` | — | Depth in the DOM tree (0 = root) |\n| `indexInParent` | `number` | — | Index among siblings (0-based) |\n| `parentTag` | `string | null` | `null` | Parent element's tag name (None if root) |\n| `isInline` | `boolean` | — | Whether this element is treated as inline vs block |\n\n\n---\n\n##### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `boolean` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset.Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `removeNavigation` | `boolean` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `removeForms` | `boolean` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n###### Methods\n\n###### default()\n\n**Signature:**\n\n```typescript\nstatic default(): PreprocessingOptions\n```\n\n###### applyUpdate()\n\nApply a partial update to these preprocessing options.\n\nAny specified fields in the update will override the current values.\nUnspecified fields (None) are left unchanged.\n\n**Signature:**\n\n```typescript\napplyUpdate(update: PreprocessingOptionsUpdate): void\n```\n\n###### fromUpdate()\n\nCreate new preprocessing options from a partial update.\n\nCreates a new `PreprocessingOptions` struct with defaults, then applies the update.\nFields not specified in the update keep their default values.\n\n**Returns:**\n\nNew `PreprocessingOptions` with specified updates applied to defaults\n\n**Signature:**\n\n```typescript\nstatic fromUpdate(update: PreprocessingOptionsUpdate): PreprocessingOptions\n```\n\n###### from()\n\n**Signature:**\n\n```typescript\nstatic from(update: PreprocessingOptionsUpdate): PreprocessingOptions\n```\n\n\n---\n\n##### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `string` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n\n---\n\n##### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `dataType` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `rawJson` | `string` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schemaType` | `string | null` | `null` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n\n---\n\n##### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `string` | — | The markdown rendering of this table. |\n\n\n---\n\n##### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `number` | — | Number of rows. |\n| `cols` | `number` | — | Number of columns. |\n| `cells` | `Array<GridCell>` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n\n---\n\n##### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `number` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `number` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n\n---\n\n##### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n\n---\n\n#### Enums\n\n##### TextDirection\n\nText directionality of document content.\n\nCorresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n| Value | Description |\n|-------|-------------|\n| `LeftToRight` | Left-to-right text flow (default for Latin scripts) |\n| `RightToLeft` | Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) |\n| `Auto` | Automatic directionality detection |\n\n\n---\n\n##### LinkType\n\nLink classification based on href value and document context.\n\nUsed to categorize links during extraction for filtering and analysis.\n\n| Value | Description |\n|-------|-------------|\n| `Anchor` | Anchor link within same document (href starts with #) |\n| `Internal` | Internal link within same domain |\n| `External` | External link to different domain |\n| `Email` | Email link (mailto:) |\n| `Phone` | Phone link (tel:) |\n| `Other` | Other protocol or unclassifiable |\n\n\n---\n\n##### ImageType\n\nImage source classification for proper handling and processing.\n\nDetermines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n| Value | Description |\n|-------|-------------|\n| `DataUri` | Data URI embedded image (base64 or other encoding) |\n| `InlineSvg` | Inline SVG element |\n| `External` | External image URL (http/https) |\n| `Relative` | Relative image path |\n\n\n---\n\n##### StructuredDataType\n\nStructured data format type.\n\nIdentifies the schema/format used for structured data markup.\n\n| Value | Description |\n|-------|-------------|\n| `JsonLd` | JSON-LD (JSON for Linking Data) script blocks |\n| `Microdata` | HTML5 Microdata attributes (itemscope, itemtype, itemprop) |\n| `RDFa` | RDF in Attributes (RDFa) markup |\n\n\n---\n\n##### PreprocessingPreset\n\nHTML preprocessing aggressiveness level.\n\nControls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n| Value | Description |\n|-------|-------------|\n| `Minimal` | Minimal cleanup. Remove only essential noise (scripts, styles). |\n| `Standard` | Standard cleanup. Default. Removes navigation, forms, and other auxiliary content. |\n| `Aggressive` | Aggressive cleanup. Remove extensive non-content elements and structure. |\n\n\n---\n\n##### HeadingStyle\n\nHeading style options for Markdown output.\n\nControls how headings (h1-h6) are rendered in the output Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Underlined` | Underlined style (=== for h1, --- for h2). |\n| `Atx` | ATX style (# for h1, ## for h2, etc.). Default. |\n| `AtxClosed` | ATX closed style (# title #, with closing hashes). |\n\n\n---\n\n##### ListIndentType\n\nList indentation character type.\n\nControls whether list items are indented with spaces or tabs.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Use spaces for indentation. Default. Width controlled by `list_indent_width`. |\n| `Tabs` | Use tabs for indentation. |\n\n\n---\n\n##### WhitespaceMode\n\nWhitespace handling strategy during conversion.\n\nDetermines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n| Value | Description |\n|-------|-------------|\n| `Normalized` | Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior. |\n| `Strict` | Preserve all whitespace exactly as it appears in the HTML. |\n\n\n---\n\n##### NewlineStyle\n\nLine break syntax in Markdown output.\n\nControls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n| Value | Description |\n|-------|-------------|\n| `Spaces` | Two trailing spaces at end of line. Default. Standard Markdown syntax. |\n| `Backslash` | Backslash at end of line. Alternative Markdown syntax. |\n\n\n---\n\n##### CodeBlockStyle\n\nCode block fence style in Markdown output.\n\nDetermines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n| Value | Description |\n|-------|-------------|\n| `Indented` | Indented code blocks (4 spaces). `CommonMark` standard. |\n| `Backticks` | Fenced code blocks with backticks (```). Default (GFM). Supports language hints. |\n| `Tildes` | Fenced code blocks with tildes (~~~). Supports language hints. |\n\n\n---\n\n##### HighlightStyle\n\nHighlight rendering style for `<mark>` elements.\n\nControls how highlighted text is rendered in Markdown output.\n\n| Value | Description |\n|-------|-------------|\n| `DoubleEqual` | Double equals syntax (==text==). Default. Pandoc-compatible. |\n| `Html` | Preserve as HTML (==text==). Original HTML tag. |\n| `Bold` | Render as bold (**text**). Uses strong emphasis. |\n| `None` | Strip formatting, render as plain text. No markup. |\n\n\n---\n\n##### LinkStyle\n\nLink rendering style in Markdown output.\n\nControls whether links and images use inline `[text](url)` syntax or\nreference-style `[text][1]` syntax with definitions collected at the end.\n\n| Value | Description |\n|-------|-------------|\n| `Inline` | Inline links: `[text](url)`. Default. |\n| `Reference` | Reference-style links: `[text][1]` with `[1]: url` at end of document. |\n\n\n---\n\n##### OutputFormat\n\nOutput format for conversion.\n\nSpecifies the target markup language format for the conversion output.\n\n| Value | Description |\n|-------|-------------|\n| `Markdown` | Standard Markdown (CommonMark compatible). Default. |\n| `Djot` | Djot lightweight markup language. |\n| `Plain` | Plain text output (no markup, visible text only). |\n\n\n---\n\n##### NodeContent\n\nThe semantic content type of a document node.\n\nUses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Heading` | A heading element (h1-h6). — Fields: `level`: `number`, `text`: `string` |\n| `Paragraph` | A paragraph of text. — Fields: `text`: `string` |\n| `List` | A list container (ordered or unordered). Children are `ListItem` nodes. — Fields: `ordered`: `boolean` |\n| `ListItem` | A single list item. — Fields: `text`: `string` |\n| `Table` | A table with structured cell data. — Fields: `grid`: `TableGrid` |\n| `Image` | An image element. — Fields: `description`: `string`, `src`: `string`, `imageIndex`: `number` |\n| `Code` | A code block or inline code. — Fields: `text`: `string`, `language`: `string` |\n| `Quote` | A block quote container. |\n| `DefinitionList` | A definition list container. |\n| `DefinitionItem` | A definition list entry with term and description. — Fields: `term`: `string`, `definition`: `string` |\n| `RawBlock` | A raw block preserved as-is (e.g. `<script>`, `<style>` content). — Fields: `format`: `string`, `content`: `string` |\n| `MetadataBlock` | A block of key-value metadata pairs (from `<head>` meta tags). — Fields: `entries`: `Array<string>` |\n| `Group` | A section grouping container (auto-generated from heading hierarchy). — Fields: `label`: `string`, `headingLevel`: `number`, `headingText`: `string` |\n\n\n---\n\n##### AnnotationKind\n\nThe type of an inline text annotation.\n\nUses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n| Value | Description |\n|-------|-------------|\n| `Bold` | Bold / strong emphasis. |\n| `Italic` | Italic / emphasis. |\n| `Underline` | Underline. |\n| `Strikethrough` | Strikethrough / deleted text. |\n| `Code` | Inline code. |\n| `Subscript` | Subscript text. |\n| `Superscript` | Superscript text. |\n| `Highlight` | Highlighted / marked text. |\n| `Link` | A hyperlink. — Fields: `url`: `string`, `title`: `string` |\n\n\n---\n\n##### WarningKind\n\nCategories of processing warnings.\n\n| Value | Description |\n|-------|-------------|\n| `ImageExtractionFailed` | An image could not be extracted (e.g. invalid data URI, unsupported format). |\n| `EncodingFallback` | The input encoding was not recognized; fell back to UTF-8. |\n| `TruncatedInput` | The input was truncated due to size limits. |\n| `MalformedHtml` | The HTML was malformed but processing continued with best effort. |\n| `SanitizationApplied` | Sanitization was applied to remove potentially unsafe content. |\n| `DepthLimitExceeded` | DOM traversal was truncated because max_depth was exceeded. |\n\n\n---\n\n##### NodeType\n\nNode type enumeration covering all HTML element types.\n\nThis enum categorizes all HTML elements that the converter recognizes,\nproviding a coarse-grained classification for visitor dispatch.\n\n| Value | Description |\n|-------|-------------|\n| `Text` | Text node (most frequent - 100+ per document) |\n| `Element` | Generic element node |\n| `Heading` | Heading elements (h1-h6) |\n| `Paragraph` | Paragraph element |\n| `Div` | Generic div container |\n| `Blockquote` | Blockquote element |\n| `Pre` | Preformatted text block |\n| `Hr` | Horizontal rule |\n| `List` | Ordered or unordered list (ul, ol) |\n| `ListItem` | List item (li) |\n| `DefinitionList` | Definition list (dl) |\n| `DefinitionTerm` | Definition term (dt) |\n| `DefinitionDescription` | Definition description (dd) |\n| `Table` | Table element |\n| `TableRow` | Table row (tr) |\n| `TableCell` | Table cell (td, th) |\n| `TableHeader` | Table header cell (th) |\n| `TableBody` | Table body (tbody) |\n| `TableHead` | Table head (thead) |\n| `TableFoot` | Table foot (tfoot) |\n| `Link` | Anchor link (a) |\n| `Image` | Image (img) |\n| `Strong` | Strong/bold (strong, b) |\n| `Em` | Emphasis/italic (em, i) |\n| `Code` | Inline code (code) |\n| `Strikethrough` | Strikethrough (s, del, strike) |\n| `Underline` | Underline (u, ins) |\n| `Subscript` | Subscript (sub) |\n| `Superscript` | Superscript (sup) |\n| `Mark` | Mark/highlight (mark) |\n| `Small` | Small text (small) |\n| `Br` | Line break (br) |\n| `Span` | Span element |\n| `Article` | Article element |\n| `Section` | Section element |\n| `Nav` | Navigation element |\n| `Aside` | Aside element |\n| `Header` | Header element |\n| `Footer` | Footer element |\n| `Main` | Main element |\n| `Figure` | Figure element |\n| `Figcaption` | Figure caption |\n| `Time` | Time element |\n| `Details` | Details element |\n| `Summary` | Summary element |\n| `Form` | Form element |\n| `Input` | Input element |\n| `Select` | Select element |\n| `Option` | Option element |\n| `Button` | Button element |\n| `Textarea` | Textarea element |\n| `Label` | Label element |\n| `Fieldset` | Fieldset element |\n| `Legend` | Legend element |\n| `Audio` | Audio element |\n| `Video` | Video element |\n| `Picture` | Picture element |\n| `Source` | Source element |\n| `Iframe` | Iframe element |\n| `Svg` | SVG element |\n| `Canvas` | Canvas element |\n| `Ruby` | Ruby annotation |\n| `Rt` | Ruby text |\n| `Rp` | Ruby parenthesis |\n| `Abbr` | Abbreviation |\n| `Kbd` | Keyboard input |\n| `Samp` | Sample output |\n| `Var` | Variable |\n| `Cite` | Citation |\n| `Q` | Quote |\n| `Del` | Deleted text |\n| `Ins` | Inserted text |\n| `Data` | Data element |\n| `Meter` | Meter element |\n| `Progress` | Progress element |\n| `Output` | Output element |\n| `Template` | Template element |\n| `Slot` | Slot element |\n| `Html` | HTML root element |\n| `Head` | Head element |\n| `Body` | Body element |\n| `Title` | Title element |\n| `Meta` | Meta element |\n| `LinkTag` | Link element (not anchor) |\n| `Style` | Style element |\n| `Script` | Script element |\n| `Base` | Base element |\n| `Custom` | Custom element (web components) or unknown tag |\n\n\n---\n\n##### VisitResult\n\nResult of a visitor callback.\n\nAllows visitors to control the conversion flow by either proceeding\nwith default behavior, providing custom output, skipping elements,\npreserving HTML, or signaling errors.\n\n| Value | Description |\n|-------|-------------|\n| `Continue` | Continue with default conversion behavior |\n| `Custom` | Replace default output with custom markdown The visitor takes full responsibility for the markdown output of this node and its children. — Fields: `0`: `string` |\n| `Skip` | Skip this element entirely (don't output anything) The element and all its children are ignored in the output. |\n| `PreserveHtml` | Preserve original HTML (don't convert to markdown) The element's raw HTML is included verbatim in the output. |\n| `Error` | Stop conversion with an error The conversion process halts and returns this error message. — Fields: `0`: `string` |\n\n\n---\n\n#### Errors\n\n##### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\nErrors are thrown as plain `Error` objects with descriptive messages.\n\n| Variant | Description |\n|---------|-------------|\n| `ParseError` | HTML parsing error |\n| `SanitizationError` | HTML sanitization error |\n| `ConfigError` | Invalid configuration |\n| `IoError` | I/O error |\n| `Panic` | Internal error caught during conversion |\n| `InvalidInput` | Invalid input data |\n| `Other` | Generic conversion error |\n\n\n---\n"
  },
  {
    "path": "docs/reference/configuration.md",
    "content": "---\ntitle: \"Configuration Reference\"\n---\n\n## Configuration Reference\n\nThis page documents all configuration types and their defaults across all languages.\n\n### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `str | None` | `None` | Document title from `<title>` tag |\n| `description` | `str | None` | `None` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `list[str]` | `[]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `str | None` | `None` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `str | None` | `None` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `str | None` | `None` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `str | None` | `None` | Document language from `lang` attribute |\n| `text_direction` | `TextDirection | None` | `None` | Document text direction from `dir` attribute |\n| `open_graph` | `dict[str, str]` | `{}` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `dict[str, str]` | `{}` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `dict[str, str]` | `{}` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n---\n\n### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `list[HeaderMetadata]` | `[]` | Extracted header elements with hierarchy |\n| `links` | `list[LinkMetadata]` | `[]` | Extracted hyperlinks with type classification |\n| `images` | `list[ImageMetadata]` | `[]` | Extracted images with source and dimensions |\n| `structured_data` | `list[StructuredData]` | `[]` | Extracted structured data blocks |\n\n---\n\n### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `HeadingStyle.ATX` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `ListIndentType.SPACES` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `int` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `str` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `str` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `bool` | `False` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `bool` | `False` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `bool` | `False` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `bool` | `False` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `str` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `True` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `bool` | `False` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `bool` | `False` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `HighlightStyle.DOUBLE_EQUAL` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `bool` | `True` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `WhitespaceMode.NORMALIZED` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `bool` | `False` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `False` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `int` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `bool` | `False` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `str` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `str` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `NewlineStyle.SPACES` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `CodeBlockStyle.BACKTICKS` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `list[str]` | `[]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `str` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `False` | Emit debug information during conversion. |\n| `strip_tags` | `list[str]` | `[]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `list[str]` | `[]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `bool` | `False` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `LinkStyle.INLINE` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `OutputFormat.MARKDOWN` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `bool` | `False` | Include structured document tree in result. |\n| `extract_images` | `bool` | `False` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `int` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `bool` | `False` | Capture SVG elements as images. |\n| `infer_dimensions` | `bool` | `True` | Infer image dimensions from data. |\n| `max_depth` | `int | None` | `None` | Maximum DOM traversal depth. `None` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `list[str]` | `[]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `HtmlVisitor(Protocol)` | `None` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n---\n\n### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `True` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset.STANDARD` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `bool` | `True` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `bool` | `True` | Remove form elements (forms, inputs, buttons, etc.) |\n\n---\n\n### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `str | None` | `None` | Converted text output (markdown, djot, or plain text). `None` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `DocumentStructure | None` | `None` | Structured document tree with semantic elements. Populated when `include_document_structure` is `True` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `list[TableData]` | `[]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `list[str]` | `[]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `True` in options. |\n| `warnings` | `list[ProcessingWarning]` | `[]` | Non-fatal processing warnings. |\n\n---\n\n### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `int` | — | Number of rows. |\n| `cols` | `int` | — | Number of columns. |\n| `cells` | `list[GridCell]` | `[]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n---\n"
  },
  {
    "path": "docs/reference/errors.md",
    "content": "---\ntitle: \"Error Reference\"\n---\n\n## Error Reference\n\nAll error types thrown by the library across all languages.\n\n### ConversionError\n\nErrors that can occur during HTML to Markdown conversion.\n\n| Variant | Message | Description |\n|---------|---------|-------------|\n| `ParseError` | HTML parsing error: {0} | HTML parsing error |\n| `SanitizationError` | Sanitization error: {0} | HTML sanitization error |\n| `ConfigError` | Invalid configuration: {0} | Invalid configuration |\n| `IoError` | I/O error: {0} | I/O error |\n| `Panic` | Internal panic: {0} | Internal error caught during conversion |\n| `InvalidInput` | Invalid input: {0} | Invalid input data |\n| `Other` | Conversion error: {0} | Generic conversion error |\n\n---\n"
  },
  {
    "path": "docs/reference/types.md",
    "content": "---\ntitle: \"Types Reference\"\n---\n\n## Types Reference\n\nAll types defined by the library, grouped by category. Types are shown using Rust as the canonical representation.\n\n### Result Types\n\n#### ConversionResult\n\nThe primary result of HTML conversion and extraction.\n\nContains the converted text output, optional structured document tree,\nmetadata, extracted tables, images, and processing warnings.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `Option<String>` | `Default::default()` | Converted text output (markdown, djot, or plain text). `None` when `output_format` is set to `OutputFormat.None`, indicating extraction-only mode. |\n| `document` | `Option<DocumentStructure>` | `Default::default()` | Structured document tree with semantic elements. Populated when `include_document_structure` is `true` in options. |\n| `metadata` | `HtmlMetadata` | — | Extracted HTML metadata (title, OG, links, images, structured data). |\n| `tables` | `Vec<TableData>` | `vec![]` | Extracted tables with structured cell data and markdown representation. |\n| `images` | `Vec<String>` | `vec![]` | Extracted inline images (data URIs and SVGs). Populated when `extract_images` is `true` in options. |\n| `warnings` | `Vec<ProcessingWarning>` | `vec![]` | Non-fatal processing warnings. |\n\n---\n\n### Configuration Types\n\nSee [Configuration Reference](configuration.md) for detailed defaults and language-specific representations.\n\n#### ConversionOptions\n\nMain conversion options for HTML to Markdown conversion.\n\nUse `ConversionOptions.builder()` to construct, or `the default constructor` for defaults.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `heading_style` | `HeadingStyle` | `HeadingStyle::Atx` | Heading style to use in Markdown output (ATX `#` or Setext underline). |\n| `list_indent_type` | `ListIndentType` | `ListIndentType::Spaces` | How to indent nested list items (spaces or tab). |\n| `list_indent_width` | `usize` | `2` | Number of spaces (or tabs) to use for each level of list indentation. |\n| `bullets` | `String` | `\"-*+\"` | Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`). |\n| `strong_em_symbol` | `String` | `\"*\"` | Character used for bold/italic emphasis markers (`*` or `_`). |\n| `escape_asterisks` | `bool` | `false` | Escape `*` characters in plain text to avoid unintended bold/italic. |\n| `escape_underscores` | `bool` | `false` | Escape `_` characters in plain text to avoid unintended bold/italic. |\n| `escape_misc` | `bool` | `false` | Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text. |\n| `escape_ascii` | `bool` | `false` | Escape ASCII characters that have special meaning in certain Markdown dialects. |\n| `code_language` | `String` | `\"\"` | Default language annotation for fenced code blocks that have no language hint. |\n| `autolinks` | `bool` | `true` | Automatically convert bare URLs into Markdown autolinks. |\n| `default_title` | `bool` | `false` | Emit a default title when no `<title>` tag is present. |\n| `br_in_tables` | `bool` | `false` | Render `<br>` elements inside table cells as literal line breaks. |\n| `highlight_style` | `HighlightStyle` | `HighlightStyle::DoubleEqual` | Style used for `<mark>` / highlighted text (e.g. `==text==`). |\n| `extract_metadata` | `bool` | `true` | Extract `<meta>` and `<head>` information into the result metadata. |\n| `whitespace_mode` | `WhitespaceMode` | `WhitespaceMode::Normalized` | Controls how whitespace is normalised during conversion. |\n| `strip_newlines` | `bool` | `false` | Strip all newlines from the output, producing a single-line result. |\n| `wrap` | `bool` | `false` | Wrap long lines at `wrap_width` characters. |\n| `wrap_width` | `usize` | `80` | Maximum line width when `wrap` is enabled (default `80`). |\n| `convert_as_inline` | `bool` | `false` | Treat the entire document as inline content (no block-level wrappers). |\n| `sub_symbol` | `String` | `\"\"` | Markdown notation for subscript text (e.g. `\"~\"`). |\n| `sup_symbol` | `String` | `\"\"` | Markdown notation for superscript text (e.g. `\"^\"`). |\n| `newline_style` | `NewlineStyle` | `NewlineStyle::Spaces` | How to encode hard line breaks (`<br>`) in Markdown. |\n| `code_block_style` | `CodeBlockStyle` | `CodeBlockStyle::Backticks` | Style used for fenced code blocks (backticks or tilde). |\n| `keep_inline_images_in` | `Vec<String>` | `vec![]` | HTML tag names whose `<img>` children are kept inline instead of block. |\n| `preprocessing` | `PreprocessingOptions` | — | Pre-processing options applied to the HTML before conversion. |\n| `encoding` | `String` | `\"utf-8\"` | Expected character encoding of the input HTML (default `\"utf-8\"`). |\n| `debug` | `bool` | `false` | Emit debug information during conversion. |\n| `strip_tags` | `Vec<String>` | `vec![]` | HTML tag names whose content is stripped from the output entirely. |\n| `preserve_tags` | `Vec<String>` | `vec![]` | HTML tag names that are preserved verbatim in the output. |\n| `skip_images` | `bool` | `false` | Skip conversion of `<img>` elements (omit images from output). |\n| `link_style` | `LinkStyle` | `LinkStyle::Inline` | Link rendering style (inline or reference). |\n| `output_format` | `OutputFormat` | `OutputFormat::Markdown` | Target output format (Markdown, plain text, etc.). |\n| `include_document_structure` | `bool` | `false` | Include structured document tree in result. |\n| `extract_images` | `bool` | `false` | Extract inline images from data URIs and SVGs. |\n| `max_image_size` | `u64` | `5242880` | Maximum decoded image size in bytes (default 5MB). |\n| `capture_svg` | `bool` | `false` | Capture SVG elements as images. |\n| `infer_dimensions` | `bool` | `true` | Infer image dimensions from data. |\n| `max_depth` | `Option<usize>` | `None` | Maximum DOM traversal depth. `None` means unlimited. When set, subtrees beyond this depth are silently truncated. |\n| `exclude_selectors` | `Vec<String>` | `vec![]` | CSS selectors for elements to exclude entirely (element + all content). Unlike `strip_tags` (which removes the tag wrapper but keeps children), excluded elements and all their descendants are dropped from the output. Supports any CSS selector that `tl` supports: tag names, `.class`, `#id`, `[attribute]`, etc. Invalid selectors are silently skipped at conversion time. Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]` |\n| `visitor` | `Option<VisitorHandle>` | `None` | Optional visitor for custom traversal logic. When set, the visitor's callbacks are invoked for matching HTML elements during conversion, allowing custom output, skipping, or HTML preservation. See `crate.visitor.HtmlVisitor`. |\n\n---\n\n#### PreprocessingOptions\n\nHTML preprocessing options for document cleanup before conversion.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `enabled` | `bool` | `true` | Enable HTML preprocessing globally |\n| `preset` | `PreprocessingPreset` | `PreprocessingPreset::Standard` | Preprocessing preset level (Minimal, Standard, Aggressive) |\n| `remove_navigation` | `bool` | `true` | Remove navigation elements (nav, breadcrumbs, menus, sidebars) |\n| `remove_forms` | `bool` | `true` | Remove form elements (forms, inputs, buttons, etc.) |\n\n---\n\n#### TableGrid\n\nA structured table grid with cell-level data including spans.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `rows` | `u32` | — | Number of rows. |\n| `cols` | `u32` | — | Number of columns. |\n| `cells` | `Vec<GridCell>` | `vec![]` | All cells in the table (may be fewer than rows*cols due to spans). |\n\n---\n\n### Metadata Types\n\n#### DocumentMetadata\n\nDocument-level metadata extracted from `<head>` and top-level elements.\n\nContains all metadata typically used by search engines, social media platforms,\nand browsers for document indexing and presentation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `title` | `Option<String>` | `Default::default()` | Document title from `<title>` tag |\n| `description` | `Option<String>` | `Default::default()` | Document description from `<meta name=\"description\">` tag |\n| `keywords` | `Vec<String>` | `vec![]` | Document keywords from `<meta name=\"keywords\">` tag, split on commas |\n| `author` | `Option<String>` | `Default::default()` | Document author from `<meta name=\"author\">` tag |\n| `canonical_url` | `Option<String>` | `Default::default()` | Canonical URL from `<link rel=\"canonical\">` tag |\n| `base_href` | `Option<String>` | `Default::default()` | Base URL from `<base href=\"\">` tag for resolving relative URLs |\n| `language` | `Option<String>` | `Default::default()` | Document language from `lang` attribute |\n| `text_direction` | `Option<TextDirection>` | `Default::default()` | Document text direction from `dir` attribute |\n| `open_graph` | `HashMap<String, String>` | `HashMap::new()` | Open Graph metadata (og:* properties) for social media Keys like \"title\", \"description\", \"image\", \"url\", etc. |\n| `twitter_card` | `HashMap<String, String>` | `HashMap::new()` | Twitter Card metadata (twitter:* properties) Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc. |\n| `meta_tags` | `HashMap<String, String>` | `HashMap::new()` | Additional meta tags not covered by specific fields Keys are meta name/property attributes, values are content |\n\n---\n\n#### HeaderMetadata\n\nHeader element metadata with hierarchy tracking.\n\nCaptures heading elements (h1-h6) with their text content, identifiers,\nand position in the document structure.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `level` | `u8` | — | Header level: 1 (h1) through 6 (h6) |\n| `text` | `String` | — | Normalized text content of the header |\n| `id` | `Option<String>` | `None` | HTML id attribute if present |\n| `depth` | `usize` | — | Document tree depth at the header element |\n| `html_offset` | `usize` | — | Byte offset in original HTML document |\n\n---\n\n#### LinkMetadata\n\nHyperlink metadata with categorization and attributes.\n\nRepresents `<a>` elements with parsed href values, text content, and link type classification.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `href` | `String` | — | The href URL value |\n| `text` | `String` | — | Link text content (normalized, concatenated if mixed with elements) |\n| `title` | `Option<String>` | `None` | Optional title attribute (often shown as tooltip) |\n| `link_type` | `LinkType` | — | Link type classification |\n| `rel` | `Vec<String>` | — | Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") |\n| `attributes` | `HashMap<String, String>` | — | Additional HTML attributes |\n\n---\n\n#### ImageMetadata\n\nImage metadata with source and dimensions.\n\nCaptures `<img>` elements and inline `<svg>` elements with metadata\nfor image analysis and optimization.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `src` | `String` | — | Image source (URL, data URI, or SVG content identifier) |\n| `alt` | `Option<String>` | `None` | Alternative text from alt attribute (for accessibility) |\n| `title` | `Option<String>` | `None` | Title attribute (often shown as tooltip) |\n| `dimensions` | `Vec<u32>` | `None` | Image dimensions as (width, height) if available |\n| `image_type` | `ImageType` | — | Image type classification |\n| `attributes` | `HashMap<String, String>` | — | Additional HTML attributes |\n\n---\n\n#### HtmlMetadata\n\nComprehensive metadata extraction result from HTML document.\n\nContains all extracted metadata types in a single structure,\nsuitable for serialization and transmission across language boundaries.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `document` | `DocumentMetadata` | — | Document-level metadata (title, description, canonical, etc.) |\n| `headers` | `Vec<HeaderMetadata>` | `vec![]` | Extracted header elements with hierarchy |\n| `links` | `Vec<LinkMetadata>` | `vec![]` | Extracted hyperlinks with type classification |\n| `images` | `Vec<ImageMetadata>` | `vec![]` | Extracted images with source and dimensions |\n| `structured_data` | `Vec<StructuredData>` | `vec![]` | Extracted structured data blocks |\n\n---\n\n### Document Structure\n\n#### DocumentStructure\n\nA structured document tree representing the semantic content of an HTML document.\n\nUses a flat node array with index-based parent/child references for efficient traversal.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `nodes` | `Vec<DocumentNode>` | — | All nodes in document reading order. |\n| `source_format` | `Option<String>` | `None` | The source format (always \"html\" for this library). |\n\n---\n\n#### DocumentNode\n\nA single node in the document tree.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `id` | `String` | — | Deterministic node identifier. |\n| `content` | `NodeContent` | — | The semantic content of this node. |\n| `parent` | `Option<u32>` | `None` | Index of the parent node (None for root nodes). |\n| `children` | `Vec<u32>` | — | Indices of child nodes in reading order. |\n| `annotations` | `Vec<TextAnnotation>` | — | Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. |\n| `attributes` | `HashMap<String, String>` | `None` | Format-specific attributes (e.g. class, id, data-* attributes). |\n\n---\n\n#### GridCell\n\nA single cell in a table grid.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `content` | `String` | — | The text content of the cell. |\n| `row` | `u32` | — | 0-indexed row position. |\n| `col` | `u32` | — | 0-indexed column position. |\n| `row_span` | `u32` | — | Number of rows this cell spans (default 1). |\n| `col_span` | `u32` | — | Number of columns this cell spans (default 1). |\n| `is_header` | `bool` | — | Whether this is a header cell (`<th>`). |\n\n---\n\n#### TableData\n\nA top-level extracted table with both structured data and markdown representation.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `grid` | `TableGrid` | — | The structured table grid. |\n| `markdown` | `String` | — | The markdown rendering of this table. |\n\n---\n\n#### NodeContext\n\nContext information passed to all visitor methods.\n\nProvides comprehensive metadata about the current node being visited,\nincluding its type, attributes, position in the DOM tree, and parent context.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `node_type` | `NodeType` | — | Coarse-grained node type classification |\n| `tag_name` | `String` | — | Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\") |\n| `attributes` | `HashMap<String, String>` | — | All HTML attributes as key-value pairs |\n| `depth` | `usize` | — | Depth in the DOM tree (0 = root) |\n| `index_in_parent` | `usize` | — | Index among siblings (0-based) |\n| `parent_tag` | `Option<String>` | `None` | Parent element's tag name (None if root) |\n| `is_inline` | `bool` | — | Whether this element is treated as inline vs block |\n\n---\n\n### Other Types\n\n#### StructuredData\n\nStructured data block (JSON-LD, Microdata, or RDFa).\n\nRepresents machine-readable structured data found in the document.\nJSON-LD blocks are collected as raw JSON strings for flexibility.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `data_type` | `StructuredDataType` | — | Type of structured data (JSON-LD, Microdata, RDFa) |\n| `raw_json` | `String` | — | Raw JSON string (for JSON-LD) or serialized representation |\n| `schema_type` | `Option<String>` | `None` | Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") |\n\n---\n\n#### ConversionOptionsBuilder\n\nBuilder for `ConversionOptions`.\n\nAll fields start with default values. Call `.build()` to produce the final options.\n\n*Opaque type — fields are not directly accessible.*\n\n---\n\n#### TextAnnotation\n\nAn inline text annotation with byte-range offsets.\n\nAnnotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `start` | `u32` | — | Start byte offset (inclusive) into the parent node's text. |\n| `end` | `u32` | — | End byte offset (exclusive) into the parent node's text. |\n| `kind` | `AnnotationKind` | — | The type of annotation. |\n\n---\n\n#### ProcessingWarning\n\nA non-fatal warning generated during HTML processing.\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `message` | `String` | — | Human-readable warning message. |\n| `kind` | `WarningKind` | — | The category of warning. |\n\n---\n\n#### VisitorHandle\n\nType alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n\nThis allows visitors to be passed around and shared while still being mutable.\n\n*Opaque type — fields are not directly accessible.*\n\n---\n\n#### HtmlVisitor\n\nVisitor trait for HTML→Markdown conversion.\n\nImplement this trait to customize the conversion behavior for any HTML element type.\nAll methods have default implementations that return `VisitResult.Continue`, allowing\nselective override of only the elements you care about.\n\n## Method Naming Convention\n\n- `visit_*_start`: Called before entering an element (pre-order traversal)\n- `visit_*_end`: Called after exiting an element (post-order traversal)\n- `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)\n\n## Execution Order\n\nFor a typical element like `<div><p>text</p></div>`:\n\n1. `visit_element_start` for `<div>`\n2. `visit_element_start` for `<p>`\n3. `visit_text` for \"text\"\n4. `visit_element_end` for `<p>`\n5. `visit_element_end` for `</div>`\n\n## Performance Notes\n\n- `visit_text` is the most frequently called method (~100+ times per document)\n- Return `VisitResult.Continue` quickly for elements you don't need to customize\n- Avoid heavy computation in visitor methods; consider caching if needed\n\n*Opaque type — fields are not directly accessible.*\n\n---\n"
  },
  {
    "path": "docs/snippets/c/getting-started/basic_usage.md",
    "content": "```c\n#include \"html_to_markdown.h\"\n#include <stdio.h>\n\nint main(void) {\n    const char *html = \"<h1>Hello</h1><p>World</p>\";\n    /* Returns JSON: {\"content\":\"...\",\"metadata\":null,\"tables\":null} */\n    char *json = html_to_markdown_convert(html, NULL);\n    if (json) {\n        /* Parse JSON to extract content field */\n        printf(\"%s\\n\", json);\n        html_to_markdown_free_string(json);\n    }\n    return 0;\n}\n```\n"
  },
  {
    "path": "docs/snippets/c/getting-started/with_options.md",
    "content": "```c\n#include \"html_to_markdown.h\"\n#include <stdio.h>\n#include <string.h>\n\nint main(void) {\n    const char *html = \"<h1>Title</h1><p>Paragraph</p>\";\n    const char *options_json = \"{\\\"heading_style\\\":\\\"atx\\\"}\";\n\n    /* Returns JSON: {\"content\":\"...\",\"metadata\":null,\"tables\":null} */\n    char *json = html_to_markdown_convert_with_len(\n        html, strlen(html), options_json, strlen(options_json));\n    if (json) {\n        /* Parse JSON to extract content field */\n        printf(\"%s\\n\", json);\n        html_to_markdown_free_string(json);\n    }\n    return 0;\n}\n```\n"
  },
  {
    "path": "docs/snippets/c/metadata/basic_extraction.md",
    "content": "```c\n#include \"html_to_markdown.h\"\n#include <stdio.h>\n#include <string.h>\n\nint main(void) {\n    const char *html = \"<html><head><title>Page</title></head>\"\n                       \"<body><h1>Hello</h1></body></html>\";\n    const char *options_json = \"{\\\"extract_metadata\\\":true}\";\n\n    /* Returns JSON: {\"content\":\"...\",\"metadata\":{...},\"tables\":null} */\n    char *json = html_to_markdown_convert_with_len(\n        html, strlen(html), options_json, strlen(options_json));\n    if (json) {\n        /* Parse JSON to access content and metadata fields */\n        printf(\"%s\\n\", json);\n        html_to_markdown_free_string(json);\n    }\n    return 0;\n}\n```\n"
  },
  {
    "path": "docs/snippets/c/table-extraction/basic_extraction.md",
    "content": "```c\n#include \"html_to_markdown.h\"\n#include <stdio.h>\n\nconst char* html =\n    \"<table>\"\n    \"<tr><th>Name</th><th>Age</th></tr>\"\n    \"<tr><td>Alice</td><td>30</td></tr>\"\n    \"<tr><td>Bob</td><td>25</td></tr>\"\n    \"</table>\";\n\nconst char* options_json = \"{\\\"extract_tables\\\":true}\";\n\n/* Returns JSON: {\"content\":\"...\",\"metadata\":null,\"tables\":[...]} */\nchar* json = html_to_markdown_convert(html, options_json);\nif (json != NULL) {\n    /* Parse JSON to access tables array */\n    printf(\"%s\\n\", json);\n    html_to_markdown_free_string(json);\n}\n```\n"
  },
  {
    "path": "docs/snippets/c/visitor/basic_visitor.md",
    "content": "```c\n#include \"html_to_markdown.h\"\n#include <stdio.h>\n#include <string.h>\n\n/* Custom heading visitor: prefix all headings with a section marker */\nstatic HtmlToMarkdownVisitResult visit_heading(\n    const HtmlToMarkdownVisitHeadingData *data, void *user_data) {\n    (void)user_data;\n    HtmlToMarkdownVisitResult result = {0};\n    /* Use default conversion for all headings */\n    result.type = Continue;\n    return result;\n}\n\nint main(void) {\n    const char *html = \"<h1>Title</h1><p>Content</p>\";\n\n    html_to_markdown_visitor_callbacks_t callbacks = {0};\n    callbacks.visit_heading = (struct Option_HtmlToMarkdownVisitHeadingCallback){\n        .is_some = true,\n        .value = visit_heading,\n    };\n\n    HtmlToMarkdownVisitor *visitor = html_to_markdown_visitor_new(&callbacks);\n    /* Returns JSON: {\"content\":\"...\",\"metadata\":null,\"tables\":null} */\n    char *json = html_to_markdown_convert_with_visitor(html, visitor, NULL);\n    if (json) {\n        /* Parse JSON to extract content field */\n        printf(\"%s\\n\", json);\n        html_to_markdown_free_string(json);\n    }\n    html_to_markdown_visitor_free(visitor);\n    return 0;\n}\n```\n"
  },
  {
    "path": "docs/snippets/csharp/getting-started/basic_usage.md",
    "content": "```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Hello World</h1><p>This is a paragraph.</p>\";\nvar result = HtmlToMarkdownConverter.Convert(html);\nConsole.WriteLine(result.Content);\n```\n"
  },
  {
    "path": "docs/snippets/csharp/getting-started/with_options.md",
    "content": "```csharp\nusing HtmlToMarkdown;\n\nvar options = new ConversionOptions\n{\n    HeadingStyle = \"atx\",\n    Wrap = true,\n    WrapWidth = 80,\n    ListIndentWidth = 4,\n};\n\nvar html = \"<h1>Hello</h1><p>This is <strong>formatted</strong> content.</p>\";\nvar result = HtmlToMarkdownConverter.Convert(html, options);\nConsole.WriteLine(result.Content);\n```\n"
  },
  {
    "path": "docs/snippets/csharp/metadata/basic_extraction.md",
    "content": "```csharp\nusing HtmlToMarkdown;\n\nvar html = @\"<html><head><title>My Page</title></head>\n<body><h1>Hello</h1><a href=\"\"https://example.com\"\">Link</a></body></html>\";\n\nvar options = new ConversionOptions { ExtractMetadata = true };\nvar result = HtmlToMarkdownConverter.Convert(html, options);\nConsole.WriteLine($\"Markdown: {result.Content}\");\nConsole.WriteLine($\"Title: {result.Metadata?.Title}\");\nConsole.WriteLine($\"Links: {string.Join(\", \", result.Metadata?.Links ?? [])}\");\n```\n"
  },
  {
    "path": "docs/snippets/csharp/table-extraction/basic_extraction.md",
    "content": "```csharp\nusing HtmlToMarkdown;\n\nvar html = @\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\";\n\nvar options = new ConversionOptions { ExtractTables = true };\nvar result = HtmlToMarkdownConverter.Convert(html, options);\n\nforeach (var table in result.Tables ?? [])\n{\n    for (int i = 0; i < table.Cells.Count; i++)\n    {\n        var prefix = table.IsHeaderRow[i] ? \"Header\" : \"Row\";\n        Console.WriteLine($\"  {prefix}: {string.Join(\", \", table.Cells[i])}\");\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/csharp/visitor/basic_visitor.md",
    "content": "```csharp\n// The visitor pattern is not yet supported in the C# binding.\n// Use Convert() with ConversionOptions instead.\n```\n"
  },
  {
    "path": "docs/snippets/elixir/getting-started/basic_usage.md",
    "content": "```elixir\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\")\nIO.puts(result.content)\n```\n"
  },
  {
    "path": "docs/snippets/elixir/getting-started/with_options.md",
    "content": "```elixir\nopts = %HtmlToMarkdown.Options{wrap: true, wrap_width: 40}\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\", opts)\nIO.puts(result.content)\n```\n"
  },
  {
    "path": "docs/snippets/elixir/metadata/basic_extraction.md",
    "content": "# Metadata Extraction - Elixir\n\nExtract structured metadata from HTML documents during conversion.\n\n## Basic Metadata Extraction\n\nUse `convert/2` with `extract_metadata: true` in options to extract document metadata alongside Markdown:\n\n```elixir\nhtml = \"\"\"\n<html>\n  <head>\n    <title>Example</title>\n    <meta name=\"description\" content=\"Demo page\">\n  </head>\n  <body>\n    <h1 id=\"welcome\">Welcome</h1>\n    <a href=\"https://example.com\" rel=\"nofollow external\">Example link</a>\n  </body>\n</html>\n\"\"\"\n\nopts = %HtmlToMarkdown.Options{extract_metadata: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nresult.metadata[\"document\"][\"title\"]        # \"Example\"\nresult.metadata[\"headers\"] |> hd() |> Map.get(\"text\") # \"Welcome\"\nresult.metadata[\"links\"]   |> hd() |> Map.get(\"link_type\") # \"external\"\n```\n\n## Extracted Metadata Structure\n\nThe metadata map includes:\n\n- **Document**: Title and meta tags from `<head>`\n- **Headers**: All headings extracted with level, text, and optional ID\n- **Links**: All links with href, text, rel attributes, and link_type classification\n- **Images**: Image sources and alt text\n- **Forms**: Form action and method data\n- **Other**: Tables, code blocks, and additional structural information\n"
  },
  {
    "path": "docs/snippets/elixir/table-extraction/basic_extraction.md",
    "content": "```elixir\nhtml = \"\"\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"\"\"\n\nopts = %HtmlToMarkdown.Options{extract_tables: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nfor %{cells: cells, is_header_row: is_header_row} <- result.tables do\n  cells\n  |> Enum.with_index()\n  |> Enum.each(fn {row, i} ->\n    prefix = if Enum.at(is_header_row, i), do: \"Header\", else: \"Row\"\n    IO.puts(\"  #{prefix}: #{Enum.join(row, \", \")}\")\n  end)\nend\n```\n"
  },
  {
    "path": "docs/snippets/elixir/visitor/basic_visitor.md",
    "content": "# Visitor Pattern - Elixir\n\nCustomize HTML to Markdown conversion by implementing visitor callbacks.\n\n## Basic Visitor Example\n\nDefine a visitor module implementing `HtmlToMarkdown.Visitor`:\n\n```elixir\ndefmodule MyLinkFilter do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_context, _href, text, _title) do\n    # Convert all links to plain text\n    {:custom, text}\n  end\nend\n\nhtml = \"<p>Visit <a href='https://example.com'>our site</a> for more!</p>\"\nopts = %HtmlToMarkdown.Options{visitor: MyLinkFilter}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n# result.content == \"Visit our site for more!\\n\"\n```\n\n## Available Callbacks\n\n### Generic Hooks\n\n- `handle_element_start(context)` - called before entering any element\n- `handle_element_end(context, output)` - called after exiting an element\n\n### Text & Formatting\n\n- `handle_text(context, text)` - text nodes\n- `handle_strong(context, text)` - `<strong>`, `<b>`\n- `handle_emphasis(context, text)` - `<em>`, `<i>`\n- `handle_strikethrough(context, text)` - `<s>`, `<del>`, `<strike>`\n- `handle_underline(context, text)` - `<u>`, `<ins>`\n- `handle_subscript(context, text)` - `<sub>`\n- `handle_superscript(context, text)` - `<sup>`\n- `handle_mark(context, text)` - `<mark>`\n\n### Links & Media\n\n- `handle_link(context, href, text, title)` - `<a>` elements\n- `handle_image(context, src, alt, title)` - `<img>` elements\n- `handle_audio(context, src)` - `<audio>` elements\n- `handle_video(context, src)` - `<video>` elements\n- `handle_iframe(context, src)` - `<iframe>` elements\n\n### Code\n\n- `handle_code_block(context, lang, code)` - `<pre><code>` blocks\n- `handle_code_inline(context, code)` - `<code>` inline\n\n### Headings & Structure\n\n- `handle_heading(context, level, text, id)` - `<h1>` through `<h6>`\n- `handle_blockquote(context, content, depth)` - `<blockquote>`\n- `handle_horizontal_rule(context)` - `<hr>`\n- `handle_line_break(context)` - `<br>`\n\n### Lists\n\n- `handle_list_start(context, ordered)` - `<ul>` or `<ol>` start\n- `handle_list_item(context, ordered, marker, text)` - `<li>` elements\n- `handle_list_end(context, ordered, output)` - list end\n\n### Tables\n\n- `handle_table_start(context)` - `<table>` start\n- `handle_table_row(context, cells, is_header)` - `<tr>` elements\n- `handle_table_end(context, output)` - table end\n\n### Forms\n\n- `handle_form(context, action, method)` - `<form>`\n- `handle_input(context, type, name, value)` - `<input>`\n- `handle_button(context, text)` - `<button>`\n\n### Definition Lists\n\n- `handle_definition_list_start(context)` - `<dl>` start\n- `handle_definition_term(context, text)` - `<dt>`\n- `handle_definition_description(context, text)` - `<dd>`\n- `handle_definition_list_end(context, output)` - list end\n\n### Custom Elements\n\n- `handle_custom_element(context, tag_name, html)` - web components or unknown tags\n- `handle_other(callback, context, args)` - catch-all for unimplemented callbacks\n\n## Visitor Return Values\n\nEach callback must return one of:\n\n- `:continue` - proceed with default conversion\n- `{:custom, markdown}` - replace output with custom markdown\n- `:skip` - omit this element entirely\n- `:preserve_html` - include raw HTML verbatim\n- `{:error, reason}` - stop conversion with error\n\n## Node Context\n\nAll callbacks receive a `NodeContext` struct with element metadata:\n\n```elixir\n%{\n  node_type: :link,           # coarse-grained classification\n  tag_name: \"a\",              # raw HTML tag name\n  attributes: %{...},         # HTML attributes as a map\n  depth: 2,                   # nesting depth in DOM\n  index_in_parent: 0,         # zero-based sibling index\n  parent_tag: \"p\",            # parent element's tag (nil if root)\n  is_inline: true             # whether treated as inline vs block\n}\n```\n\n## Remove All Links Example\n\n```elixir\ndefmodule NoLinksVisitor do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_context, _href, text, _title) do\n    # Convert links to plain text\n    {:custom, text}\n  end\nend\n\nhtml = \"<p>Check <a href='#'>this</a> out.</p>\"\nopts = %HtmlToMarkdown.Options{visitor: NoLinksVisitor}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n# result.content == \"Check this out.\\n\"\n```\n\n## Advanced Example: Stateful Image Collection\n\nUse a GenServer to maintain state across callbacks:\n\n```elixir\ndefmodule ImageCollector do\n  use GenServer\n  use HtmlToMarkdown.Visitor\n\n  def start_link(_), do: GenServer.start_link(__MODULE__, [])\n\n  def init(_), do: {:ok, []}\n\n  @impl true\n  def handle_image(_context, src, alt, _title) do\n    GenServer.cast(self(), {:collect, src, alt})\n    :continue\n  end\n\n  def handle_cast({:collect, src, alt}, images) do\n    {:noreply, [%{src: src, alt: alt} | images]}\n  end\nend\n\n{:ok, pid} = ImageCollector.start_link(nil)\nopts = %HtmlToMarkdown.Options{visitor: pid}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n# Can query collected images via GenServer API\n```\n\n## Execution Order\n\nCallbacks are invoked during depth-first traversal. For `<div><p>text</p></div>`:\n\n1. `handle_element_start` for `<div>`\n2. `handle_element_start` for `<p>`\n3. `handle_text` for \"text\"\n4. `handle_element_end` for `<p>`\n5. `handle_element_end` for `</div>`\n"
  },
  {
    "path": "docs/snippets/feedback.md",
    "content": "---\n\n!!! question \"Found a bug or mistake on this page?\"\n    If something here is wrong or out of date, [open an issue](https://github.com/kreuzberg-dev/html-to-markdown/issues/new?labels=documentation) on GitHub or [contribute a fix](contributing.md) via pull request.\n"
  },
  {
    "path": "docs/snippets/go/getting-started/basic_usage.md",
    "content": "```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    html := \"<h1>Hello World</h1><p>This is a paragraph.</p>\"\n\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/go/getting-started/with_options.md",
    "content": "```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    // Check library version\n    version := htmltomarkdown.Version()\n    fmt.Printf(\"html-to-markdown version: %s\\n\", version)\n\n    html := \"<h1>Hello</h1><p>Welcome</p>\"\n\n    // Convert with error handling\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatalf(\"Conversion failed: %v\", err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/go/metadata/basic_extraction.md",
    "content": "```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n\thtml := `<html><head><title>My Page</title></head>\n\t<body><h1>Hello</h1><a href=\"https://example.com\">Link</a></body></html>`\n\n\topts := htmltomarkdown.ConversionOptions{ExtractMetadata: true}\n\tresult, err := htmltomarkdown.Convert(html, opts)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif result.Content != nil {\n\t\tfmt.Println(\"Markdown:\", *result.Content)\n\t}\n\tif result.Metadata != nil {\n\t\tfmt.Println(\"Title:\", result.Metadata.Title)\n\t\tfmt.Println(\"Links:\", result.Metadata.Links)\n\t}\n}\n```\n"
  },
  {
    "path": "docs/snippets/go/table-extraction/basic_extraction.md",
    "content": "```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n\nhtml := `\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n`\n\nopts := htmltomarkdown.ConversionOptions{ExtractTables: true}\nresult, err := htmltomarkdown.Convert(html, opts)\nif err != nil {\n    log.Fatal(err)\n}\n\nfor _, table := range result.Tables {\n    for i, row := range table.Cells {\n        prefix := \"Row\"\n        if table.IsHeaderRow[i] {\n            prefix = \"Header\"\n        }\n        fmt.Printf(\"  %s: %v\\n\", prefix, row)\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/go/visitor/basic_visitor.md",
    "content": "```go\n// The visitor pattern is not yet supported in the Go binding.\n// Use Convert() with ConversionOptions instead.\n```\n"
  },
  {
    "path": "docs/snippets/java/getting-started/basic_usage.md",
    "content": "```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class Example {\n    public static void main(String[] args) {\n        String html = \"<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>\";\n        ConversionResult result = HtmlToMarkdown.convert(html);\n        System.out.println(result.content());\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/java/getting-started/with_options.md",
    "content": "```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class MetadataExample {\n    public static void main(String[] args) {\n        String html = \"<html><head><title>My Page</title></head>\"\n            + \"<body><h1>Welcome</h1><a href=\\\"https://example.com\\\">Link</a></body></html>\";\n\n        ConversionOptions options = ConversionOptions.builder()\n            .extractMetadata(true)\n            .build();\n        ConversionResult result = HtmlToMarkdown.convert(html, options);\n\n        System.out.println(\"Markdown: \" + result.content());\n        System.out.println(\"Title: \" + result.metadata().document().title());\n        System.out.println(\"Headers: \" + result.metadata().headers().size());\n        System.out.println(\"Links: \" + result.metadata().links().size());\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/java/metadata/basic_extraction.md",
    "content": "```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class MetadataExample {\n    public static void main(String[] args) {\n        String html = \"\"\"\n            <html><head><title>My Page</title></head>\n            <body><h1>Hello</h1><a href=\"https://example.com\">Link</a></body></html>\n            \"\"\";\n\n        ConversionOptions options = ConversionOptions.builder()\n            .extractMetadata(true)\n            .build();\n        ConversionResult result = HtmlToMarkdown.convert(html, options);\n        System.out.println(\"Markdown: \" + result.content());\n        System.out.println(\"Title: \" + result.metadata().getTitle());\n        System.out.println(\"Links: \" + result.metadata().getLinks());\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/java/table-extraction/basic_extraction.md",
    "content": "```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\nString html = \"\"\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"\"\";\n\nConversionResult result = HtmlToMarkdown.convert(html, new ConversionOptions());\n\nfor (var table : result.tables()) {\n    for (var cell : table.grid().cells()) {\n        String prefix = cell.isHeader() ? \"Header\" : \"Cell\";\n        System.out.printf(\"  %s (r%d,c%d): %s%n\", prefix, cell.row(), cell.col(), cell.content());\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/java/visitor/basic_visitor.md",
    "content": "```java\n// The visitor pattern is not yet supported in the Java binding.\n// Use convert() with ConversionOptions instead.\n```\n"
  },
  {
    "path": "docs/snippets/php/getting-started/basic_usage.md",
    "content": "```php\nuse HtmlToMarkdown\\Service\\Converter;\nuse function HtmlToMarkdown\\convert;\n\n// Object-oriented usage\n$converter = Converter::create();\n$result = $converter->convert('<h1>Hello</h1><p>This is <strong>fast</strong>!</p>');\n$markdown = $result['content'];\n\n// Procedural helper\n$result = convert('<h1>Hello</h1>');\n$markdown = $result['content'];\n```\n"
  },
  {
    "path": "docs/snippets/php/getting-started/with_options.md",
    "content": "```php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$converter = Converter::create();\n\n$options = new ConversionOptions(\n    headingStyle: 'Atx',\n    listIndentWidth: 2,\n);\n\n$result = $converter->convert('<h1>Hello</h1>', $options);\n$markdown = $result['content'];\n```\n"
  },
  {
    "path": "docs/snippets/php/metadata/basic_extraction.md",
    "content": "```php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$html = '<html><head><title>Example</title></head><body><h1>Welcome</h1><a href=\"https://example.com\">Link</a></body></html>';\n\n$converter = Converter::create();\n$result = $converter->convert(\n    $html,\n    new ConversionOptions(\n        headingStyle: 'Atx',\n        extractMetadata: true,\n        extractHeaders: true,\n        extractLinks: true,\n        extractImages: true,\n    )\n);\n\necho $result['content'];\necho $result['metadata']->document->title;\nforeach ($result['metadata']->links as $link) {\n    echo $link->href . ': ' . $link->text;\n}\n```\n"
  },
  {
    "path": "docs/snippets/php/table-extraction/basic_extraction.md",
    "content": "```php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$html = <<<HTML\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\nHTML;\n\n$converter = Converter::create();\n$result = $converter->convert($html, new ConversionOptions(extractTables: true));\n\nforeach ($result['tables'] as $table) {\n    foreach ($table->cells as $i => $row) {\n        $prefix = $table->isHeaderRow[$i] ? 'Header' : 'Row';\n        echo \"  {$prefix}: \" . implode(', ', $row) . \"\\n\";\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/php/visitor/basic_visitor.md",
    "content": "```php\nuse HtmlToMarkdown\\Visitor\\AbstractVisitor;\nuse HtmlToMarkdown\\Visitor\\NodeContext;\nuse HtmlToMarkdown\\Visitor\\VisitResult;\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\nclass CustomVisitor extends AbstractVisitor\n{\n    public function visitImage(NodeContext $context, string $src, string $alt, ?string $title): array\n    {\n        // Skip all images\n        return VisitResult::skip();\n    }\n\n    public function visitLink(NodeContext $context, string $href, string $text, ?string $title): array\n    {\n        // Custom link handling\n        return VisitResult::custom(\"[{$text}]({$href})\");\n    }\n}\n\n$converter = Converter::create();\n$result = $converter->convert(\n    '<a href=\"/page\">Link</a><img src=\"pic.png\" alt=\"pic\">',\n    new ConversionOptions(visitor: new CustomVisitor())\n);\n$markdown = $result['content'];\n```\n"
  },
  {
    "path": "docs/snippets/python/getting-started/basic_usage.md",
    "content": "```python\nfrom html_to_markdown import convert\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = convert(html)\nmarkdown = result.content\n```\n"
  },
  {
    "path": "docs/snippets/python/getting-started/with_options.md",
    "content": "```python\nfrom html_to_markdown import ConversionOptions, convert\n\nhtml = \"<h1>Hello</h1><p>This is <strong>formatted</strong> content.</p>\"\noptions = ConversionOptions(\n    heading_style=\"atx\",\n    list_indent_width=2,\n)\nresult = convert(html, options)\nmarkdown = result.content\n```\n"
  },
  {
    "path": "docs/snippets/python/metadata/basic_extraction.md",
    "content": "```python\nfrom html_to_markdown import ConversionOptions, convert\n\noptions = ConversionOptions(\n    extract_metadata=True,\n    extract_images=True,\n)\nresult = convert(html, options)\nmarkdown = result.content\nmetadata = result.metadata\n```\n"
  },
  {
    "path": "docs/snippets/python/table-extraction/basic_extraction.md",
    "content": "```python\nfrom html_to_markdown import ConversionOptions, convert\n\nhtml = \"\"\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"\"\"\n\nresult = convert(html, ConversionOptions())\n\nfor table in result.tables:\n    for cell in table.grid.cells:\n        prefix = \"Header\" if cell.is_header else \"Cell\"\n        print(f\"  {prefix} (r{cell.row},c{cell.col}): {cell.content}\")\n```\n"
  },
  {
    "path": "docs/snippets/python/visitor/basic_visitor.md",
    "content": "```python\nfrom html_to_markdown import ConversionOptions, convert\n\nclass CustomVisitor:\n    def visit_link(self, ctx, href, text, title):\n        return {\"type\": \"continue\"}\n\n    def visit_image(self, ctx, src, alt, title):\n        return {\"type\": \"continue\"}\n\noptions = ConversionOptions(visitor=CustomVisitor())\nresult = convert(html, options)\nmarkdown = result.content\n```\n"
  },
  {
    "path": "docs/snippets/r/getting-started/basic_usage.md",
    "content": "```r\nlibrary(htmltomarkdown)\n\nhtml <- \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult <- convert(html)\nmarkdown <- result$content\ncat(markdown)\n```\n"
  },
  {
    "path": "docs/snippets/r/getting-started/with_options.md",
    "content": "```r\nlibrary(htmltomarkdown)\n\nopts <- conversion_options(\n  heading_style = \"atx\",\n  wrap = TRUE,\n  wrap_width = 80L\n)\n\nresult <- convert(\"<h1>Hello</h1><p>World</p>\", opts)\ncat(result$content)\n```\n"
  },
  {
    "path": "docs/snippets/r/metadata/basic_extraction.md",
    "content": "```r\nlibrary(htmltomarkdown)\n\nhtml <- '\n<html>\n  <head><title>Example</title></head>\n  <body>\n    <h1 id=\"welcome\">Welcome</h1>\n    <a href=\"https://example.com\">Example link</a>\n  </body>\n</html>'\n\nopts <- conversion_options(extract_metadata = TRUE)\nresult <- convert(html, opts)\n\ncat(result$content)\nresult$metadata$document$title\nresult$metadata$headers[[1]]$text\nresult$metadata$links[[1]]$link_type\n```\n"
  },
  {
    "path": "docs/snippets/r/table-extraction/basic_extraction.md",
    "content": "```r\nlibrary(htmltomarkdown)\n\nhtml <- \"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"\n\nopts <- conversion_options(extract_tables = TRUE)\nresult <- convert(html, opts)\n\nfor (table in result$tables) {\n  for (i in seq_along(table$cells)) {\n    prefix <- if (table$is_header_row[[i]]) \"Header\" else \"Row\"\n    cat(sprintf(\"  %s: %s\\n\", prefix, paste(table$cells[[i]], collapse = \", \")))\n  }\n}\n```\n"
  },
  {
    "path": "docs/snippets/r/visitor/basic_visitor.md",
    "content": "```r\nlibrary(htmltomarkdown)\n\nhtml <- \"<p>Visit <a href='https://example.com'>our site</a> for more!</p>\"\n\nopts <- conversion_options(extract_metadata = FALSE)\nresult <- convert(html, opts)\ncat(result$content)\n```\n"
  },
  {
    "path": "docs/snippets/ruby/getting-started/basic_usage.md",
    "content": "```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]\n```\n"
  },
  {
    "path": "docs/snippets/ruby/getting-started/with_options.md",
    "content": "```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = HtmlToMarkdown.convert(html, heading_style: :atx, code_block_style: :fenced)\nmarkdown = result[:content]\n```\n"
  },
  {
    "path": "docs/snippets/ruby/metadata/basic_extraction.md",
    "content": "```ruby\nrequire 'html_to_markdown'\n\nhtml = '<html lang=\"en\"><head><title>Test</title></head><body><h1>Hello</h1></body></html>'\nresult = HtmlToMarkdown.convert(html, extract_metadata: true)\n\nmarkdown = result[:content]\nputs result[:metadata][:document][:title]     # \"Test\"\nputs result[:metadata][:headers].first[:text] # \"Hello\"\n```\n"
  },
  {
    "path": "docs/snippets/ruby/table-extraction/basic_extraction.md",
    "content": "```ruby\nrequire 'html_to_markdown'\n\nhtml = <<~HTML\n  <table>\n      <tr><th>Name</th><th>Age</th></tr>\n      <tr><td>Alice</td><td>30</td></tr>\n      <tr><td>Bob</td><td>25</td></tr>\n  </table>\nHTML\n\nresult = HtmlToMarkdown.convert(html, extract_tables: true)\n\nresult[:tables].each do |table|\n  table[:cells].each_with_index do |row, i|\n    prefix = table[:is_header_row][i] ? \"Header\" : \"Row\"\n    puts \"  #{prefix}: #{row.join(', ')}\"\n  end\nend\n```\n"
  },
  {
    "path": "docs/snippets/ruby/visitor/basic_visitor.md",
    "content": "```ruby\nrequire 'html_to_markdown'\n\nclass MyVisitor\n  def visit_link(ctx, href, text, title = nil)\n    { type: :custom, output: \"[#{text}](#{href})\" }\n  end\n\n  def visit_image(ctx, src, alt, title = nil)\n    { type: :skip }  # Remove images\n  end\nend\n\nhtml = \"<p><a href='https://example.com'>Link</a></p>\"\nresult = HtmlToMarkdown.convert(html, visitor: MyVisitor.new)\nmarkdown = result[:content]\n```\n"
  },
  {
    "path": "docs/snippets/rust/getting-started/basic_usage.md",
    "content": "```rust\nuse html_to_markdown_rs::convert;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let html = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\";\n    let result = convert(html, None)?;\n    let markdown = result.content.unwrap_or_default();\n    println!(\"{markdown}\");\n    Ok(())\n}\n```\n"
  },
  {
    "path": "docs/snippets/rust/getting-started/with_options.md",
    "content": "```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let options = ConversionOptions::builder()\n        .heading_style(HeadingStyle::Atx)\n        .skip_images(true)\n        .build();\n    let result = convert(\"<h1>Hello</h1><img src='pic.jpg'>\", Some(options))?;\n    let markdown = result.content.unwrap_or_default();\n    println!(\"{markdown}\");\n    Ok(())\n}\n```\n"
  },
  {
    "path": "docs/snippets/rust/metadata/basic_extraction.md",
    "content": "```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let html = r#\"<html><head><title>My Page</title></head>\n    <body><h1>Hello</h1><a href=\"https://example.com\">Link</a></body></html>\"#;\n\n    let options = ConversionOptions::builder()\n        .extract_metadata(true)\n        .build();\n    let result = convert(html, Some(options))?;\n    let markdown = result.content.unwrap_or_default();\n    println!(\"Markdown: {}\", markdown);\n    println!(\"Title: {:?}\", result.metadata.as_ref().and_then(|m| m.title.as_deref()));\n    println!(\"Links: {:?}\", result.metadata.as_ref().map(|m| &m.links));\n    Ok(())\n}\n```\n"
  },
  {
    "path": "docs/snippets/rust/table-extraction/basic_extraction.md",
    "content": "```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nlet html = r#\"\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n\"#;\n\nlet options = ConversionOptions::builder()\n    .extract_tables(true)\n    .build();\nlet result = convert(html, Some(options))?;\n\nfor table in result.tables.unwrap_or_default() {\n    for (i, row) in table.cells.iter().enumerate() {\n        let prefix = if table.is_header_row[i] { \"Header\" } else { \"Row\" };\n        println!(\"  {prefix}: {}\", row.join(\", \"));\n    }\n}\n```\n"
  },
  {
    "path": "docs/snippets/rust/visitor/basic_visitor.md",
    "content": "```rust\nuse html_to_markdown_rs::{convert, ConversionOptions, Visitor, VisitResult};\n\nstruct LinkRewriter;\n\nimpl Visitor for LinkRewriter {\n    fn visit_link(&self, url: &str, text: &str) -> VisitResult {\n        // Rewrite all links to use a tracking prefix\n        VisitResult::Replace(format!(\"[{text}](https://track.example.com?url={url})\"))\n    }\n}\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let html = r#\"<a href=\"https://example.com\">Click here</a>\"#;\n    let options = ConversionOptions::builder()\n        .visitor(LinkRewriter)\n        .build();\n    let result = convert(html, Some(options))?;\n    let markdown = result.content.unwrap_or_default();\n    println!(\"{markdown}\");\n    Ok(())\n}\n```\n"
  },
  {
    "path": "docs/snippets/typescript/getting-started/basic_usage.md",
    "content": "```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst result = convert('<h1>Hello World</h1>');\nconst markdown: string = result.content;\nconsole.log(markdown); // # Hello World\n```\n"
  },
  {
    "path": "docs/snippets/typescript/getting-started/with_options.md",
    "content": "```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst options: ConversionOptions = {\n  headingStyle: 'atx',\n  listIndentWidth: 2,\n  wrap: true,\n};\n\nconst result = convert('<h1>Title</h1><p>Content</p>', options);\nconst markdown = result.content;\n```\n"
  },
  {
    "path": "docs/snippets/typescript/metadata/basic_extraction.md",
    "content": "```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst options: ConversionOptions = { extractMetadata: true };\nconst result = convert('<h1>Title</h1><p>Content</p>', options);\n\nconsole.log(result.content);           // Converted markdown\nconsole.log(result.metadata?.document); // Document metadata (title, description, etc.)\nconsole.log(result.metadata?.headers);  // Header elements (h1-h6)\nconsole.log(result.metadata?.links);    // Extracted links\nconsole.log(result.metadata?.images);   // Extracted images\n```\n"
  },
  {
    "path": "docs/snippets/typescript/table-extraction/basic_extraction.md",
    "content": "```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst html = `\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n`;\n\nconst options: ConversionOptions = { extractTables: true };\nconst result = convert(html, options);\n\nfor (const table of result.tables ?? []) {\n  for (let i = 0; i < table.cells.length; i++) {\n    const prefix = table.isHeaderRow[i] ? 'Header' : 'Row';\n    console.log(`  ${prefix}: ${table.cells[i].join(', ')}`);\n  }\n}\n```\n"
  },
  {
    "path": "docs/snippets/typescript/visitor/basic_visitor.md",
    "content": "```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\nimport { Visitor, NodeContext, VisitResult } from '@kreuzberg/html-to-markdown';\n\nconst visitor: Visitor = {\n  visitLink(ctx: NodeContext, href: string, text: string): VisitResult {\n    // Custom handling for links\n    return {\n      type: 'custom',\n      output: `[${text}](${href})`,\n    };\n  },\n  visitHeading(ctx: NodeContext, level: number, text: string): VisitResult {\n    // Custom handling for headings\n    return {\n      type: 'continue',\n    };\n  },\n};\n\nconst options: ConversionOptions = { visitor };\nconst result = convert('<h1>Title</h1><a href=\"url\">Link</a>', options);\nconst markdown = result.content;\n```\n"
  },
  {
    "path": "docs/snippets/wasm/getting-started/basic_usage.md",
    "content": "```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init();\n\nconst html = '<h1>Hello</h1><p>This is <strong>fast</strong>!</p>';\nconst result = convert(html);\nconst markdown = result.content;\nconsole.log(markdown);\n```\n"
  },
  {
    "path": "docs/snippets/wasm/getting-started/with_options.md",
    "content": "```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init();\n\nconst result = convert('<h1>Hello</h1><img src=\"pic.jpg\">', {\n  headingStyle: 'atx',\n  skipImages: true,\n});\nconst markdown = result.content;\nconsole.log(markdown);\n```\n"
  },
  {
    "path": "docs/snippets/wasm/metadata/basic_extraction.md",
    "content": "```javascript\nimport init, { convert } from \"@kreuzberg/html-to-markdown-wasm\";\n\nawait init();\n\nconst html = '<html><head><title>My Page</title></head><body><h1>Hello</h1><a href=\"https://example.com\">Link</a></body></html>';\nconst result = convert(html, { extractMetadata: true });\n\nconsole.log(\"Markdown:\", result.content);\nconsole.log(\"Title:\", result.metadata?.title);\nconsole.log(\"Links:\", result.metadata?.links);\n```\n"
  },
  {
    "path": "docs/snippets/wasm/table-extraction/basic_extraction.md",
    "content": "```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init();\n\nconst html = `\n<table>\n    <tr><th>Name</th><th>Age</th></tr>\n    <tr><td>Alice</td><td>30</td></tr>\n    <tr><td>Bob</td><td>25</td></tr>\n</table>\n`;\n\nconst result = convert(html, { extractTables: true });\n\nfor (const table of result.tables ?? []) {\n  for (let i = 0; i < table.cells.length; i++) {\n    const prefix = table.isHeaderRow[i] ? 'Header' : 'Row';\n    console.log(`  ${prefix}: ${table.cells[i].join(', ')}`);\n  }\n}\n```\n"
  },
  {
    "path": "docs/snippets/wasm/visitor/basic_visitor.md",
    "content": "```javascript\nimport init, { convert } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init();\n\nconst visitor = {\n  visit_link(ctx, href, text, title) {\n    return { type: 'continue' };\n  },\n  visit_image(ctx, src, alt, title) {\n    return { type: 'continue' };\n  },\n};\n\nconst result = convert('<h1>Hello</h1><a href=\"https://example.com\">link</a>', undefined, visitor);\nconsole.log(result.content);\n```\n"
  },
  {
    "path": "docs/tables.md",
    "content": "# Table Extraction\n\nEvery call to `convert()` populates `result.tables` with one entry per `<table>` found in the input. Each entry has both a rendered Markdown string and a structured cell grid, so you can embed the Markdown in downstream documents or walk the grid for analysis without re-parsing.\n\nTable extraction runs on every call. There is no opt-in flag. Set `output_format` to `\"none\"` if you only want the table data and not the rendered content.\n\n## TableData\n\n`result.tables` is a `Vec<TableData>` (or the equivalent list in each binding).\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `grid` | `TableGrid` | Structured cell grid. |\n| `markdown` | `String` | The Markdown rendering of this table, identical to what appears in `result.content`. |\n\n## TableGrid\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `rows` | `u32` | Number of rows in the table. |\n| `cols` | `u32` | Number of columns in the table. |\n| `cells` | `Vec<GridCell>` | Flat list of cells. May be shorter than `rows * cols` when cells span multiple rows or columns. |\n\n## GridCell\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `content` | `String` | Cell text. Inline formatting is flattened to plain text. |\n| `row` | `u32` | 0-indexed row position. |\n| `col` | `u32` | 0-indexed column position. |\n| `row_span` | `u32` | How many rows the cell occupies. Defaults to `1`. |\n| `col_span` | `u32` | How many columns the cell occupies. Defaults to `1`. |\n| `is_header` | `bool` | `true` for `<th>`, `false` for `<td>`. |\n\n## Basic Extraction\n\n=== \"Rust\"\n    --8<-- \"snippets/rust/table-extraction/basic_extraction.md\"\n\n=== \"Python\"\n    --8<-- \"snippets/python/table-extraction/basic_extraction.md\"\n\n=== \"TypeScript\"\n    --8<-- \"snippets/typescript/table-extraction/basic_extraction.md\"\n\n=== \"Go\"\n    --8<-- \"snippets/go/table-extraction/basic_extraction.md\"\n\n=== \"Ruby\"\n    --8<-- \"snippets/ruby/table-extraction/basic_extraction.md\"\n\n=== \"PHP\"\n    --8<-- \"snippets/php/table-extraction/basic_extraction.md\"\n\n=== \"Java\"\n    --8<-- \"snippets/java/table-extraction/basic_extraction.md\"\n\n=== \"C#\"\n    --8<-- \"snippets/csharp/table-extraction/basic_extraction.md\"\n\n=== \"Elixir\"\n    --8<-- \"snippets/elixir/table-extraction/basic_extraction.md\"\n\n=== \"R\"\n    --8<-- \"snippets/r/table-extraction/basic_extraction.md\"\n\n=== \"C\"\n    --8<-- \"snippets/c/table-extraction/basic_extraction.md\"\n\n=== \"WASM\"\n    --8<-- \"snippets/wasm/table-extraction/basic_extraction.md\"\n\n## Relationship to `result.content`\n\nThe Markdown in `TableData.markdown` is the same Markdown that appears inline inside `result.content`. The grid exists for code that needs cell-level access: headers vs body rows, span detection, or programmatic lookup by `(row, col)`.\n\nIf the input has no tables, `result.tables` is an empty list. If the output format is `\"plain\"` or `\"none\"`, tables are still extracted and their grids are still populated; only the Markdown rendering in `result.content` changes.\n\n## Spans\n\nA cell with `row_span > 1` or `col_span > 1` appears once in `cells`, positioned at its top-left coordinates. Downstream code that iterates by `(row, col)` should respect the span or use the spans to reconstruct a dense grid.\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Usage\n\n## Basic Conversion\n\n`convert()` accepts an HTML string and returns a `ConversionResult`.\n\n=== \"Rust\"\n    --8<-- \"snippets/rust/getting-started/basic_usage.md\"\n\n=== \"Python\"\n    --8<-- \"snippets/python/getting-started/basic_usage.md\"\n\n=== \"TypeScript\"\n    --8<-- \"snippets/typescript/getting-started/basic_usage.md\"\n\n=== \"Go\"\n    --8<-- \"snippets/go/getting-started/basic_usage.md\"\n\n=== \"Ruby\"\n    --8<-- \"snippets/ruby/getting-started/basic_usage.md\"\n\n=== \"PHP\"\n    --8<-- \"snippets/php/getting-started/basic_usage.md\"\n\n=== \"Java\"\n    --8<-- \"snippets/java/getting-started/basic_usage.md\"\n\n=== \"C#\"\n    --8<-- \"snippets/csharp/getting-started/basic_usage.md\"\n\n=== \"Elixir\"\n    --8<-- \"snippets/elixir/getting-started/basic_usage.md\"\n\n=== \"R\"\n    --8<-- \"snippets/r/getting-started/basic_usage.md\"\n\n=== \"C\"\n    --8<-- \"snippets/c/getting-started/basic_usage.md\"\n\n=== \"WASM\"\n    --8<-- \"snippets/wasm/getting-started/basic_usage.md\"\n\n## ConversionResult Fields\n\nEvery call to `convert()` returns a `ConversionResult` with the following fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `content` | `Optional<String>` | The converted text (Markdown, Djot, or plain). `None`/`null` when `output_format` is `\"none\"`. |\n| `document` | `Optional<DocumentStructure>` | Structured document tree (headings, paragraphs, lists, tables). Only populated when `include_document_structure` is `true`. |\n| `metadata` | `HtmlMetadata` | Extracted HTML metadata (title, description, Open Graph, Twitter Card, JSON-LD, links, images). |\n| `tables` | `Vec<TableData>` | Extracted tables with full grid data (headers, rows, colspan/rowspan). |\n| `images` | `Vec<ExtractedImage>` | Extracted inline images (data URIs, embedded SVGs). Only populated when `extract_images` is `true`. |\n| `warnings` | `Vec<ProcessingWarning>` | Non-fatal warnings raised during conversion. |\n\n## Using Options\n\nControl output style, metadata extraction, and more via `ConversionOptions`.\n\n=== \"Rust\"\n    --8<-- \"snippets/rust/getting-started/with_options.md\"\n\n=== \"Python\"\n    --8<-- \"snippets/python/getting-started/with_options.md\"\n\n=== \"TypeScript\"\n    --8<-- \"snippets/typescript/getting-started/with_options.md\"\n\n=== \"Go\"\n    --8<-- \"snippets/go/getting-started/with_options.md\"\n\n=== \"Ruby\"\n    --8<-- \"snippets/ruby/getting-started/with_options.md\"\n\n=== \"PHP\"\n    --8<-- \"snippets/php/getting-started/with_options.md\"\n\n=== \"Java\"\n    --8<-- \"snippets/java/getting-started/with_options.md\"\n\n=== \"C#\"\n    --8<-- \"snippets/csharp/getting-started/with_options.md\"\n\n=== \"Elixir\"\n    --8<-- \"snippets/elixir/getting-started/with_options.md\"\n\n=== \"R\"\n    --8<-- \"snippets/r/getting-started/with_options.md\"\n\n## Metadata Extraction\n\nEnable `extract_metadata` to populate the `metadata` field with structured data parsed from the HTML `<head>` and document body.\n\n=== \"Rust\"\n    --8<-- \"snippets/rust/metadata/basic_extraction.md\"\n\n=== \"Python\"\n    --8<-- \"snippets/python/metadata/basic_extraction.md\"\n\n=== \"TypeScript\"\n    --8<-- \"snippets/typescript/metadata/basic_extraction.md\"\n\n=== \"Go\"\n    --8<-- \"snippets/go/metadata/basic_extraction.md\"\n\n=== \"Ruby\"\n    --8<-- \"snippets/ruby/metadata/basic_extraction.md\"\n\n=== \"PHP\"\n    --8<-- \"snippets/php/metadata/basic_extraction.md\"\n\n=== \"Java\"\n    --8<-- \"snippets/java/metadata/basic_extraction.md\"\n\n=== \"C#\"\n    --8<-- \"snippets/csharp/metadata/basic_extraction.md\"\n\n=== \"Elixir\"\n    --8<-- \"snippets/elixir/metadata/basic_extraction.md\"\n\n=== \"R\"\n    --8<-- \"snippets/r/metadata/basic_extraction.md\"\n\n### Metadata Fields\n\n`result.metadata` is an `HtmlMetadata` with five top-level fields: `document`, `headers`, `links`, `images`, and `structured_data`. Everything is populated in a single pass.\n\n#### `document` (DocumentMetadata)\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `title` | `Option<String>` | Page title from the `<title>` element. |\n| `description` | `Option<String>` | `<meta name=\"description\">` content. |\n| `keywords` | `Vec<String>` | Parsed `<meta name=\"keywords\">`, split on commas. |\n| `author` | `Option<String>` | `<meta name=\"author\">` content. |\n| `canonical_url` | `Option<String>` | `<link rel=\"canonical\">` href. |\n| `base_href` | `Option<String>` | `<base href=\"…\">` value. |\n| `language` | `Option<String>` | `lang` attribute on `<html>`. |\n| `text_direction` | `Option<TextDirection>` | `dir` attribute on `<html>`. One of `left_to_right`, `right_to_left`, `auto`. |\n| `open_graph` | `BTreeMap<String, String>` | All `og:*` meta tags keyed by property (without the `og:` prefix). |\n| `twitter_card` | `BTreeMap<String, String>` | All `twitter:*` meta tags keyed by name (without the prefix). |\n| `meta_tags` | `BTreeMap<String, String>` | Every other `<meta name>` tag, keyed by name. |\n\n#### `headers`, `links`, `images`, `structured_data`\n\n| Field | Description |\n|-------|-------------|\n| `headers` | `HeaderMetadata` entries for every `<h1>`–`<h6>` with level, text, and id. |\n| `links` | `LinkMetadata` entries for every `<a>` with href, text, `rel` values, and classified `link_type`. |\n| `images` | `ImageMetadata` entries for every `<img>` with src, alt, dimensions, and classified `image_type`. |\n| `structured_data` | JSON-LD, Microdata, and RDFa blocks with a `data_type` tag and the raw content. |\n\n#### `links[].link_type`\n\n| Value | Matches |\n|-------|---------|\n| `anchor` | href starts with `#` (same-page anchors). |\n| `internal` | relative href or href that resolves inside the document's own host. |\n| `external` | absolute URL on a different host. |\n| `email` | `mailto:` URI. |\n| `phone` | `tel:` URI. |\n| `other` | anything else (`javascript:`, `data:`, custom schemes). |\n\n#### `images[].image_type`\n\n| Value | Matches |\n|-------|---------|\n| `data_uri` | `src` starts with `data:`. |\n| `inline_svg` | inline `<svg>` element (captured when `extract_images` is enabled). |\n| `external` | absolute URL on a remote host. |\n| `relative` | relative path or same-host URL. |\n\n#### `structured_data[].data_type`\n\n| Value | Matches |\n|-------|---------|\n| `json_ld` | `<script type=\"application/ld+json\">` blocks. |\n| `microdata` | `itemscope`/`itemprop` subtrees. |\n| `rdfa` | `typeof`/`property` subtrees. |\n\n## Document Structure Extraction\n\nEnable `include_document_structure` to get a parsed tree of the document's structural elements.\n\n=== \"Rust\"\n    ```rust\n    use html_to_markdown_rs::{convert, ConversionOptions};\n\n    let options = ConversionOptions::builder()\n        .include_document_structure(true)\n        .build();\n    let result = convert(\"<h1>Title</h1><p>Paragraph</p>\", Some(options))?;\n\n    if let Some(doc) = &result.document {\n        for node in &doc.nodes {\n            println!(\"{:?}\", node);\n        }\n    }\n    ```\n\n=== \"Python\"\n    ```python\n    from html_to_markdown import ConversionOptions, convert\n\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(\"<h1>Title</h1><p>Paragraph</p>\", options)\n    doc = result.document\n    for node in doc.nodes:\n        print(node)\n    ```\n\n=== \"TypeScript\"\n    ```typescript\n    import { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\n    const options: ConversionOptions = { includeDocumentStructure: true };\n    const result = convert('<h1>Title</h1><p>Paragraph</p>', options);\n    const nodes = result.document?.nodes ?? [];\n    for (const node of nodes) {\n      console.log(node);\n    }\n    ```\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "docs/visitor.md",
    "content": "# Visitor Pattern\n\nThe visitor system is the library's main extensibility point. Implement `HtmlVisitor` and you can replace, skip, or augment how any HTML element becomes Markdown. No fork required.\n\nRust users must opt in with `features = [\"visitor\"]`. The other bindings expose the visitor through their native idiom (`Visitor` interface in Java, callback object in Python, etc.) and link against a Rust core built with the feature enabled.\n\n## Execution Order\n\nTraversal is pre-order. For `<div><p>text</p></div>`:\n\n1. `visit_element_start` fires for `<div>`\n2. `visit_element_start` fires for `<p>`\n3. `visit_text` fires for `\"text\"`\n4. `visit_element_end` fires for `<p>` with the rendered output\n5. `visit_element_end` fires for `<div>` with the rendered output\n\n`visit_text` is hot. It runs for every text node in the document, often 100+ times on a single page. Return `Continue` fast when you don't care about the node, and avoid allocations in the method body.\n\n## VisitResult\n\nEvery callback returns a `VisitResult`.\n\n| Variant | Effect |\n|---------|--------|\n| `Continue` | Use the default rendering. |\n| `Custom(String)` | Replace the default output with the supplied Markdown. The visitor owns the rendering for this node and its children. |\n| `Skip` | Drop the element and all of its children. |\n| `PreserveHtml` | Emit the raw HTML for this element verbatim. |\n| `Error(String)` | Halt conversion. The message surfaces as `ConversionError::Visitor` in Rust (behind `features = [\"visitor\"]`). |\n\n## NodeContext\n\nEvery callback receives a `NodeContext` describing the current node.\n\n| Field | Type | Meaning |\n|-------|------|---------|\n| `node_type` | `NodeType` | Coarse-grained classification (heading, list, link, form, …). 87 variants. |\n| `tag_name` | `String` | Raw HTML tag name. Lowercased. |\n| `attributes` | `BTreeMap<String, String>` | All attributes on the element. |\n| `depth` | `usize` | Depth in the DOM tree. Root is 0. |\n| `index_in_parent` | `usize` | 0-based position among siblings. |\n| `parent_tag` | `Option<String>` | Parent element's tag, or `None` at the root. |\n| `is_inline` | `bool` | `true` when the element is rendered inline (inside a paragraph, link text, cell, …). |\n\n## Method Reference\n\nAll 40 methods have default implementations that return `Continue`. Override only the ones you care about.\n\n### Generic element callbacks\n\n| Method | When it fires |\n|--------|---------------|\n| `visit_element_start(ctx)` | Before any element. First callback for every node. |\n| `visit_element_end(ctx, output)` | After an element, with the rendered Markdown. |\n| `visit_text(ctx, text)` | Every text node. HTML entities already decoded. |\n| `visit_custom_element(ctx, tag_name, html)` | Unknown tags and web components. |\n\n### Links and images\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_link(ctx, href, text, title)` | `<a>` anchor with href, rendered text, and optional title. |\n| `visit_image(ctx, src, alt, title)` | `<img>` with src, alt text, and optional title. |\n\n### Headings, rules, breaks\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_heading(ctx, level, text, id)` | `<h1>`–`<h6>` with level (1-6), text, and optional id. |\n| `visit_horizontal_rule(ctx)` | `<hr>`. |\n| `visit_line_break(ctx)` | `<br>`. |\n\n### Code\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_code_block(ctx, lang, code)` | `<pre><code>` with language tag and raw code. |\n| `visit_code_inline(ctx, code)` | Inline `<code>`. |\n\n### Lists\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_list_start(ctx, ordered)` | Before `<ul>` or `<ol>`. |\n| `visit_list_item(ctx, ordered, marker, text)` | Each `<li>` with marker and rendered text. |\n| `visit_list_end(ctx, ordered, output)` | After the list, with the rendered block. |\n\n### Definition lists\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_definition_list_start(ctx)` | Before `<dl>`. |\n| `visit_definition_term(ctx, text)` | `<dt>`. |\n| `visit_definition_description(ctx, text)` | `<dd>`. |\n| `visit_definition_list_end(ctx, output)` | After `<dl>`. |\n\n### Tables\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_table_start(ctx)` | Before `<table>`. |\n| `visit_table_row(ctx, cells, is_header)` | Each `<tr>`. Cells are pre-rendered Markdown. `is_header` is true for rows inside `<thead>`. |\n| `visit_table_end(ctx, output)` | After `<table>`. |\n\n### Blockquote\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_blockquote(ctx, content, depth)` | `<blockquote>` with rendered content and nesting depth. |\n\n### Inline formatting\n\n| Method | Covers |\n|--------|--------|\n| `visit_strong(ctx, text)` | `<strong>`, `<b>`. |\n| `visit_emphasis(ctx, text)` | `<em>`, `<i>`. |\n| `visit_strikethrough(ctx, text)` | `<s>`, `<del>`, `<strike>`. |\n| `visit_underline(ctx, text)` | `<u>`, `<ins>`. |\n| `visit_subscript(ctx, text)` | `<sub>`. |\n| `visit_superscript(ctx, text)` | `<sup>`. |\n| `visit_mark(ctx, text)` | `<mark>`. |\n\n### Forms\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_form(ctx, action, method)` | `<form>` with optional action URL and method. |\n| `visit_input(ctx, input_type, name, value)` | `<input>`. |\n| `visit_button(ctx, text)` | `<button>`. |\n\n### Media\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_audio(ctx, src)` | `<audio>`. |\n| `visit_video(ctx, src)` | `<video>`. |\n| `visit_iframe(ctx, src)` | `<iframe>`. |\n\n### Interactive\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_details(ctx, open)` | `<details>` with the `open` attribute. |\n| `visit_summary(ctx, text)` | `<summary>`. |\n\n### Figures\n\n| Method | Arguments |\n|--------|-----------|\n| `visit_figure_start(ctx)` | Before `<figure>`. |\n| `visit_figcaption(ctx, text)` | `<figcaption>`. |\n| `visit_figure_end(ctx, output)` | After `<figure>`. |\n\n## Basic Visitor\n\n=== \"Rust\"\n    --8<-- \"snippets/rust/visitor/basic_visitor.md\"\n\n=== \"Python\"\n    --8<-- \"snippets/python/visitor/basic_visitor.md\"\n\n=== \"TypeScript\"\n    --8<-- \"snippets/typescript/visitor/basic_visitor.md\"\n\n=== \"Go\"\n    --8<-- \"snippets/go/visitor/basic_visitor.md\"\n\n=== \"Ruby\"\n    --8<-- \"snippets/ruby/visitor/basic_visitor.md\"\n\n=== \"PHP\"\n    --8<-- \"snippets/php/visitor/basic_visitor.md\"\n\n=== \"Java\"\n    --8<-- \"snippets/java/visitor/basic_visitor.md\"\n\n=== \"C#\"\n    --8<-- \"snippets/csharp/visitor/basic_visitor.md\"\n\n=== \"Elixir\"\n    --8<-- \"snippets/elixir/visitor/basic_visitor.md\"\n\n=== \"R\"\n    --8<-- \"snippets/r/visitor/basic_visitor.md\"\n\n=== \"C\"\n    --8<-- \"snippets/c/visitor/basic_visitor.md\"\n\n=== \"WASM\"\n    --8<-- \"snippets/wasm/visitor/basic_visitor.md\"\n\n## Common Patterns\n\n### Link rewriting\n\nOverride `visit_link`, return `VisitResult::Custom(...)` with the new URL baked in. Useful for rewriting relative links to absolute, stripping tracking parameters, or converting internal links to anchor references.\n\n### Element filtering\n\nOverride `visit_element_start` and return `VisitResult::Skip` when `ctx.tag_name` matches an unwanted tag. The element and every descendant is dropped. A class filter works too: check `ctx.attributes.get(\"class\")` and skip on match.\n\n### Content extraction\n\nOverride `visit_text` and push each text fragment into an external buffer. The visitor becomes a simple text-extraction pass that bypasses Markdown rendering. Combine with `Skip` on unwanted elements to exclude code blocks, navigation, or footers.\n\n## Performance\n\n`visit_text` fires on every text node. Keep the handler small. Match the few element kinds you care about in `visit_element_start` and return `Continue` for everything else. Allocations inside the handler multiply by the number of text nodes in the input.\n\nThe visitor trait is synchronous. The core walker calls each method in place during the single-pass DOM traversal.\n\n--8<-- \"snippets/feedback.md\"\n"
  },
  {
    "path": "e2e/c/Makefile",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:c56e794f5b4fe60fae5ebe68ce87194a8b83df9a3a617a0b92282cea88e91f0c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nCC = gcc\nFFI_DIR = ffi\n\nifneq ($(wildcard $(FFI_DIR)/include/html_to_markdown.h),)\n    CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include\n    LDFLAGS = -L$(FFI_DIR)/lib -lhtml_to_markdown_ffi -Wl,-rpath,$(FFI_DIR)/lib\nelse ifneq ($(wildcard ../../crates/html-to-markdown-ffi/include/html_to_markdown.h),)\n    CFLAGS = -Wall -Wextra -I. -I../../crates/html-to-markdown-ffi/include\n    LDFLAGS = -L../../target/release -lhtml_to_markdown_ffi -Wl,-rpath,../../target/release\nelse\n    CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags html_to_markdown_ffi 2>/dev/null)\n    LDFLAGS = $(shell pkg-config --libs html_to_markdown_ffi 2>/dev/null)\nendif\n\nSRCS = main.c test_conversion.c test_edge_cases.c test_metadata.c test_options.c test_real_world.c test_result.c test_smoke.c test_structure.c\nTARGET = run_tests\n\n.PHONY: all clean test\n\nall: $(TARGET)\n\n$(TARGET): $(SRCS)\n\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)\n\ntest: $(TARGET)\n\t./$(TARGET)\n\nclean:\n\trm -f $(TARGET)\n"
  },
  {
    "path": "e2e/c/download_ffi.sh",
    "content": "#!/usr/bin/env bash\n# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:50e0af0cb3327cb9a5e81e02be2e94e26dbb1c205ee5c50b41b56042197dd685\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nset -euo pipefail\n\nREPO_URL=\"https://github.com/kreuzberg-dev/html-to-markdown\"\nVERSION=\"3.4.0-rc.25\"\nFFI_PKG_NAME=\"html-to-markdown-ffi\"\nFFI_DIR=\"ffi\"\n\n# Detect OS and architecture.\nOS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"\nARCH=\"$(uname -m)\"\n\ncase \"$ARCH\" in\nx86_64 | amd64) ARCH=\"x86_64\" ;;\narm64 | aarch64) ARCH=\"aarch64\" ;;\n*)\n  echo \"Unsupported architecture: $ARCH\" >&2\n  exit 1\n  ;;\nesac\n\ncase \"$OS\" in\nlinux) TRIPLE=\"${ARCH}-unknown-linux-gnu\" ;;\ndarwin) TRIPLE=\"${ARCH}-apple-darwin\" ;;\n*)\n  echo \"Unsupported OS: $OS\" >&2\n  exit 1\n  ;;\nesac\n\nARCHIVE=\"${FFI_PKG_NAME}-${TRIPLE}.tar.gz\"\nURL=\"${REPO_URL}/releases/download/v${VERSION}/${ARCHIVE}\"\n\necho \"Downloading ${ARCHIVE} from v${VERSION}...\"\nmkdir -p \"$FFI_DIR\"\ncurl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"\necho \"FFI library extracted to $FFI_DIR/\"\n"
  },
  {
    "path": "e2e/c/main.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e66d77beedf486c083f584eb68762ac582a374510fc3e6ad5b49bfc8f3185019\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n#include \"test_runner.h\"\n#include <stdio.h>\n\nint main(void) {\n    int passed = 0;\n    int failed = 0;\n\n    /* Category: conversion */\n    printf(\"  Running test_blockquote_multiple_paragraphs...\");\n    test_blockquote_multiple_paragraphs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_nested...\");\n    test_blockquote_nested();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_simple...\");\n    test_blockquote_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_with_list...\");\n    test_blockquote_with_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_bold_and_italic...\");\n    test_bold_and_italic();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_bold_strong...\");\n    test_bold_strong();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_block...\");\n    test_code_block();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_block_no_language...\");\n    test_code_block_no_language();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_inline_in_paragraph...\");\n    test_code_inline_in_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_with_backticks_in_content...\");\n    test_code_with_backticks_in_content();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_mark_highlight...\");\n    test_emphasis_mark_highlight();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_strikethrough_del...\");\n    test_emphasis_strikethrough_del();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_strikethrough_s...\");\n    test_emphasis_strikethrough_s();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_subscript...\");\n    test_emphasis_subscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_superscript...\");\n    test_emphasis_superscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_underline_u...\");\n    test_emphasis_underline_u();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_input_elements...\");\n    test_form_input_elements();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_select_options...\");\n    test_form_select_options();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_textarea...\");\n    test_form_textarea();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h1...\");\n    test_heading_h1();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h2...\");\n    test_heading_h2();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h3...\");\n    test_heading_h3();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h4...\");\n    test_heading_h4();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h5...\");\n    test_heading_h5();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h6...\");\n    test_heading_h6();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_figure_figcaption...\");\n    test_image_figure_figcaption();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_linked...\");\n    test_image_linked();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_no_alt...\");\n    test_image_no_alt();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_simple...\");\n    test_image_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_with_title...\");\n    test_image_with_title();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_inline_code...\");\n    test_inline_code();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_italic_em...\");\n    test_italic_em();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_br_tag...\");\n    test_line_break_br_tag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_hr_tag...\");\n    test_line_break_hr_tag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_multiple_br...\");\n    test_line_break_multiple_br();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_anchor_fragment...\");\n    test_link_anchor_fragment();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_empty_href...\");\n    test_link_empty_href();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_image_inside...\");\n    test_link_image_inside();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_mailto...\");\n    test_link_mailto();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_simple...\");\n    test_link_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_with_bold_text...\");\n    test_link_with_bold_text();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_with_title...\");\n    test_link_with_title();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_definition_dl...\");\n    test_list_definition_dl();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_item_multiple_paragraphs...\");\n    test_list_item_multiple_paragraphs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_mixed_nested...\");\n    test_list_mixed_nested();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_nested_ordered...\");\n    test_list_nested_ordered();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_nested_unordered...\");\n    test_list_nested_unordered();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_task_checkboxes...\");\n    test_list_task_checkboxes();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_ordered_list...\");\n    test_ordered_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_multiple...\");\n    test_paragraph_multiple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_nested_divs...\");\n    test_paragraph_nested_divs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_simple...\");\n    test_paragraph_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_with_inline_formatting...\");\n    test_paragraph_with_inline_formatting();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_with_line_breaks...\");\n    test_paragraph_with_line_breaks();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_abbr...\");\n    test_semantic_abbr();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_article...\");\n    test_semantic_article();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_definition_list...\");\n    test_semantic_definition_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_details_summary...\");\n    test_semantic_details_summary();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_hr...\");\n    test_semantic_hr();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_mark_highlight...\");\n    test_semantic_mark_highlight();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_section_with_heading...\");\n    test_semantic_section_with_heading();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_sub_superscript...\");\n    test_semantic_sub_superscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_simple_table...\");\n    test_simple_table();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_empty...\");\n    test_table_empty();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_no_thead...\");\n    test_table_no_thead();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_pipe_chars_in_content...\");\n    test_table_pipe_chars_in_content();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_with_alignment...\");\n    test_table_with_alignment();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_with_colspan...\");\n    test_table_with_colspan();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_unordered_list...\");\n    test_unordered_list();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: edge-cases */\n    printf(\"  Running test_empty_html...\");\n    test_empty_html();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_encoding_cjk_characters...\");\n    test_encoding_cjk_characters();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_encoding_html_entities...\");\n    test_encoding_html_entities();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_encoding_named_entities...\");\n    test_encoding_named_entities();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_encoding_numeric_entities...\");\n    test_encoding_numeric_entities();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_encoding_unicode_emoji...\");\n    test_encoding_unicode_emoji();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_html_comments_only...\");\n    test_html_comments_only();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_just_whitespace_input...\");\n    test_just_whitespace_input();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_malformed_deeply_nested_elements...\");\n    test_malformed_deeply_nested_elements();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_malformed_missing_block_closing_tags...\");\n    test_malformed_missing_block_closing_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_malformed_overlapping_tags...\");\n    test_malformed_overlapping_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_malformed_unclosed_paragraph...\");\n    test_malformed_unclosed_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_script_tags_only...\");\n    test_script_tags_only();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_style_tags_only...\");\n    test_style_tags_only();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_whitespace_only...\");\n    test_whitespace_only();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_xss_onclick_handler_removed...\");\n    test_xss_onclick_handler_removed();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_xss_script_tag_stripped...\");\n    test_xss_script_tag_stripped();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_xss_svg_nested_script_stripped...\");\n    test_xss_svg_nested_script_stripped();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: metadata */\n    printf(\"  Running test_metadata_author_meta...\");\n    test_metadata_author_meta();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_canonical_url...\");\n    test_metadata_canonical_url();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_description_meta...\");\n    test_metadata_description_meta();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_dublin_core...\");\n    test_metadata_dublin_core();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_extract_all_images...\");\n    test_metadata_extract_all_images();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_extract_all_links...\");\n    test_metadata_extract_all_links();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_headers_hierarchy...\");\n    test_metadata_headers_hierarchy();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_keywords_meta...\");\n    test_metadata_keywords_meta();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_lang_attribute...\");\n    test_metadata_lang_attribute();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_microdata_schema_article...\");\n    test_metadata_microdata_schema_article();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_microdata_schema_breadcrumb...\");\n    test_metadata_microdata_schema_breadcrumb();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_microdata_schema_organization...\");\n    test_metadata_microdata_schema_organization();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_microdata_schema_person...\");\n    test_metadata_microdata_schema_person();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_microdata_schema_product...\");\n    test_metadata_microdata_schema_product();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_text_direction_ltr...\");\n    test_metadata_text_direction_ltr();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_text_direction_rtl...\");\n    test_metadata_text_direction_rtl();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_metadata_title_tag...\");\n    test_metadata_title_tag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_og_basic_tags...\");\n    test_og_basic_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_og_multiple_tags...\");\n    test_og_multiple_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structured_data_json_ld...\");\n    test_structured_data_json_ld();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structured_data_multiple_json_ld...\");\n    test_structured_data_multiple_json_ld();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_twitter_card_tags...\");\n    test_twitter_card_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: options */\n    printf(\"  Running test_options_autolinks_false...\");\n    test_options_autolinks_false();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_br_in_tables_false...\");\n    test_options_br_in_tables_false();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_br_in_tables_true...\");\n    test_options_br_in_tables_true();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_code_block_backticks...\");\n    test_options_code_block_backticks();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_code_block_indented...\");\n    test_options_code_block_indented();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_code_block_tildes...\");\n    test_options_code_block_tildes();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_code_block_tildes_style...\");\n    test_options_code_block_tildes_style();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_code_language_python...\");\n    test_options_code_language_python();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_convert_as_inline...\");\n    test_options_convert_as_inline();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_debug_true...\");\n    test_options_debug_true();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_default_title_true...\");\n    test_options_default_title_true();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_encoding_utf8...\");\n    test_options_encoding_utf8();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_escape_ascii_enabled...\");\n    test_options_escape_ascii_enabled();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_escape_asterisks...\");\n    test_options_escape_asterisks();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_escape_misc...\");\n    test_options_escape_misc();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_escape_underscores...\");\n    test_options_escape_underscores();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_attribute...\");\n    test_options_exclude_selectors_attribute();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_class...\");\n    test_options_exclude_selectors_class();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_empty_noop...\");\n    test_options_exclude_selectors_empty_noop();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_id...\");\n    test_options_exclude_selectors_id();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_multiple...\");\n    test_options_exclude_selectors_multiple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_nested_content_dropped...\");\n    test_options_exclude_selectors_nested_content_dropped();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_plain_text_mode...\");\n    test_options_exclude_selectors_plain_text_mode();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_exclude_selectors_vs_strip_tags...\");\n    test_options_exclude_selectors_vs_strip_tags();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_extract_metadata_true...\");\n    test_options_extract_metadata_true();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_heading_style_atx...\");\n    test_options_heading_style_atx();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_heading_style_atx_closed...\");\n    test_options_heading_style_atx_closed();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_heading_style_underlined...\");\n    test_options_heading_style_underlined();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_highlight_bold...\");\n    test_options_highlight_bold();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_highlight_double_equal...\");\n    test_options_highlight_double_equal();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_highlight_none...\");\n    test_options_highlight_none();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_keep_inline_images_in_paragraph...\");\n    test_options_keep_inline_images_in_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_link_style_reference...\");\n    test_options_link_style_reference();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_list_custom_bullets...\");\n    test_options_list_custom_bullets();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_list_indent_tabs...\");\n    test_options_list_indent_tabs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_list_indent_width_four...\");\n    test_options_list_indent_width_four();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_max_depth_default_unlimited...\");\n    test_options_max_depth_default_unlimited();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_max_depth_truncates...\");\n    test_options_max_depth_truncates();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_max_depth_zero_empty...\");\n    test_options_max_depth_zero_empty();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_newline_backslash...\");\n    test_options_newline_backslash();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_newline_spaces...\");\n    test_options_newline_spaces();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_output_format_djot...\");\n    test_options_output_format_djot();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_output_format_markdown...\");\n    test_options_output_format_markdown();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_output_format_plain...\");\n    test_options_output_format_plain();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_preprocessing_aggressive...\");\n    test_options_preprocessing_aggressive();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_preprocessing_minimal...\");\n    test_options_preprocessing_minimal();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_preprocessing_remove_forms...\");\n    test_options_preprocessing_remove_forms();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_preserve_tags_iframe...\");\n    test_options_preserve_tags_iframe();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_skip_images_true...\");\n    test_options_skip_images_true();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_strip_newlines...\");\n    test_options_strip_newlines();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_strip_tags_div_span...\");\n    test_options_strip_tags_div_span();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_strong_em_underscore...\");\n    test_options_strong_em_underscore();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_sub_symbol_tilde...\");\n    test_options_sub_symbol_tilde();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_sup_symbol_caret...\");\n    test_options_sup_symbol_caret();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_whitespace_normalized...\");\n    test_options_whitespace_normalized();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_whitespace_strict...\");\n    test_options_whitespace_strict();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_wrap_disabled...\");\n    test_options_wrap_disabled();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_options_wrap_enabled...\");\n    test_options_wrap_enabled();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: real-world */\n    printf(\"  Running test_real_world_blog_post...\");\n    test_real_world_blog_post();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_real_world_documentation_page...\");\n    test_real_world_documentation_page();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_real_world_product_page...\");\n    test_real_world_product_page();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: result */\n    printf(\"  Running test_result_tables_empty_when_no_tables...\");\n    test_result_tables_empty_when_no_tables();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_tables_multiple...\");\n    test_result_tables_multiple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_tables_simple...\");\n    test_result_tables_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_tables_without_structure_flag...\");\n    test_result_tables_without_structure_flag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_warnings_empty_for_clean_input...\");\n    test_result_warnings_empty_for_clean_input();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_warnings_empty_for_complex_input...\");\n    test_result_warnings_empty_for_complex_input();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_result_warnings_empty_for_malformed_html...\");\n    test_result_warnings_empty_for_malformed_html();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: smoke */\n    printf(\"  Running test_smoke_empty_string...\");\n    test_smoke_empty_string();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_smoke_simple_heading...\");\n    test_smoke_simple_heading();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_smoke_simple_paragraph...\");\n    test_smoke_simple_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: structure */\n    printf(\"  Running test_structure_code_block...\");\n    test_structure_code_block();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_deep_nesting_h1_h2_h3...\");\n    test_structure_deep_nesting_h1_h2_h3();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_h1_h2_nested_group...\");\n    test_structure_h1_h2_nested_group();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_heading_paragraph...\");\n    test_structure_heading_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_list...\");\n    test_structure_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_multiple_headings...\");\n    test_structure_multiple_headings();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_structure_sibling_h1_groups...\");\n    test_structure_sibling_h1_groups();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);\n    return failed > 0 ? 1 : 0;\n}\n"
  },
  {
    "path": "e2e/c/test_conversion.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2ad14e00aaa978f3143f593ca5d5866ef557b61997f82af3f68a731940260260\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: conversion */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_blockquote_multiple_paragraphs(void) {\n    /* Blockquote with multiple paragraphs has each paragraph prefixed */\n    HTMConversionResult *result = htm_convert(\n        \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"> First paragraph.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"> Second paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_blockquote_nested(void) {\n    /* Nested blockquote produces double-prefixed lines */\n    HTMConversionResult *result = htm_convert(\n        \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Outer quote.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Inner quote.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_blockquote_simple(void) {\n    /* Simple blockquote */\n    HTMConversionResult *result = htm_convert(\"<blockquote><p>Quote text</p></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"> Quote text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_blockquote_with_list(void) {\n    /* Blockquote containing a list preserves list items inside quote */\n    HTMConversionResult *result = htm_convert(\n        \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Quote intro:\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Point one\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Point two\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_bold_and_italic(void) {\n    /* Nested bold and italic */\n    HTMConversionResult *result = htm_convert(\"<p><strong><em>both</em></strong></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"***both***\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_bold_strong(void) {\n    /* Strong tag converts to bold */\n    HTMConversionResult *result = htm_convert(\"<p><strong>bold</strong></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"**bold**\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_code_block(void) {\n    /* Code block with language preserves content */\n    HTMConversionResult *result =\n        htm_convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"print('hello')\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_code_block_no_language(void) {\n    /* Code block without a language class preserves content */\n    HTMConversionResult *result = htm_convert(\"<pre><code>plain code here</code></pre>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"plain code here\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_code_inline_in_paragraph(void) {\n    /* Inline code element nested inside a paragraph */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Call the <code>initialize()</code> method first.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"`initialize()`\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_code_with_backticks_in_content(void) {\n    /* Inline code containing backtick characters is properly escaped */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"backtick\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_mark_highlight(void) {\n    /* mark tag produces highlighted output */\n    HTMConversionResult *result = htm_convert(\"<p><mark>highlighted</mark></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"highlighted\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_strikethrough_del(void) {\n    /* del tag converts to GFM strikethrough */\n    HTMConversionResult *result = htm_convert(\"<p><del>deleted text</del></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"~~deleted text~~\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_strikethrough_s(void) {\n    /* s tag converts to GFM strikethrough */\n    HTMConversionResult *result = htm_convert(\"<p><s>strikethrough</s></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"~~strikethrough~~\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_subscript(void) {\n    /* sub tag content is preserved */\n    HTMConversionResult *result = htm_convert(\"<p>H<sub>2</sub>O</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"H\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"O\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_superscript(void) {\n    /* sup tag content is preserved */\n    HTMConversionResult *result = htm_convert(\"<p>x<sup>2</sup></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"x\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_emphasis_underline_u(void) {\n    /* u tag content is preserved in output */\n    HTMConversionResult *result = htm_convert(\"<p><u>underlined</u></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"underlined\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_form_input_elements(void) {\n    /* Form input elements produce readable output without form mechanics */\n    HTMConversionResult *result =\n        htm_convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" \"\n                    \"placeholder=\\\"Enter name\\\"></form>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Name\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_form_select_options(void) {\n    /* Select element with options produces readable output */\n    HTMConversionResult *result =\n        htm_convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option \"\n                    \"value=\\\"blue\\\" selected>Blue</option><option \"\n                    \"value=\\\"green\\\">Green</option></select></form>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Color\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_form_textarea(void) {\n    /* Textarea element produces readable output */\n    HTMConversionResult *result = htm_convert(\n        \"<form><label>Message:</label><textarea>Default text content</textarea></form>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Message\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h1(void) {\n    /* H1 heading */\n    HTMConversionResult *result = htm_convert(\"<h1>Heading 1</h1>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"# Heading 1\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h2(void) {\n    /* H2 heading */\n    HTMConversionResult *result = htm_convert(\"<h2>Heading 2</h2>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"## Heading 2\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h3(void) {\n    /* H3 heading */\n    HTMConversionResult *result = htm_convert(\"<h3>Heading 3</h3>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"### Heading 3\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h4(void) {\n    /* H4 heading */\n    HTMConversionResult *result = htm_convert(\"<h4>Heading 4</h4>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"#### Heading 4\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h5(void) {\n    /* H5 heading */\n    HTMConversionResult *result = htm_convert(\"<h5>Heading 5</h5>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"##### Heading 5\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_heading_h6(void) {\n    /* H6 heading */\n    HTMConversionResult *result = htm_convert(\"<h6>Heading 6</h6>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"###### Heading 6\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_image_figure_figcaption(void) {\n    /* Figure with figcaption preserves both image and caption */\n    HTMConversionResult *result =\n        htm_convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset \"\n                    \"over the ocean</figcaption></figure>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"![A sunset](sunset.jpg)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Beautiful sunset over the ocean\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_image_linked(void) {\n    /* Image inside an anchor produces a linked image */\n    HTMConversionResult *result = htm_convert(\n        \"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"![Icon](icon.png)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_image_no_alt(void) {\n    /* Image without alt text produces image markdown */\n    HTMConversionResult *result = htm_convert(\"<img src=\\\"banner.jpg\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"banner.jpg\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_image_simple(void) {\n    /* Image with alt text */\n    HTMConversionResult *result = htm_convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"![A photo](photo.jpg)\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_image_with_title(void) {\n    /* Image with title attribute includes title in output */\n    HTMConversionResult *result =\n        htm_convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"![Sales chart](chart.png\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Q3 Sales\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_inline_code(void) {\n    /* Inline code */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Use <code>console.log()</code> to debug</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"`console.log()`\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_italic_em(void) {\n    /* Em tag converts to italic */\n    HTMConversionResult *result = htm_convert(\"<p><em>italic</em></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"*italic*\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_line_break_br_tag(void) {\n    /* Single br tag produces a line break in output */\n    HTMConversionResult *result = htm_convert(\"<p>First line.<br>Second line.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"First line.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second line.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_line_break_hr_tag(void) {\n    /* hr tag produces a horizontal separator between content */\n    HTMConversionResult *result = htm_convert(\"<p>Before rule.</p><hr><p>After rule.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Before rule.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After rule.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_line_break_multiple_br(void) {\n    /* Multiple consecutive br tags in sequence */\n    HTMConversionResult *result = htm_convert(\"<p>Start.<br><br>End.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Start.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"End.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_anchor_fragment(void) {\n    /* Fragment-only anchor link is preserved */\n    HTMConversionResult *result = htm_convert(\"<a href=\\\"#section\\\">Jump to section</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"[Jump to section](#section)\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_empty_href(void) {\n    /* Link with empty href produces output with the link text */\n    HTMConversionResult *result = htm_convert(\"<a href=\\\"\\\">No destination</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"No destination\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_image_inside(void) {\n    /* Image inside a link produces a linked image */\n    HTMConversionResult *result = htm_convert(\n        \"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"![Logo](logo.png)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_mailto(void) {\n    /* Mailto link is preserved with mailto: scheme */\n    HTMConversionResult *result =\n        htm_convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"mailto:user@example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_simple(void) {\n    /* Simple link */\n    HTMConversionResult *result = htm_convert(\"<a href=\\\"https://example.com\\\">Example</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"[Example](https://example.com)\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_with_bold_text(void) {\n    /* Link containing bold text preserves formatting */\n    HTMConversionResult *result =\n        htm_convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"**Bold link**\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_link_with_title(void) {\n    /* Link with title attribute */\n    HTMConversionResult *result =\n        htm_convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"[Example](https://example.com\") != NULL &&\n           \"expected to contain substring\");\n    assert(strstr(content, \"Example Site\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_definition_dl(void) {\n    /* Definition list with dt and dd elements */\n    HTMConversionResult *result =\n        htm_convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term \"\n                    \"Two</dt><dd>Definition of term two.</dd></dl>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Term One\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Definition of term one.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Term Two\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Definition of term two.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_item_multiple_paragraphs(void) {\n    /* List item containing multiple paragraphs */\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in \"\n                    \"item.</p></li><li>Simple item</li></ul>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"First paragraph in item.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph in item.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Simple item\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_mixed_nested(void) {\n    /* Mixed list: ordered list nested inside unordered list */\n    HTMConversionResult *result = htm_convert(\n        \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Item A\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Sub 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Sub 2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Item B\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_nested_ordered(void) {\n    /* Nested ordered list with two levels of depth */\n    HTMConversionResult *result = htm_convert(\n        \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Step 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 1a\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 1b\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 2\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_nested_unordered(void) {\n    /* Nested unordered list with two levels of depth */\n    HTMConversionResult *result = htm_convert(\n        \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Parent A\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Child A1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Child A2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Parent B\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_list_task_checkboxes(void) {\n    /* Task list with checked and unchecked checkboxes */\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input \"\n                    \"type=\\\"checkbox\\\"> Pending task</li></ul>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Done task\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Pending task\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_ordered_list(void) {\n    /* Ordered list */\n    HTMConversionResult *result =\n        htm_convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"1. First\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2. Second\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"3. Third\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_paragraph_multiple(void) {\n    /* Multiple paragraphs are separated by a blank line */\n    HTMConversionResult *result =\n        htm_convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"First paragraph.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_paragraph_nested_divs(void) {\n    /* Text nested inside divs is extracted correctly */\n    HTMConversionResult *result = htm_convert(\"<div><div><p>Nested text</p></div></div>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Nested text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_paragraph_simple(void) {\n    /* Simple paragraph converts to plain text */\n    HTMConversionResult *result = htm_convert(\"<p>Hello World</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"Hello World\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_paragraph_with_inline_formatting(void) {\n    /* Paragraph with bold, italic, and a link */\n    HTMConversionResult *result = htm_convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, \"\n                                              \"and a <a href=\\\"https://example.com\\\">link</a>.</p>\",\n                                              NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"**bold**\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"*italic*\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"[link](https://example.com)\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_paragraph_with_line_breaks(void) {\n    /* Paragraph with br tags produces line breaks in output */\n    HTMConversionResult *result = htm_convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Line one.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line two.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line three.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_abbr(void) {\n    /* Abbreviation element text is preserved */\n    HTMConversionResult *result =\n        htm_convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"WWW\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_article(void) {\n    /* Article element wrapping content preserves inner content */\n    HTMConversionResult *result =\n        htm_convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Article Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Article body.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_definition_list(void) {\n    /* Definition list with term and description */\n    HTMConversionResult *result =\n        htm_convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading \"\n                    \"Style Sheets</dd></dl>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"HTML\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"HyperText Markup Language\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"CSS\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Cascading Style Sheets\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_details_summary(void) {\n    /* Details and summary elements produce readable output */\n    HTMConversionResult *result = htm_convert(\n        \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Click to expand\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_hr(void) {\n    /* Horizontal rule produces a separator in output */\n    HTMConversionResult *result = htm_convert(\"<p>Above</p><hr><p>Below</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Above\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Below\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_mark_highlight(void) {\n    /* Mark tag produces highlighted output */\n    HTMConversionResult *result =\n        htm_convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"highlighted text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_section_with_heading(void) {\n    /* Section element with heading preserves structure */\n    HTMConversionResult *result =\n        htm_convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Section Heading\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Section content.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_semantic_sub_superscript(void) {\n    /* Subscript and superscript elements are preserved in output */\n    HTMConversionResult *result = htm_convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"H\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"O\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"E=mc\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_simple_table(void) {\n    /* Simple table with header */\n    HTMConversionResult *result =\n        htm_convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</\"\n                    \"td><td>30</td></tr></tbody></table>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Name\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Age\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Alice\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"30\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"---\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_table_empty(void) {\n    /* Empty table produces no output or minimal output */\n    HTMConversionResult *result = htm_convert(\"<table></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_table_no_thead(void) {\n    /* Table without thead uses first row as implied header */\n    HTMConversionResult *result = htm_convert(\"<table><tr><td>Product</td><td>Price</td></\"\n                                              \"tr><tr><td>Apple</td><td>1.00</td></tr></table>\",\n                                              NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Product\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Price\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Apple\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"1.00\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_table_pipe_chars_in_content(void) {\n    /* Table cells containing pipe characters are escaped in output */\n    HTMConversionResult *result =\n        htm_convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></\"\n                    \"thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Expression\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Result\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"true\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_table_with_alignment(void) {\n    /* Table with column alignment attributes */\n    HTMConversionResult *result = htm_convert(\n        \"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th \"\n        \"align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></\"\n        \"tbody></table>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Left\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Center\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Right\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"L\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"C\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"R\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_table_with_colspan(void) {\n    /* Table with colspan attribute in a header cell */\n    HTMConversionResult *result = htm_convert(\n        \"<table><thead><tr><th colspan=\\\"2\\\">Full \"\n        \"Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Full Name\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"John\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Doe\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_unordered_list(void) {\n    /* Unordered list */\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"- Item 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"- Item 2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"- Item 3\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_edge_cases.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c3c67b7b1a8e7411847b1750edea8794716bae9e760f305f0bbcb8ad6240889c\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: edge-cases */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_empty_html(void) {\n    /* Empty HTML document */\n    HTMConversionResult *result = htm_convert(\"<html><head></head><body></body></html>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_encoding_cjk_characters(void) {\n    /* CJK (Chinese, Japanese, Korean) characters are preserved */\n    HTMConversionResult *result =\n        htm_convert(\"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"中文内容\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"日本語テキスト\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"한국어 텍스트\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_encoding_html_entities(void) {\n    /* Common HTML entities are decoded in output */\n    HTMConversionResult *result = htm_convert(\"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"&\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"<\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \">\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_encoding_named_entities(void) {\n    /* Named HTML entities like &mdash; and &hellip; are decoded */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. \"\n                    \"Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"—\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"…\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_encoding_numeric_entities(void) {\n    /* Numeric HTML entities (decimal and hex) are decoded */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"©\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"®\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"€\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_encoding_unicode_emoji(void) {\n    /* Emoji and Unicode characters are preserved */\n    HTMConversionResult *result = htm_convert(\"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"🌍\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"🚀\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"⭐\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_html_comments_only(void) {\n    /* Document containing only HTML comments produces empty output */\n    HTMConversionResult *result =\n        htm_convert(\"<!-- This is a comment --><!-- Another comment -->\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_just_whitespace_input(void) {\n    /* Input that is only whitespace characters (spaces, tabs, newlines) produces empty output */\n    HTMConversionResult *result = htm_convert(\"   \", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_malformed_deeply_nested_elements(void) {\n    /* Deeply nested elements (100 levels) are handled without stack overflow */\n    HTMConversionResult *result = htm_convert(\n        \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div>\"\n        \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div>\"\n        \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested \"\n        \"content</p></div></div></div></div></div></div></div></div></div></div></div></div></\"\n        \"div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></\"\n        \"div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></\"\n        \"div></div></div></div></div></div></div></div>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Deeply nested content\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_malformed_missing_block_closing_tags(void) {\n    /* Missing closing tags on block elements are auto-closed by parser */\n    HTMConversionResult *result =\n        htm_convert(\"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"First paragraph\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_malformed_overlapping_tags(void) {\n    /* Overlapping bold/italic tags are recovered by the HTML parser without panic */\n    HTMConversionResult *result = htm_convert(\"<p><b><i>bold and italic</b></i></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"bold and italic\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_malformed_unclosed_paragraph(void) {\n    /* Unclosed <p> tag is recovered gracefully and content is preserved */\n    HTMConversionResult *result = htm_convert(\"<p>This paragraph is never closed\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"This paragraph is never closed\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_script_tags_only(void) {\n    /* Document with only script tags produces empty output (scripts are stripped) */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><script>alert('xss')</script></\"\n                    \"head><body><script>document.write('hello')</script></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_style_tags_only(void) {\n    /* Document with only style tags produces empty output (styles are stripped) */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><style>body { color: red; }</style></head><body><style>.foo { \"\n                    \"margin: 0; }</style></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_whitespace_only(void) {\n    /* Whitespace-only content */\n    HTMConversionResult *result = htm_convert(\"<p>   </p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_xss_onclick_handler_removed(void) {\n    /* onclick and other on* event handlers are removed from elements */\n    HTMConversionResult *result =\n        htm_convert(\"<p><a href=\\\"https://example.com\\\" onclick=\\\"alert('xss')\\\">Click \"\n                    \"me</a></p><button onmouseover=\\\"steal_data()\\\">Hover me</button>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Click me\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_xss_script_tag_stripped(void) {\n    /* Script tag content is stripped and does not appear in output */\n    HTMConversionResult *result = htm_convert(\n        \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Safe content\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"More safe content\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_xss_svg_nested_script_stripped(void) {\n    /* Script tags nested inside SVG are stripped */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Before SVG.</p><svg \"\n                    \"xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</\"\n                    \"script><text>SVG text</text></svg><p>After SVG.</p>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Before SVG\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After SVG\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_metadata.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:36d894de858a7b8ff1fc5d0b6f7641b0e674b7f7543b0cd763a100615bb6395b\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: metadata */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_metadata_author_meta(void) {\n    /* Extract author from <meta name='author'> tag */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Page</title><meta name=\\\"author\\\" content=\\\"Jane \"\n                    \"Doe\\\"></head><body><p>Content</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_author = htm_document_metadata_author(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_author, \"Jane Doe\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_free_string(metadata_author);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_canonical_url(void) {\n    /* Extract canonical URL from <link rel='canonical'> tag */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Page</title><link rel=\\\"canonical\\\" \"\n        \"href=\\\"https://example.com/canonical-page\\\"></head><body><p>Content</p></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_canonical_url = htm_document_metadata_canonical_url(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_canonical_url, \"https://example.com/canonical-page\") == 0 &&\n           \"equals assertion failed\");\n    htm_free_string(content);\n    htm_free_string(metadata_canonical_url);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_description_meta(void) {\n    /* Extract description from <meta name='description'> tag */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Page</title><meta name=\\\"description\\\" content=\\\"This is \"\n                    \"the page description.\\\"></head><body><p>Content</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_description = htm_document_metadata_description(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_description, \"This is the page description.\") == 0 &&\n           \"equals assertion failed\");\n    htm_free_string(content);\n    htm_free_string(metadata_description);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_dublin_core(void) {\n    /* Extract Dublin Core metadata tags */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Scholarly Work</title><meta name=\\\"DC.title\\\" content=\\\"Principles of \"\n        \"Knowledge Management\\\"><meta name=\\\"DC.creator\\\" content=\\\"Dr. Alice Johnson\\\"><meta \"\n        \"name=\\\"DC.date\\\" content=\\\"2023-06-15\\\"><meta name=\\\"DC.subject\\\" content=\\\"Knowledge \"\n        \"Management\\\"><meta name=\\\"DC.publisher\\\" content=\\\"Academic Press\\\"></head><body><p>This \"\n        \"is a scholarly article.</p></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"scholarly article\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_extract_all_images(void) {\n    /* Extract all images from a document into metadata */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Gallery</title></head><body><img \"\n                    \"src=\\\"https://example.com/photo1.jpg\\\" alt=\\\"Photo 1\\\"><img \"\n                    \"src=\\\"https://example.com/photo2.png\\\" alt=\\\"Photo 2\\\"><img \"\n                    \"src=\\\"/local/image.webp\\\" alt=\\\"Local image\\\"></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    char *metadata_images = htm_html_metadata_images(metadata_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(metadata_images != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(metadata_images);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(metadata_images);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_extract_all_links(void) {\n    /* Extract all links from a document into metadata */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Links Page</title></head><body><p>Visit <a \"\n                    \"href=\\\"https://example.com\\\">Example</a> or <a \"\n                    \"href=\\\"https://docs.example.com\\\">Docs</a>.</p><p>Also see <a \"\n                    \"href=\\\"/relative/path\\\">relative link</a> and <a \"\n                    \"href=\\\"mailto:hello@example.com\\\">email us</a>.</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    char *metadata_links = htm_html_metadata_links(metadata_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(metadata_links != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(metadata_links);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(metadata_links);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_headers_hierarchy(void) {\n    /* Extract heading hierarchy from document into metadata */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting \"\n                    \"Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced \"\n                    \"Usage</h2><h3>Custom Options</h3></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    char *metadata_headings = htm_html_metadata_headers(metadata_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(metadata_headings != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(metadata_headings);\n        assert(elem_count >= 5 && \"expected at least 5 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(metadata_headings);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_keywords_meta(void) {\n    /* Extract keywords from <meta name='keywords'> tag */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Page</title><meta name=\\\"keywords\\\" content=\\\"rust, \"\n                    \"markdown, html, converter\\\"></head><body><p>Content</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_document_keywords = htm_document_metadata_keywords(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(metadata_document_keywords != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(metadata_document_keywords);\n        assert(elem_count >= 1 && \"expected at least 1 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(metadata_document_keywords);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_lang_attribute(void) {\n    /* Extract language from html lang attribute */\n    HTMConversionResult *result =\n        htm_convert(\"<html lang=\\\"es\\\"><head><title>Spanish Page</title></head><body><h1>Hola \"\n                    \"Mundo</h1><p>Este es un documento en español.</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Hola Mundo\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_microdata_schema_article(void) {\n    /* Extract schema.org microdata for Article */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Article</title></head><body><article itemscope \"\n                    \"itemtype=\\\"https://schema.org/Article\\\"><h1 itemprop=\\\"headline\\\">Breaking \"\n                    \"News Today</h1><span itemprop=\\\"author\\\">Jane Reporter</span><span \"\n                    \"itemprop=\\\"datePublished\\\">2024-04-22</span><div \"\n                    \"itemprop=\\\"articleBody\\\"><p>The article content goes here with important \"\n                    \"information about the breaking news story.</p></div></article></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Breaking News Today\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Jane Reporter\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"important information\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_microdata_schema_breadcrumb(void) {\n    /* Extract schema.org breadcrumb navigation microdata */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Navigation</title></head><body><nav itemscope \"\n        \"itemtype=\\\"https://schema.org/BreadcrumbList\\\"><span itemprop=\\\"itemListElement\\\" \"\n        \"itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" \"\n        \"href=\\\"https://example.com\\\"><span itemprop=\\\"name\\\">Home</span></a></span><span \"\n        \"itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a \"\n        \"itemprop=\\\"item\\\" href=\\\"https://example.com/products\\\"><span \"\n        \"itemprop=\\\"name\\\">Products</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope \"\n        \"itemtype=\\\"https://schema.org/ListItem\\\"><span itemprop=\\\"name\\\">Current \"\n        \"Page</span></span></nav></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Home\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Products\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Current Page\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_microdata_schema_organization(void) {\n    /* Extract schema.org microdata for Organization */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Company</title></head><body><div itemscope \"\n        \"itemtype=\\\"https://schema.org/Organization\\\"><span itemprop=\\\"name\\\">Acme \"\n        \"Corp</span><span itemprop=\\\"foundingDate\\\">2020</span><span \"\n        \"itemprop=\\\"url\\\">https://acmecorp.example.com</span><span \"\n        \"itemprop=\\\"logo\\\">https://acmecorp.example.com/logo.png</span></div></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Acme Corp\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2020\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_microdata_schema_person(void) {\n    /* Extract schema.org microdata for Person */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Contact</title></head><body><div itemscope \"\n                    \"itemtype=\\\"https://schema.org/Person\\\"><span itemprop=\\\"name\\\">John \"\n                    \"Smith</span><span itemprop=\\\"email\\\">john@example.com</span><span \"\n                    \"itemprop=\\\"telephone\\\">+1-555-0100</span></div></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"John Smith\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"john@example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_microdata_schema_product(void) {\n    /* Extract schema.org microdata for Product */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Product</title></head><body><div itemscope \"\n        \"itemtype=\\\"https://schema.org/Product\\\"><h1 itemprop=\\\"name\\\">Awesome Widget</h1><span \"\n        \"itemprop=\\\"description\\\">The best widget on the market</span><span \"\n        \"itemprop=\\\"price\\\">29.99</span><span itemprop=\\\"priceCurrency\\\">USD</span><img \"\n        \"itemprop=\\\"image\\\" src=\\\"widget.jpg\\\" alt=\\\"Widget\\\"><span \"\n        \"itemprop=\\\"ratingValue\\\">4.5</span></div></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Awesome Widget\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"best widget\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"29.99\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_text_direction_ltr(void) {\n    /* Extract text direction from lang attribute on html element */\n    HTMConversionResult *result =\n        htm_convert(\"<html lang=\\\"en\\\" dir=\\\"ltr\\\"><head><title>LTR \"\n                    \"Document</title></head><body><p>This is left-to-right text.</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"left-to-right text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_text_direction_rtl(void) {\n    /* Extract right-to-left text direction */\n    HTMConversionResult *result =\n        htm_convert(\"<html lang=\\\"ar\\\" dir=\\\"rtl\\\"><head><title>RTL \"\n                    \"Document</title></head><body><p>This is right-to-left text.</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"right-to-left text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_metadata_title_tag(void) {\n    /* Extract title from <title> tag */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_title = htm_document_metadata_title(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_title, \"My Page\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_free_string(metadata_title);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_og_basic_tags(void) {\n    /* Extract og:title, og:description, and og:image from Open Graph meta tags */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Fallback Title</title><meta property=\\\"og:title\\\" content=\\\"OG \"\n        \"Title\\\"><meta property=\\\"og:description\\\" content=\\\"OG description text.\\\"><meta \"\n        \"property=\\\"og:image\\\" \"\n        \"content=\\\"https://example.com/image.jpg\\\"></head><body><p>Content</p></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *open_graph_json = htm_document_metadata_open_graph(document_handle);\n    assert(open_graph_json != NULL);\n    char *metadata_open_graph_title = alef_json_get_string(open_graph_json, \"title\");\n    char *metadata_open_graph_description = alef_json_get_string(open_graph_json, \"description\");\n    char *metadata_open_graph_image = alef_json_get_string(open_graph_json, \"image\");\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_open_graph_title, \"OG Title\") == 0 && \"equals assertion failed\");\n    assert(str_trim_eq(metadata_open_graph_description, \"OG description text.\") == 0 &&\n           \"equals assertion failed\");\n    assert(str_trim_eq(metadata_open_graph_image, \"https://example.com/image.jpg\") == 0 &&\n           \"equals assertion failed\");\n    htm_free_string(content);\n    free(metadata_open_graph_title);\n    free(metadata_open_graph_description);\n    free(metadata_open_graph_image);\n    htm_free_string(open_graph_json);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_og_multiple_tags(void) {\n    /* Extract multiple Open Graph tags including type, url, and site_name */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><meta property=\\\"og:title\\\" content=\\\"Article Title\\\"><meta \"\n                    \"property=\\\"og:type\\\" content=\\\"article\\\"><meta property=\\\"og:url\\\" \"\n                    \"content=\\\"https://example.com/article\\\"><meta property=\\\"og:site_name\\\" \"\n                    \"content=\\\"Example Site\\\"><meta property=\\\"og:description\\\" content=\\\"An \"\n                    \"interesting article.\\\"><meta property=\\\"og:image\\\" \"\n                    \"content=\\\"https://example.com/article.jpg\\\"></head><body><article><p>Article \"\n                    \"content here.</p></article></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *open_graph_json = htm_document_metadata_open_graph(document_handle);\n    assert(open_graph_json != NULL);\n    char *metadata_open_graph_title = alef_json_get_string(open_graph_json, \"title\");\n    char *metadata_open_graph_type = alef_json_get_string(open_graph_json, \"type\");\n    char *metadata_open_graph_url = alef_json_get_string(open_graph_json, \"url\");\n    char *metadata_open_graph_site_name = alef_json_get_string(open_graph_json, \"site_name\");\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_open_graph_title, \"Article Title\") == 0 &&\n           \"equals assertion failed\");\n    assert(str_trim_eq(metadata_open_graph_type, \"article\") == 0 && \"equals assertion failed\");\n    assert(str_trim_eq(metadata_open_graph_url, \"https://example.com/article\") == 0 &&\n           \"equals assertion failed\");\n    assert(str_trim_eq(metadata_open_graph_site_name, \"Example Site\") == 0 &&\n           \"equals assertion failed\");\n    htm_free_string(content);\n    free(metadata_open_graph_title);\n    free(metadata_open_graph_type);\n    free(metadata_open_graph_url);\n    free(metadata_open_graph_site_name);\n    htm_free_string(open_graph_json);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structured_data_json_ld(void) {\n    /* JSON-LD script tag is stripped from output (security) but metadata may be extracted */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Article</title><script \"\n                    \"type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://\"\n                    \"schema.org\\\",\\\"@type\\\":\\\"Article\\\",\\\"headline\\\":\\\"My \"\n                    \"Article\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Jane \"\n                    \"Doe\\\"},\\\"datePublished\\\":\\\"2024-01-15\\\"}</script></head><body><h1>My \"\n                    \"Article</h1><p>Article body text.</p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"My Article\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structured_data_multiple_json_ld(void) {\n    /* Multiple JSON-LD blocks are all stripped from output */\n    HTMConversionResult *result = htm_convert(\n        \"<html><head><title>Shop Page</title><script \"\n        \"type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://\"\n        \"schema.org\\\",\\\"@type\\\":\\\"Product\\\",\\\"name\\\":\\\"Widget\\\",\\\"price\\\":\\\"9.99\\\"}</\"\n        \"script><script \"\n        \"type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://\"\n        \"schema.org\\\",\\\"@type\\\":\\\"BreadcrumbList\\\",\\\"itemListElement\\\":[{\\\"@type\\\":\\\"ListItem\\\",\"\n        \"\\\"position\\\":1,\\\"name\\\":\\\"Home\\\"}]}</script></head><body><h1>Widget</h1><p>A great widget \"\n        \"for all purposes.</p></body></html>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Widget\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_twitter_card_tags(void) {\n    /* Extract Twitter card meta tags */\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><meta name=\\\"twitter:card\\\" content=\\\"summary_large_image\\\"><meta \"\n                    \"name=\\\"twitter:site\\\" content=\\\"@examplesite\\\"><meta name=\\\"twitter:title\\\" \"\n                    \"content=\\\"Twitter Card Title\\\"><meta name=\\\"twitter:description\\\" \"\n                    \"content=\\\"Twitter card description.\\\"><meta name=\\\"twitter:image\\\" \"\n                    \"content=\\\"https://example.com/twitter-image.jpg\\\"></head><body><p>Content</\"\n                    \"p></body></html>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *twitter_card_json = htm_document_metadata_twitter_card(document_handle);\n    assert(twitter_card_json != NULL);\n    char *metadata_twitter_card = alef_json_get_string(twitter_card_json, \"card\");\n    char *metadata_twitter_title = alef_json_get_string(twitter_card_json, \"title\");\n    char *metadata_twitter_description = alef_json_get_string(twitter_card_json, \"description\");\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_twitter_card, \"summary_large_image\") == 0 &&\n           \"equals assertion failed\");\n    assert(str_trim_eq(metadata_twitter_title, \"Twitter Card Title\") == 0 &&\n           \"equals assertion failed\");\n    assert(str_trim_eq(metadata_twitter_description, \"Twitter card description.\") == 0 &&\n           \"equals assertion failed\");\n    htm_free_string(content);\n    free(metadata_twitter_card);\n    free(metadata_twitter_title);\n    free(metadata_twitter_description);\n    htm_free_string(twitter_card_json);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_options.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e5a1bbae90788c9edc1a2849e560bd2924f9d46a8efb66c97f42eee020afed93\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: options */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_options_autolinks_false(void) {\n    /* Bare URL links rendered as regular markdown links when autolinks disabled */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"autolinks\\\":false}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p><a href='https://example.com'>https://example.com</a></p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_br_in_tables_false(void) {\n    /* BR elements in table cells are stripped when disabled */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"br_in_tables\\\":false}\");\n    HTMConversionResult *result =\n        htm_convert(\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Col\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_br_in_tables_true(void) {\n    /* BR elements in table cells render as line breaks */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"br_in_tables\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Header\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line 2\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_code_block_backticks(void) {\n    /* Backticks code block style uses triple backtick fences */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"code_block_style\\\":\\\"Backticks\\\"}\");\n    HTMConversionResult *result = htm_convert(\n        \"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"```\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"console.log('hi');\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_code_block_indented(void) {\n    /* Code blocks use 4-space indentation */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"code_block_style\\\":\\\"Indented\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<pre><code>print('hello')</code></pre>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"print('hello')\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"```\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_code_block_tildes(void) {\n    /* Code blocks use tilde fences */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"code_block_style\\\":\\\"Tildes\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<pre><code>let x = 1;</code></pre>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"~~~\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"let x = 1;\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_code_block_tildes_style(void) {\n    /* Tildes code block style uses triple tilde fences */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"code_block_style\\\":\\\"Tildes\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<pre><code>some code</code></pre>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"~~~\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"some code\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_code_language_python(void) {\n    /* Default code language annotation on blocks without lang attribute */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"code_language\\\":\\\"python\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<pre><code>def hello(): pass</code></pre>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"```python\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"def hello\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_convert_as_inline(void) {\n    /* Block elements treated as inline */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"convert_as_inline\\\":true}\");\n    HTMConversionResult *result = htm_convert(\"<p>One</p><p>Two</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"One\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Two\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_debug_true(void) {\n    /* Debug mode enabled does not crash and produces output */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\"{\\\"debug\\\":true}\");\n    HTMConversionResult *result = htm_convert(\"<p>Debug test</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Debug test\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_default_title_true(void) {\n    /* Links without title get empty title attribute when defaultTitle is true */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"default_title\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p><a href='https://example.com'>Link</a></p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Link\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_encoding_utf8(void) {\n    /* UTF-8 encoding hint for special characters */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"encoding\\\":\\\"utf-8\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>Café naïve résumé</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_escape_ascii_enabled(void) {\n    /* ASCII Markdown characters are escaped when escapeAscii is true */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"escape_ascii\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text with # hash and [brackets] and * star</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"hash\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"brackets\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"star\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_escape_asterisks(void) {\n    /* escape_asterisks option escapes asterisks in plain text */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"escape_asterisks\\\":true}\");\n    HTMConversionResult *result = htm_convert(\"<p>Use 2*3 = 6 in math.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"3\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"6\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_escape_misc(void) {\n    /* escape_misc option escapes miscellaneous markdown characters */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"escape_misc\\\":true}\");\n    HTMConversionResult *result = htm_convert(\"<p>Use # and | and ~ in text.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Use\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"and\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"in text.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_escape_underscores(void) {\n    /* escape_underscores option escapes underscores in plain text */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"escape_underscores\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>The variable_name is defined.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"variable\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"name\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"defined.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_attribute(void) {\n    /* Elements matching CSS attribute selector are excluded entirely */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\"[role='complementary']\\\"]}\");\n    HTMConversionResult *result =\n        htm_convert(\"<body><div role=\\\"complementary\\\">Sidebar</div><p>Primary text</p></body>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Primary text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Sidebar\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_class(void) {\n    /* Elements matching CSS class selector are excluded entirely */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\".cookie-banner\\\"]}\");\n    HTMConversionResult *result = htm_convert(\n        \"<body><div class=\\\"cookie-banner\\\">Accept cookies</div><p>Main content</p></body>\",\n        options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Main content\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"cookies\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_empty_noop(void) {\n    /* Empty exclude_selectors list does not affect output */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[]}\");\n    HTMConversionResult *result = htm_convert(\"<p>Hello world</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Hello world\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_id(void) {\n    /* Elements matching CSS id selector are excluded entirely */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\"#ad-container\\\"]}\");\n    HTMConversionResult *result = htm_convert(\n        \"<body><div id=\\\"ad-container\\\">Buy stuff</div><p>Article text</p></body>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Article text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Buy stuff\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_multiple(void) {\n    /* Multiple CSS selectors each exclude their matched elements */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\".nav\\\",\\\"footer\\\"]}\");\n    HTMConversionResult *result = htm_convert(\n        \"<body><nav class=\\\"nav\\\">Menu</nav><p>Content</p><footer>Footer</footer></body>\",\n        options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Content\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Menu\") == NULL && \"expected NOT to contain substring\");\n    assert(strstr(content, \"Footer\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_nested_content_dropped(void) {\n    /* All descendants of excluded elements are dropped */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\".sidebar\\\"]}\");\n    HTMConversionResult *result =\n        htm_convert(\"<body><aside class=\\\"sidebar\\\"><h2>Related</h2><p>Sidebar \"\n                    \"text</p></aside><main><p>Main text</p></main></body>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Main text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Related\") == NULL && \"expected NOT to contain substring\");\n    assert(strstr(content, \"Sidebar text\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_plain_text_mode(void) {\n    /* Exclude selectors work in plain text output mode */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\n        \"{\\\"exclude_selectors\\\":[\\\".nav\\\"],\\\"output_format\\\":\\\"Plain\\\"}\");\n    HTMConversionResult *result = htm_convert(\n        \"<body><div class=\\\"nav\\\">Navigation</div><p>Article body</p></body>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Article body\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Navigation\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_exclude_selectors_vs_strip_tags(void) {\n    /* exclude_selectors drops entire subtree unlike strip_tags which keeps children */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"exclude_selectors\\\":[\\\".wrapper\\\"]}\");\n    HTMConversionResult *result = htm_convert(\n        \"<body><div class=\\\"wrapper\\\"><p>Inner paragraph</p></div><p>Outer text</p></body>\",\n        options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Outer text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Inner paragraph\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_extract_metadata_true(void) {\n    /* Extract metadata returns document metadata when enabled */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"extract_metadata\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<html><head><title>Test Page</title><meta name='description' content='A test \"\n                    \"page'></head><body><p>Content</p></body></html>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMHtmlMetadata *metadata_handle = htm_conversion_result_metadata(result);\n    assert(metadata_handle != NULL);\n    HTMDocumentMetadata *document_handle = htm_html_metadata_document(metadata_handle);\n    assert(document_handle != NULL);\n    char *metadata_title = htm_document_metadata_title(document_handle);\n    char *metadata_description = htm_document_metadata_description(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(str_trim_eq(metadata_title, \"Test Page\") == 0 && \"equals assertion failed\");\n    assert(str_trim_eq(metadata_description, \"A test page\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_free_string(metadata_title);\n    htm_free_string(metadata_description);\n    htm_document_metadata_free(document_handle);\n    htm_html_metadata_free(metadata_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_heading_style_atx(void) {\n    /* ATX heading style produces hash-prefixed headings */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"heading_style\\\":\\\"Atx\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<h1>Title</h1><h2>Subtitle</h2>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"# Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## Subtitle\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_heading_style_atx_closed(void) {\n    /* ATX closed heading style adds closing hashes */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"heading_style\\\":\\\"AtxClosed\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<h1>Closed Heading</h1>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"# Closed Heading #\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_heading_style_underlined(void) {\n    /* Underlined heading style produces setext-style headings for h1 and h2 */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"heading_style\\\":\\\"Underlined\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<h1>Main Title</h1>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Main Title\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_highlight_bold(void) {\n    /* Mark tag rendered as bold */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"highlight_style\\\":\\\"Bold\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text with <mark>highlighted</mark> text.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"**highlighted**\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_highlight_double_equal(void) {\n    /* Mark tag with double equal highlight style */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"highlight_style\\\":\\\"DoubleEqual\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text with <mark>highlighted</mark> here.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"==highlighted==\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_highlight_none(void) {\n    /* Mark tag with no highlight style strips the mark */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"highlight_style\\\":\\\"None\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text with <mark>plain</mark> content.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"plain\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"==\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_keep_inline_images_in_paragraph(void) {\n    /* Images inside specified tags stay inline */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"keep_inline_images_in\\\":[\\\"p\\\"]}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text <img src='icon.png' alt='icon'> more text</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"more text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_link_style_reference(void) {\n    /* Links use reference-style formatting */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"link_style\\\":\\\"Reference\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p><a href='https://example.com'>Example</a> and <a \"\n                                              \"href='https://other.com'>Other</a></p>\",\n                                              options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Example\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Other\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_list_custom_bullets(void) {\n    /* Custom bullet character for unordered lists */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\"{\\\"bullets\\\":\\\"*\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li>Item A</li><li>Item B</li></ul>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"* Item A\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"* Item B\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_list_indent_tabs(void) {\n    /* Tab indentation type for nested list items */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"list_indent_type\\\":\\\"Tabs\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Parent\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Child\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_list_indent_width_four(void) {\n    /* Nested lists indented with 4 spaces per level */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"list_indent_width\\\":4}\");\n    HTMConversionResult *result =\n        htm_convert(\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Outer\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Inner\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_max_depth_default_unlimited(void) {\n    /* Default max_depth (null) converts deeply nested content fully */\n    HTMConversionResult *result =\n        htm_convert(\"<div><div><div><div><p>Deep content</p></div></div></div></div>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Deep content\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_max_depth_truncates(void) {\n    /* max_depth truncates content beyond the specified depth */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\"{\\\"max_depth\\\":3}\");\n    HTMConversionResult *result =\n        htm_convert(\"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Shallow\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Too deep\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_max_depth_zero_empty(void) {\n    /* max_depth of 0 produces empty output */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\"{\\\"max_depth\\\":0}\");\n    HTMConversionResult *result = htm_convert(\"<p>Hello</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_newline_backslash(void) {\n    /* Hard line breaks rendered with backslash */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"newline_style\\\":\\\"Backslash\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>Line one<br>Line two</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Line one\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line two\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_newline_spaces(void) {\n    /* Hard line breaks rendered with trailing spaces */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"newline_style\\\":\\\"Spaces\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>First<br>Second</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"First\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_output_format_djot(void) {\n    /* Djot output format produces djot-compatible markup */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"output_format\\\":\\\"Djot\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>Simple paragraph.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Simple paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_output_format_markdown(void) {\n    /* Default markdown output format produces standard markdown */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\n        \"{\\\"heading_style\\\":\\\"Atx\\\",\\\"output_format\\\":\\\"Markdown\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<h1>Title</h1><p>Some text.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"# Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Some text.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_output_format_plain(void) {\n    /* Plain text output format strips markdown syntax */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"output_format\\\":\\\"Plain\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"bold\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"text.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_preprocessing_aggressive(void) {\n    /* Aggressive preset removes nav, footer, aside unconditionally */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"preprocessing\\\":{\\\"preset\\\":\\\"Aggressive\\\"}}\");\n    HTMConversionResult *result =\n        htm_convert(\"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</\"\n                    \"aside><footer>Footer</footer>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Content\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Menu\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_preprocessing_minimal(void) {\n    /* Minimal preset preserves nav, footer, aside */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"preprocessing\\\":{\\\"preset\\\":\\\"Minimal\\\"}}\");\n    HTMConversionResult *result =\n        htm_convert(\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Navigation\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Content\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Footer\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_preprocessing_remove_forms(void) {\n    /* Forms are removed when remove_forms is true */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"preprocessing\\\":{\\\"remove_forms\\\":true}}\");\n    HTMConversionResult *result = htm_convert(\n        \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\",\n        options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Before\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Submit\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_preserve_tags_iframe(void) {\n    /* Iframe tags preserved as raw HTML in output */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"preserve_tags\\\":[\\\"iframe\\\"]}\");\n    HTMConversionResult *result = htm_convert(\n        \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Before\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"<iframe\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_skip_images_true(void) {\n    /* Images are omitted from output when skipImages is true */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"skip_images\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Before <img src='test.jpg' alt='photo'> After</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Before\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"photo\") == NULL && \"expected NOT to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_strip_newlines(void) {\n    /* Strip newlines produces single-line paragraphs */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"strip_newlines\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"First paragraph.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_strip_tags_div_span(void) {\n    /* Div and span tags stripped but content preserved */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"strip_tags\\\":[\\\"div\\\",\\\"span\\\"]}\");\n    HTMConversionResult *result =\n        htm_convert(\"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span \"\n                    \"text</span></p>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"Inside div\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"span text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_strong_em_underscore(void) {\n    /* Strong and em tags use underscore symbol instead of asterisk */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"strong_em_symbol\\\":\\\"_\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p><strong>bold</strong> and <em>italic</em></p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"__bold__\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"_italic_\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_sub_symbol_tilde(void) {\n    /* Subscript rendered with tilde symbol */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"sub_symbol\\\":\\\"~\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>H<sub>2</sub>O</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"~2~\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_sup_symbol_caret(void) {\n    /* Superscript rendered with caret symbol */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"sup_symbol\\\":\\\"^\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>x<sup>2</sup></p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"^2^\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_whitespace_normalized(void) {\n    /* Normalized whitespace mode collapses multiple spaces */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"whitespace_mode\\\":\\\"Normalized\\\"}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Text   with    extra   spaces.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Text\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"with\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"extra\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"spaces.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_whitespace_strict(void) {\n    /* Strict whitespace mode preserves whitespace as-is */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"whitespace_mode\\\":\\\"Strict\\\"}\");\n    HTMConversionResult *result = htm_convert(\"<p>Preserved   spacing.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Preserved\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"spacing.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_wrap_disabled(void) {\n    /* Wrap option disabled preserves long lines without breaking */\n    HTMConversionOptions *options_handle = htm_conversion_options_from_json(\"{\\\"wrap\\\":false}\");\n    HTMConversionResult *result = htm_convert(\"<p>This is a long paragraph that should not be \"\n                                              \"wrapped at all because wrapping is disabled.</p>\",\n                                              options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"This is a long paragraph that should not be wrapped at all because \"\n                           \"wrapping is disabled.\") != NULL &&\n           \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_options_wrap_enabled(void) {\n    /* Wrap option enabled with custom width wraps long lines */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"wrap\\\":true,\\\"wrap_width\\\":40}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>This is a long paragraph that should be wrapped at the specified column \"\n                    \"width when the wrap option is enabled.</p>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"This is a long paragraph\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_real_world.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1a122b394838d236586779ba6b8063c279753df8d843fa29792153c15d41eaf2\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: real-world */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_real_world_blog_post(void) {\n    /* Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown */\n    HTMConversionResult *result = htm_convert(\n        \"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language \"\n        \"focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created \"\n        \"by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in \"\n        \"popularity.</p><h2>Installation</h2><p>Install Rust using the official \"\n        \"installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf \"\n        \"https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust \"\n        \"program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, \"\n        \"world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project \"\n        \"directory.</p><h2>Key Concepts</h2><ul><li>Ownership and \"\n        \"borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern \"\n        \"matching</li></ul><p>For more information, visit the <a \"\n        \"href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"# Getting Started with Rust\") != NULL &&\n           \"expected to contain substring\");\n    assert(strstr(content, \"## Installation\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## Hello World\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## Key Concepts\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"cargo run\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"[Mozilla](https://www.mozilla.org)\") != NULL &&\n           \"expected to contain substring\");\n    assert(strstr(content, \"- Ownership and borrowing\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_real_world_documentation_page(void) {\n    /* Documentation page with nested lists, code examples, and blockquotes converts correctly */\n    HTMConversionResult *result = htm_convert(\n        \"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the \"\n        \"<code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All \"\n        \"functions are thread-safe and can be called from multiple threads \"\n        \"concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown \"\n        \"format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; \"\n        \"Result&lt;String, \"\n        \"ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML \"\n        \"input string<ul><li>Must be valid UTF-8</li><li>Maximum size: \"\n        \"50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted \"\n        \"Markdown</li><li><code>Err(ConversionError)</code> - If conversion \"\n        \"fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = \"\n        \"convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"# \"\n        \"Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using \"\n        \"the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = \"\n        \"ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    \"\n        \".code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See \"\n        \"the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration \"\n        \"values.</p></blockquote></div>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"# API Reference\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## convert_html\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"### Parameters\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"### Returns\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"### Example\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## ConversionOptions\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"> \") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"thread-safe\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"convert_html\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"ConversionOptions\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_real_world_product_page(void) {\n    /* Product page with table, images, and lists converts correctly */\n    HTMConversionResult *result = htm_convert(\n        \"<div class=\\\"product\\\"><h1>Wireless Keyboard Pro</h1><img \"\n        \"src=\\\"https://example.com/keyboard.jpg\\\" alt=\\\"Wireless Keyboard Pro\\\"><p>The ultimate \"\n        \"wireless keyboard for professionals. Features a comfortable layout with <strong>backlit \"\n        \"keys</strong> and <em>ultra-long battery \"\n        \"life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></\"\n        \"tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 \"\n        \"months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key \"\n        \"Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></\"\n        \"table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging \"\n        \"cable</li><li>USB receiver dongle</li><li>Quick start \"\n        \"guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, \"\n        \"<strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and \"\n        \"<strong>Android</strong>.</p></div>\",\n        NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"# Wireless Keyboard Pro\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\") != NULL &&\n           \"expected to contain substring\");\n    assert(strstr(content, \"## Specifications\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Battery Life\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"12 months\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Bluetooth 5.0\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"## What's in the Box\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"USB-C charging cable\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"---\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_result.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:cf8fa42c3e9bf3bd2116aadbbe4128b62cf7a10dd1597bc51e2bd95f3c0df9a7\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: result */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_result_tables_empty_when_no_tables(void) {\n    /* Result tables array is empty when input has no tables */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result = htm_convert(\"<p>No tables here</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *tables = htm_conversion_result_tables(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_equals: count elements in array */\n        assert(tables != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(tables);\n        assert(elem_count == 0 && \"expected 0 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(tables);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_tables_multiple(void) {\n    /* Multiple tables each appear in the tables array */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</\"\n                    \"p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *tables = htm_conversion_result_tables(result);\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(tables != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(tables);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(tables);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_tables_simple(void) {\n    /* Simple table populates the tables array in result */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</\"\n                    \"td><td>30</td></tr></tbody></table>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *tables = htm_conversion_result_tables(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(tables != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(tables);\n        assert(elem_count >= 1 && \"expected at least 1 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(tables);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_tables_without_structure_flag(void) {\n    /* Tables array is empty when includeDocumentStructure is false */\n    HTMConversionResult *result =\n        htm_convert(\"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *tables = htm_conversion_result_tables(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_equals: count elements in array */\n        assert(tables != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(tables);\n        assert(elem_count == 0 && \"expected 0 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(tables);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_warnings_empty_for_clean_input(void) {\n    /* Warnings array is empty for well-formed HTML without problematic content */\n    HTMConversionResult *result = htm_convert(\n        \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *warnings = htm_conversion_result_warnings(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_equals: count elements in array */\n        assert(warnings != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(warnings);\n        assert(elem_count == 0 && \"expected 0 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(warnings);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_warnings_empty_for_complex_input(void) {\n    /* Warnings array is empty for complex but valid HTML */\n    HTMConversionResult *result =\n        htm_convert(\"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and \"\n                    \"<em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></\"\n                    \"table><ul><li>Item 1</li><li>Item 2</li></ul></article>\",\n                    NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *warnings = htm_conversion_result_warnings(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_equals: count elements in array */\n        assert(warnings != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(warnings);\n        assert(elem_count == 0 && \"expected 0 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(warnings);\n    htm_conversion_result_free(result);\n}\n\nvoid test_result_warnings_empty_for_malformed_html(void) {\n    /* Warnings array is empty even for malformed HTML (parser is lenient) */\n    HTMConversionResult *result =\n        htm_convert(\"<p>Unclosed paragraph<div>Mixed nesting</p></div>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    char *warnings = htm_conversion_result_warnings(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    {\n        /* count_equals: count elements in array */\n        assert(warnings != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(warnings);\n        assert(elem_count == 0 && \"expected 0 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(warnings);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_runner.h",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:57f0906acf84ec064f4c3743feb3d7cf5ca996b1b8542109f919958f10905225\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n#ifndef TEST_RUNNER_H\n#define TEST_RUNNER_H\n\n#include <stdlib.h>\n#include <string.h>\n\n/**\n * Compare a string against an expected value, trimming trailing whitespace.\n * Returns 0 if the trimmed actual string equals the expected string.\n */\nstatic inline int str_trim_eq(const char *actual, const char *expected) {\n    if (actual == NULL || expected == NULL)\n        return actual != expected;\n    size_t alen = strlen(actual);\n    while (alen > 0 && (actual[alen - 1] == ' ' || actual[alen - 1] == '\\n' ||\n                        actual[alen - 1] == '\\r' || actual[alen - 1] == '\\t'))\n        alen--;\n    size_t elen = strlen(expected);\n    if (alen != elen)\n        return 1;\n    return memcmp(actual, expected, elen);\n}\n\n/**\n * Extract a string value for a given key from a JSON object string.\n * Returns a heap-allocated copy of the value, or NULL if not found.\n * Caller must free() the returned string.\n */\nstatic inline char *alef_json_get_string(const char *json, const char *key) {\n    if (json == NULL || key == NULL)\n        return NULL;\n    /* Build search pattern: \"key\":  */\n    size_t key_len = strlen(key);\n    char *pattern = (char *)malloc(key_len + 5);\n    if (!pattern)\n        return NULL;\n    pattern[0] = '\"';\n    memcpy(pattern + 1, key, key_len);\n    pattern[key_len + 1] = '\"';\n    pattern[key_len + 2] = ':';\n    pattern[key_len + 3] = '\\0';\n    const char *found = strstr(json, pattern);\n    free(pattern);\n    if (!found)\n        return NULL;\n    found += key_len + 3; /* skip past \"key\": */\n    while (*found == ' ' || *found == '\\t')\n        found++;\n    if (*found != '\"')\n        return NULL; /* not a string value */\n    found++;         /* skip opening quote */\n    const char *end = found;\n    while (*end && *end != '\"') {\n        if (*end == '\\\\') {\n            end++;\n            if (*end)\n                end++;\n        } else\n            end++;\n    }\n    size_t val_len = (size_t)(end - found);\n    char *result_str = (char *)malloc(val_len + 1);\n    if (!result_str)\n        return NULL;\n    memcpy(result_str, found, val_len);\n    result_str[val_len] = '\\0';\n    return result_str;\n}\n\n/**\n * Count top-level elements in a JSON array string.\n * Returns 0 for empty arrays (\"[]\") or NULL input.\n */\nstatic inline int alef_json_array_count(const char *json) {\n    if (json == NULL)\n        return 0;\n    /* Skip leading whitespace */\n    while (*json == ' ' || *json == '\\t' || *json == '\\n')\n        json++;\n    if (*json != '[')\n        return 0;\n    json++;\n    /* Skip whitespace after '[' */\n    while (*json == ' ' || *json == '\\t' || *json == '\\n')\n        json++;\n    if (*json == ']')\n        return 0;\n    int count = 1;\n    int depth = 0;\n    int in_string = 0;\n    for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {\n        if (*json == '\\\\' && in_string) {\n            json++;\n            continue;\n        }\n        if (*json == '\"') {\n            in_string = !in_string;\n            continue;\n        }\n        if (in_string)\n            continue;\n        if (*json == '[' || *json == '{')\n            depth++;\n        else if (*json == ']' || *json == '}')\n            depth--;\n        else if (*json == ',' && depth == 0)\n            count++;\n    }\n    return count;\n}\n\n/* Tests for category: conversion */\nvoid test_blockquote_multiple_paragraphs(void);\nvoid test_blockquote_nested(void);\nvoid test_blockquote_simple(void);\nvoid test_blockquote_with_list(void);\nvoid test_bold_and_italic(void);\nvoid test_bold_strong(void);\nvoid test_code_block(void);\nvoid test_code_block_no_language(void);\nvoid test_code_inline_in_paragraph(void);\nvoid test_code_with_backticks_in_content(void);\nvoid test_emphasis_mark_highlight(void);\nvoid test_emphasis_strikethrough_del(void);\nvoid test_emphasis_strikethrough_s(void);\nvoid test_emphasis_subscript(void);\nvoid test_emphasis_superscript(void);\nvoid test_emphasis_underline_u(void);\nvoid test_form_input_elements(void);\nvoid test_form_select_options(void);\nvoid test_form_textarea(void);\nvoid test_heading_h1(void);\nvoid test_heading_h2(void);\nvoid test_heading_h3(void);\nvoid test_heading_h4(void);\nvoid test_heading_h5(void);\nvoid test_heading_h6(void);\nvoid test_image_figure_figcaption(void);\nvoid test_image_linked(void);\nvoid test_image_no_alt(void);\nvoid test_image_simple(void);\nvoid test_image_with_title(void);\nvoid test_inline_code(void);\nvoid test_italic_em(void);\nvoid test_line_break_br_tag(void);\nvoid test_line_break_hr_tag(void);\nvoid test_line_break_multiple_br(void);\nvoid test_link_anchor_fragment(void);\nvoid test_link_empty_href(void);\nvoid test_link_image_inside(void);\nvoid test_link_mailto(void);\nvoid test_link_simple(void);\nvoid test_link_with_bold_text(void);\nvoid test_link_with_title(void);\nvoid test_list_definition_dl(void);\nvoid test_list_item_multiple_paragraphs(void);\nvoid test_list_mixed_nested(void);\nvoid test_list_nested_ordered(void);\nvoid test_list_nested_unordered(void);\nvoid test_list_task_checkboxes(void);\nvoid test_ordered_list(void);\nvoid test_paragraph_multiple(void);\nvoid test_paragraph_nested_divs(void);\nvoid test_paragraph_simple(void);\nvoid test_paragraph_with_inline_formatting(void);\nvoid test_paragraph_with_line_breaks(void);\nvoid test_semantic_abbr(void);\nvoid test_semantic_article(void);\nvoid test_semantic_definition_list(void);\nvoid test_semantic_details_summary(void);\nvoid test_semantic_hr(void);\nvoid test_semantic_mark_highlight(void);\nvoid test_semantic_section_with_heading(void);\nvoid test_semantic_sub_superscript(void);\nvoid test_simple_table(void);\nvoid test_table_empty(void);\nvoid test_table_no_thead(void);\nvoid test_table_pipe_chars_in_content(void);\nvoid test_table_with_alignment(void);\nvoid test_table_with_colspan(void);\nvoid test_unordered_list(void);\n\n/* Tests for category: edge-cases */\nvoid test_empty_html(void);\nvoid test_encoding_cjk_characters(void);\nvoid test_encoding_html_entities(void);\nvoid test_encoding_named_entities(void);\nvoid test_encoding_numeric_entities(void);\nvoid test_encoding_unicode_emoji(void);\nvoid test_html_comments_only(void);\nvoid test_just_whitespace_input(void);\nvoid test_malformed_deeply_nested_elements(void);\nvoid test_malformed_missing_block_closing_tags(void);\nvoid test_malformed_overlapping_tags(void);\nvoid test_malformed_unclosed_paragraph(void);\nvoid test_script_tags_only(void);\nvoid test_style_tags_only(void);\nvoid test_whitespace_only(void);\nvoid test_xss_onclick_handler_removed(void);\nvoid test_xss_script_tag_stripped(void);\nvoid test_xss_svg_nested_script_stripped(void);\n\n/* Tests for category: metadata */\nvoid test_metadata_author_meta(void);\nvoid test_metadata_canonical_url(void);\nvoid test_metadata_description_meta(void);\nvoid test_metadata_dublin_core(void);\nvoid test_metadata_extract_all_images(void);\nvoid test_metadata_extract_all_links(void);\nvoid test_metadata_headers_hierarchy(void);\nvoid test_metadata_keywords_meta(void);\nvoid test_metadata_lang_attribute(void);\nvoid test_metadata_microdata_schema_article(void);\nvoid test_metadata_microdata_schema_breadcrumb(void);\nvoid test_metadata_microdata_schema_organization(void);\nvoid test_metadata_microdata_schema_person(void);\nvoid test_metadata_microdata_schema_product(void);\nvoid test_metadata_text_direction_ltr(void);\nvoid test_metadata_text_direction_rtl(void);\nvoid test_metadata_title_tag(void);\nvoid test_og_basic_tags(void);\nvoid test_og_multiple_tags(void);\nvoid test_structured_data_json_ld(void);\nvoid test_structured_data_multiple_json_ld(void);\nvoid test_twitter_card_tags(void);\n\n/* Tests for category: options */\nvoid test_options_autolinks_false(void);\nvoid test_options_br_in_tables_false(void);\nvoid test_options_br_in_tables_true(void);\nvoid test_options_code_block_backticks(void);\nvoid test_options_code_block_indented(void);\nvoid test_options_code_block_tildes(void);\nvoid test_options_code_block_tildes_style(void);\nvoid test_options_code_language_python(void);\nvoid test_options_convert_as_inline(void);\nvoid test_options_debug_true(void);\nvoid test_options_default_title_true(void);\nvoid test_options_encoding_utf8(void);\nvoid test_options_escape_ascii_enabled(void);\nvoid test_options_escape_asterisks(void);\nvoid test_options_escape_misc(void);\nvoid test_options_escape_underscores(void);\nvoid test_options_exclude_selectors_attribute(void);\nvoid test_options_exclude_selectors_class(void);\nvoid test_options_exclude_selectors_empty_noop(void);\nvoid test_options_exclude_selectors_id(void);\nvoid test_options_exclude_selectors_multiple(void);\nvoid test_options_exclude_selectors_nested_content_dropped(void);\nvoid test_options_exclude_selectors_plain_text_mode(void);\nvoid test_options_exclude_selectors_vs_strip_tags(void);\nvoid test_options_extract_metadata_true(void);\nvoid test_options_heading_style_atx(void);\nvoid test_options_heading_style_atx_closed(void);\nvoid test_options_heading_style_underlined(void);\nvoid test_options_highlight_bold(void);\nvoid test_options_highlight_double_equal(void);\nvoid test_options_highlight_none(void);\nvoid test_options_keep_inline_images_in_paragraph(void);\nvoid test_options_link_style_reference(void);\nvoid test_options_list_custom_bullets(void);\nvoid test_options_list_indent_tabs(void);\nvoid test_options_list_indent_width_four(void);\nvoid test_options_max_depth_default_unlimited(void);\nvoid test_options_max_depth_truncates(void);\nvoid test_options_max_depth_zero_empty(void);\nvoid test_options_newline_backslash(void);\nvoid test_options_newline_spaces(void);\nvoid test_options_output_format_djot(void);\nvoid test_options_output_format_markdown(void);\nvoid test_options_output_format_plain(void);\nvoid test_options_preprocessing_aggressive(void);\nvoid test_options_preprocessing_minimal(void);\nvoid test_options_preprocessing_remove_forms(void);\nvoid test_options_preserve_tags_iframe(void);\nvoid test_options_skip_images_true(void);\nvoid test_options_strip_newlines(void);\nvoid test_options_strip_tags_div_span(void);\nvoid test_options_strong_em_underscore(void);\nvoid test_options_sub_symbol_tilde(void);\nvoid test_options_sup_symbol_caret(void);\nvoid test_options_whitespace_normalized(void);\nvoid test_options_whitespace_strict(void);\nvoid test_options_wrap_disabled(void);\nvoid test_options_wrap_enabled(void);\n\n/* Tests for category: real-world */\nvoid test_real_world_blog_post(void);\nvoid test_real_world_documentation_page(void);\nvoid test_real_world_product_page(void);\n\n/* Tests for category: result */\nvoid test_result_tables_empty_when_no_tables(void);\nvoid test_result_tables_multiple(void);\nvoid test_result_tables_simple(void);\nvoid test_result_tables_without_structure_flag(void);\nvoid test_result_warnings_empty_for_clean_input(void);\nvoid test_result_warnings_empty_for_complex_input(void);\nvoid test_result_warnings_empty_for_malformed_html(void);\n\n/* Tests for category: smoke */\nvoid test_smoke_empty_string(void);\nvoid test_smoke_simple_heading(void);\nvoid test_smoke_simple_paragraph(void);\n\n/* Tests for category: structure */\nvoid test_structure_code_block(void);\nvoid test_structure_deep_nesting_h1_h2_h3(void);\nvoid test_structure_h1_h2_nested_group(void);\nvoid test_structure_heading_paragraph(void);\nvoid test_structure_list(void);\nvoid test_structure_multiple_headings(void);\nvoid test_structure_sibling_h1_groups(void);\n\n#endif /* TEST_RUNNER_H */\n"
  },
  {
    "path": "e2e/c/test_smoke.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c57b1edf9099d44581e6eeb760df675ceadf63790886b086a8e3503cd5e2db1a\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: smoke */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_smoke_empty_string(void) {\n    /* Empty string produces empty output */\n    HTMConversionResult *result = htm_convert(\"\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_smoke_simple_heading(void) {\n    /* H1 heading converts to ATX markdown */\n    HTMConversionResult *result = htm_convert(\"<h1>Title</h1>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(strstr(content, \"# Title\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n\nvoid test_smoke_simple_paragraph(void) {\n    /* Simple paragraph converts correctly */\n    HTMConversionResult *result = htm_convert(\"<p>Hello World</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    assert(str_trim_eq(content, \"Hello World\") == 0 && \"equals assertion failed\");\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    htm_free_string(content);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/c/test_structure.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:5e970bb8ec063b2867ae3356a1b637a4a834a90f7298cca8fd44af72493fb32e\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: structure */\n\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nvoid test_structure_code_block(void) {\n    /* Fenced code block produces Code node */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<p>Example code:</p><pre><code class=\\\"language-rust\\\">fn main() { \"\n                    \"println!(\\\"Hello\\\"); }</code></pre>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_deep_nesting_h1_h2_h3(void) {\n    /* H1 > H2 > H3 creates three levels of heading nesting */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid \"\n                    \"content.</p><h3>Deep Level</h3><p>Deep content.</p>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 5 && \"expected at least 5 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_h1_h2_nested_group(void) {\n    /* H1 followed by H2 creates a nested group under the H1 */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result = htm_convert(\n        \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\",\n        options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 3 && \"expected at least 3 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_heading_paragraph(void) {\n    /* Simple heading followed by paragraph produces Heading and Paragraph nodes */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<h1>Title</h1><p>A paragraph of text.</p>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_list(void) {\n    /* Unordered list produces List and ListItem nodes */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result = htm_convert(\n        \"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 2 && \"expected at least 2 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_multiple_headings(void) {\n    /* Multiple headings create multiple Heading nodes with correct levels */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section \"\n                    \"Two</h2><p>Section two content.</p>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 4 && \"expected at least 4 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n\nvoid test_structure_sibling_h1_groups(void) {\n    /* H1, H2, then another H1 creates two sibling top-level groups */\n    HTMConversionOptions *options_handle =\n        htm_conversion_options_from_json(\"{\\\"include_document_structure\\\":true}\");\n    HTMConversionResult *result =\n        htm_convert(\"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter \"\n                    \"Two</h1><h2>Section B</h2><p>Section B content.</p>\",\n                    options_handle);\n    assert(result != NULL && \"expected call to succeed\");\n    char *content = htm_conversion_result_content(result);\n    HTMDocumentStructure *document_handle = htm_conversion_result_document(result);\n    assert(document_handle != NULL);\n    char *document_nodes = htm_document_structure_nodes(document_handle);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strlen(document_nodes) > 0 && \"expected non-empty value\");\n    {\n        /* count_min: count top-level JSON array elements */\n        assert(document_nodes != NULL && \"expected non-null collection JSON\");\n        int elem_count = alef_json_array_count(document_nodes);\n        assert(elem_count >= 4 && \"expected at least 4 elements\");\n    }\n    htm_free_string(content);\n    htm_free_string(document_nodes);\n    htm_document_structure_free(document_handle);\n    htm_conversion_options_free(options_handle);\n    htm_conversion_result_free(result);\n}\n"
  },
  {
    "path": "e2e/csharp/HtmlToMarkdown.E2eTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.5.1\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../../packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "e2e/csharp/tests/ConversionTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:11a27c882e65fb427ee9bfec6656e2f912626f961139a8d30bf00b0765d5b87f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: conversion.</summary>\npublic class ConversionTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteMultipleParagraphs()\n    {\n        // Blockquote with multiple paragraphs has each paragraph prefixed\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteNested()\n    {\n        // Nested blockquote produces double-prefixed lines\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteSimple()\n    {\n        // Simple blockquote\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteWithList()\n    {\n        // Blockquote containing a list preserves list items inside quote\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BoldAndItalic()\n    {\n        // Nested bold and italic\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BoldStrong()\n    {\n        // Strong tag converts to bold\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeBlock()\n    {\n        // Code block with language preserves content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeBlockNoLanguage()\n    {\n        // Code block without a language class preserves content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeInlineInParagraph()\n    {\n        // Inline code element nested inside a paragraph\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeWithBackticksInContent()\n    {\n        // Inline code containing backtick characters is properly escaped\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisMarkHighlight()\n    {\n        // mark tag produces highlighted output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisStrikethroughDel()\n    {\n        // del tag converts to GFM strikethrough\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisStrikethroughS()\n    {\n        // s tag converts to GFM strikethrough\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisSubscript()\n    {\n        // sub tag content is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisSuperscript()\n    {\n        // sup tag content is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisUnderlineU()\n    {\n        // u tag content is preserved in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormInputElements()\n    {\n        // Form input elements produce readable output without form mechanics\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormSelectOptions()\n    {\n        // Select element with options produces readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormTextarea()\n    {\n        // Textarea element produces readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH1()\n    {\n        // H1 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH2()\n    {\n        // H2 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH3()\n    {\n        // H3 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH4()\n    {\n        // H4 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH5()\n    {\n        // H5 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH6()\n    {\n        // H6 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageFigureFigcaption()\n    {\n        // Figure with figcaption preserves both image and caption\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageLinked()\n    {\n        // Image inside an anchor produces a linked image\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageNoAlt()\n    {\n        // Image without alt text produces image markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageSimple()\n    {\n        // Image with alt text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageWithTitle()\n    {\n        // Image with title attribute includes title in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_InlineCode()\n    {\n        // Inline code\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ItalicEm()\n    {\n        // Em tag converts to italic\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakBrTag()\n    {\n        // Single br tag produces a line break in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakHrTag()\n    {\n        // hr tag produces a horizontal separator between content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakMultipleBr()\n    {\n        // Multiple consecutive br tags in sequence\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkAnchorFragment()\n    {\n        // Fragment-only anchor link is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkEmptyHref()\n    {\n        // Link with empty href produces output with the link text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkImageInside()\n    {\n        // Image inside a link produces a linked image\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkMailto()\n    {\n        // Mailto link is preserved with mailto: scheme\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkSimple()\n    {\n        // Simple link\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkWithBoldText()\n    {\n        // Link containing bold text preserves formatting\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkWithTitle()\n    {\n        // Link with title attribute\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListDefinitionDl()\n    {\n        // Definition list with dt and dd elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListItemMultipleParagraphs()\n    {\n        // List item containing multiple paragraphs\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListMixedNested()\n    {\n        // Mixed list: ordered list nested inside unordered list\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListNestedOrdered()\n    {\n        // Nested ordered list with two levels of depth\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListNestedUnordered()\n    {\n        // Nested unordered list with two levels of depth\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListTaskCheckboxes()\n    {\n        // Task list with checked and unchecked checkboxes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OrderedList()\n    {\n        // Ordered list\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphMultiple()\n    {\n        // Multiple paragraphs are separated by a blank line\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphNestedDivs()\n    {\n        // Text nested inside divs is extracted correctly\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphSimple()\n    {\n        // Simple paragraph converts to plain text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphWithInlineFormatting()\n    {\n        // Paragraph with bold, italic, and a link\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphWithLineBreaks()\n    {\n        // Paragraph with br tags produces line breaks in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticAbbr()\n    {\n        // Abbreviation element text is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticArticle()\n    {\n        // Article element wrapping content preserves inner content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticDefinitionList()\n    {\n        // Definition list with term and description\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticDetailsSummary()\n    {\n        // Details and summary elements produce readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticHr()\n    {\n        // Horizontal rule produces a separator in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticMarkHighlight()\n    {\n        // Mark tag produces highlighted output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticSectionWithHeading()\n    {\n        // Section element with heading preserves structure\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticSubSuperscript()\n    {\n        // Subscript and superscript elements are preserved in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SimpleTable()\n    {\n        // Simple table with header\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableEmpty()\n    {\n        // Empty table produces no output or minimal output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableNoThead()\n    {\n        // Table without thead uses first row as implied header\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TablePipeCharsInContent()\n    {\n        // Table cells containing pipe characters are escaped in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableWithAlignment()\n    {\n        // Table with column alignment attributes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableWithColspan()\n    {\n        // Table with colspan attribute in a header cell\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_UnorderedList()\n    {\n        // Unordered list\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/EdgeCasesTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a6eeaf627e940244d18f799cee8581e0e86ed232c104e5a216c6aae446a7797d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: edge-cases.</summary>\npublic class EdgeCasesTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmptyHtml()\n    {\n        // Empty HTML document\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EncodingCjkCharacters()\n    {\n        // CJK (Chinese, Japanese, Korean) characters are preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EncodingHtmlEntities()\n    {\n        // Common HTML entities are decoded in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EncodingNamedEntities()\n    {\n        // Named HTML entities like &mdash; and &hellip; are decoded\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EncodingNumericEntities()\n    {\n        // Numeric HTML entities (decimal and hex) are decoded\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EncodingUnicodeEmoji()\n    {\n        // Emoji and Unicode characters are preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HtmlCommentsOnly()\n    {\n        // Document containing only HTML comments produces empty output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_JustWhitespaceInput()\n    {\n        // Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MalformedDeeplyNestedElements()\n    {\n        // Deeply nested elements (100 levels) are handled without stack overflow\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MalformedMissingBlockClosingTags()\n    {\n        // Missing closing tags on block elements are auto-closed by parser\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MalformedOverlappingTags()\n    {\n        // Overlapping bold/italic tags are recovered by the HTML parser without panic\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MalformedUnclosedParagraph()\n    {\n        // Unclosed <p> tag is recovered gracefully and content is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ScriptTagsOnly()\n    {\n        // Document with only script tags produces empty output (scripts are stripped)\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StyleTagsOnly()\n    {\n        // Document with only style tags produces empty output (styles are stripped)\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomElementWithNesting()\n    {\n        // Visitor handles custom elements with nested content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDeeplyNestedSkip()\n    {\n        // Visitor skips deeply nested elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorElementEndModification()\n    {\n        // Visitor modifies element at end after children processed\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorElementStartSkipEntireSubtree()\n    {\n        // Visitor skips at element_start level removes entire subtree\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorUnknownTagPreservation()\n    {\n        // Visitor preserves unknown HTML tags as raw HTML\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_WhitespaceOnly()\n    {\n        // Whitespace-only content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_XssOnclickHandlerRemoved()\n    {\n        // onclick and other on* event handlers are removed from elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_XssScriptTagStripped()\n    {\n        // Script tag content is stripped and does not appear in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_XssSvgNestedScriptStripped()\n    {\n        // Script tags nested inside SVG are stripped\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/MetadataTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:edcdfa894f2adfc8388025c9cdf1bb394317c71b89b22a781df3556aa6008b29\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: metadata.</summary>\npublic class MetadataTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataAuthorMeta()\n    {\n        // Extract author from <meta name='author'> tag\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataCanonicalUrl()\n    {\n        // Extract canonical URL from <link rel='canonical'> tag\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataDescriptionMeta()\n    {\n        // Extract description from <meta name='description'> tag\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataDublinCore()\n    {\n        // Extract Dublin Core metadata tags\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataExtractAllImages()\n    {\n        // Extract all images from a document into metadata\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataExtractAllLinks()\n    {\n        // Extract all links from a document into metadata\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataHeadersHierarchy()\n    {\n        // Extract heading hierarchy from document into metadata\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataKeywordsMeta()\n    {\n        // Extract keywords from <meta name='keywords'> tag\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataLangAttribute()\n    {\n        // Extract language from html lang attribute\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataMicrodataSchemaArticle()\n    {\n        // Extract schema.org microdata for Article\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataMicrodataSchemaBreadcrumb()\n    {\n        // Extract schema.org breadcrumb navigation microdata\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataMicrodataSchemaOrganization()\n    {\n        // Extract schema.org microdata for Organization\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataMicrodataSchemaPerson()\n    {\n        // Extract schema.org microdata for Person\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataMicrodataSchemaProduct()\n    {\n        // Extract schema.org microdata for Product\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataTextDirectionLtr()\n    {\n        // Extract text direction from lang attribute on html element\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataTextDirectionRtl()\n    {\n        // Extract right-to-left text direction\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_MetadataTitleTag()\n    {\n        // Extract title from <title> tag\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OgBasicTags()\n    {\n        // Extract og:title, og:description, and og:image from Open Graph meta tags\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OgMultipleTags()\n    {\n        // Extract multiple Open Graph tags including type, url, and site_name\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructuredDataJsonLd()\n    {\n        // JSON-LD script tag is stripped from output (security) but metadata may be extracted\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructuredDataMultipleJsonLd()\n    {\n        // Multiple JSON-LD blocks are all stripped from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TwitterCardTags()\n    {\n        // Extract Twitter card meta tags\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/OptionsTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:aebb1275973463d48f11878472b7bcf29397ff905f29473cc694f37b2725983d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: options.</summary>\npublic class OptionsTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsAutolinksFalse()\n    {\n        // Bare URL links rendered as regular markdown links when autolinks disabled\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsBrInTablesFalse()\n    {\n        // BR elements in table cells are stripped when disabled\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsBrInTablesTrue()\n    {\n        // BR elements in table cells render as line breaks\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsCodeBlockBackticks()\n    {\n        // Backticks code block style uses triple backtick fences\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsCodeBlockIndented()\n    {\n        // Code blocks use 4-space indentation\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsCodeBlockTildes()\n    {\n        // Code blocks use tilde fences\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsCodeBlockTildesStyle()\n    {\n        // Tildes code block style uses triple tilde fences\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsCodeLanguagePython()\n    {\n        // Default code language annotation on blocks without lang attribute\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsConvertAsInline()\n    {\n        // Block elements treated as inline\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsDebugTrue()\n    {\n        // Debug mode enabled does not crash and produces output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsDefaultTitleTrue()\n    {\n        // Links without title get empty title attribute when defaultTitle is true\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsEncodingUtf8()\n    {\n        // UTF-8 encoding hint for special characters\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsEscapeAsciiEnabled()\n    {\n        // ASCII Markdown characters are escaped when escapeAscii is true\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsEscapeAsterisks()\n    {\n        // escape_asterisks option escapes asterisks in plain text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsEscapeMisc()\n    {\n        // escape_misc option escapes miscellaneous markdown characters\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsEscapeUnderscores()\n    {\n        // escape_underscores option escapes underscores in plain text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsAttribute()\n    {\n        // Elements matching CSS attribute selector are excluded entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsClass()\n    {\n        // Elements matching CSS class selector are excluded entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsEmptyNoop()\n    {\n        // Empty exclude_selectors list does not affect output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsId()\n    {\n        // Elements matching CSS id selector are excluded entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsMultiple()\n    {\n        // Multiple CSS selectors each exclude their matched elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsNestedContentDropped()\n    {\n        // All descendants of excluded elements are dropped\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsPlainTextMode()\n    {\n        // Exclude selectors work in plain text output mode\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExcludeSelectorsVsStripTags()\n    {\n        // exclude_selectors drops entire subtree unlike strip_tags which keeps children\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsExtractMetadataTrue()\n    {\n        // Extract metadata returns document metadata when enabled\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHeadingStyleAtx()\n    {\n        // ATX heading style produces hash-prefixed headings\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHeadingStyleAtxClosed()\n    {\n        // ATX closed heading style adds closing hashes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHeadingStyleUnderlined()\n    {\n        // Underlined heading style produces setext-style headings for h1 and h2\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHighlightBold()\n    {\n        // Mark tag rendered as bold\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHighlightDoubleEqual()\n    {\n        // Mark tag with double equal highlight style\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsHighlightNone()\n    {\n        // Mark tag with no highlight style strips the mark\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsKeepInlineImagesInParagraph()\n    {\n        // Images inside specified tags stay inline\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsLinkStyleReference()\n    {\n        // Links use reference-style formatting\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsListCustomBullets()\n    {\n        // Custom bullet character for unordered lists\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsListIndentTabs()\n    {\n        // Tab indentation type for nested list items\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsListIndentWidthFour()\n    {\n        // Nested lists indented with 4 spaces per level\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsMaxDepthDefaultUnlimited()\n    {\n        // Default max_depth (null) converts deeply nested content fully\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsMaxDepthTruncates()\n    {\n        // max_depth truncates content beyond the specified depth\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsMaxDepthZeroEmpty()\n    {\n        // max_depth of 0 produces empty output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsNewlineBackslash()\n    {\n        // Hard line breaks rendered with backslash\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsNewlineSpaces()\n    {\n        // Hard line breaks rendered with trailing spaces\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsOutputFormatDjot()\n    {\n        // Djot output format produces djot-compatible markup\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsOutputFormatMarkdown()\n    {\n        // Default markdown output format produces standard markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsOutputFormatPlain()\n    {\n        // Plain text output format strips markdown syntax\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsPreprocessingAggressive()\n    {\n        // Aggressive preset removes nav, footer, aside unconditionally\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsPreprocessingMinimal()\n    {\n        // Minimal preset preserves nav, footer, aside\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsPreprocessingRemoveForms()\n    {\n        // Forms are removed when remove_forms is true\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsPreserveTagsIframe()\n    {\n        // Iframe tags preserved as raw HTML in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsSkipImagesTrue()\n    {\n        // Images are omitted from output when skipImages is true\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsStripNewlines()\n    {\n        // Strip newlines produces single-line paragraphs\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsStripTagsDivSpan()\n    {\n        // Div and span tags stripped but content preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsStrongEmUnderscore()\n    {\n        // Strong and em tags use underscore symbol instead of asterisk\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsSubSymbolTilde()\n    {\n        // Subscript rendered with tilde symbol\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsSupSymbolCaret()\n    {\n        // Superscript rendered with caret symbol\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsWhitespaceNormalized()\n    {\n        // Normalized whitespace mode collapses multiple spaces\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsWhitespaceStrict()\n    {\n        // Strict whitespace mode preserves whitespace as-is\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsWrapDisabled()\n    {\n        // Wrap option disabled preserves long lines without breaking\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OptionsWrapEnabled()\n    {\n        // Wrap option enabled with custom width wraps long lines\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/RealWorldTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:32a2f1199d4eb0ad1cb144c3d889b744bdd543e8489c04644c37f9e990c9c0ad\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: real-world.</summary>\npublic class RealWorldTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_RealWorldBlogPost()\n    {\n        // Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_RealWorldDocumentationPage()\n    {\n        // Documentation page with nested lists, code examples, and blockquotes converts correctly\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_RealWorldProductPage()\n    {\n        // Product page with table, images, and lists converts correctly\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/ResultTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:dd17a02b1e6986be0e566fa149dd633712f6f22013a8cc01a63f82f65f75e943\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: result.</summary>\npublic class ResultTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultTablesEmptyWhenNoTables()\n    {\n        // Result tables array is empty when input has no tables\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultTablesMultiple()\n    {\n        // Multiple tables each appear in the tables array\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultTablesSimple()\n    {\n        // Simple table populates the tables array in result\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultTablesWithoutStructureFlag()\n    {\n        // Tables array is empty when includeDocumentStructure is false\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultWarningsEmptyForCleanInput()\n    {\n        // Warnings array is empty for well-formed HTML without problematic content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultWarningsEmptyForComplexInput()\n    {\n        // Warnings array is empty for complex but valid HTML\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ResultWarningsEmptyForMalformedHtml()\n    {\n        // Warnings array is empty even for malformed HTML (parser is lenient)\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/SmokeTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d35c32a1caac43ed9835e31a3e42fdea9dd2e6deca70aba6441b6ab90c579a3a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: smoke.</summary>\npublic class SmokeTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeEmptyString()\n    {\n        // Empty string produces empty output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeSimpleHeading()\n    {\n        // H1 heading converts to ATX markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeSimpleParagraph()\n    {\n        // Simple paragraph converts correctly\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/StructureTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:048c960c6fe7f7a755cd4ef62a4aa0934433261a35d923eb8d3cb636596f2d75\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: structure.</summary>\npublic class StructureTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureCodeBlock()\n    {\n        // Fenced code block produces Code node\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureDeepNestingH1H2H3()\n    {\n        // H1 > H2 > H3 creates three levels of heading nesting\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureH1H2NestedGroup()\n    {\n        // H1 followed by H2 creates a nested group under the H1\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureHeadingParagraph()\n    {\n        // Simple heading followed by paragraph produces Heading and Paragraph nodes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureList()\n    {\n        // Unordered list produces List and ListItem nodes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureMultipleHeadings()\n    {\n        // Multiple headings create multiple Heading nodes with correct levels\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_StructureSiblingH1Groups()\n    {\n        // H1, H2, then another H1 creates two sibling top-level groups\n    }\n}\n"
  },
  {
    "path": "e2e/csharp/tests/VisitorTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:368ab484acaabc23d5a17128c435c3c113f8b0221f11c32bf121b274b7934a76\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: visitor.</summary>\npublic class VisitorTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorAudioCustom()\n    {\n        // Visitor replaces audio element with custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorAudioSkip()\n    {\n        // Visitor removes audio elements from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorButtonCustom()\n    {\n        // Visitor replaces button with bracketed text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorButtonSkip()\n    {\n        // Visitor removes all buttons from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorContinueDefault()\n    {\n        // Visitor continue action preserves default conversion\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomBlockquote()\n    {\n        // Visitor replaces blockquote with custom format\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomEmphasis()\n    {\n        // Visitor replaces emphasis with custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomHeading()\n    {\n        // Visitor replaces heading with custom format\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomImage()\n    {\n        // Visitor replaces image with custom output using template\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomLinkFormat()\n    {\n        // Visitor reformats links using template interpolation\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomLinkStatic()\n    {\n        // Visitor replaces link with static custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorCustomOutput()\n    {\n        // Visitor custom action replaces element output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDefinitionListCustom()\n    {\n        // Visitor customizes definition list items\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDefinitionListCustomFormat()\n    {\n        // Visitor formats definition lists with custom templates\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDefinitionListSkip()\n    {\n        // Visitor skips definition list items from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDetailsSummaryCustom()\n    {\n        // Visitor customizes details/summary disclosure elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorDetailsSummarySkip()\n    {\n        // Visitor removes details/summary elements entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorFigureCustom()\n    {\n        // Visitor customizes figure and figcaption elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorFigureCustomWrap()\n    {\n        // Visitor wraps figure content with custom formatting\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorFigureSkip()\n    {\n        // Visitor removes figure elements with their captions\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorFormCustom()\n    {\n        // Visitor replaces form with custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorFormSkip()\n    {\n        // Visitor skips form elements entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorHorizontalRuleCustom()\n    {\n        // Visitor replaces horizontal rule with custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorHorizontalRuleSkip()\n    {\n        // Visitor removes all horizontal rules\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorIframeCustom()\n    {\n        // Visitor replaces embedded iframe with custom text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorIframeSkip()\n    {\n        // Visitor removes embedded iframes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorInputCustom()\n    {\n        // Visitor replaces input with labeled output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorInputSkip()\n    {\n        // Visitor skips all input elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorLineBreakCustom()\n    {\n        // Visitor replaces line break with custom output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorLineBreakSkip()\n    {\n        // Visitor removes all line breaks\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorMarkCustom()\n    {\n        // Visitor replaces highlight/mark with custom template\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorMarkSkip()\n    {\n        // Visitor skips mark elements entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorPreserveHtml()\n    {\n        // Visitor preserve_html action includes raw HTML in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipAllHeadings()\n    {\n        // Visitor skips all headings from document\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipCodeBlocks()\n    {\n        // Visitor skips code blocks from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipHeading()\n    {\n        // Visitor skip action omits all headings from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipImages()\n    {\n        // Visitor skips all images from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipLinks()\n    {\n        // Visitor skips all links entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSkipStrong()\n    {\n        // Visitor skips bold/strong elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSubscriptCustom()\n    {\n        // Visitor replaces subscript with custom template\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSubscriptSkip()\n    {\n        // Visitor skips subscript elements entirely\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSuperscriptCustom()\n    {\n        // Visitor replaces superscript with custom template\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorSuperscriptSkip()\n    {\n        // Visitor skips superscript from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorUnderlineCustom()\n    {\n        // Visitor replaces underline with custom markup\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorUnderlineSkip()\n    {\n        // Visitor skips underline elements from output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorVideoCustom()\n    {\n        // Visitor replaces video with custom link\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_VisitorVideoSkip()\n    {\n        // Visitor removes video elements entirely\n    }\n}\n"
  },
  {
    "path": "e2e/dart/pubspec.yaml",
    "content": "name: e2e_dart\nversion: 0.1.0\npublish_to: none\n\nenvironment:\n  sdk: \">=3.0.0 <4.0.0\"\n\ndependencies:\n  html_to_markdown_rs:\n    path: ../../packages/dart\n\ndev_dependencies:\n  test: ^1.25.0\n"
  },
  {
    "path": "e2e/elixir/mix.exs",
    "content": "defmodule E2eElixir.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :e2e_elixir,\n      version: \"0.1.0\",\n      elixir: \"~> 1.14\",\n      deps: deps()\n    ]\n  end\n\n  defp deps do\n    [\n      {:html_to_markdown, path: \"../../packages/elixir\"},\n      {:rustler_precompiled, \"~> 0.9\"},\n      {:rustler, \"~> 0.37.0\", optional: true, runtime: false}\n    ]\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/conversion_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:850d5b9646a5a810a00e799e42feb06eda6c7bdb94262ffd8355cc64e86cce8e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: conversion\ndefmodule E2e.ConversionTest do\n  use ExUnit.Case, async: false\n\n  describe \"blockquote_multiple_paragraphs\" do\n    @tag :skip\n    test \"Blockquote with multiple paragraphs has each paragraph prefixed\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"blockquote_nested\" do\n    @tag :skip\n    test \"Nested blockquote produces double-prefixed lines\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"blockquote_simple\" do\n    @tag :skip\n    test \"Simple blockquote\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"blockquote_with_list\" do\n    @tag :skip\n    test \"Blockquote containing a list preserves list items inside quote\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"bold_and_italic\" do\n    @tag :skip\n    test \"Nested bold and italic\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"bold_strong\" do\n    @tag :skip\n    test \"Strong tag converts to bold\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"code_block\" do\n    @tag :skip\n    test \"Code block with language preserves content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"code_block_no_language\" do\n    @tag :skip\n    test \"Code block without a language class preserves content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"code_inline_in_paragraph\" do\n    @tag :skip\n    test \"Inline code element nested inside a paragraph\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"code_with_backticks_in_content\" do\n    @tag :skip\n    test \"Inline code containing backtick characters is properly escaped\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_mark_highlight\" do\n    @tag :skip\n    test \"mark tag produces highlighted output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_strikethrough_del\" do\n    @tag :skip\n    test \"del tag converts to GFM strikethrough\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_strikethrough_s\" do\n    @tag :skip\n    test \"s tag converts to GFM strikethrough\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_subscript\" do\n    @tag :skip\n    test \"sub tag content is preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_superscript\" do\n    @tag :skip\n    test \"sup tag content is preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"emphasis_underline_u\" do\n    @tag :skip\n    test \"u tag content is preserved in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"form_input_elements\" do\n    @tag :skip\n    test \"Form input elements produce readable output without form mechanics\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"form_select_options\" do\n    @tag :skip\n    test \"Select element with options produces readable output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"form_textarea\" do\n    @tag :skip\n    test \"Textarea element produces readable output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h1\" do\n    @tag :skip\n    test \"H1 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h2\" do\n    @tag :skip\n    test \"H2 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h3\" do\n    @tag :skip\n    test \"H3 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h4\" do\n    @tag :skip\n    test \"H4 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h5\" do\n    @tag :skip\n    test \"H5 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"heading_h6\" do\n    @tag :skip\n    test \"H6 heading\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"image_figure_figcaption\" do\n    @tag :skip\n    test \"Figure with figcaption preserves both image and caption\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"image_linked\" do\n    @tag :skip\n    test \"Image inside an anchor produces a linked image\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"image_no_alt\" do\n    @tag :skip\n    test \"Image without alt text produces image markdown\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"image_simple\" do\n    @tag :skip\n    test \"Image with alt text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"image_with_title\" do\n    @tag :skip\n    test \"Image with title attribute includes title in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"inline_code\" do\n    @tag :skip\n    test \"Inline code\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"italic_em\" do\n    @tag :skip\n    test \"Em tag converts to italic\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"line_break_br_tag\" do\n    @tag :skip\n    test \"Single br tag produces a line break in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"line_break_hr_tag\" do\n    @tag :skip\n    test \"hr tag produces a horizontal separator between content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"line_break_multiple_br\" do\n    @tag :skip\n    test \"Multiple consecutive br tags in sequence\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_anchor_fragment\" do\n    @tag :skip\n    test \"Fragment-only anchor link is preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_empty_href\" do\n    @tag :skip\n    test \"Link with empty href produces output with the link text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_image_inside\" do\n    @tag :skip\n    test \"Image inside a link produces a linked image\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_mailto\" do\n    @tag :skip\n    test \"Mailto link is preserved with mailto: scheme\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_simple\" do\n    @tag :skip\n    test \"Simple link\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_with_bold_text\" do\n    @tag :skip\n    test \"Link containing bold text preserves formatting\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"link_with_title\" do\n    @tag :skip\n    test \"Link with title attribute\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_definition_dl\" do\n    @tag :skip\n    test \"Definition list with dt and dd elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_item_multiple_paragraphs\" do\n    @tag :skip\n    test \"List item containing multiple paragraphs\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_mixed_nested\" do\n    @tag :skip\n    test \"Mixed list: ordered list nested inside unordered list\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_nested_ordered\" do\n    @tag :skip\n    test \"Nested ordered list with two levels of depth\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_nested_unordered\" do\n    @tag :skip\n    test \"Nested unordered list with two levels of depth\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"list_task_checkboxes\" do\n    @tag :skip\n    test \"Task list with checked and unchecked checkboxes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"ordered_list\" do\n    @tag :skip\n    test \"Ordered list\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"paragraph_multiple\" do\n    @tag :skip\n    test \"Multiple paragraphs are separated by a blank line\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"paragraph_nested_divs\" do\n    @tag :skip\n    test \"Text nested inside divs is extracted correctly\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"paragraph_simple\" do\n    @tag :skip\n    test \"Simple paragraph converts to plain text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"paragraph_with_inline_formatting\" do\n    @tag :skip\n    test \"Paragraph with bold, italic, and a link\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"paragraph_with_line_breaks\" do\n    @tag :skip\n    test \"Paragraph with br tags produces line breaks in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_abbr\" do\n    @tag :skip\n    test \"Abbreviation element text is preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_article\" do\n    @tag :skip\n    test \"Article element wrapping content preserves inner content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_definition_list\" do\n    @tag :skip\n    test \"Definition list with term and description\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_details_summary\" do\n    @tag :skip\n    test \"Details and summary elements produce readable output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_hr\" do\n    @tag :skip\n    test \"Horizontal rule produces a separator in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_mark_highlight\" do\n    @tag :skip\n    test \"Mark tag produces highlighted output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_section_with_heading\" do\n    @tag :skip\n    test \"Section element with heading preserves structure\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"semantic_sub_superscript\" do\n    @tag :skip\n    test \"Subscript and superscript elements are preserved in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"simple_table\" do\n    @tag :skip\n    test \"Simple table with header\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"table_empty\" do\n    @tag :skip\n    test \"Empty table produces no output or minimal output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"table_no_thead\" do\n    @tag :skip\n    test \"Table without thead uses first row as implied header\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"table_pipe_chars_in_content\" do\n    @tag :skip\n    test \"Table cells containing pipe characters are escaped in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"table_with_alignment\" do\n    @tag :skip\n    test \"Table with column alignment attributes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"table_with_colspan\" do\n    @tag :skip\n    test \"Table with colspan attribute in a header cell\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"unordered_list\" do\n    @tag :skip\n    test \"Unordered list\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/edge_cases_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:5dbf0d892a9296f402787489f097bcd0056f582b898cec0c1c7dd94651886107\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: edge-cases\ndefmodule E2e.EdgeCasesTest do\n  use ExUnit.Case, async: false\n\n  describe \"empty_html\" do\n    @tag :skip\n    test \"Empty HTML document\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"encoding_cjk_characters\" do\n    @tag :skip\n    test \"CJK (Chinese, Japanese, Korean) characters are preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"encoding_html_entities\" do\n    @tag :skip\n    test \"Common HTML entities are decoded in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"encoding_named_entities\" do\n    @tag :skip\n    test \"Named HTML entities like &mdash; and &hellip; are decoded\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"encoding_numeric_entities\" do\n    @tag :skip\n    test \"Numeric HTML entities (decimal and hex) are decoded\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"encoding_unicode_emoji\" do\n    @tag :skip\n    test \"Emoji and Unicode characters are preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"html_comments_only\" do\n    @tag :skip\n    test \"Document containing only HTML comments produces empty output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"just_whitespace_input\" do\n    @tag :skip\n    test \"Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"malformed_deeply_nested_elements\" do\n    @tag :skip\n    test \"Deeply nested elements (100 levels) are handled without stack overflow\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"malformed_missing_block_closing_tags\" do\n    @tag :skip\n    test \"Missing closing tags on block elements are auto-closed by parser\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"malformed_overlapping_tags\" do\n    @tag :skip\n    test \"Overlapping bold/italic tags are recovered by the HTML parser without panic\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"malformed_unclosed_paragraph\" do\n    @tag :skip\n    test \"Unclosed <p> tag is recovered gracefully and content is preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"script_tags_only\" do\n    @tag :skip\n    test \"Document with only script tags produces empty output (scripts are stripped)\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"style_tags_only\" do\n    @tag :skip\n    test \"Document with only style tags produces empty output (styles are stripped)\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_element_with_nesting\" do\n    @tag :skip\n    test \"Visitor handles custom elements with nested content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_deeply_nested_skip\" do\n    @tag :skip\n    test \"Visitor skips deeply nested elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_element_end_modification\" do\n    @tag :skip\n    test \"Visitor modifies element at end after children processed\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_element_start_skip_entire_subtree\" do\n    @tag :skip\n    test \"Visitor skips at element_start level removes entire subtree\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_unknown_tag_preservation\" do\n    @tag :skip\n    test \"Visitor preserves unknown HTML tags as raw HTML\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"whitespace_only\" do\n    @tag :skip\n    test \"Whitespace-only content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"xss_onclick_handler_removed\" do\n    @tag :skip\n    test \"onclick and other on* event handlers are removed from elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"xss_script_tag_stripped\" do\n    @tag :skip\n    test \"Script tag content is stripped and does not appear in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"xss_svg_nested_script_stripped\" do\n    @tag :skip\n    test \"Script tags nested inside SVG are stripped\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/metadata_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f11b7ccc2fa7d056c027f83281c3e58c08a3cc5978bc6f56f318c92258884ace\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: metadata\ndefmodule E2e.MetadataTest do\n  use ExUnit.Case, async: false\n\n  describe \"metadata_author_meta\" do\n    @tag :skip\n    test \"Extract author from <meta name='author'> tag\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_canonical_url\" do\n    @tag :skip\n    test \"Extract canonical URL from <link rel='canonical'> tag\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_description_meta\" do\n    @tag :skip\n    test \"Extract description from <meta name='description'> tag\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_dublin_core\" do\n    @tag :skip\n    test \"Extract Dublin Core metadata tags\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_extract_all_images\" do\n    @tag :skip\n    test \"Extract all images from a document into metadata\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_extract_all_links\" do\n    @tag :skip\n    test \"Extract all links from a document into metadata\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_headers_hierarchy\" do\n    @tag :skip\n    test \"Extract heading hierarchy from document into metadata\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_keywords_meta\" do\n    @tag :skip\n    test \"Extract keywords from <meta name='keywords'> tag\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_lang_attribute\" do\n    @tag :skip\n    test \"Extract language from html lang attribute\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_microdata_schema_article\" do\n    @tag :skip\n    test \"Extract schema.org microdata for Article\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_microdata_schema_breadcrumb\" do\n    @tag :skip\n    test \"Extract schema.org breadcrumb navigation microdata\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_microdata_schema_organization\" do\n    @tag :skip\n    test \"Extract schema.org microdata for Organization\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_microdata_schema_person\" do\n    @tag :skip\n    test \"Extract schema.org microdata for Person\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_microdata_schema_product\" do\n    @tag :skip\n    test \"Extract schema.org microdata for Product\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_text_direction_ltr\" do\n    @tag :skip\n    test \"Extract text direction from lang attribute on html element\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_text_direction_rtl\" do\n    @tag :skip\n    test \"Extract right-to-left text direction\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"metadata_title_tag\" do\n    @tag :skip\n    test \"Extract title from <title> tag\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"og_basic_tags\" do\n    @tag :skip\n    test \"Extract og:title, og:description, and og:image from Open Graph meta tags\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"og_multiple_tags\" do\n    @tag :skip\n    test \"Extract multiple Open Graph tags including type, url, and site_name\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structured_data_json_ld\" do\n    @tag :skip\n    test \"JSON-LD script tag is stripped from output (security) but metadata may be extracted\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structured_data_multiple_json_ld\" do\n    @tag :skip\n    test \"Multiple JSON-LD blocks are all stripped from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"twitter_card_tags\" do\n    @tag :skip\n    test \"Extract Twitter card meta tags\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/options_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:0dd916bdfbf01f2061f2bb96a6f167eb5bc1972b59b35b4b418bd7a57284ab35\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: options\ndefmodule E2e.OptionsTest do\n  use ExUnit.Case, async: false\n\n  describe \"options_autolinks_false\" do\n    @tag :skip\n    test \"Bare URL links rendered as regular markdown links when autolinks disabled\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_br_in_tables_false\" do\n    @tag :skip\n    test \"BR elements in table cells are stripped when disabled\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_br_in_tables_true\" do\n    @tag :skip\n    test \"BR elements in table cells render as line breaks\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_code_block_backticks\" do\n    @tag :skip\n    test \"Backticks code block style uses triple backtick fences\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_code_block_indented\" do\n    @tag :skip\n    test \"Code blocks use 4-space indentation\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_code_block_tildes\" do\n    @tag :skip\n    test \"Code blocks use tilde fences\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_code_block_tildes_style\" do\n    @tag :skip\n    test \"Tildes code block style uses triple tilde fences\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_code_language_python\" do\n    @tag :skip\n    test \"Default code language annotation on blocks without lang attribute\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_convert_as_inline\" do\n    @tag :skip\n    test \"Block elements treated as inline\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_debug_true\" do\n    @tag :skip\n    test \"Debug mode enabled does not crash and produces output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_default_title_true\" do\n    @tag :skip\n    test \"Links without title get empty title attribute when defaultTitle is true\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_encoding_utf8\" do\n    @tag :skip\n    test \"UTF-8 encoding hint for special characters\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_escape_ascii_enabled\" do\n    @tag :skip\n    test \"ASCII Markdown characters are escaped when escapeAscii is true\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_escape_asterisks\" do\n    @tag :skip\n    test \"escape_asterisks option escapes asterisks in plain text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_escape_misc\" do\n    @tag :skip\n    test \"escape_misc option escapes miscellaneous markdown characters\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_escape_underscores\" do\n    @tag :skip\n    test \"escape_underscores option escapes underscores in plain text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_attribute\" do\n    @tag :skip\n    test \"Elements matching CSS attribute selector are excluded entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_class\" do\n    @tag :skip\n    test \"Elements matching CSS class selector are excluded entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_empty_noop\" do\n    @tag :skip\n    test \"Empty exclude_selectors list does not affect output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_id\" do\n    @tag :skip\n    test \"Elements matching CSS id selector are excluded entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_multiple\" do\n    @tag :skip\n    test \"Multiple CSS selectors each exclude their matched elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_nested_content_dropped\" do\n    @tag :skip\n    test \"All descendants of excluded elements are dropped\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_plain_text_mode\" do\n    @tag :skip\n    test \"Exclude selectors work in plain text output mode\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_exclude_selectors_vs_strip_tags\" do\n    @tag :skip\n    test \"exclude_selectors drops entire subtree unlike strip_tags which keeps children\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_extract_metadata_true\" do\n    @tag :skip\n    test \"Extract metadata returns document metadata when enabled\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_heading_style_atx\" do\n    @tag :skip\n    test \"ATX heading style produces hash-prefixed headings\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_heading_style_atx_closed\" do\n    @tag :skip\n    test \"ATX closed heading style adds closing hashes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_heading_style_underlined\" do\n    @tag :skip\n    test \"Underlined heading style produces setext-style headings for h1 and h2\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_highlight_bold\" do\n    @tag :skip\n    test \"Mark tag rendered as bold\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_highlight_double_equal\" do\n    @tag :skip\n    test \"Mark tag with double equal highlight style\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_highlight_none\" do\n    @tag :skip\n    test \"Mark tag with no highlight style strips the mark\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_keep_inline_images_in_paragraph\" do\n    @tag :skip\n    test \"Images inside specified tags stay inline\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_link_style_reference\" do\n    @tag :skip\n    test \"Links use reference-style formatting\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_list_custom_bullets\" do\n    @tag :skip\n    test \"Custom bullet character for unordered lists\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_list_indent_tabs\" do\n    @tag :skip\n    test \"Tab indentation type for nested list items\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_list_indent_width_four\" do\n    @tag :skip\n    test \"Nested lists indented with 4 spaces per level\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_max_depth_default_unlimited\" do\n    @tag :skip\n    test \"Default max_depth (null) converts deeply nested content fully\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_max_depth_truncates\" do\n    @tag :skip\n    test \"max_depth truncates content beyond the specified depth\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_max_depth_zero_empty\" do\n    @tag :skip\n    test \"max_depth of 0 produces empty output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_newline_backslash\" do\n    @tag :skip\n    test \"Hard line breaks rendered with backslash\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_newline_spaces\" do\n    @tag :skip\n    test \"Hard line breaks rendered with trailing spaces\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_output_format_djot\" do\n    @tag :skip\n    test \"Djot output format produces djot-compatible markup\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_output_format_markdown\" do\n    @tag :skip\n    test \"Default markdown output format produces standard markdown\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_output_format_plain\" do\n    @tag :skip\n    test \"Plain text output format strips markdown syntax\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_preprocessing_aggressive\" do\n    @tag :skip\n    test \"Aggressive preset removes nav, footer, aside unconditionally\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_preprocessing_minimal\" do\n    @tag :skip\n    test \"Minimal preset preserves nav, footer, aside\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_preprocessing_remove_forms\" do\n    @tag :skip\n    test \"Forms are removed when remove_forms is true\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_preserve_tags_iframe\" do\n    @tag :skip\n    test \"Iframe tags preserved as raw HTML in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_skip_images_true\" do\n    @tag :skip\n    test \"Images are omitted from output when skipImages is true\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_strip_newlines\" do\n    @tag :skip\n    test \"Strip newlines produces single-line paragraphs\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_strip_tags_div_span\" do\n    @tag :skip\n    test \"Div and span tags stripped but content preserved\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_strong_em_underscore\" do\n    @tag :skip\n    test \"Strong and em tags use underscore symbol instead of asterisk\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_sub_symbol_tilde\" do\n    @tag :skip\n    test \"Subscript rendered with tilde symbol\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_sup_symbol_caret\" do\n    @tag :skip\n    test \"Superscript rendered with caret symbol\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_whitespace_normalized\" do\n    @tag :skip\n    test \"Normalized whitespace mode collapses multiple spaces\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_whitespace_strict\" do\n    @tag :skip\n    test \"Strict whitespace mode preserves whitespace as-is\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_wrap_disabled\" do\n    @tag :skip\n    test \"Wrap option disabled preserves long lines without breaking\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"options_wrap_enabled\" do\n    @tag :skip\n    test \"Wrap option enabled with custom width wraps long lines\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/real_world_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:773f65d7adb227ee57302709f13e28158c7c5e3a5a6b6efc3fddbd277b4505cd\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: real-world\ndefmodule E2e.RealWorldTest do\n  use ExUnit.Case, async: false\n\n  describe \"real_world_blog_post\" do\n    @tag :skip\n    test \"Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"real_world_documentation_page\" do\n    @tag :skip\n    test \"Documentation page with nested lists, code examples, and blockquotes converts correctly\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"real_world_product_page\" do\n    @tag :skip\n    test \"Product page with table, images, and lists converts correctly\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/result_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:95734755d8432540ee0247d8df010c3ec096e5feee249d7b304f5bbf22c27496\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: result\ndefmodule E2e.ResultTest do\n  use ExUnit.Case, async: false\n\n  describe \"result_tables_empty_when_no_tables\" do\n    @tag :skip\n    test \"Result tables array is empty when input has no tables\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_tables_multiple\" do\n    @tag :skip\n    test \"Multiple tables each appear in the tables array\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_tables_simple\" do\n    @tag :skip\n    test \"Simple table populates the tables array in result\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_tables_without_structure_flag\" do\n    @tag :skip\n    test \"Tables array is empty when includeDocumentStructure is false\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_warnings_empty_for_clean_input\" do\n    @tag :skip\n    test \"Warnings array is empty for well-formed HTML without problematic content\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_warnings_empty_for_complex_input\" do\n    @tag :skip\n    test \"Warnings array is empty for complex but valid HTML\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"result_warnings_empty_for_malformed_html\" do\n    @tag :skip\n    test \"Warnings array is empty even for malformed HTML (parser is lenient)\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/smoke_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:a3ac5de40274ca015ff6ad06f5c2bc390e4d18c4bbba840119d3c2f5d9436998\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: smoke\ndefmodule E2e.SmokeTest do\n  use ExUnit.Case, async: false\n\n  describe \"smoke_empty_string\" do\n    @tag :skip\n    test \"Empty string produces empty output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"smoke_simple_heading\" do\n    @tag :skip\n    test \"H1 heading converts to ATX markdown\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"smoke_simple_paragraph\" do\n    @tag :skip\n    test \"Simple paragraph converts correctly\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/structure_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:ecf4a70e6404f5e8126a179537bda66d275cc1959e316c57174b53d51223c4cb\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: structure\ndefmodule E2e.StructureTest do\n  use ExUnit.Case, async: false\n\n  describe \"structure_code_block\" do\n    @tag :skip\n    test \"Fenced code block produces Code node\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_deep_nesting_h1_h2_h3\" do\n    @tag :skip\n    test \"H1 > H2 > H3 creates three levels of heading nesting\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_h1_h2_nested_group\" do\n    @tag :skip\n    test \"H1 followed by H2 creates a nested group under the H1\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_heading_paragraph\" do\n    @tag :skip\n    test \"Simple heading followed by paragraph produces Heading and Paragraph nodes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_list\" do\n    @tag :skip\n    test \"Unordered list produces List and ListItem nodes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_multiple_headings\" do\n    @tag :skip\n    test \"Multiple headings create multiple Heading nodes with correct levels\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"structure_sibling_h1_groups\" do\n    @tag :skip\n    test \"H1, H2, then another H1 creates two sibling top-level groups\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/elixir/test/test_helper.exs",
    "content": "ExUnit.start()\n"
  },
  {
    "path": "e2e/elixir/test/visitor_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:3d84daaacf7dae0bf02b11df696b008bed1ca3723c9853145f100e85ae668a21\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: visitor\ndefmodule E2e.VisitorTest do\n  use ExUnit.Case, async: false\n\n  describe \"visitor_audio_custom\" do\n    @tag :skip\n    test \"Visitor replaces audio element with custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_audio_skip\" do\n    @tag :skip\n    test \"Visitor removes audio elements from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_button_custom\" do\n    @tag :skip\n    test \"Visitor replaces button with bracketed text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_button_skip\" do\n    @tag :skip\n    test \"Visitor removes all buttons from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_continue_default\" do\n    @tag :skip\n    test \"Visitor continue action preserves default conversion\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_blockquote\" do\n    @tag :skip\n    test \"Visitor replaces blockquote with custom format\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_emphasis\" do\n    @tag :skip\n    test \"Visitor replaces emphasis with custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_heading\" do\n    @tag :skip\n    test \"Visitor replaces heading with custom format\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_image\" do\n    @tag :skip\n    test \"Visitor replaces image with custom output using template\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_link_format\" do\n    @tag :skip\n    test \"Visitor reformats links using template interpolation\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_link_static\" do\n    @tag :skip\n    test \"Visitor replaces link with static custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_custom_output\" do\n    @tag :skip\n    test \"Visitor custom action replaces element output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_definition_list_custom\" do\n    @tag :skip\n    test \"Visitor customizes definition list items\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_definition_list_custom_format\" do\n    @tag :skip\n    test \"Visitor formats definition lists with custom templates\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_definition_list_skip\" do\n    @tag :skip\n    test \"Visitor skips definition list items from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_details_summary_custom\" do\n    @tag :skip\n    test \"Visitor customizes details/summary disclosure elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_details_summary_skip\" do\n    @tag :skip\n    test \"Visitor removes details/summary elements entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_figure_custom\" do\n    @tag :skip\n    test \"Visitor customizes figure and figcaption elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_figure_custom_wrap\" do\n    @tag :skip\n    test \"Visitor wraps figure content with custom formatting\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_figure_skip\" do\n    @tag :skip\n    test \"Visitor removes figure elements with their captions\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_form_custom\" do\n    @tag :skip\n    test \"Visitor replaces form with custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_form_skip\" do\n    @tag :skip\n    test \"Visitor skips form elements entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_horizontal_rule_custom\" do\n    @tag :skip\n    test \"Visitor replaces horizontal rule with custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_horizontal_rule_skip\" do\n    @tag :skip\n    test \"Visitor removes all horizontal rules\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_iframe_custom\" do\n    @tag :skip\n    test \"Visitor replaces embedded iframe with custom text\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_iframe_skip\" do\n    @tag :skip\n    test \"Visitor removes embedded iframes\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_input_custom\" do\n    @tag :skip\n    test \"Visitor replaces input with labeled output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_input_skip\" do\n    @tag :skip\n    test \"Visitor skips all input elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_line_break_custom\" do\n    @tag :skip\n    test \"Visitor replaces line break with custom output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_line_break_skip\" do\n    @tag :skip\n    test \"Visitor removes all line breaks\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_mark_custom\" do\n    @tag :skip\n    test \"Visitor replaces highlight/mark with custom template\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_mark_skip\" do\n    @tag :skip\n    test \"Visitor skips mark elements entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_preserve_html\" do\n    @tag :skip\n    test \"Visitor preserve_html action includes raw HTML in output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_all_headings\" do\n    @tag :skip\n    test \"Visitor skips all headings from document\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_code_blocks\" do\n    @tag :skip\n    test \"Visitor skips code blocks from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_heading\" do\n    @tag :skip\n    test \"Visitor skip action omits all headings from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_images\" do\n    @tag :skip\n    test \"Visitor skips all images from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_links\" do\n    @tag :skip\n    test \"Visitor skips all links entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_skip_strong\" do\n    @tag :skip\n    test \"Visitor skips bold/strong elements\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_subscript_custom\" do\n    @tag :skip\n    test \"Visitor replaces subscript with custom template\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_subscript_skip\" do\n    @tag :skip\n    test \"Visitor skips subscript elements entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_superscript_custom\" do\n    @tag :skip\n    test \"Visitor replaces superscript with custom template\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_superscript_skip\" do\n    @tag :skip\n    test \"Visitor skips superscript from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_underline_custom\" do\n    @tag :skip\n    test \"Visitor replaces underline with custom markup\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_underline_skip\" do\n    @tag :skip\n    test \"Visitor skips underline elements from output\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_video_custom\" do\n    @tag :skip\n    test \"Visitor replaces video with custom link\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\n\n  describe \"visitor_video_skip\" do\n    @tag :skip\n    test \"Visitor removes video elements entirely\" do\n      # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "e2e/gleam/gleam.toml",
    "content": "name = \"e2e_gleam\"\nversion = \"0.1.0\"\ntarget = \"erlang\"\n\n[dependencies]\nhtml_to_markdown_rs = { path = \"../../packages/gleam\" }\ngleam_stdlib = \">= 0.34.0 and < 2.0.0\"\ngleeunit = \">= 1.0.0 and < 2.0.0\"\n"
  },
  {
    "path": "e2e/go/conversion_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bd7ba7579444da182e09063281b0218347dcfded17b29fdd8708fb34423e7a87\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: conversion\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_BlockquoteMultipleParagraphs(t *testing.T) {\n\t// Blockquote with multiple paragraphs has each paragraph prefixed\n\tresult, err := htmd.Convert(`<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `> First paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `> First paragraph.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `> Second paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `> Second paragraph.`)\n\t\t}\n\t}\n}\n\nfunc Test_BlockquoteNested(t *testing.T) {\n\t// Nested blockquote produces double-prefixed lines\n\tresult, err := htmd.Convert(`<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Outer quote.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Outer quote.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Inner quote.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Inner quote.`)\n\t\t}\n\t}\n}\n\nfunc Test_BlockquoteSimple(t *testing.T) {\n\t// Simple blockquote\n\tresult, err := htmd.Convert(`<blockquote><p>Quote text</p></blockquote>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `> Quote text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `> Quote text`)\n\t\t}\n\t}\n}\n\nfunc Test_BlockquoteWithList(t *testing.T) {\n\t// Blockquote containing a list preserves list items inside quote\n\tresult, err := htmd.Convert(`<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Quote intro:`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Quote intro:`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Point one`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Point one`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Point two`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Point two`)\n\t\t}\n\t}\n}\n\nfunc Test_BoldAndItalic(t *testing.T) {\n\t// Nested bold and italic\n\tresult, err := htmd.Convert(`<p><strong><em>both</em></strong></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `***both***`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `***both***`)\n\t\t}\n\t}\n}\n\nfunc Test_BoldStrong(t *testing.T) {\n\t// Strong tag converts to bold\n\tresult, err := htmd.Convert(`<p><strong>bold</strong></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `**bold**`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `**bold**`)\n\t\t}\n\t}\n}\n\nfunc Test_CodeBlock(t *testing.T) {\n\t// Code block with language preserves content\n\tresult, err := htmd.Convert(`<pre><code class=\"language-python\">print('hello')</code></pre>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `print('hello')`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `print('hello')`)\n\t\t}\n\t}\n}\n\nfunc Test_CodeBlockNoLanguage(t *testing.T) {\n\t// Code block without a language class preserves content\n\tresult, err := htmd.Convert(`<pre><code>plain code here</code></pre>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `plain code here`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `plain code here`)\n\t\t}\n\t}\n}\n\nfunc Test_CodeInlineInParagraph(t *testing.T) {\n\t// Inline code element nested inside a paragraph\n\tresult, err := htmd.Convert(`<p>Call the <code>initialize()</code> method first.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), \"`initialize()`\") {\n\t\t\tt.Errorf(\"expected to contain %s\", \"`initialize()`\")\n\t\t}\n\t}\n}\n\nfunc Test_CodeWithBackticksInContent(t *testing.T) {\n\t// Inline code containing backtick characters is properly escaped\n\tresult, err := htmd.Convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `backtick`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `backtick`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisMarkHighlight(t *testing.T) {\n\t// mark tag produces highlighted output\n\tresult, err := htmd.Convert(`<p><mark>highlighted</mark></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `highlighted`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `highlighted`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisStrikethroughDel(t *testing.T) {\n\t// del tag converts to GFM strikethrough\n\tresult, err := htmd.Convert(`<p><del>deleted text</del></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `~~deleted text~~`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `~~deleted text~~`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisStrikethroughS(t *testing.T) {\n\t// s tag converts to GFM strikethrough\n\tresult, err := htmd.Convert(`<p><s>strikethrough</s></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `~~strikethrough~~`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `~~strikethrough~~`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisSubscript(t *testing.T) {\n\t// sub tag content is preserved\n\tresult, err := htmd.Convert(`<p>H<sub>2</sub>O</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `H`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `H`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `O`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `O`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisSuperscript(t *testing.T) {\n\t// sup tag content is preserved\n\tresult, err := htmd.Convert(`<p>x<sup>2</sup></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `x`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `x`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `2`)\n\t\t}\n\t}\n}\n\nfunc Test_EmphasisUnderlineU(t *testing.T) {\n\t// u tag content is preserved in output\n\tresult, err := htmd.Convert(`<p><u>underlined</u></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `underlined`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `underlined`)\n\t\t}\n\t}\n}\n\nfunc Test_FormInputElements(t *testing.T) {\n\t// Form input elements produce readable output without form mechanics\n\tresult, err := htmd.Convert(`<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Name`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Name`)\n\t\t}\n\t}\n}\n\nfunc Test_FormSelectOptions(t *testing.T) {\n\t// Select element with options produces readable output\n\tresult, err := htmd.Convert(`<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Color`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Color`)\n\t\t}\n\t}\n}\n\nfunc Test_FormTextarea(t *testing.T) {\n\t// Textarea element produces readable output\n\tresult, err := htmd.Convert(`<form><label>Message:</label><textarea>Default text content</textarea></form>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Message`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Message`)\n\t\t}\n\t}\n}\n\nfunc Test_HeadingH1(t *testing.T) {\n\t// H1 heading\n\tresult, err := htmd.Convert(`<h1>Heading 1</h1>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `# Heading 1` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_HeadingH2(t *testing.T) {\n\t// H2 heading\n\tresult, err := htmd.Convert(`<h2>Heading 2</h2>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `## Heading 2` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_HeadingH3(t *testing.T) {\n\t// H3 heading\n\tresult, err := htmd.Convert(`<h3>Heading 3</h3>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `### Heading 3` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_HeadingH4(t *testing.T) {\n\t// H4 heading\n\tresult, err := htmd.Convert(`<h4>Heading 4</h4>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `#### Heading 4` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_HeadingH5(t *testing.T) {\n\t// H5 heading\n\tresult, err := htmd.Convert(`<h5>Heading 5</h5>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `##### Heading 5` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_HeadingH6(t *testing.T) {\n\t// H6 heading\n\tresult, err := htmd.Convert(`<h6>Heading 6</h6>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `###### Heading 6` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_ImageFigureFigcaption(t *testing.T) {\n\t// Figure with figcaption preserves both image and caption\n\tresult, err := htmd.Convert(`<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![A sunset](sunset.jpg)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![A sunset](sunset.jpg)`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Beautiful sunset over the ocean`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Beautiful sunset over the ocean`)\n\t\t}\n\t}\n}\n\nfunc Test_ImageLinked(t *testing.T) {\n\t// Image inside an anchor produces a linked image\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![Icon](icon.png)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![Icon](icon.png)`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `https://example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `https://example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_ImageNoAlt(t *testing.T) {\n\t// Image without alt text produces image markdown\n\tresult, err := htmd.Convert(`<img src=\"banner.jpg\">`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `banner.jpg`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `banner.jpg`)\n\t\t}\n\t}\n}\n\nfunc Test_ImageSimple(t *testing.T) {\n\t// Image with alt text\n\tresult, err := htmd.Convert(`<img src=\"photo.jpg\" alt=\"A photo\">`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![A photo](photo.jpg)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![A photo](photo.jpg)`)\n\t\t}\n\t}\n}\n\nfunc Test_ImageWithTitle(t *testing.T) {\n\t// Image with title attribute includes title in output\n\tresult, err := htmd.Convert(`<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![Sales chart](chart.png`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![Sales chart](chart.png`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Q3 Sales`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Q3 Sales`)\n\t\t}\n\t}\n}\n\nfunc Test_InlineCode(t *testing.T) {\n\t// Inline code\n\tresult, err := htmd.Convert(`<p>Use <code>console.log()</code> to debug</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), \"`console.log()`\") {\n\t\t\tt.Errorf(\"expected to contain %s\", \"`console.log()`\")\n\t\t}\n\t}\n}\n\nfunc Test_ItalicEm(t *testing.T) {\n\t// Em tag converts to italic\n\tresult, err := htmd.Convert(`<p><em>italic</em></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `*italic*`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `*italic*`)\n\t\t}\n\t}\n}\n\nfunc Test_LineBreakBrTag(t *testing.T) {\n\t// Single br tag produces a line break in output\n\tresult, err := htmd.Convert(`<p>First line.<br>Second line.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First line.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First line.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second line.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second line.`)\n\t\t}\n\t}\n}\n\nfunc Test_LineBreakHrTag(t *testing.T) {\n\t// hr tag produces a horizontal separator between content\n\tresult, err := htmd.Convert(`<p>Before rule.</p><hr><p>After rule.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Before rule.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Before rule.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `After rule.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `After rule.`)\n\t\t}\n\t}\n}\n\nfunc Test_LineBreakMultipleBr(t *testing.T) {\n\t// Multiple consecutive br tags in sequence\n\tresult, err := htmd.Convert(`<p>Start.<br><br>End.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Start.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Start.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `End.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `End.`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkAnchorFragment(t *testing.T) {\n\t// Fragment-only anchor link is preserved\n\tresult, err := htmd.Convert(`<a href=\"#section\">Jump to section</a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `[Jump to section](#section)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `[Jump to section](#section)`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkEmptyHref(t *testing.T) {\n\t// Link with empty href produces output with the link text\n\tresult, err := htmd.Convert(`<a href=\"\">No destination</a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `No destination`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `No destination`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkImageInside(t *testing.T) {\n\t// Image inside a link produces a linked image\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![Logo](logo.png)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![Logo](logo.png)`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `https://example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `https://example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkMailto(t *testing.T) {\n\t// Mailto link is preserved with mailto: scheme\n\tresult, err := htmd.Convert(`<a href=\"mailto:user@example.com\">Email us</a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `mailto:user@example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `mailto:user@example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkSimple(t *testing.T) {\n\t// Simple link\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\">Example</a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `[Example](https://example.com)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `[Example](https://example.com)`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkWithBoldText(t *testing.T) {\n\t// Link containing bold text preserves formatting\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\"><strong>Bold link</strong></a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `**Bold link**`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `**Bold link**`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `https://example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `https://example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_LinkWithTitle(t *testing.T) {\n\t// Link with title attribute\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\" title=\"Example Site\">Example</a>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `[Example](https://example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `[Example](https://example.com`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Example Site`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Example Site`)\n\t\t}\n\t}\n}\n\nfunc Test_ListDefinitionDl(t *testing.T) {\n\t// Definition list with dt and dd elements\n\tresult, err := htmd.Convert(`<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Term One`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Term One`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Definition of term one.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Definition of term one.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Term Two`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Term Two`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Definition of term two.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Definition of term two.`)\n\t\t}\n\t}\n}\n\nfunc Test_ListItemMultipleParagraphs(t *testing.T) {\n\t// List item containing multiple paragraphs\n\tresult, err := htmd.Convert(`<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First paragraph in item.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First paragraph in item.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second paragraph in item.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second paragraph in item.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Simple item`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Simple item`)\n\t\t}\n\t}\n}\n\nfunc Test_ListMixedNested(t *testing.T) {\n\t// Mixed list: ordered list nested inside unordered list\n\tresult, err := htmd.Convert(`<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Item A`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Item A`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Sub 1`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Sub 1`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Sub 2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Sub 2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Item B`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Item B`)\n\t\t}\n\t}\n}\n\nfunc Test_ListNestedOrdered(t *testing.T) {\n\t// Nested ordered list with two levels of depth\n\tresult, err := htmd.Convert(`<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Step 1`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Step 1`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Step 1a`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Step 1a`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Step 1b`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Step 1b`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Step 2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Step 2`)\n\t\t}\n\t}\n}\n\nfunc Test_ListNestedUnordered(t *testing.T) {\n\t// Nested unordered list with two levels of depth\n\tresult, err := htmd.Convert(`<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Parent A`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Parent A`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Child A1`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Child A1`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Child A2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Child A2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Parent B`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Parent B`)\n\t\t}\n\t}\n}\n\nfunc Test_ListTaskCheckboxes(t *testing.T) {\n\t// Task list with checked and unchecked checkboxes\n\tresult, err := htmd.Convert(`<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Done task`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Done task`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Pending task`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Pending task`)\n\t\t}\n\t}\n}\n\nfunc Test_OrderedList(t *testing.T) {\n\t// Ordered list\n\tresult, err := htmd.Convert(`<ol><li>First</li><li>Second</li><li>Third</li></ol>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `1. First`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `1. First`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `2. Second`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `2. Second`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `3. Third`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `3. Third`)\n\t\t}\n\t}\n}\n\nfunc Test_ParagraphMultiple(t *testing.T) {\n\t// Multiple paragraphs are separated by a blank line\n\tresult, err := htmd.Convert(`<p>First paragraph.</p><p>Second paragraph.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First paragraph.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second paragraph.`)\n\t\t}\n\t}\n}\n\nfunc Test_ParagraphNestedDivs(t *testing.T) {\n\t// Text nested inside divs is extracted correctly\n\tresult, err := htmd.Convert(`<div><div><p>Nested text</p></div></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Nested text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Nested text`)\n\t\t}\n\t}\n}\n\nfunc Test_ParagraphSimple(t *testing.T) {\n\t// Simple paragraph converts to plain text\n\tresult, err := htmd.Convert(`<p>Hello World</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `Hello World` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_ParagraphWithInlineFormatting(t *testing.T) {\n\t// Paragraph with bold, italic, and a link\n\tresult, err := htmd.Convert(`<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `**bold**`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `**bold**`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `*italic*`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `*italic*`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `[link](https://example.com)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `[link](https://example.com)`)\n\t\t}\n\t}\n}\n\nfunc Test_ParagraphWithLineBreaks(t *testing.T) {\n\t// Paragraph with br tags produces line breaks in output\n\tresult, err := htmd.Convert(`<p>Line one.<br>Line two.<br>Line three.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line one.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line one.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line two.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line two.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line three.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line three.`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticAbbr(t *testing.T) {\n\t// Abbreviation element text is preserved\n\tresult, err := htmd.Convert(`<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `WWW`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `WWW`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticArticle(t *testing.T) {\n\t// Article element wrapping content preserves inner content\n\tresult, err := htmd.Convert(`<article><h2>Article Title</h2><p>Article body.</p></article>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Article Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Article Title`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Article body.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Article body.`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticDefinitionList(t *testing.T) {\n\t// Definition list with term and description\n\tresult, err := htmd.Convert(`<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `HTML`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `HTML`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `HyperText Markup Language`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `HyperText Markup Language`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `CSS`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `CSS`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Cascading Style Sheets`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Cascading Style Sheets`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticDetailsSummary(t *testing.T) {\n\t// Details and summary elements produce readable output\n\tresult, err := htmd.Convert(`<details><summary>Click to expand</summary><p>Hidden content here.</p></details>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Click to expand`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Click to expand`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticHr(t *testing.T) {\n\t// Horizontal rule produces a separator in output\n\tresult, err := htmd.Convert(`<p>Above</p><hr><p>Below</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Above`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Above`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Below`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Below`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticMarkHighlight(t *testing.T) {\n\t// Mark tag produces highlighted output\n\tresult, err := htmd.Convert(`<p>This is <mark>highlighted text</mark> in a sentence.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `highlighted text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `highlighted text`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticSectionWithHeading(t *testing.T) {\n\t// Section element with heading preserves structure\n\tresult, err := htmd.Convert(`<section><h3>Section Heading</h3><p>Section content.</p></section>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Section Heading`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Section Heading`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Section content.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Section content.`)\n\t\t}\n\t}\n}\n\nfunc Test_SemanticSubSuperscript(t *testing.T) {\n\t// Subscript and superscript elements are preserved in output\n\tresult, err := htmd.Convert(`<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `H`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `H`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `O`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `O`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `E=mc`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `E=mc`)\n\t\t}\n\t}\n}\n\nfunc Test_SimpleTable(t *testing.T) {\n\t// Simple table with header\n\tresult, err := htmd.Convert(`<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Name`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Name`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Age`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Age`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Alice`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Alice`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `30`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `30`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `|`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `|`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `---`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `---`)\n\t\t}\n\t}\n}\n\nfunc Test_TableEmpty(t *testing.T) {\n\t// Empty table produces no output or minimal output\n\tresult, err := htmd.Convert(`<table></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_TableNoThead(t *testing.T) {\n\t// Table without thead uses first row as implied header\n\tresult, err := htmd.Convert(`<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Product`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Product`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Price`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Price`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Apple`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Apple`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `1.00`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `1.00`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `|`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `|`)\n\t\t}\n\t}\n}\n\nfunc Test_TablePipeCharsInContent(t *testing.T) {\n\t// Table cells containing pipe characters are escaped in output\n\tresult, err := htmd.Convert(`<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Expression`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Expression`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Result`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Result`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `true`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `true`)\n\t\t}\n\t}\n}\n\nfunc Test_TableWithAlignment(t *testing.T) {\n\t// Table with column alignment attributes\n\tresult, err := htmd.Convert(`<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Left`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Left`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Center`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Center`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Right`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Right`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `L`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `L`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `C`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `C`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `R`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `R`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `|`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `|`)\n\t\t}\n\t}\n}\n\nfunc Test_TableWithColspan(t *testing.T) {\n\t// Table with colspan attribute in a header cell\n\tresult, err := htmd.Convert(`<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Full Name`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Full Name`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `John`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `John`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Doe`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Doe`)\n\t\t}\n\t}\n}\n\nfunc Test_UnorderedList(t *testing.T) {\n\t// Unordered list\n\tresult, err := htmd.Convert(`<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `- Item 1`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `- Item 1`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `- Item 2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `- Item 2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `- Item 3`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `- Item 3`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "e2e/go/edge_cases_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:fd167d6dba2bf5e6b14de70da54c31cedec315f9b1796ab5f0c940ef26fbf1d7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: edge-cases\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\ntype testVisitorVisitorCustomElementWithNesting struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomElementWithNesting) VisitCustomElement(_ htmd.NodeContext, tagName string, html string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`[CUSTOM WIDGET]`)\n}\n\ntype testVisitorVisitorDeeplyNestedSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDeeplyNestedSkip) VisitMark(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorElementEndModification struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorElementEndModification) VisitElementEnd(_ htmd.NodeContext, output string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`MODIFIED OUTPUT`)\n}\n\ntype testVisitorVisitorElementStartSkipEntireSubtree struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorElementStartSkipEntireSubtree) VisitElementStart(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorUnknownTagPreservation struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorUnknownTagPreservation) VisitCustomElement(_ htmd.NodeContext, tagName string, html string) htmd.VisitResult {\n\treturn htmd.VisitResultPreserveHTML()\n}\n\nfunc Test_EmptyHtml(t *testing.T) {\n\t// Empty HTML document\n\tresult, err := htmd.Convert(`<html><head></head><body></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_EncodingCjkCharacters(t *testing.T) {\n\t// CJK (Chinese, Japanese, Korean) characters are preserved\n\tresult, err := htmd.Convert(`<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `中文内容`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `中文内容`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `日本語テキスト`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `日本語テキスト`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `한국어 텍스트`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `한국어 텍스트`)\n\t\t}\n\t}\n}\n\nfunc Test_EncodingHtmlEntities(t *testing.T) {\n\t// Common HTML entities are decoded in output\n\tresult, err := htmd.Convert(`<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `&`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `&`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `<`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `<`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `>`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `>`)\n\t\t}\n\t}\n}\n\nfunc Test_EncodingNamedEntities(t *testing.T) {\n\t// Named HTML entities like &mdash; and &hellip; are decoded\n\tresult, err := htmd.Convert(`<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `—`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `—`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `…`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `…`)\n\t\t}\n\t}\n}\n\nfunc Test_EncodingNumericEntities(t *testing.T) {\n\t// Numeric HTML entities (decimal and hex) are decoded\n\tresult, err := htmd.Convert(`<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `©`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `©`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `®`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `®`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `€`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `€`)\n\t\t}\n\t}\n}\n\nfunc Test_EncodingUnicodeEmoji(t *testing.T) {\n\t// Emoji and Unicode characters are preserved\n\tresult, err := htmd.Convert(`<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `🌍`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `🌍`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `🚀`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `🚀`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `⭐`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `⭐`)\n\t\t}\n\t}\n}\n\nfunc Test_HtmlCommentsOnly(t *testing.T) {\n\t// Document containing only HTML comments produces empty output\n\tresult, err := htmd.Convert(`<!-- This is a comment --><!-- Another comment -->`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_JustWhitespaceInput(t *testing.T) {\n\t// Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\n\tresult, err := htmd.Convert(`   `, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_MalformedDeeplyNestedElements(t *testing.T) {\n\t// Deeply nested elements (100 levels) are handled without stack overflow\n\tresult, err := htmd.Convert(`<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Deeply nested content`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Deeply nested content`)\n\t\t}\n\t}\n}\n\nfunc Test_MalformedMissingBlockClosingTags(t *testing.T) {\n\t// Missing closing tags on block elements are auto-closed by parser\n\tresult, err := htmd.Convert(`<div><h1>Title<p>First paragraph<p>Second paragraph</div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Title`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First paragraph`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First paragraph`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second paragraph`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second paragraph`)\n\t\t}\n\t}\n}\n\nfunc Test_MalformedOverlappingTags(t *testing.T) {\n\t// Overlapping bold/italic tags are recovered by the HTML parser without panic\n\tresult, err := htmd.Convert(`<p><b><i>bold and italic</b></i></p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `bold and italic`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `bold and italic`)\n\t\t}\n\t}\n}\n\nfunc Test_MalformedUnclosedParagraph(t *testing.T) {\n\t// Unclosed <p> tag is recovered gracefully and content is preserved\n\tresult, err := htmd.Convert(`<p>This paragraph is never closed`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `This paragraph is never closed`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `This paragraph is never closed`)\n\t\t}\n\t}\n}\n\nfunc Test_ScriptTagsOnly(t *testing.T) {\n\t// Document with only script tags produces empty output (scripts are stripped)\n\tresult, err := htmd.Convert(`<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_StyleTagsOnly(t *testing.T) {\n\t// Document with only style tags produces empty output (styles are stripped)\n\tresult, err := htmd.Convert(`<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_VisitorCustomElementWithNesting(t *testing.T) {\n\t// Visitor handles custom elements with nested content\n\tvisitor := &testVisitorVisitorCustomElementWithNesting{}\n\tresult, err := htmd.Convert(`<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[CUSTOM WIDGET]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[CUSTOM WIDGET]`, content)\n\t}\n\tif strings.Contains(string(content), `Widget content here`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Widget content here`, content)\n\t}\n}\n\nfunc Test_VisitorDeeplyNestedSkip(t *testing.T) {\n\t// Visitor skips deeply nested elements\n\tvisitor := &testVisitorVisitorDeeplyNestedSkip{}\n\tresult, err := htmd.Convert(`<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Outer`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Outer`, content)\n\t}\n\tif !strings.Contains(string(content), `text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `text`, content)\n\t}\n\tif strings.Contains(string(content), `highlight`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `highlight`, content)\n\t}\n}\n\nfunc Test_VisitorElementEndModification(t *testing.T) {\n\t// Visitor modifies element at end after children processed\n\tvisitor := &testVisitorVisitorElementEndModification{}\n\tresult, err := htmd.Convert(`<blockquote><p>Original quote</p></blockquote>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n}\n\nfunc Test_VisitorElementStartSkipEntireSubtree(t *testing.T) {\n\t// Visitor skips at element_start level removes entire subtree\n\tvisitor := &testVisitorVisitorElementStartSkipEntireSubtree{}\n\tresult, err := htmd.Convert(`<div><h1>Title</h1><p>Content</p></div>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n}\n\nfunc Test_VisitorUnknownTagPreservation(t *testing.T) {\n\t// Visitor preserves unknown HTML tags as raw HTML\n\tvisitor := &testVisitorVisitorUnknownTagPreservation{}\n\tresult, err := htmd.Convert(`<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Article text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Article text`, content)\n\t}\n\tif !strings.Contains(string(content), `More article text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `More article text`, content)\n\t}\n\tif !strings.Contains(string(content), `<x-custom>`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `<x-custom>`, content)\n\t}\n}\n\nfunc Test_WhitespaceOnly(t *testing.T) {\n\t// Whitespace-only content\n\tresult, err := htmd.Convert(`<p>   </p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_XssOnclickHandlerRemoved(t *testing.T) {\n\t// onclick and other on* event handlers are removed from elements\n\tresult, err := htmd.Convert(`<p><a href=\"https://example.com\" onclick=\"alert('xss')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Click me`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Click me`)\n\t\t}\n\t}\n}\n\nfunc Test_XssScriptTagStripped(t *testing.T) {\n\t// Script tag content is stripped and does not appear in output\n\tresult, err := htmd.Convert(`<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Safe content`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Safe content`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `More safe content`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `More safe content`)\n\t\t}\n\t}\n}\n\nfunc Test_XssSvgNestedScriptStripped(t *testing.T) {\n\t// Script tags nested inside SVG are stripped\n\tresult, err := htmd.Convert(`<p>Before SVG.</p><svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Before SVG`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Before SVG`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `After SVG`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `After SVG`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "e2e/go/go.mod",
    "content": "module e2e_go\n\ngo 1.26\n\nrequire (\n\tgithub.com/kreuzberg-dev/html-to-markdown/packages/go/v3 v3.0.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nreplace github.com/kreuzberg-dev/html-to-markdown/packages/go/v3 => ../../packages/go\n"
  },
  {
    "path": "e2e/go/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "e2e/go/metadata_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:3ae3c19d6d170e491777b9b60eea36af4dbd061db20c80e759dd0b05ada92dde\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: metadata\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_MetadataAuthorMeta(t *testing.T) {\n\t// Extract author from <meta name='author'> tag\n\tresult, err := htmd.Convert(`<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar metadataDocumentAuthor string\n\tif result.Metadata.Document.Author != nil {\n\t\tmetadataDocumentAuthor = *result.Metadata.Document.Author\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentAuthor) != `Jane Doe` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentAuthor)\n\t}\n}\n\nfunc Test_MetadataCanonicalUrl(t *testing.T) {\n\t// Extract canonical URL from <link rel='canonical'> tag\n\tresult, err := htmd.Convert(`<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar metadataDocumentCanonicalURL string\n\tif result.Metadata.Document.CanonicalURL != nil {\n\t\tmetadataDocumentCanonicalURL = *result.Metadata.Document.CanonicalURL\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentCanonicalURL) != `https://example.com/canonical-page` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentCanonicalURL)\n\t}\n}\n\nfunc Test_MetadataDescriptionMeta(t *testing.T) {\n\t// Extract description from <meta name='description'> tag\n\tresult, err := htmd.Convert(`<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar metadataDocumentDescription string\n\tif result.Metadata.Document.Description != nil {\n\t\tmetadataDocumentDescription = *result.Metadata.Document.Description\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentDescription) != `This is the page description.` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentDescription)\n\t}\n}\n\nfunc Test_MetadataDublinCore(t *testing.T) {\n\t// Extract Dublin Core metadata tags\n\tresult, err := htmd.Convert(`<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `scholarly article`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `scholarly article`, content)\n\t}\n}\n\nfunc Test_MetadataExtractAllImages(t *testing.T) {\n\t// Extract all images from a document into metadata\n\tresult, err := htmd.Convert(`<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.GreaterOrEqual(t, len(result.Metadata.Images), 2, \"expected at least 2 elements\")\n}\n\nfunc Test_MetadataExtractAllLinks(t *testing.T) {\n\t// Extract all links from a document into metadata\n\tresult, err := htmd.Convert(`<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.GreaterOrEqual(t, len(result.Metadata.Links), 2, \"expected at least 2 elements\")\n}\n\nfunc Test_MetadataHeadersHierarchy(t *testing.T) {\n\t// Extract heading hierarchy from document into metadata\n\tresult, err := htmd.Convert(`<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.GreaterOrEqual(t, len(result.Metadata.Headers), 5, \"expected at least 5 elements\")\n}\n\nfunc Test_MetadataKeywordsMeta(t *testing.T) {\n\t// Extract keywords from <meta name='keywords'> tag\n\tresult, err := htmd.Convert(`<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.GreaterOrEqual(t, len(result.Metadata.Document.Keywords), 1, \"expected at least 1 elements\")\n}\n\nfunc Test_MetadataLangAttribute(t *testing.T) {\n\t// Extract language from html lang attribute\n\tresult, err := htmd.Convert(`<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `Hola Mundo`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Hola Mundo`, content)\n\t}\n}\n\nfunc Test_MetadataMicrodataSchemaArticle(t *testing.T) {\n\t// Extract schema.org microdata for Article\n\tresult, err := htmd.Convert(`<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `Breaking News Today`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Breaking News Today`, content)\n\t}\n\tif !strings.Contains(string(content), `Jane Reporter`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Jane Reporter`, content)\n\t}\n\tif !strings.Contains(string(content), `important information`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `important information`, content)\n\t}\n}\n\nfunc Test_MetadataMicrodataSchemaBreadcrumb(t *testing.T) {\n\t// Extract schema.org breadcrumb navigation microdata\n\tresult, err := htmd.Convert(`<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `Home`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Home`, content)\n\t}\n\tif !strings.Contains(string(content), `Products`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Products`, content)\n\t}\n\tif !strings.Contains(string(content), `Current Page`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Current Page`, content)\n\t}\n}\n\nfunc Test_MetadataMicrodataSchemaOrganization(t *testing.T) {\n\t// Extract schema.org microdata for Organization\n\tresult, err := htmd.Convert(`<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `Acme Corp`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Acme Corp`, content)\n\t}\n\tif !strings.Contains(string(content), `2020`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `2020`, content)\n\t}\n}\n\nfunc Test_MetadataMicrodataSchemaPerson(t *testing.T) {\n\t// Extract schema.org microdata for Person\n\tresult, err := htmd.Convert(`<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `John Smith`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `John Smith`, content)\n\t}\n\tif !strings.Contains(string(content), `john@example.com`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `john@example.com`, content)\n\t}\n}\n\nfunc Test_MetadataMicrodataSchemaProduct(t *testing.T) {\n\t// Extract schema.org microdata for Product\n\tresult, err := htmd.Convert(`<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `Awesome Widget`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Awesome Widget`, content)\n\t}\n\tif !strings.Contains(string(content), `best widget`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `best widget`, content)\n\t}\n\tif !strings.Contains(string(content), `29.99`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `29.99`, content)\n\t}\n}\n\nfunc Test_MetadataTextDirectionLtr(t *testing.T) {\n\t// Extract text direction from lang attribute on html element\n\tresult, err := htmd.Convert(`<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `left-to-right text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `left-to-right text`, content)\n\t}\n}\n\nfunc Test_MetadataTextDirectionRtl(t *testing.T) {\n\t// Extract right-to-left text direction\n\tresult, err := htmd.Convert(`<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif !strings.Contains(string(content), `right-to-left text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `right-to-left text`, content)\n\t}\n}\n\nfunc Test_MetadataTitleTag(t *testing.T) {\n\t// Extract title from <title> tag\n\tresult, err := htmd.Convert(`<html><head><title>My Page</title></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar metadataDocumentTitle string\n\tif result.Metadata.Document.Title != nil {\n\t\tmetadataDocumentTitle = *result.Metadata.Document.Title\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentTitle) != `My Page` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentTitle)\n\t}\n}\n\nfunc Test_OgBasicTags(t *testing.T) {\n\t// Extract og:title, og:description, and og:image from Open Graph meta tags\n\tresult, err := htmd.Convert(`<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tmetadataDocumentOpenGraphTitle := result.Metadata.Document.OpenGraph[\"title\"]\n\tmetadataDocumentOpenGraphDescription := result.Metadata.Document.OpenGraph[\"description\"]\n\tmetadataDocumentOpenGraphImage := result.Metadata.Document.OpenGraph[\"image\"]\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphTitle) != `OG Title` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphTitle)\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphDescription) != `OG description text.` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphDescription)\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphImage) != `https://example.com/image.jpg` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphImage)\n\t}\n}\n\nfunc Test_OgMultipleTags(t *testing.T) {\n\t// Extract multiple Open Graph tags including type, url, and site_name\n\tresult, err := htmd.Convert(`<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tmetadataDocumentOpenGraphTitle := result.Metadata.Document.OpenGraph[\"title\"]\n\tmetadataDocumentOpenGraphType := result.Metadata.Document.OpenGraph[\"type\"]\n\tmetadataDocumentOpenGraphURL := result.Metadata.Document.OpenGraph[\"url\"]\n\tmetadataDocumentOpenGraphSiteName := result.Metadata.Document.OpenGraph[\"site_name\"]\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphTitle) != `Article Title` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphTitle)\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphType) != `article` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphType)\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphURL) != `https://example.com/article` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphURL)\n\t}\n\tif strings.TrimSpace(metadataDocumentOpenGraphSiteName) != `Example Site` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentOpenGraphSiteName)\n\t}\n}\n\nfunc Test_StructuredDataJsonLd(t *testing.T) {\n\t// JSON-LD script tag is stripped from output (security) but metadata may be extracted\n\tresult, err := htmd.Convert(`<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `My Article`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `My Article`)\n\t\t}\n\t}\n}\n\nfunc Test_StructuredDataMultipleJsonLd(t *testing.T) {\n\t// Multiple JSON-LD blocks are all stripped from output\n\tresult, err := htmd.Convert(`<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Widget`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Widget`)\n\t\t}\n\t}\n}\n\nfunc Test_TwitterCardTags(t *testing.T) {\n\t// Extract Twitter card meta tags\n\tresult, err := htmd.Convert(`<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tmetadataDocumentTwitterCardCard := result.Metadata.Document.TwitterCard[\"card\"]\n\tmetadataDocumentTwitterCardTitle := result.Metadata.Document.TwitterCard[\"title\"]\n\tmetadataDocumentTwitterCardDescription := result.Metadata.Document.TwitterCard[\"description\"]\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentTwitterCardCard) != `summary_large_image` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentTwitterCardCard)\n\t}\n\tif strings.TrimSpace(metadataDocumentTwitterCardTitle) != `Twitter Card Title` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentTwitterCardTitle)\n\t}\n\tif strings.TrimSpace(metadataDocumentTwitterCardDescription) != `Twitter card description.` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentTwitterCardDescription)\n\t}\n}\n"
  },
  {
    "path": "e2e/go/options_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d8a45812c519511e3b9e75e32257ad2a533d8cf224582c959e3948d27fb7321e\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: options\npackage e2e_test\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_OptionsAutolinksFalse(t *testing.T) {\n\t// Bare URL links rendered as regular markdown links when autolinks disabled\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"autolinks\":false}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p><a href='https://example.com'>https://example.com</a></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsBrInTablesFalse(t *testing.T) {\n\t// BR elements in table cells are stripped when disabled\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"br_in_tables\":false}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Col`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Col`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsBrInTablesTrue(t *testing.T) {\n\t// BR elements in table cells render as line breaks\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"br_in_tables\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Header`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Header`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line 1`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line 1`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line 2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line 2`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsCodeBlockBackticks(t *testing.T) {\n\t// Backticks code block style uses triple backtick fences\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"code_block_style\":\"backticks\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<pre><code class=\"language-js\">console.log('hi');</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), \"```\") {\n\t\t\tt.Errorf(\"expected to contain %s\", \"```\")\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `console.log('hi');`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `console.log('hi');`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsCodeBlockIndented(t *testing.T) {\n\t// Code blocks use 4-space indentation\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"code_block_style\":\"indented\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<pre><code>print('hello')</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `print('hello')`) {\n\t\tt.Errorf(\"expected to contain %s\", `print('hello')`)\n\t}\n\tif strings.Contains(string(content), \"```\") {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", \"```\", content)\n\t}\n}\n\nfunc Test_OptionsCodeBlockTildes(t *testing.T) {\n\t// Code blocks use tilde fences\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"code_block_style\":\"tildes\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<pre><code>let x = 1;</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `~~~`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `~~~`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `let x = 1;`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `let x = 1;`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsCodeBlockTildesStyle(t *testing.T) {\n\t// Tildes code block style uses triple tilde fences\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"code_block_style\":\"tildes\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<pre><code>some code</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `~~~`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `~~~`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `some code`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `some code`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsCodeLanguagePython(t *testing.T) {\n\t// Default code language annotation on blocks without lang attribute\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"code_language\":\"python\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<pre><code>def hello(): pass</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), \"```python\") {\n\t\t\tt.Errorf(\"expected to contain %s\", \"```python\")\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `def hello`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `def hello`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsConvertAsInline(t *testing.T) {\n\t// Block elements treated as inline\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"convert_as_inline\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>One</p><p>Two</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `One`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `One`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Two`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Two`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsDebugTrue(t *testing.T) {\n\t// Debug mode enabled does not crash and produces output\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"debug\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Debug test</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Debug test`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Debug test`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsDefaultTitleTrue(t *testing.T) {\n\t// Links without title get empty title attribute when defaultTitle is true\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"default_title\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p><a href='https://example.com'>Link</a></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Link`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Link`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `https://example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `https://example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsEncodingUtf8(t *testing.T) {\n\t// UTF-8 encoding hint for special characters\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"encoding\":\"utf-8\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Café naïve résumé</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n}\n\nfunc Test_OptionsEscapeAsciiEnabled(t *testing.T) {\n\t// ASCII Markdown characters are escaped when escapeAscii is true\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"escape_ascii\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text with # hash and [brackets] and * star</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Text`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `hash`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `hash`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `brackets`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `brackets`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `star`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `star`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsEscapeAsterisks(t *testing.T) {\n\t// escape_asterisks option escapes asterisks in plain text\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"escape_asterisks\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Use 2*3 = 6 in math.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `2`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `2`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `3`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `3`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `6`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `6`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsEscapeMisc(t *testing.T) {\n\t// escape_misc option escapes miscellaneous markdown characters\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"escape_misc\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Use # and | and ~ in text.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Use`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Use`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `and`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `and`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `in text.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `in text.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsEscapeUnderscores(t *testing.T) {\n\t// escape_underscores option escapes underscores in plain text\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"escape_underscores\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>The variable_name is defined.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `variable`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `variable`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `name`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `name`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `defined.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `defined.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsAttribute(t *testing.T) {\n\t// Elements matching CSS attribute selector are excluded entirely\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\"[role='complementary']\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Primary text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Primary text`, content)\n\t}\n\tif strings.Contains(string(content), `Sidebar`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Sidebar`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsClass(t *testing.T) {\n\t// Elements matching CSS class selector are excluded entirely\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\".cookie-banner\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Main content`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Main content`, content)\n\t}\n\tif strings.Contains(string(content), `cookies`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `cookies`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsEmptyNoop(t *testing.T) {\n\t// Empty exclude_selectors list does not affect output\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Hello world</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Hello world`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Hello world`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsId(t *testing.T) {\n\t// Elements matching CSS id selector are excluded entirely\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\"#ad-container\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Article text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Article text`, content)\n\t}\n\tif strings.Contains(string(content), `Buy stuff`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Buy stuff`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsMultiple(t *testing.T) {\n\t// Multiple CSS selectors each exclude their matched elements\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\".nav\",\"footer\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Content`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Content`, content)\n\t}\n\tif strings.Contains(string(content), `Menu`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Menu`, content)\n\t}\n\tif strings.Contains(string(content), `Footer`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Footer`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsNestedContentDropped(t *testing.T) {\n\t// All descendants of excluded elements are dropped\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\".sidebar\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Main text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Main text`, content)\n\t}\n\tif strings.Contains(string(content), `Related`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Related`, content)\n\t}\n\tif strings.Contains(string(content), `Sidebar text`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Sidebar text`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsPlainTextMode(t *testing.T) {\n\t// Exclude selectors work in plain text output mode\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\".nav\"],\"output_format\":\"plain\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><div class=\"nav\">Navigation</div><p>Article body</p></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Article body`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Article body`, content)\n\t}\n\tif strings.Contains(string(content), `Navigation`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Navigation`, content)\n\t}\n}\n\nfunc Test_OptionsExcludeSelectorsVsStripTags(t *testing.T) {\n\t// exclude_selectors drops entire subtree unlike strip_tags which keeps children\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"exclude_selectors\":[\".wrapper\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Outer text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Outer text`, content)\n\t}\n\tif strings.Contains(string(content), `Inner paragraph`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Inner paragraph`, content)\n\t}\n}\n\nfunc Test_OptionsExtractMetadataTrue(t *testing.T) {\n\t// Extract metadata returns document metadata when enabled\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"extract_metadata\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar metadataDocumentTitle string\n\tif result.Metadata.Document.Title != nil {\n\t\tmetadataDocumentTitle = *result.Metadata.Document.Title\n\t}\n\tvar metadataDocumentDescription string\n\tif result.Metadata.Document.Description != nil {\n\t\tmetadataDocumentDescription = *result.Metadata.Document.Description\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif strings.TrimSpace(metadataDocumentTitle) != `Test Page` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentTitle)\n\t}\n\tif strings.TrimSpace(metadataDocumentDescription) != `A test page` {\n\t\tt.Errorf(\"equals mismatch: got %v\", metadataDocumentDescription)\n\t}\n}\n\nfunc Test_OptionsHeadingStyleAtx(t *testing.T) {\n\t// ATX heading style produces hash-prefixed headings\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"heading_style\":\"atx\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Title</h1><h2>Subtitle</h2>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Title`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## Subtitle`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## Subtitle`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsHeadingStyleAtxClosed(t *testing.T) {\n\t// ATX closed heading style adds closing hashes\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"heading_style\":\"atx_closed\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Closed Heading</h1>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Closed Heading #`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Closed Heading #`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsHeadingStyleUnderlined(t *testing.T) {\n\t// Underlined heading style produces setext-style headings for h1 and h2\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"heading_style\":\"underlined\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Main Title</h1>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Main Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Main Title`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsHighlightBold(t *testing.T) {\n\t// Mark tag rendered as bold\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"highlight_style\":\"bold\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text with <mark>highlighted</mark> text.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `**highlighted**`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `**highlighted**`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsHighlightDoubleEqual(t *testing.T) {\n\t// Mark tag with double equal highlight style\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"highlight_style\":\"double_equal\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text with <mark>highlighted</mark> here.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `==highlighted==`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `==highlighted==`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsHighlightNone(t *testing.T) {\n\t// Mark tag with no highlight style strips the mark\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"highlight_style\":\"none\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text with <mark>plain</mark> content.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `plain`) {\n\t\tt.Errorf(\"expected to contain %s\", `plain`)\n\t}\n\tif strings.Contains(string(content), `==`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `==`, content)\n\t}\n}\n\nfunc Test_OptionsKeepInlineImagesInParagraph(t *testing.T) {\n\t// Images inside specified tags stay inline\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"keep_inline_images_in\":[\"p\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text <img src='icon.png' alt='icon'> more text</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Text`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `more text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `more text`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsLinkStyleReference(t *testing.T) {\n\t// Links use reference-style formatting\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"link_style\":\"reference\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Example`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Example`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Other`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Other`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `example.com`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `example.com`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsListCustomBullets(t *testing.T) {\n\t// Custom bullet character for unordered lists\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"bullets\":\"*\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<ul><li>Item A</li><li>Item B</li></ul>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `* Item A`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `* Item A`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `* Item B`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `* Item B`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsListIndentTabs(t *testing.T) {\n\t// Tab indentation type for nested list items\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"list_indent_type\":\"tabs\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<ul><li>Parent<ul><li>Child</li></ul></li></ul>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Parent`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Parent`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Child`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Child`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsListIndentWidthFour(t *testing.T) {\n\t// Nested lists indented with 4 spaces per level\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"list_indent_width\":4}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<ul><li>Outer<ul><li>Inner</li></ul></li></ul>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Outer`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Outer`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Inner`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Inner`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsMaxDepthDefaultUnlimited(t *testing.T) {\n\t// Default max_depth (null) converts deeply nested content fully\n\tresult, err := htmd.Convert(`<div><div><div><div><p>Deep content</p></div></div></div></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Deep content`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Deep content`, content)\n\t}\n}\n\nfunc Test_OptionsMaxDepthTruncates(t *testing.T) {\n\t// max_depth truncates content beyond the specified depth\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"max_depth\":3}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Shallow`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Shallow`, content)\n\t}\n\tif strings.Contains(string(content), `Too deep`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Too deep`, content)\n\t}\n}\n\nfunc Test_OptionsMaxDepthZeroEmpty(t *testing.T) {\n\t// max_depth of 0 produces empty output\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"max_depth\":0}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Hello</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_OptionsNewlineBackslash(t *testing.T) {\n\t// Hard line breaks rendered with backslash\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"newline_style\":\"backslash\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Line one<br>Line two</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line one`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line one`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Line two`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Line two`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsNewlineSpaces(t *testing.T) {\n\t// Hard line breaks rendered with trailing spaces\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"newline_style\":\"spaces\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>First<br>Second</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsOutputFormatDjot(t *testing.T) {\n\t// Djot output format produces djot-compatible markup\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"output_format\":\"djot\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Simple paragraph.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Simple paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Simple paragraph.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsOutputFormatMarkdown(t *testing.T) {\n\t// Default markdown output format produces standard markdown\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"heading_style\":\"atx\",\"output_format\":\"markdown\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>Some text.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Title`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Some text.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Some text.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsOutputFormatPlain(t *testing.T) {\n\t// Plain text output format strips markdown syntax\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"output_format\":\"plain\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>Some <strong>bold</strong> text.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Title`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `bold`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `bold`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `text.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `text.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsPreprocessingAggressive(t *testing.T) {\n\t// Aggressive preset removes nav, footer, aside unconditionally\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"preprocessing\":{\"preset\":\"aggressive\"}}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Title`) {\n\t\tt.Errorf(\"expected to contain %s\", `Title`)\n\t}\n\tif !strings.Contains(string(content), `Content`) {\n\t\tt.Errorf(\"expected to contain %s\", `Content`)\n\t}\n\tif strings.Contains(string(content), `Menu`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Menu`, content)\n\t}\n}\n\nfunc Test_OptionsPreprocessingMinimal(t *testing.T) {\n\t// Minimal preset preserves nav, footer, aside\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"preprocessing\":{\"preset\":\"minimal\"}}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<nav>Navigation</nav><p>Content</p><footer>Footer</footer>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Navigation`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Navigation`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Content`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Content`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Footer`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Footer`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsPreprocessingRemoveForms(t *testing.T) {\n\t// Forms are removed when remove_forms is true\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"preprocessing\":{\"remove_forms\":true}}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Before`) {\n\t\tt.Errorf(\"expected to contain %s\", `Before`)\n\t}\n\tif !strings.Contains(string(content), `After`) {\n\t\tt.Errorf(\"expected to contain %s\", `After`)\n\t}\n\tif strings.Contains(string(content), `Submit`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Submit`, content)\n\t}\n}\n\nfunc Test_OptionsPreserveTagsIframe(t *testing.T) {\n\t// Iframe tags preserved as raw HTML in output\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"preserve_tags\":[\"iframe\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Before`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Before`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `After`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `After`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `<iframe`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `<iframe`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsSkipImagesTrue(t *testing.T) {\n\t// Images are omitted from output when skipImages is true\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"skip_images\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Before <img src='test.jpg' alt='photo'> After</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Before`) {\n\t\tt.Errorf(\"expected to contain %s\", `Before`)\n\t}\n\tif !strings.Contains(string(content), `After`) {\n\t\tt.Errorf(\"expected to contain %s\", `After`)\n\t}\n\tif strings.Contains(string(content), `photo`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `photo`, content)\n\t}\n}\n\nfunc Test_OptionsStripNewlines(t *testing.T) {\n\t// Strip newlines produces single-line paragraphs\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"strip_newlines\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>First paragraph.</p><p>Second paragraph.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `First paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `First paragraph.`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Second paragraph.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Second paragraph.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsStripTagsDivSpan(t *testing.T) {\n\t// Div and span tags stripped but content preserved\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"strip_tags\":[\"div\",\"span\"]}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Inside div`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Inside div`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `span text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `span text`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsStrongEmUnderscore(t *testing.T) {\n\t// Strong and em tags use underscore symbol instead of asterisk\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"strong_em_symbol\":\"_\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p><strong>bold</strong> and <em>italic</em></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `__bold__`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `__bold__`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `_italic_`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `_italic_`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsSubSymbolTilde(t *testing.T) {\n\t// Subscript rendered with tilde symbol\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"sub_symbol\":\"~\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>H<sub>2</sub>O</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `~2~`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `~2~`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsSupSymbolCaret(t *testing.T) {\n\t// Superscript rendered with caret symbol\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"sup_symbol\":\"^\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>x<sup>2</sup></p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `^2^`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `^2^`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsWhitespaceNormalized(t *testing.T) {\n\t// Normalized whitespace mode collapses multiple spaces\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"whitespace_mode\":\"normalized\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Text   with    extra   spaces.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Text`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Text`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `with`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `with`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `extra`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `extra`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `spaces.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `spaces.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsWhitespaceStrict(t *testing.T) {\n\t// Strict whitespace mode preserves whitespace as-is\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"whitespace_mode\":\"strict\"}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Preserved   spacing.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Preserved`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Preserved`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `spacing.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `spacing.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsWrapDisabled(t *testing.T) {\n\t// Wrap option disabled preserves long lines without breaking\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"wrap\":false}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `This is a long paragraph that should not be wrapped at all because wrapping is disabled.`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `This is a long paragraph that should not be wrapped at all because wrapping is disabled.`)\n\t\t}\n\t}\n}\n\nfunc Test_OptionsWrapEnabled(t *testing.T) {\n\t// Wrap option enabled with custom width wraps long lines\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"wrap\":true,\"wrap_width\":40}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `This is a long paragraph`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `This is a long paragraph`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "e2e/go/real_world_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d67df5513bde8137330d1ce797517b8d7d46ca9ee14ed0af1c42f57bf416c34a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: real-world\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_RealWorldBlogPost(t *testing.T) {\n\t// Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\n\tresult, err := htmd.Convert(`<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\"https://www.mozilla.org\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\"language-bash\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\"language-rust\">fn main() {\n    println!(\"Hello, world!\");\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\"https://doc.rust-lang.org/book/\">Rust Book</a>.</p></article>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Getting Started with Rust`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Getting Started with Rust`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## Installation`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## Installation`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## Hello World`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## Hello World`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## Key Concepts`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## Key Concepts`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `cargo run`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `cargo run`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `[Mozilla](https://www.mozilla.org)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `[Mozilla](https://www.mozilla.org)`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `- Ownership and borrowing`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `- Ownership and borrowing`)\n\t\t}\n\t}\n}\n\nfunc Test_RealWorldDocumentationPage(t *testing.T) {\n\t// Documentation page with nested lists, code examples, and blockquotes converts correctly\n\tresult, err := htmd.Convert(`<div class=\"docs\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\"language-rust\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\"language-rust\">let markdown = convert_html(\"&lt;h1&gt;Hello&lt;/h1&gt;\").unwrap();\nassert_eq!(markdown, \"# Hello\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\"language-rust\">let options = ConversionOptions::builder()\n    .heading_style(HeadingStyle::ATX)\n    .code_block_style(CodeBlockStyle::Fenced)\n    .build();</code></pre><blockquote><p>See the <a href=\"/docs/options\">options reference</a> for a full list of configuration values.</p></blockquote></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# API Reference`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# API Reference`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## convert_html`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## convert_html`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `### Parameters`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `### Parameters`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `### Returns`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `### Returns`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `### Example`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `### Example`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## ConversionOptions`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## ConversionOptions`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `> `) {\n\t\t\tt.Errorf(\"expected to contain %s\", `> `)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `thread-safe`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `thread-safe`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `convert_html`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `convert_html`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `ConversionOptions`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `ConversionOptions`)\n\t\t}\n\t}\n}\n\nfunc Test_RealWorldProductPage(t *testing.T) {\n\t// Product page with table, images, and lists converts correctly\n\tresult, err := htmd.Convert(`<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Wireless Keyboard Pro`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Wireless Keyboard Pro`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `![Wireless Keyboard Pro](https://example.com/keyboard.jpg)`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `![Wireless Keyboard Pro](https://example.com/keyboard.jpg)`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## Specifications`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## Specifications`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Battery Life`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Battery Life`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `12 months`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `12 months`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `Bluetooth 5.0`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `Bluetooth 5.0`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `## What's in the Box`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `## What's in the Box`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `USB-C charging cable`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `USB-C charging cable`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `|`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `|`)\n\t\t}\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `---`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `---`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "e2e/go/result_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:db840605f87573455a036e18797308bb510a0964a1efcfb7afc977ef50ca68f5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: result\npackage e2e_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_ResultTablesEmptyWhenNoTables(t *testing.T) {\n\t// Result tables array is empty when input has no tables\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>No tables here</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.Equal(t, len(result.Tables), 0, \"expected exactly 0 elements\")\n}\n\nfunc Test_ResultTablesMultiple(t *testing.T) {\n\t// Multiple tables each appear in the tables array\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tassert.GreaterOrEqual(t, len(result.Tables), 2, \"expected at least 2 elements\")\n}\n\nfunc Test_ResultTablesSimple(t *testing.T) {\n\t// Simple table populates the tables array in result\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.GreaterOrEqual(t, len(result.Tables), 1, \"expected at least 1 elements\")\n}\n\nfunc Test_ResultTablesWithoutStructureFlag(t *testing.T) {\n\t// Tables array is empty when includeDocumentStructure is false\n\tresult, err := htmd.Convert(`<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.Equal(t, len(result.Tables), 0, \"expected exactly 0 elements\")\n}\n\nfunc Test_ResultWarningsEmptyForCleanInput(t *testing.T) {\n\t// Warnings array is empty for well-formed HTML without problematic content\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.Equal(t, len(result.Warnings), 0, \"expected exactly 0 elements\")\n}\n\nfunc Test_ResultWarningsEmptyForComplexInput(t *testing.T) {\n\t// Warnings array is empty for complex but valid HTML\n\tresult, err := htmd.Convert(`<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.Equal(t, len(result.Warnings), 0, \"expected exactly 0 elements\")\n}\n\nfunc Test_ResultWarningsEmptyForMalformedHtml(t *testing.T) {\n\t// Warnings array is empty even for malformed HTML (parser is lenient)\n\tresult, err := htmd.Convert(`<p>Unclosed paragraph<div>Mixed nesting</p></div>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tassert.Equal(t, len(result.Warnings), 0, \"expected exactly 0 elements\")\n}\n"
  },
  {
    "path": "e2e/go/smoke_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:13b436e3190e4fcefd4fd61a9f579003ca75d101a068f5d0ba35e7ee9668fece\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: smoke\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_SmokeEmptyString(t *testing.T) {\n\t// Empty string produces empty output\n\tresult, err := htmd.Convert(``, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n}\n\nfunc Test_SmokeSimpleHeading(t *testing.T) {\n\t// H1 heading converts to ATX markdown\n\tresult, err := htmd.Convert(`<h1>Title</h1>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content != nil {\n\t\tif !strings.Contains(string(*result.Content), `# Title`) {\n\t\t\tt.Errorf(\"expected to contain %s\", `# Title`)\n\t\t}\n\t}\n}\n\nfunc Test_SmokeSimpleParagraph(t *testing.T) {\n\t// Simple paragraph converts correctly\n\tresult, err := htmd.Convert(`<p>Hello World</p>`, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.TrimSpace(content) != `Hello World` {\n\t\tt.Errorf(\"equals mismatch: got %v\", content)\n\t}\n\tif len(content) == 0 {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n}\n"
  },
  {
    "path": "e2e/go/structure_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ab15ab60cc0d7a3c7f31aad48592fd1a14f23a05771b7f3b480047c87fa4f051\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: structure\npackage e2e_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\nfunc Test_StructureCodeBlock(t *testing.T) {\n\t// Fenced code block produces Code node\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 2, \"expected at least 2 elements\")\n\t}\n}\n\nfunc Test_StructureDeepNestingH1H2H3(t *testing.T) {\n\t// H1 > H2 > H3 creates three levels of heading nesting\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 5, \"expected at least 5 elements\")\n\t}\n}\n\nfunc Test_StructureH1H2NestedGroup(t *testing.T) {\n\t// H1 followed by H2 creates a nested group under the H1\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 3, \"expected at least 3 elements\")\n\t}\n}\n\nfunc Test_StructureHeadingParagraph(t *testing.T) {\n\t// Simple heading followed by paragraph produces Heading and Paragraph nodes\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>A paragraph of text.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 2, \"expected at least 2 elements\")\n\t}\n}\n\nfunc Test_StructureList(t *testing.T) {\n\t// Unordered list produces List and ListItem nodes\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 2, \"expected at least 2 elements\")\n\t}\n}\n\nfunc Test_StructureMultipleHeadings(t *testing.T) {\n\t// Multiple headings create multiple Heading nodes with correct levels\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 4, \"expected at least 4 elements\")\n\t}\n}\n\nfunc Test_StructureSiblingH1Groups(t *testing.T) {\n\t// H1, H2, then another H1 creates two sibling top-level groups\n\tvar options htmd.ConversionOptions\n\tif err := json.Unmarshal([]byte(`{\"include_document_structure\":true}`), &options); err != nil {\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}\n\tresult, err := htmd.Convert(`<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>`, &options)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tif result.Content == nil {\n\t\tt.Errorf(\"expected non-empty value\")\n\t}\n\tif result.Document != nil {\n\t\tif len(result.Document.Nodes) == 0 {\n\t\t\tt.Errorf(\"expected non-empty value\")\n\t\t}\n\t}\n\tif result.Document != nil {\n\t\tassert.GreaterOrEqual(t, len(result.Document.Nodes), 4, \"expected at least 4 elements\")\n\t}\n}\n"
  },
  {
    "path": "e2e/go/visitor_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:53b43159c8d4f16d9df78004049c866ef5c686f639db5000a9518565b57e316d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: visitor\npackage e2e_test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\thtmd \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\"\n)\n\ntype testVisitorVisitorAudioCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorAudioCustom) VisitAudio(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`[AUDIO: podcast.mp3]`)\n}\n\ntype testVisitorVisitorAudioSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorAudioSkip) VisitAudio(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorButtonCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorButtonCustom) VisitButton(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`[BTN:%s]`, text))\n}\n\ntype testVisitorVisitorButtonSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorButtonSkip) VisitButton(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorContinueDefault struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorContinueDefault) VisitStrong(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultContinue()\n}\n\ntype testVisitorVisitorCustomBlockquote struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomBlockquote) VisitBlockquote(_ htmd.NodeContext, content string, depth uint) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`QUOTE: \"%s\"`, content))\n}\n\ntype testVisitorVisitorCustomEmphasis struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomEmphasis) VisitEmphasis(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`>>>%s<<<`, text))\n}\n\ntype testVisitorVisitorCustomHeading struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomHeading) VisitHeading(_ htmd.NodeContext, level uint32, text string, id *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`--- %s ---`, text))\n}\n\ntype testVisitorVisitorCustomImage struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomImage) VisitImage(_ htmd.NodeContext, src string, alt string, title *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`[Image: %s]`, alt))\n}\n\ntype testVisitorVisitorCustomLinkFormat struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomLinkFormat) VisitLink(_ htmd.NodeContext, href string, text string, title *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`%s (%s)`, text, href))\n}\n\ntype testVisitorVisitorCustomLinkStatic struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomLinkStatic) VisitLink(_ htmd.NodeContext, href string, text string, title *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`[REDACTED LINK]`)\n}\n\ntype testVisitorVisitorCustomOutput struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorCustomOutput) VisitHeading(_ htmd.NodeContext, level uint32, text string, id *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`## REPLACED HEADING`)\n}\n\ntype testVisitorVisitorDefinitionListCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDefinitionListCustom) VisitDefinitionTerm(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`**%s**`, text))\n}\n\ntype testVisitorVisitorDefinitionListCustomFormat struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDefinitionListCustomFormat) VisitDefinitionTerm(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`### %s`, text))\n}\nfunc (v *testVisitorVisitorDefinitionListCustomFormat) VisitDefinitionDescription(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`> %s`, text))\n}\n\ntype testVisitorVisitorDefinitionListSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDefinitionListSkip) VisitDefinitionDescription(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\nfunc (v *testVisitorVisitorDefinitionListSkip) VisitDefinitionTerm(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorDetailsSummaryCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDetailsSummaryCustom) VisitSummary(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`[EXPANDABLE] %s`, text))\n}\n\ntype testVisitorVisitorDetailsSummarySkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorDetailsSummarySkip) VisitDetails(_ htmd.NodeContext, open bool) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorFigureCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorFigureCustom) VisitFigcaption(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`*%s*`, text))\n}\n\ntype testVisitorVisitorFigureCustomWrap struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorFigureCustomWrap) VisitFigureStart(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`\n[FIGURE]\n`)\n}\nfunc (v *testVisitorVisitorFigureCustomWrap) VisitFigureEnd(_ htmd.NodeContext, output string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`%s\n[/FIGURE]\n`, output))\n}\n\ntype testVisitorVisitorFigureSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorFigureSkip) VisitFigureStart(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorFormCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorFormCustom) VisitForm(_ htmd.NodeContext, action *string, method *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`[FORM PLACEHOLDER]`)\n}\n\ntype testVisitorVisitorFormSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorFormSkip) VisitForm(_ htmd.NodeContext, action *string, method *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorHorizontalRuleCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorHorizontalRuleCustom) VisitHorizontalRule(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`\n[DIVIDER]\n`)\n}\n\ntype testVisitorVisitorHorizontalRuleSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorHorizontalRuleSkip) VisitHorizontalRule(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorIframeCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorIframeCustom) VisitIframe(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(`[EMBEDDED: https://maps.example.com/embed]`)\n}\n\ntype testVisitorVisitorIframeSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorIframeSkip) VisitIframe(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorInputCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorInputCustom) VisitInput(_ htmd.NodeContext, inputType string, name *string, value *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`[INPUT:%s]`, inputType))\n}\n\ntype testVisitorVisitorInputSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorInputSkip) VisitInput(_ htmd.NodeContext, inputType string, name *string, value *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorLineBreakCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorLineBreakCustom) VisitLineBreak(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(` | `)\n}\n\ntype testVisitorVisitorLineBreakSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorLineBreakSkip) VisitLineBreak(_ htmd.NodeContext) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorMarkCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorMarkCustom) VisitMark(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`==%s==`, text))\n}\n\ntype testVisitorVisitorMarkSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorMarkSkip) VisitMark(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorPreserveHtml struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorPreserveHtml) VisitCustomElement(_ htmd.NodeContext, tagName string, html string) htmd.VisitResult {\n\treturn htmd.VisitResultPreserveHTML()\n}\n\ntype testVisitorVisitorSkipAllHeadings struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipAllHeadings) VisitHeading(_ htmd.NodeContext, level uint32, text string, id *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSkipCodeBlocks struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipCodeBlocks) VisitCodeBlock(_ htmd.NodeContext, lang *string, code string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSkipHeading struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipHeading) VisitHeading(_ htmd.NodeContext, level uint32, text string, id *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSkipImages struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipImages) VisitImage(_ htmd.NodeContext, src string, alt string, title *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSkipLinks struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipLinks) VisitLink(_ htmd.NodeContext, href string, text string, title *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSkipStrong struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSkipStrong) VisitStrong(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSubscriptCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSubscriptCustom) VisitSubscript(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`~%s~`, text))\n}\n\ntype testVisitorVisitorSubscriptSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSubscriptSkip) VisitSubscript(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorSuperscriptCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSuperscriptCustom) VisitSuperscript(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`^%s^`, text))\n}\n\ntype testVisitorVisitorSuperscriptSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorSuperscriptSkip) VisitSuperscript(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorUnderlineCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorUnderlineCustom) VisitUnderline(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`_%s_`, text))\n}\n\ntype testVisitorVisitorUnderlineSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorUnderlineSkip) VisitUnderline(_ htmd.NodeContext, text string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\ntype testVisitorVisitorVideoCustom struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorVideoCustom) VisitVideo(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultCustom(fmt.Sprintf(`[VIDEO: %s]`, *src))\n}\n\ntype testVisitorVisitorVideoSkip struct {\n\thtmd.BaseVisitor\n}\n\nfunc (v *testVisitorVisitorVideoSkip) VisitVideo(_ htmd.NodeContext, src *string) htmd.VisitResult {\n\treturn htmd.VisitResultSkip()\n}\n\nfunc Test_VisitorAudioCustom(t *testing.T) {\n\t// Visitor replaces audio element with custom output\n\tvisitor := &testVisitorVisitorAudioCustom{}\n\tresult, err := htmd.Convert(`<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[AUDIO: podcast.mp3]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[AUDIO: podcast.mp3]`, content)\n\t}\n\tif !strings.Contains(string(content), `Listen to this:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Listen to this:`, content)\n\t}\n}\n\nfunc Test_VisitorAudioSkip(t *testing.T) {\n\t// Visitor removes audio elements from output\n\tvisitor := &testVisitorVisitorAudioSkip{}\n\tresult, err := htmd.Convert(`<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Background music:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Background music:`, content)\n\t}\n\tif !strings.Contains(string(content), `Enjoy!`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Enjoy!`, content)\n\t}\n\tif strings.Contains(string(content), `music.ogg`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `music.ogg`, content)\n\t}\n}\n\nfunc Test_VisitorButtonCustom(t *testing.T) {\n\t// Visitor replaces button with bracketed text\n\tvisitor := &testVisitorVisitorButtonCustom{}\n\tresult, err := htmd.Convert(`<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[BTN:Click me]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[BTN:Click me]`, content)\n\t}\n\tif !strings.Contains(string(content), `[BTN:Cancel]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[BTN:Cancel]`, content)\n\t}\n\tif !strings.Contains(string(content), `Confirm action:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Confirm action:`, content)\n\t}\n}\n\nfunc Test_VisitorButtonSkip(t *testing.T) {\n\t// Visitor removes all buttons from output\n\tvisitor := &testVisitorVisitorButtonSkip{}\n\tresult, err := htmd.Convert(`<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Actions available:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Actions available:`, content)\n\t}\n\tif strings.Contains(string(content), `Save`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Save`, content)\n\t}\n\tif strings.Contains(string(content), `Delete`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Delete`, content)\n\t}\n\tif strings.Contains(string(content), `Export`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Export`, content)\n\t}\n}\n\nfunc Test_VisitorContinueDefault(t *testing.T) {\n\t// Visitor continue action preserves default conversion\n\tvisitor := &testVisitorVisitorContinueDefault{}\n\tresult, err := htmd.Convert(`<p>Hello <strong>World</strong></p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `**World**`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `**World**`, content)\n\t}\n}\n\nfunc Test_VisitorCustomBlockquote(t *testing.T) {\n\t// Visitor replaces blockquote with custom format\n\tvisitor := &testVisitorVisitorCustomBlockquote{}\n\tresult, err := htmd.Convert(`<blockquote><p>A wise quote.</p></blockquote>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `QUOTE:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `QUOTE:`, content)\n\t}\n\tif !strings.Contains(string(content), `A wise quote.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `A wise quote.`, content)\n\t}\n}\n\nfunc Test_VisitorCustomEmphasis(t *testing.T) {\n\t// Visitor replaces emphasis with custom output\n\tvisitor := &testVisitorVisitorCustomEmphasis{}\n\tresult, err := htmd.Convert(`<p>This is <em>important</em> text.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `>>>important<<<`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `>>>important<<<`, content)\n\t}\n\tif strings.Contains(string(content), `*important*`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `*important*`, content)\n\t}\n}\n\nfunc Test_VisitorCustomHeading(t *testing.T) {\n\t// Visitor replaces heading with custom format\n\tvisitor := &testVisitorVisitorCustomHeading{}\n\tresult, err := htmd.Convert(`<h2>Section Title</h2><p>Content below heading.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `--- Section Title ---`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `--- Section Title ---`, content)\n\t}\n\tif strings.Contains(string(content), `## Section Title`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `## Section Title`, content)\n\t}\n\tif !strings.Contains(string(content), `Content below heading.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Content below heading.`, content)\n\t}\n}\n\nfunc Test_VisitorCustomImage(t *testing.T) {\n\t// Visitor replaces image with custom output using template\n\tvisitor := &testVisitorVisitorCustomImage{}\n\tresult, err := htmd.Convert(`<img src=\"banner.png\" alt=\"Banner\">`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[Image: Banner]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[Image: Banner]`, content)\n\t}\n\tif strings.Contains(string(content), `banner.png`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `banner.png`, content)\n\t}\n}\n\nfunc Test_VisitorCustomLinkFormat(t *testing.T) {\n\t// Visitor reformats links using template interpolation\n\tvisitor := &testVisitorVisitorCustomLinkFormat{}\n\tresult, err := htmd.Convert(`<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Example (https://example.com)`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Example (https://example.com)`, content)\n\t}\n\tif strings.Contains(string(content), `[Example]`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `[Example]`, content)\n\t}\n}\n\nfunc Test_VisitorCustomLinkStatic(t *testing.T) {\n\t// Visitor replaces link with static custom output\n\tvisitor := &testVisitorVisitorCustomLinkStatic{}\n\tresult, err := htmd.Convert(`<a href=\"https://example.com\">Click here</a>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[REDACTED LINK]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[REDACTED LINK]`, content)\n\t}\n\tif strings.Contains(string(content), `example.com`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `example.com`, content)\n\t}\n}\n\nfunc Test_VisitorCustomOutput(t *testing.T) {\n\t// Visitor custom action replaces element output\n\tvisitor := &testVisitorVisitorCustomOutput{}\n\tresult, err := htmd.Convert(`<h1>Original Heading</h1>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `## REPLACED HEADING`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `## REPLACED HEADING`, content)\n\t}\n\tif strings.Contains(string(content), `# Original Heading`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `# Original Heading`, content)\n\t}\n}\n\nfunc Test_VisitorDefinitionListCustom(t *testing.T) {\n\t// Visitor customizes definition list items\n\tvisitor := &testVisitorVisitorDefinitionListCustom{}\n\tresult, err := htmd.Convert(`<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `**HTML**`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `**HTML**`, content)\n\t}\n\tif !strings.Contains(string(content), `**CSS**`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `**CSS**`, content)\n\t}\n\tif !strings.Contains(string(content), `HyperText Markup Language`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `HyperText Markup Language`, content)\n\t}\n}\n\nfunc Test_VisitorDefinitionListCustomFormat(t *testing.T) {\n\t// Visitor formats definition lists with custom templates\n\tvisitor := &testVisitorVisitorDefinitionListCustomFormat{}\n\tresult, err := htmd.Convert(`<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `### Python`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `### Python`, content)\n\t}\n\tif !strings.Contains(string(content), `### JavaScript`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `### JavaScript`, content)\n\t}\n\tif !strings.Contains(string(content), `> A high-level programming language`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `> A high-level programming language`, content)\n\t}\n\tif !strings.Contains(string(content), `> A scripting language for web browsers`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `> A scripting language for web browsers`, content)\n\t}\n}\n\nfunc Test_VisitorDefinitionListSkip(t *testing.T) {\n\t// Visitor skips definition list items from output\n\tvisitor := &testVisitorVisitorDefinitionListSkip{}\n\tresult, err := htmd.Convert(`<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Glossary:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Glossary:`, content)\n\t}\n\tif !strings.Contains(string(content), `End of glossary`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `End of glossary`, content)\n\t}\n\tif strings.Contains(string(content), `Term A`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Term A`, content)\n\t}\n\tif strings.Contains(string(content), `Definition`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Definition`, content)\n\t}\n}\n\nfunc Test_VisitorDetailsSummaryCustom(t *testing.T) {\n\t// Visitor customizes details/summary disclosure elements\n\tvisitor := &testVisitorVisitorDetailsSummaryCustom{}\n\tresult, err := htmd.Convert(`<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[EXPANDABLE] Click to expand`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[EXPANDABLE] Click to expand`, content)\n\t}\n\tif !strings.Contains(string(content), `This content is initially hidden.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `This content is initially hidden.`, content)\n\t}\n}\n\nfunc Test_VisitorDetailsSummarySkip(t *testing.T) {\n\t// Visitor removes details/summary elements entirely\n\tvisitor := &testVisitorVisitorDetailsSummarySkip{}\n\tresult, err := htmd.Convert(`<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Main content here.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Main content here.`, content)\n\t}\n\tif !strings.Contains(string(content), `More main content.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `More main content.`, content)\n\t}\n\tif strings.Contains(string(content), `Hidden section`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Hidden section`, content)\n\t}\n\tif strings.Contains(string(content), `Secret details`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Secret details`, content)\n\t}\n}\n\nfunc Test_VisitorFigureCustom(t *testing.T) {\n\t// Visitor customizes figure and figcaption elements\n\tvisitor := &testVisitorVisitorFigureCustom{}\n\tresult, err := htmd.Convert(`<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Article Title`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Article Title`, content)\n\t}\n\tif !strings.Contains(string(content), `*Figure 1: System Architecture*`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `*Figure 1: System Architecture*`, content)\n\t}\n\tif !strings.Contains(string(content), `Explanation of the figure.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Explanation of the figure.`, content)\n\t}\n}\n\nfunc Test_VisitorFigureCustomWrap(t *testing.T) {\n\t// Visitor wraps figure content with custom formatting\n\tvisitor := &testVisitorVisitorFigureCustomWrap{}\n\tresult, err := htmd.Convert(`<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[FIGURE]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[FIGURE]`, content)\n\t}\n\tif !strings.Contains(string(content), `[/FIGURE]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[/FIGURE]`, content)\n\t}\n\tif !strings.Contains(string(content), `Gallery`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Gallery`, content)\n\t}\n}\n\nfunc Test_VisitorFigureSkip(t *testing.T) {\n\t// Visitor removes figure elements with their captions\n\tvisitor := &testVisitorVisitorFigureSkip{}\n\tresult, err := htmd.Convert(`<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `See the chart below:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `See the chart below:`, content)\n\t}\n\tif !strings.Contains(string(content), `As shown in the chart above.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `As shown in the chart above.`, content)\n\t}\n\tif strings.Contains(string(content), `Revenue Trends`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Revenue Trends`, content)\n\t}\n\tif strings.Contains(string(content), `chart.svg`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `chart.svg`, content)\n\t}\n}\n\nfunc Test_VisitorFormCustom(t *testing.T) {\n\t// Visitor replaces form with custom output\n\tvisitor := &testVisitorVisitorFormCustom{}\n\tresult, err := htmd.Convert(`<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[FORM PLACEHOLDER]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[FORM PLACEHOLDER]`, content)\n\t}\n\tif strings.Contains(string(content), `submit`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `submit`, content)\n\t}\n\tif strings.Contains(string(content), `input`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `input`, content)\n\t}\n}\n\nfunc Test_VisitorFormSkip(t *testing.T) {\n\t// Visitor skips form elements entirely\n\tvisitor := &testVisitorVisitorFormSkip{}\n\tresult, err := htmd.Convert(`<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Before form`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Before form`, content)\n\t}\n\tif !strings.Contains(string(content), `After form`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `After form`, content)\n\t}\n\tif strings.Contains(string(content), `email`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `email`, content)\n\t}\n}\n\nfunc Test_VisitorHorizontalRuleCustom(t *testing.T) {\n\t// Visitor replaces horizontal rule with custom output\n\tvisitor := &testVisitorVisitorHorizontalRuleCustom{}\n\tresult, err := htmd.Convert(`<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[DIVIDER]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[DIVIDER]`, content)\n\t}\n\tif !strings.Contains(string(content), `Section A`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Section A`, content)\n\t}\n\tif !strings.Contains(string(content), `Section B`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Section B`, content)\n\t}\n\tif strings.Contains(string(content), `---`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `---`, content)\n\t}\n}\n\nfunc Test_VisitorHorizontalRuleSkip(t *testing.T) {\n\t// Visitor removes all horizontal rules\n\tvisitor := &testVisitorVisitorHorizontalRuleSkip{}\n\tresult, err := htmd.Convert(`<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Part 1`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Part 1`, content)\n\t}\n\tif !strings.Contains(string(content), `Part 2`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Part 2`, content)\n\t}\n\tif !strings.Contains(string(content), `Part 3`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Part 3`, content)\n\t}\n\tif strings.Contains(string(content), `---`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `---`, content)\n\t}\n}\n\nfunc Test_VisitorIframeCustom(t *testing.T) {\n\t// Visitor replaces embedded iframe with custom text\n\tvisitor := &testVisitorVisitorIframeCustom{}\n\tresult, err := htmd.Convert(`<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[EMBEDDED: https://maps.example.com/embed]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[EMBEDDED: https://maps.example.com/embed]`, content)\n\t}\n\tif !strings.Contains(string(content), `Embedded map:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Embedded map:`, content)\n\t}\n\tif !strings.Contains(string(content), `End of map`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `End of map`, content)\n\t}\n}\n\nfunc Test_VisitorIframeSkip(t *testing.T) {\n\t// Visitor removes embedded iframes\n\tvisitor := &testVisitorVisitorIframeSkip{}\n\tresult, err := htmd.Convert(`<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Reviews`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Reviews`, content)\n\t}\n\tif !strings.Contains(string(content), `See reviews from our partners.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `See reviews from our partners.`, content)\n\t}\n\tif strings.Contains(string(content), `widget.example.com`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `widget.example.com`, content)\n\t}\n}\n\nfunc Test_VisitorInputCustom(t *testing.T) {\n\t// Visitor replaces input with labeled output\n\tvisitor := &testVisitorVisitorInputCustom{}\n\tresult, err := htmd.Convert(`<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[INPUT:text]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[INPUT:text]`, content)\n\t}\n\tif !strings.Contains(string(content), `[INPUT:password]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[INPUT:password]`, content)\n\t}\n}\n\nfunc Test_VisitorInputSkip(t *testing.T) {\n\t// Visitor skips all input elements\n\tvisitor := &testVisitorVisitorInputSkip{}\n\tresult, err := htmd.Convert(`<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Sign up:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Sign up:`, content)\n\t}\n\tif !strings.Contains(string(content), `Continue`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Continue`, content)\n\t}\n\tif strings.Contains(string(content), `email`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `email`, content)\n\t}\n}\n\nfunc Test_VisitorLineBreakCustom(t *testing.T) {\n\t// Visitor replaces line break with custom output\n\tvisitor := &testVisitorVisitorLineBreakCustom{}\n\tresult, err := htmd.Convert(`<p>First line<br>Second line<br>Third line</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `First line | Second line | Third line`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `First line | Second line | Third line`, content)\n\t}\n\tif strings.Contains(string(content), `\n\n`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `\n\n`, content)\n\t}\n}\n\nfunc Test_VisitorLineBreakSkip(t *testing.T) {\n\t// Visitor removes all line breaks\n\tvisitor := &testVisitorVisitorLineBreakSkip{}\n\tresult, err := htmd.Convert(`<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Address Line 1Address Line 2Address Line 3`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Address Line 1Address Line 2Address Line 3`, content)\n\t}\n}\n\nfunc Test_VisitorMarkCustom(t *testing.T) {\n\t// Visitor replaces highlight/mark with custom template\n\tvisitor := &testVisitorVisitorMarkCustom{}\n\tresult, err := htmd.Convert(`<p>This is a <mark>highlighted passage</mark> in the text.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `==highlighted passage==`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `==highlighted passage==`, content)\n\t}\n\tif !strings.Contains(string(content), `This is a`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `This is a`, content)\n\t}\n}\n\nfunc Test_VisitorMarkSkip(t *testing.T) {\n\t// Visitor skips mark elements entirely\n\tvisitor := &testVisitorVisitorMarkSkip{}\n\tresult, err := htmd.Convert(`<p>Key insight: <mark>always validate input</mark> for security.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.Contains(string(content), `always validate input`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `always validate input`, content)\n\t}\n\tif !strings.Contains(string(content), `Key insight:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Key insight:`, content)\n\t}\n\tif !strings.Contains(string(content), `for security.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `for security.`, content)\n\t}\n}\n\nfunc Test_VisitorPreserveHtml(t *testing.T) {\n\t// Visitor preserve_html action includes raw HTML in output\n\tvisitor := &testVisitorVisitorPreserveHtml{}\n\tresult, err := htmd.Convert(`<div><custom-tag>Custom content</custom-tag></div>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `<custom-tag>`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `<custom-tag>`, content)\n\t}\n}\n\nfunc Test_VisitorSkipAllHeadings(t *testing.T) {\n\t// Visitor skips all headings from document\n\tvisitor := &testVisitorVisitorSkipAllHeadings{}\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>Body text remains.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.Contains(string(content), `Title`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Title`, content)\n\t}\n\tif !strings.Contains(string(content), `Body text remains.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Body text remains.`, content)\n\t}\n}\n\nfunc Test_VisitorSkipCodeBlocks(t *testing.T) {\n\t// Visitor skips code blocks from output\n\tvisitor := &testVisitorVisitorSkipCodeBlocks{}\n\tresult, err := htmd.Convert(`<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Intro text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Intro text`, content)\n\t}\n\tif !strings.Contains(string(content), `Outro text`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Outro text`, content)\n\t}\n\tif strings.Contains(string(content), `let x = 42`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `let x = 42`, content)\n\t}\n}\n\nfunc Test_VisitorSkipHeading(t *testing.T) {\n\t// Visitor skip action omits all headings from output\n\tvisitor := &testVisitorVisitorSkipHeading{}\n\tresult, err := htmd.Convert(`<h1>Title</h1><p>Body text remains.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.Contains(string(content), `Title`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `Title`, content)\n\t}\n\tif !strings.Contains(string(content), `Body text remains.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Body text remains.`, content)\n\t}\n}\n\nfunc Test_VisitorSkipImages(t *testing.T) {\n\t// Visitor skips all images from output\n\tvisitor := &testVisitorVisitorSkipImages{}\n\tresult, err := htmd.Convert(`<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Before image`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Before image`, content)\n\t}\n\tif !strings.Contains(string(content), `After image`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `After image`, content)\n\t}\n\tif strings.Contains(string(content), `photo.jpg`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `photo.jpg`, content)\n\t}\n\tif strings.Contains(string(content), `A photo`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `A photo`, content)\n\t}\n}\n\nfunc Test_VisitorSkipLinks(t *testing.T) {\n\t// Visitor skips all links entirely\n\tvisitor := &testVisitorVisitorSkipLinks{}\n\tresult, err := htmd.Convert(`<p>Before <a href=\"https://example.com\">link text</a> after</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.Contains(string(content), `link text`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `link text`, content)\n\t}\n\tif strings.Contains(string(content), `example.com`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `example.com`, content)\n\t}\n}\n\nfunc Test_VisitorSkipStrong(t *testing.T) {\n\t// Visitor skips bold/strong elements\n\tvisitor := &testVisitorVisitorSkipStrong{}\n\tresult, err := htmd.Convert(`<p>Normal <strong>bold text</strong> normal</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif strings.Contains(string(content), `bold text`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `bold text`, content)\n\t}\n\tif !strings.Contains(string(content), `Normal`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Normal`, content)\n\t}\n}\n\nfunc Test_VisitorSubscriptCustom(t *testing.T) {\n\t// Visitor replaces subscript with custom template\n\tvisitor := &testVisitorVisitorSubscriptCustom{}\n\tresult, err := htmd.Convert(`<p>H<sub>2</sub>O is water.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `H~2~O`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `H~2~O`, content)\n\t}\n\tif !strings.Contains(string(content), `is water`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `is water`, content)\n\t}\n}\n\nfunc Test_VisitorSubscriptSkip(t *testing.T) {\n\t// Visitor skips subscript elements entirely\n\tvisitor := &testVisitorVisitorSubscriptSkip{}\n\tresult, err := htmd.Convert(`<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `The formula CHO is sugar.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `The formula CHO is sugar.`, content)\n\t}\n}\n\nfunc Test_VisitorSuperscriptCustom(t *testing.T) {\n\t// Visitor replaces superscript with custom template\n\tvisitor := &testVisitorVisitorSuperscriptCustom{}\n\tresult, err := htmd.Convert(`<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `E=mc^2^`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `E=mc^2^`, content)\n\t}\n\tif !strings.Contains(string(content), `revolutionized physics`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `revolutionized physics`, content)\n\t}\n}\n\nfunc Test_VisitorSuperscriptSkip(t *testing.T) {\n\t// Visitor skips superscript from output\n\tvisitor := &testVisitorVisitorSuperscriptSkip{}\n\tresult, err := htmd.Convert(`<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `The equation x + y = z has no solutions.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `The equation x + y = z has no solutions.`, content)\n\t}\n}\n\nfunc Test_VisitorUnderlineCustom(t *testing.T) {\n\t// Visitor replaces underline with custom markup\n\tvisitor := &testVisitorVisitorUnderlineCustom{}\n\tresult, err := htmd.Convert(`<p>This is <u>very important</u> text.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `_very important_`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `_very important_`, content)\n\t}\n\tif strings.Contains(string(content), `**`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `**`, content)\n\t}\n}\n\nfunc Test_VisitorUnderlineSkip(t *testing.T) {\n\t// Visitor skips underline elements from output\n\tvisitor := &testVisitorVisitorUnderlineSkip{}\n\tresult, err := htmd.Convert(`<p>Normal text with <u>underlined part</u> and more text.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Normal text with`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Normal text with`, content)\n\t}\n\tif !strings.Contains(string(content), `and more text.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `and more text.`, content)\n\t}\n\tif strings.Contains(string(content), `underlined part`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `underlined part`, content)\n\t}\n}\n\nfunc Test_VisitorVideoCustom(t *testing.T) {\n\t// Visitor replaces video with custom link\n\tvisitor := &testVisitorVisitorVideoCustom{}\n\tresult, err := htmd.Convert(`<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `[VIDEO: tutorial.mp4]`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `[VIDEO: tutorial.mp4]`, content)\n\t}\n\tif !strings.Contains(string(content), `Watch our tutorial:`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Watch our tutorial:`, content)\n\t}\n\tif !strings.Contains(string(content), `Great content!`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Great content!`, content)\n\t}\n}\n\nfunc Test_VisitorVideoSkip(t *testing.T) {\n\t// Visitor removes video elements entirely\n\tvisitor := &testVisitorVisitorVideoSkip{}\n\tresult, err := htmd.Convert(`<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>`, nil, visitor)\n\tif err != nil {\n\t\tt.Fatalf(\"call failed: %v\", err)\n\t}\n\tvar content string\n\tif result.Content != nil {\n\t\tcontent = *result.Content\n\t}\n\tif !strings.Contains(string(content), `Demo`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `Demo`, content)\n\t}\n\tif !strings.Contains(string(content), `See the demo above.`) {\n\t\tt.Errorf(\"expected to contain %s, got %v\", `See the demo above.`, content)\n\t}\n\tif strings.Contains(string(content), `demo.webm`) {\n\t\tt.Errorf(\"expected NOT to contain %s, got %v\", `demo.webm`, content)\n\t}\n}\n"
  },
  {
    "path": "e2e/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>dev.kreuzberg.htmltomarkdown</groupId>\n    <artifactId>html-to-markdown-e2e-java</artifactId>\n    <version>0.1.0</version>\n\n    <properties>\n        <maven.compiler.source>25</maven.compiler.source>\n        <maven.compiler.target>25</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <junit.version>6.0.3</junit.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>dev.kreuzberg</groupId>\n            <artifactId>html-to-markdown</artifactId>\n            <version>3.4.0-rc.25</version>\n            <scope>system</scope>\n            <systemPath>${project.basedir}/../../packages/java/target/html-to-markdown-3.4.0-rc.25.jar</systemPath>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>2.18.2</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <version>2.18.2</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <version>3.6.1</version>\n                <executions>\n                    <execution>\n                        <id>add-test-source</id>\n                        <phase>generate-test-sources</phase>\n                        <goals>\n                            <goal>add-test-source</goal>\n                        </goals>\n                        <configuration>\n                            <sources>\n                                <source>src/test/java</source>\n                            </sources>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>3.5.2</version>\n                <configuration>\n                    <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=${project.basedir}/../../target/release</argLine>\n                    <workingDirectory>${project.basedir}/../../test_documents</workingDirectory>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/ConversionTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0b0c26a30cc866c659ac71eb0a514a7dfe8143af93f0e3da9b0d889d1c16a3cf\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: conversion. */\nclass ConversionTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testBlockquoteMultipleParagraphs() throws Exception {\n        // Blockquote with multiple paragraphs has each paragraph prefixed\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"> First paragraph.\"), \"expected to contain: \" + \"> First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"> Second paragraph.\"), \"expected to contain: \" + \"> Second paragraph.\");\n    }\n\n    @Test\n    void testBlockquoteNested() throws Exception {\n        // Nested blockquote produces double-prefixed lines\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Outer quote.\"), \"expected to contain: \" + \"Outer quote.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Inner quote.\"), \"expected to contain: \" + \"Inner quote.\");\n    }\n\n    @Test\n    void testBlockquoteSimple() throws Exception {\n        // Simple blockquote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote text</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"> Quote text\"), \"expected to contain: \" + \"> Quote text\");\n    }\n\n    @Test\n    void testBlockquoteWithList() throws Exception {\n        // Blockquote containing a list preserves list items inside quote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Quote intro:\"), \"expected to contain: \" + \"Quote intro:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point one\"), \"expected to contain: \" + \"Point one\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point two\"), \"expected to contain: \" + \"Point two\");\n    }\n\n    @Test\n    void testBoldAndItalic() throws Exception {\n        // Nested bold and italic\n        var result = HtmlToMarkdown.convert(\"<p><strong><em>both</em></strong></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"***both***\"), \"expected to contain: \" + \"***both***\");\n    }\n\n    @Test\n    void testBoldStrong() throws Exception {\n        // Strong tag converts to bold\n        var result = HtmlToMarkdown.convert(\"<p><strong>bold</strong></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n    }\n\n    @Test\n    void testCodeBlock() throws Exception {\n        // Code block with language preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"print('hello')\"), \"expected to contain: \" + \"print('hello')\");\n    }\n\n    @Test\n    void testCodeBlockNoLanguage() throws Exception {\n        // Code block without a language class preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code>plain code here</code></pre>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"plain code here\"), \"expected to contain: \" + \"plain code here\");\n    }\n\n    @Test\n    void testCodeInlineInParagraph() throws Exception {\n        // Inline code element nested inside a paragraph\n        var result = HtmlToMarkdown.convert(\"<p>Call the <code>initialize()</code> method first.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"`initialize()`\"), \"expected to contain: \" + \"`initialize()`\");\n    }\n\n    @Test\n    void testCodeWithBackticksInContent() throws Exception {\n        // Inline code containing backtick characters is properly escaped\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"backtick\"), \"expected to contain: \" + \"backtick\");\n    }\n\n    @Test\n    void testEmphasisMarkHighlight() throws Exception {\n        // mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p><mark>highlighted</mark></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted\"), \"expected to contain: \" + \"highlighted\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughDel() throws Exception {\n        // del tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><del>deleted text</del></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"~~deleted text~~\"), \"expected to contain: \" + \"~~deleted text~~\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughS() throws Exception {\n        // s tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><s>strikethrough</s></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"~~strikethrough~~\"), \"expected to contain: \" + \"~~strikethrough~~\");\n    }\n\n    @Test\n    void testEmphasisSubscript() throws Exception {\n        // sub tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n    }\n\n    @Test\n    void testEmphasisSuperscript() throws Exception {\n        // sup tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>x<sup>2</sup></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"x\"), \"expected to contain: \" + \"x\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n    }\n\n    @Test\n    void testEmphasisUnderlineU() throws Exception {\n        // u tag content is preserved in output\n        var result = HtmlToMarkdown.convert(\"<p><u>underlined</u></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"underlined\"), \"expected to contain: \" + \"underlined\");\n    }\n\n    @Test\n    void testFormInputElements() throws Exception {\n        // Form input elements produce readable output without form mechanics\n        var result = HtmlToMarkdown.convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n    }\n\n    @Test\n    void testFormSelectOptions() throws Exception {\n        // Select element with options produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Color\"), \"expected to contain: \" + \"Color\");\n    }\n\n    @Test\n    void testFormTextarea() throws Exception {\n        // Textarea element produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Message\"), \"expected to contain: \" + \"Message\");\n    }\n\n    @Test\n    void testHeadingH1() throws Exception {\n        // H1 heading\n        var result = HtmlToMarkdown.convert(\"<h1>Heading 1</h1>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"# Heading 1\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH2() throws Exception {\n        // H2 heading\n        var result = HtmlToMarkdown.convert(\"<h2>Heading 2</h2>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"## Heading 2\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH3() throws Exception {\n        // H3 heading\n        var result = HtmlToMarkdown.convert(\"<h3>Heading 3</h3>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"### Heading 3\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH4() throws Exception {\n        // H4 heading\n        var result = HtmlToMarkdown.convert(\"<h4>Heading 4</h4>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"#### Heading 4\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH5() throws Exception {\n        // H5 heading\n        var result = HtmlToMarkdown.convert(\"<h5>Heading 5</h5>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"##### Heading 5\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH6() throws Exception {\n        // H6 heading\n        var result = HtmlToMarkdown.convert(\"<h6>Heading 6</h6>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"###### Heading 6\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testImageFigureFigcaption() throws Exception {\n        // Figure with figcaption preserves both image and caption\n        var result = HtmlToMarkdown.convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![A sunset](sunset.jpg)\"), \"expected to contain: \" + \"![A sunset](sunset.jpg)\");\n        assertTrue(result.content().orElse(\"\").contains(\"Beautiful sunset over the ocean\"), \"expected to contain: \" + \"Beautiful sunset over the ocean\");\n    }\n\n    @Test\n    void testImageLinked() throws Exception {\n        // Image inside an anchor produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Icon](icon.png)\"), \"expected to contain: \" + \"![Icon](icon.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testImageNoAlt() throws Exception {\n        // Image without alt text produces image markdown\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"banner.jpg\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"banner.jpg\"), \"expected to contain: \" + \"banner.jpg\");\n    }\n\n    @Test\n    void testImageSimple() throws Exception {\n        // Image with alt text\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![A photo](photo.jpg)\"), \"expected to contain: \" + \"![A photo](photo.jpg)\");\n    }\n\n    @Test\n    void testImageWithTitle() throws Exception {\n        // Image with title attribute includes title in output\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Sales chart](chart.png\"), \"expected to contain: \" + \"![Sales chart](chart.png\");\n        assertTrue(result.content().orElse(\"\").contains(\"Q3 Sales\"), \"expected to contain: \" + \"Q3 Sales\");\n    }\n\n    @Test\n    void testInlineCode() throws Exception {\n        // Inline code\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>console.log()</code> to debug</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"`console.log()`\"), \"expected to contain: \" + \"`console.log()`\");\n    }\n\n    @Test\n    void testItalicEm() throws Exception {\n        // Em tag converts to italic\n        var result = HtmlToMarkdown.convert(\"<p><em>italic</em></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n    }\n\n    @Test\n    void testLineBreakBrTag() throws Exception {\n        // Single br tag produces a line break in output\n        var result = HtmlToMarkdown.convert(\"<p>First line.<br>Second line.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First line.\"), \"expected to contain: \" + \"First line.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second line.\"), \"expected to contain: \" + \"Second line.\");\n    }\n\n    @Test\n    void testLineBreakHrTag() throws Exception {\n        // hr tag produces a horizontal separator between content\n        var result = HtmlToMarkdown.convert(\"<p>Before rule.</p><hr><p>After rule.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Before rule.\"), \"expected to contain: \" + \"Before rule.\");\n        assertTrue(result.content().orElse(\"\").contains(\"After rule.\"), \"expected to contain: \" + \"After rule.\");\n    }\n\n    @Test\n    void testLineBreakMultipleBr() throws Exception {\n        // Multiple consecutive br tags in sequence\n        var result = HtmlToMarkdown.convert(\"<p>Start.<br><br>End.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Start.\"), \"expected to contain: \" + \"Start.\");\n        assertTrue(result.content().orElse(\"\").contains(\"End.\"), \"expected to contain: \" + \"End.\");\n    }\n\n    @Test\n    void testLinkAnchorFragment() throws Exception {\n        // Fragment-only anchor link is preserved\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"#section\\\">Jump to section</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Jump to section](#section)\"), \"expected to contain: \" + \"[Jump to section](#section)\");\n    }\n\n    @Test\n    void testLinkEmptyHref() throws Exception {\n        // Link with empty href produces output with the link text\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"\\\">No destination</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"No destination\"), \"expected to contain: \" + \"No destination\");\n    }\n\n    @Test\n    void testLinkImageInside() throws Exception {\n        // Image inside a link produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Logo](logo.png)\"), \"expected to contain: \" + \"![Logo](logo.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkMailto() throws Exception {\n        // Mailto link is preserved with mailto: scheme\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"mailto:user@example.com\"), \"expected to contain: \" + \"mailto:user@example.com\");\n    }\n\n    @Test\n    void testLinkSimple() throws Exception {\n        // Simple link\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\">Example</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com)\"), \"expected to contain: \" + \"[Example](https://example.com)\");\n    }\n\n    @Test\n    void testLinkWithBoldText() throws Exception {\n        // Link containing bold text preserves formatting\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**Bold link**\"), \"expected to contain: \" + \"**Bold link**\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkWithTitle() throws Exception {\n        // Link with title attribute\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com\"), \"expected to contain: \" + \"[Example](https://example.com\");\n        assertTrue(result.content().orElse(\"\").contains(\"Example Site\"), \"expected to contain: \" + \"Example Site\");\n    }\n\n    @Test\n    void testListDefinitionDl() throws Exception {\n        // Definition list with dt and dd elements\n        var result = HtmlToMarkdown.convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Term One\"), \"expected to contain: \" + \"Term One\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term one.\"), \"expected to contain: \" + \"Definition of term one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Term Two\"), \"expected to contain: \" + \"Term Two\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term two.\"), \"expected to contain: \" + \"Definition of term two.\");\n    }\n\n    @Test\n    void testListItemMultipleParagraphs() throws Exception {\n        // List item containing multiple paragraphs\n        var result = HtmlToMarkdown.convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph in item.\"), \"expected to contain: \" + \"First paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph in item.\"), \"expected to contain: \" + \"Second paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Simple item\"), \"expected to contain: \" + \"Simple item\");\n    }\n\n    @Test\n    void testListMixedNested() throws Exception {\n        // Mixed list: ordered list nested inside unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Item A\"), \"expected to contain: \" + \"Item A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 1\"), \"expected to contain: \" + \"Sub 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 2\"), \"expected to contain: \" + \"Sub 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Item B\"), \"expected to contain: \" + \"Item B\");\n    }\n\n    @Test\n    void testListNestedOrdered() throws Exception {\n        // Nested ordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1\"), \"expected to contain: \" + \"Step 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1a\"), \"expected to contain: \" + \"Step 1a\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1b\"), \"expected to contain: \" + \"Step 1b\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 2\"), \"expected to contain: \" + \"Step 2\");\n    }\n\n    @Test\n    void testListNestedUnordered() throws Exception {\n        // Nested unordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Parent A\"), \"expected to contain: \" + \"Parent A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A1\"), \"expected to contain: \" + \"Child A1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A2\"), \"expected to contain: \" + \"Child A2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Parent B\"), \"expected to contain: \" + \"Parent B\");\n    }\n\n    @Test\n    void testListTaskCheckboxes() throws Exception {\n        // Task list with checked and unchecked checkboxes\n        var result = HtmlToMarkdown.convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Done task\"), \"expected to contain: \" + \"Done task\");\n        assertTrue(result.content().orElse(\"\").contains(\"Pending task\"), \"expected to contain: \" + \"Pending task\");\n    }\n\n    @Test\n    void testOrderedList() throws Exception {\n        // Ordered list\n        var result = HtmlToMarkdown.convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"1. First\"), \"expected to contain: \" + \"1. First\");\n        assertTrue(result.content().orElse(\"\").contains(\"2. Second\"), \"expected to contain: \" + \"2. Second\");\n        assertTrue(result.content().orElse(\"\").contains(\"3. Third\"), \"expected to contain: \" + \"3. Third\");\n    }\n\n    @Test\n    void testParagraphMultiple() throws Exception {\n        // Multiple paragraphs are separated by a blank line\n        var result = HtmlToMarkdown.convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph.\"), \"expected to contain: \" + \"First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph.\"), \"expected to contain: \" + \"Second paragraph.\");\n    }\n\n    @Test\n    void testParagraphNestedDivs() throws Exception {\n        // Text nested inside divs is extracted correctly\n        var result = HtmlToMarkdown.convert(\"<div><div><p>Nested text</p></div></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Nested text\"), \"expected to contain: \" + \"Nested text\");\n    }\n\n    @Test\n    void testParagraphSimple() throws Exception {\n        // Simple paragraph converts to plain text\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testParagraphWithInlineFormatting() throws Exception {\n        // Paragraph with bold, italic, and a link\n        var result = HtmlToMarkdown.convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n        assertTrue(result.content().orElse(\"\").contains(\"[link](https://example.com)\"), \"expected to contain: \" + \"[link](https://example.com)\");\n    }\n\n    @Test\n    void testParagraphWithLineBreaks() throws Exception {\n        // Paragraph with br tags produces line breaks in output\n        var result = HtmlToMarkdown.convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line one.\"), \"expected to contain: \" + \"Line one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line two.\"), \"expected to contain: \" + \"Line two.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line three.\"), \"expected to contain: \" + \"Line three.\");\n    }\n\n    @Test\n    void testSemanticAbbr() throws Exception {\n        // Abbreviation element text is preserved\n        var result = HtmlToMarkdown.convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"WWW\"), \"expected to contain: \" + \"WWW\");\n    }\n\n    @Test\n    void testSemanticArticle() throws Exception {\n        // Article element wrapping content preserves inner content\n        var result = HtmlToMarkdown.convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Article Title\"), \"expected to contain: \" + \"Article Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Article body.\"), \"expected to contain: \" + \"Article body.\");\n    }\n\n    @Test\n    void testSemanticDefinitionList() throws Exception {\n        // Definition list with term and description\n        var result = HtmlToMarkdown.convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"HTML\"), \"expected to contain: \" + \"HTML\");\n        assertTrue(result.content().orElse(\"\").contains(\"HyperText Markup Language\"), \"expected to contain: \" + \"HyperText Markup Language\");\n        assertTrue(result.content().orElse(\"\").contains(\"CSS\"), \"expected to contain: \" + \"CSS\");\n        assertTrue(result.content().orElse(\"\").contains(\"Cascading Style Sheets\"), \"expected to contain: \" + \"Cascading Style Sheets\");\n    }\n\n    @Test\n    void testSemanticDetailsSummary() throws Exception {\n        // Details and summary elements produce readable output\n        var result = HtmlToMarkdown.convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Click to expand\"), \"expected to contain: \" + \"Click to expand\");\n    }\n\n    @Test\n    void testSemanticHr() throws Exception {\n        // Horizontal rule produces a separator in output\n        var result = HtmlToMarkdown.convert(\"<p>Above</p><hr><p>Below</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Above\"), \"expected to contain: \" + \"Above\");\n        assertTrue(result.content().orElse(\"\").contains(\"Below\"), \"expected to contain: \" + \"Below\");\n    }\n\n    @Test\n    void testSemanticMarkHighlight() throws Exception {\n        // Mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted text\"), \"expected to contain: \" + \"highlighted text\");\n    }\n\n    @Test\n    void testSemanticSectionWithHeading() throws Exception {\n        // Section element with heading preserves structure\n        var result = HtmlToMarkdown.convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Section Heading\"), \"expected to contain: \" + \"Section Heading\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section content.\"), \"expected to contain: \" + \"Section content.\");\n    }\n\n    @Test\n    void testSemanticSubSuperscript() throws Exception {\n        // Subscript and superscript elements are preserved in output\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n        assertTrue(result.content().orElse(\"\").contains(\"E=mc\"), \"expected to contain: \" + \"E=mc\");\n    }\n\n    @Test\n    void testSimpleTable() throws Exception {\n        // Simple table with header\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"Age\"), \"expected to contain: \" + \"Age\");\n        assertTrue(result.content().orElse(\"\").contains(\"Alice\"), \"expected to contain: \" + \"Alice\");\n        assertTrue(result.content().orElse(\"\").contains(\"30\"), \"expected to contain: \" + \"30\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n        assertTrue(result.content().orElse(\"\").contains(\"---\"), \"expected to contain: \" + \"---\");\n    }\n\n    @Test\n    void testTableEmpty() throws Exception {\n        // Empty table produces no output or minimal output\n        var result = HtmlToMarkdown.convert(\"<table></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testTableNoThead() throws Exception {\n        // Table without thead uses first row as implied header\n        var result = HtmlToMarkdown.convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Product\"), \"expected to contain: \" + \"Product\");\n        assertTrue(result.content().orElse(\"\").contains(\"Price\"), \"expected to contain: \" + \"Price\");\n        assertTrue(result.content().orElse(\"\").contains(\"Apple\"), \"expected to contain: \" + \"Apple\");\n        assertTrue(result.content().orElse(\"\").contains(\"1.00\"), \"expected to contain: \" + \"1.00\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTablePipeCharsInContent() throws Exception {\n        // Table cells containing pipe characters are escaped in output\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Expression\"), \"expected to contain: \" + \"Expression\");\n        assertTrue(result.content().orElse(\"\").contains(\"Result\"), \"expected to contain: \" + \"Result\");\n        assertTrue(result.content().orElse(\"\").contains(\"true\"), \"expected to contain: \" + \"true\");\n    }\n\n    @Test\n    void testTableWithAlignment() throws Exception {\n        // Table with column alignment attributes\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Left\"), \"expected to contain: \" + \"Left\");\n        assertTrue(result.content().orElse(\"\").contains(\"Center\"), \"expected to contain: \" + \"Center\");\n        assertTrue(result.content().orElse(\"\").contains(\"Right\"), \"expected to contain: \" + \"Right\");\n        assertTrue(result.content().orElse(\"\").contains(\"L\"), \"expected to contain: \" + \"L\");\n        assertTrue(result.content().orElse(\"\").contains(\"C\"), \"expected to contain: \" + \"C\");\n        assertTrue(result.content().orElse(\"\").contains(\"R\"), \"expected to contain: \" + \"R\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTableWithColspan() throws Exception {\n        // Table with colspan attribute in a header cell\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Full Name\"), \"expected to contain: \" + \"Full Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"John\"), \"expected to contain: \" + \"John\");\n        assertTrue(result.content().orElse(\"\").contains(\"Doe\"), \"expected to contain: \" + \"Doe\");\n    }\n\n    @Test\n    void testUnorderedList() throws Exception {\n        // Unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 1\"), \"expected to contain: \" + \"- Item 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 2\"), \"expected to contain: \" + \"- Item 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 3\"), \"expected to contain: \" + \"- Item 3\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/EdgeCasesTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b6059d439347ea34b6669b35cd190269163d6ba6bee2798c85b8b36fbc1d8515\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.TestVisitor;\nimport dev.kreuzberg.htmltomarkdown.VisitContext;\nimport dev.kreuzberg.htmltomarkdown.VisitResult;\n\n/** E2e tests for category: edge-cases. */\nclass EdgeCasesTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testEmptyHtml() throws Exception {\n        // Empty HTML document\n        var result = HtmlToMarkdown.convert(\"<html><head></head><body></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testEncodingCjkCharacters() throws Exception {\n        // CJK (Chinese, Japanese, Korean) characters are preserved\n        var result = HtmlToMarkdown.convert(\"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"中文内容\"), \"expected to contain: \" + \"中文内容\");\n        assertTrue(result.content().orElse(\"\").contains(\"日本語テキスト\"), \"expected to contain: \" + \"日本語テキスト\");\n        assertTrue(result.content().orElse(\"\").contains(\"한국어 텍스트\"), \"expected to contain: \" + \"한국어 텍스트\");\n    }\n\n    @Test\n    void testEncodingHtmlEntities() throws Exception {\n        // Common HTML entities are decoded in output\n        var result = HtmlToMarkdown.convert(\"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"&\"), \"expected to contain: \" + \"&\");\n        assertTrue(result.content().orElse(\"\").contains(\"<\"), \"expected to contain: \" + \"<\");\n        assertTrue(result.content().orElse(\"\").contains(\">\"), \"expected to contain: \" + \">\");\n    }\n\n    @Test\n    void testEncodingNamedEntities() throws Exception {\n        // Named HTML entities like &mdash; and &hellip; are decoded\n        var result = HtmlToMarkdown.convert(\"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"—\"), \"expected to contain: \" + \"—\");\n        assertTrue(result.content().orElse(\"\").contains(\"…\"), \"expected to contain: \" + \"…\");\n    }\n\n    @Test\n    void testEncodingNumericEntities() throws Exception {\n        // Numeric HTML entities (decimal and hex) are decoded\n        var result = HtmlToMarkdown.convert(\"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"©\"), \"expected to contain: \" + \"©\");\n        assertTrue(result.content().orElse(\"\").contains(\"®\"), \"expected to contain: \" + \"®\");\n        assertTrue(result.content().orElse(\"\").contains(\"€\"), \"expected to contain: \" + \"€\");\n    }\n\n    @Test\n    void testEncodingUnicodeEmoji() throws Exception {\n        // Emoji and Unicode characters are preserved\n        var result = HtmlToMarkdown.convert(\"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"🌍\"), \"expected to contain: \" + \"🌍\");\n        assertTrue(result.content().orElse(\"\").contains(\"🚀\"), \"expected to contain: \" + \"🚀\");\n        assertTrue(result.content().orElse(\"\").contains(\"⭐\"), \"expected to contain: \" + \"⭐\");\n    }\n\n    @Test\n    void testHtmlCommentsOnly() throws Exception {\n        // Document containing only HTML comments produces empty output\n        var result = HtmlToMarkdown.convert(\"<!-- This is a comment --><!-- Another comment -->\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testJustWhitespaceInput() throws Exception {\n        // Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\n        var result = HtmlToMarkdown.convert(\"   \", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testMalformedDeeplyNestedElements() throws Exception {\n        // Deeply nested elements (100 levels) are handled without stack overflow\n        var result = HtmlToMarkdown.convert(\"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Deeply nested content\"), \"expected to contain: \" + \"Deeply nested content\");\n    }\n\n    @Test\n    void testMalformedMissingBlockClosingTags() throws Exception {\n        // Missing closing tags on block elements are auto-closed by parser\n        var result = HtmlToMarkdown.convert(\"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Title\"), \"expected to contain: \" + \"Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph\"), \"expected to contain: \" + \"First paragraph\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph\"), \"expected to contain: \" + \"Second paragraph\");\n    }\n\n    @Test\n    void testMalformedOverlappingTags() throws Exception {\n        // Overlapping bold/italic tags are recovered by the HTML parser without panic\n        var result = HtmlToMarkdown.convert(\"<p><b><i>bold and italic</b></i></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"bold and italic\"), \"expected to contain: \" + \"bold and italic\");\n    }\n\n    @Test\n    void testMalformedUnclosedParagraph() throws Exception {\n        // Unclosed <p> tag is recovered gracefully and content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>This paragraph is never closed\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"This paragraph is never closed\"), \"expected to contain: \" + \"This paragraph is never closed\");\n    }\n\n    @Test\n    void testScriptTagsOnly() throws Exception {\n        // Document with only script tags produces empty output (scripts are stripped)\n        var result = HtmlToMarkdown.convert(\"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testStyleTagsOnly() throws Exception {\n        // Document with only style tags produces empty output (styles are stripped)\n        var result = HtmlToMarkdown.convert(\"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testVisitorCustomElementWithNesting() throws Exception {\n        // Visitor handles custom elements with nested content\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitCustomElement(VisitContext ctx, String tagName, String html) {\n                return VisitResult.custom(\"[CUSTOM WIDGET]\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<div><custom-widget data-value=\\\"123\\\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[CUSTOM WIDGET]\"), \"expected to contain: \" + \"[CUSTOM WIDGET]\");\n        assertFalse(result.content().orElse(\"\").contains(\"Widget content here\"), \"expected NOT to contain: \" + \"Widget content here\");\n    }\n\n    @Test\n    void testVisitorDeeplyNestedSkip() throws Exception {\n        // Visitor skips deeply nested elements\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitMark(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Outer\"), \"expected to contain: \" + \"Outer\");\n        assertTrue(result.content().orElse(\"\").contains(\"text\"), \"expected to contain: \" + \"text\");\n        assertFalse(result.content().orElse(\"\").contains(\"highlight\"), \"expected NOT to contain: \" + \"highlight\");\n    }\n\n    @Test\n    void testVisitorElementEndModification() throws Exception {\n        // Visitor modifies element at end after children processed\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitElementEnd(VisitContext ctx, String output) {\n                return VisitResult.custom(\"MODIFIED OUTPUT\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Original quote</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n    }\n\n    @Test\n    void testVisitorElementStartSkipEntireSubtree() throws Exception {\n        // Visitor skips at element_start level removes entire subtree\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitElementStart(VisitContext ctx) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<div><h1>Title</h1><p>Content</p></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n    }\n\n    @Test\n    void testVisitorUnknownTagPreservation() throws Exception {\n        // Visitor preserves unknown HTML tags as raw HTML\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitCustomElement(VisitContext ctx, String tagName, String html) {\n                return VisitResult.preserveHtml();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Article text\"), \"expected to contain: \" + \"Article text\");\n        assertTrue(result.content().orElse(\"\").contains(\"More article text\"), \"expected to contain: \" + \"More article text\");\n        assertTrue(result.content().orElse(\"\").contains(\"<x-custom>\"), \"expected to contain: \" + \"<x-custom>\");\n    }\n\n    @Test\n    void testWhitespaceOnly() throws Exception {\n        // Whitespace-only content\n        var result = HtmlToMarkdown.convert(\"<p>   </p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testXssOnclickHandlerRemoved() throws Exception {\n        // onclick and other on* event handlers are removed from elements\n        var result = HtmlToMarkdown.convert(\"<p><a href=\\\"https://example.com\\\" onclick=\\\"alert('xss')\\\">Click me</a></p><button onmouseover=\\\"steal_data()\\\">Hover me</button>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Click me\"), \"expected to contain: \" + \"Click me\");\n    }\n\n    @Test\n    void testXssScriptTagStripped() throws Exception {\n        // Script tag content is stripped and does not appear in output\n        var result = HtmlToMarkdown.convert(\"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Safe content\"), \"expected to contain: \" + \"Safe content\");\n        assertTrue(result.content().orElse(\"\").contains(\"More safe content\"), \"expected to contain: \" + \"More safe content\");\n    }\n\n    @Test\n    void testXssSvgNestedScriptStripped() throws Exception {\n        // Script tags nested inside SVG are stripped\n        var result = HtmlToMarkdown.convert(\"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Before SVG\"), \"expected to contain: \" + \"Before SVG\");\n        assertTrue(result.content().orElse(\"\").contains(\"After SVG\"), \"expected to contain: \" + \"After SVG\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/MetadataTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b31efb4bb3d9922ad5ab19c012f78ab712e959f3581bb9886a3d383b6466ad85\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: metadata. */\nclass MetadataTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testMetadataAuthorMeta() throws Exception {\n        // Extract author from <meta name='author'> tag\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Page</title><meta name=\\\"author\\\" content=\\\"Jane Doe\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"Jane Doe\", result.metadata().document().author().orElse(\"\").trim());\n    }\n\n    @Test\n    void testMetadataCanonicalUrl() throws Exception {\n        // Extract canonical URL from <link rel='canonical'> tag\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Page</title><link rel=\\\"canonical\\\" href=\\\"https://example.com/canonical-page\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"https://example.com/canonical-page\", result.metadata().document().canonicalUrl().orElse(\"\").trim());\n    }\n\n    @Test\n    void testMetadataDescriptionMeta() throws Exception {\n        // Extract description from <meta name='description'> tag\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Page</title><meta name=\\\"description\\\" content=\\\"This is the page description.\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"This is the page description.\", result.metadata().document().description().orElse(\"\").trim());\n    }\n\n    @Test\n    void testMetadataDublinCore() throws Exception {\n        // Extract Dublin Core metadata tags\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Scholarly Work</title><meta name=\\\"DC.title\\\" content=\\\"Principles of Knowledge Management\\\"><meta name=\\\"DC.creator\\\" content=\\\"Dr. Alice Johnson\\\"><meta name=\\\"DC.date\\\" content=\\\"2023-06-15\\\"><meta name=\\\"DC.subject\\\" content=\\\"Knowledge Management\\\"><meta name=\\\"DC.publisher\\\" content=\\\"Academic Press\\\"></head><body><p>This is a scholarly article.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"scholarly article\"), \"expected to contain: \" + \"scholarly article\");\n    }\n\n    @Test\n    void testMetadataExtractAllImages() throws Exception {\n        // Extract all images from a document into metadata\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Gallery</title></head><body><img src=\\\"https://example.com/photo1.jpg\\\" alt=\\\"Photo 1\\\"><img src=\\\"https://example.com/photo2.png\\\" alt=\\\"Photo 2\\\"><img src=\\\"/local/image.webp\\\" alt=\\\"Local image\\\"></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.metadata().images().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testMetadataExtractAllLinks() throws Exception {\n        // Extract all links from a document into metadata\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Links Page</title></head><body><p>Visit <a href=\\\"https://example.com\\\">Example</a> or <a href=\\\"https://docs.example.com\\\">Docs</a>.</p><p>Also see <a href=\\\"/relative/path\\\">relative link</a> and <a href=\\\"mailto:hello@example.com\\\">email us</a>.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.metadata().links().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testMetadataHeadersHierarchy() throws Exception {\n        // Extract heading hierarchy from document into metadata\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.metadata().headers().size() >= 5, \"expected at least 5 elements\");\n    }\n\n    @Test\n    void testMetadataKeywordsMeta() throws Exception {\n        // Extract keywords from <meta name='keywords'> tag\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Page</title><meta name=\\\"keywords\\\" content=\\\"rust, markdown, html, converter\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.metadata().document().keywords().size() >= 1, \"expected at least 1 elements\");\n    }\n\n    @Test\n    void testMetadataLangAttribute() throws Exception {\n        // Extract language from html lang attribute\n        var result = HtmlToMarkdown.convert(\"<html lang=\\\"es\\\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Hola Mundo\"), \"expected to contain: \" + \"Hola Mundo\");\n    }\n\n    @Test\n    void testMetadataMicrodataSchemaArticle() throws Exception {\n        // Extract schema.org microdata for Article\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Article</title></head><body><article itemscope itemtype=\\\"https://schema.org/Article\\\"><h1 itemprop=\\\"headline\\\">Breaking News Today</h1><span itemprop=\\\"author\\\">Jane Reporter</span><span itemprop=\\\"datePublished\\\">2024-04-22</span><div itemprop=\\\"articleBody\\\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Breaking News Today\"), \"expected to contain: \" + \"Breaking News Today\");\n        assertTrue(result.content().orElse(\"\").contains(\"Jane Reporter\"), \"expected to contain: \" + \"Jane Reporter\");\n        assertTrue(result.content().orElse(\"\").contains(\"important information\"), \"expected to contain: \" + \"important information\");\n    }\n\n    @Test\n    void testMetadataMicrodataSchemaBreadcrumb() throws Exception {\n        // Extract schema.org breadcrumb navigation microdata\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\\\"https://schema.org/BreadcrumbList\\\"><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com\\\"><span itemprop=\\\"name\\\">Home</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com/products\\\"><span itemprop=\\\"name\\\">Products</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><span itemprop=\\\"name\\\">Current Page</span></span></nav></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Home\"), \"expected to contain: \" + \"Home\");\n        assertTrue(result.content().orElse(\"\").contains(\"Products\"), \"expected to contain: \" + \"Products\");\n        assertTrue(result.content().orElse(\"\").contains(\"Current Page\"), \"expected to contain: \" + \"Current Page\");\n    }\n\n    @Test\n    void testMetadataMicrodataSchemaOrganization() throws Exception {\n        // Extract schema.org microdata for Organization\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Company</title></head><body><div itemscope itemtype=\\\"https://schema.org/Organization\\\"><span itemprop=\\\"name\\\">Acme Corp</span><span itemprop=\\\"foundingDate\\\">2020</span><span itemprop=\\\"url\\\">https://acmecorp.example.com</span><span itemprop=\\\"logo\\\">https://acmecorp.example.com/logo.png</span></div></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Acme Corp\"), \"expected to contain: \" + \"Acme Corp\");\n        assertTrue(result.content().orElse(\"\").contains(\"2020\"), \"expected to contain: \" + \"2020\");\n    }\n\n    @Test\n    void testMetadataMicrodataSchemaPerson() throws Exception {\n        // Extract schema.org microdata for Person\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Contact</title></head><body><div itemscope itemtype=\\\"https://schema.org/Person\\\"><span itemprop=\\\"name\\\">John Smith</span><span itemprop=\\\"email\\\">john@example.com</span><span itemprop=\\\"telephone\\\">+1-555-0100</span></div></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"John Smith\"), \"expected to contain: \" + \"John Smith\");\n        assertTrue(result.content().orElse(\"\").contains(\"john@example.com\"), \"expected to contain: \" + \"john@example.com\");\n    }\n\n    @Test\n    void testMetadataMicrodataSchemaProduct() throws Exception {\n        // Extract schema.org microdata for Product\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Product</title></head><body><div itemscope itemtype=\\\"https://schema.org/Product\\\"><h1 itemprop=\\\"name\\\">Awesome Widget</h1><span itemprop=\\\"description\\\">The best widget on the market</span><span itemprop=\\\"price\\\">29.99</span><span itemprop=\\\"priceCurrency\\\">USD</span><img itemprop=\\\"image\\\" src=\\\"widget.jpg\\\" alt=\\\"Widget\\\"><span itemprop=\\\"ratingValue\\\">4.5</span></div></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Awesome Widget\"), \"expected to contain: \" + \"Awesome Widget\");\n        assertTrue(result.content().orElse(\"\").contains(\"best widget\"), \"expected to contain: \" + \"best widget\");\n        assertTrue(result.content().orElse(\"\").contains(\"29.99\"), \"expected to contain: \" + \"29.99\");\n    }\n\n    @Test\n    void testMetadataTextDirectionLtr() throws Exception {\n        // Extract text direction from lang attribute on html element\n        var result = HtmlToMarkdown.convert(\"<html lang=\\\"en\\\" dir=\\\"ltr\\\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"left-to-right text\"), \"expected to contain: \" + \"left-to-right text\");\n    }\n\n    @Test\n    void testMetadataTextDirectionRtl() throws Exception {\n        // Extract right-to-left text direction\n        var result = HtmlToMarkdown.convert(\"<html lang=\\\"ar\\\" dir=\\\"rtl\\\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"right-to-left text\"), \"expected to contain: \" + \"right-to-left text\");\n    }\n\n    @Test\n    void testMetadataTitleTag() throws Exception {\n        // Extract title from <title> tag\n        var result = HtmlToMarkdown.convert(\"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"My Page\", result.metadata().document().title().orElse(\"\").trim());\n    }\n\n    @Test\n    void testOgBasicTags() throws Exception {\n        // Extract og:title, og:description, and og:image from Open Graph meta tags\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Fallback Title</title><meta property=\\\"og:title\\\" content=\\\"OG Title\\\"><meta property=\\\"og:description\\\" content=\\\"OG description text.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/image.jpg\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"OG Title\", result.metadata().document().openGraph().get(\"title\").trim());\n        assertEquals(\"OG description text.\", result.metadata().document().openGraph().get(\"description\").trim());\n        assertEquals(\"https://example.com/image.jpg\", result.metadata().document().openGraph().get(\"image\").trim());\n    }\n\n    @Test\n    void testOgMultipleTags() throws Exception {\n        // Extract multiple Open Graph tags including type, url, and site_name\n        var result = HtmlToMarkdown.convert(\"<html><head><meta property=\\\"og:title\\\" content=\\\"Article Title\\\"><meta property=\\\"og:type\\\" content=\\\"article\\\"><meta property=\\\"og:url\\\" content=\\\"https://example.com/article\\\"><meta property=\\\"og:site_name\\\" content=\\\"Example Site\\\"><meta property=\\\"og:description\\\" content=\\\"An interesting article.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/article.jpg\\\"></head><body><article><p>Article content here.</p></article></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"Article Title\", result.metadata().document().openGraph().get(\"title\").trim());\n        assertEquals(\"article\", result.metadata().document().openGraph().get(\"type\").trim());\n        assertEquals(\"https://example.com/article\", result.metadata().document().openGraph().get(\"url\").trim());\n        assertEquals(\"Example Site\", result.metadata().document().openGraph().get(\"site_name\").trim());\n    }\n\n    @Test\n    void testStructuredDataJsonLd() throws Exception {\n        // JSON-LD script tag is stripped from output (security) but metadata may be extracted\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Article</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Article\\\",\\\"headline\\\":\\\"My Article\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Jane Doe\\\"},\\\"datePublished\\\":\\\"2024-01-15\\\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"My Article\"), \"expected to contain: \" + \"My Article\");\n    }\n\n    @Test\n    void testStructuredDataMultipleJsonLd() throws Exception {\n        // Multiple JSON-LD blocks are all stripped from output\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Shop Page</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Product\\\",\\\"name\\\":\\\"Widget\\\",\\\"price\\\":\\\"9.99\\\"}</script><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"BreadcrumbList\\\",\\\"itemListElement\\\":[{\\\"@type\\\":\\\"ListItem\\\",\\\"position\\\":1,\\\"name\\\":\\\"Home\\\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Widget\"), \"expected to contain: \" + \"Widget\");\n    }\n\n    @Test\n    void testTwitterCardTags() throws Exception {\n        // Extract Twitter card meta tags\n        var result = HtmlToMarkdown.convert(\"<html><head><meta name=\\\"twitter:card\\\" content=\\\"summary_large_image\\\"><meta name=\\\"twitter:site\\\" content=\\\"@examplesite\\\"><meta name=\\\"twitter:title\\\" content=\\\"Twitter Card Title\\\"><meta name=\\\"twitter:description\\\" content=\\\"Twitter card description.\\\"><meta name=\\\"twitter:image\\\" content=\\\"https://example.com/twitter-image.jpg\\\"></head><body><p>Content</p></body></html>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"summary_large_image\", result.metadata().document().twitterCard().get(\"card\").trim());\n        assertEquals(\"Twitter Card Title\", result.metadata().document().twitterCard().get(\"title\").trim());\n        assertEquals(\"Twitter card description.\", result.metadata().document().twitterCard().get(\"description\").trim());\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/OptionsTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2abeda09d96144d4f0c0d1d47b9cfd278c94f79c8f4f5d5148feff0c4c39580f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: options. */\nclass OptionsTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testOptionsAutolinksFalse() throws Exception {\n        // Bare URL links rendered as regular markdown links when autolinks disabled\n        var options = MAPPER.readValue(\"{\\\"autolinks\\\":false}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p><a href='https://example.com'>https://example.com</a></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"example.com\"), \"expected to contain: \" + \"example.com\");\n    }\n\n    @Test\n    void testOptionsBrInTablesFalse() throws Exception {\n        // BR elements in table cells are stripped when disabled\n        var options = MAPPER.readValue(\"{\\\"br_in_tables\\\":false}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Col\"), \"expected to contain: \" + \"Col\");\n    }\n\n    @Test\n    void testOptionsBrInTablesTrue() throws Exception {\n        // BR elements in table cells render as line breaks\n        var options = MAPPER.readValue(\"{\\\"br_in_tables\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Header\"), \"expected to contain: \" + \"Header\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line 1\"), \"expected to contain: \" + \"Line 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line 2\"), \"expected to contain: \" + \"Line 2\");\n    }\n\n    @Test\n    void testOptionsCodeBlockBackticks() throws Exception {\n        // Backticks code block style uses triple backtick fences\n        var options = MAPPER.readValue(\"{\\\"code_block_style\\\":\\\"Backticks\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"```\"), \"expected to contain: \" + \"```\");\n        assertTrue(result.content().orElse(\"\").contains(\"console.log('hi');\"), \"expected to contain: \" + \"console.log('hi');\");\n    }\n\n    @Test\n    void testOptionsCodeBlockIndented() throws Exception {\n        // Code blocks use 4-space indentation\n        var options = MAPPER.readValue(\"{\\\"code_block_style\\\":\\\"Indented\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<pre><code>print('hello')</code></pre>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"print('hello')\"), \"expected to contain: \" + \"print('hello')\");\n        assertFalse(result.content().orElse(\"\").contains(\"```\"), \"expected NOT to contain: \" + \"```\");\n    }\n\n    @Test\n    void testOptionsCodeBlockTildes() throws Exception {\n        // Code blocks use tilde fences\n        var options = MAPPER.readValue(\"{\\\"code_block_style\\\":\\\"Tildes\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<pre><code>let x = 1;</code></pre>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"~~~\"), \"expected to contain: \" + \"~~~\");\n        assertTrue(result.content().orElse(\"\").contains(\"let x = 1;\"), \"expected to contain: \" + \"let x = 1;\");\n    }\n\n    @Test\n    void testOptionsCodeBlockTildesStyle() throws Exception {\n        // Tildes code block style uses triple tilde fences\n        var options = MAPPER.readValue(\"{\\\"code_block_style\\\":\\\"Tildes\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<pre><code>some code</code></pre>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"~~~\"), \"expected to contain: \" + \"~~~\");\n        assertTrue(result.content().orElse(\"\").contains(\"some code\"), \"expected to contain: \" + \"some code\");\n    }\n\n    @Test\n    void testOptionsCodeLanguagePython() throws Exception {\n        // Default code language annotation on blocks without lang attribute\n        var options = MAPPER.readValue(\"{\\\"code_language\\\":\\\"python\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<pre><code>def hello(): pass</code></pre>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"```python\"), \"expected to contain: \" + \"```python\");\n        assertTrue(result.content().orElse(\"\").contains(\"def hello\"), \"expected to contain: \" + \"def hello\");\n    }\n\n    @Test\n    void testOptionsConvertAsInline() throws Exception {\n        // Block elements treated as inline\n        var options = MAPPER.readValue(\"{\\\"convert_as_inline\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>One</p><p>Two</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"One\"), \"expected to contain: \" + \"One\");\n        assertTrue(result.content().orElse(\"\").contains(\"Two\"), \"expected to contain: \" + \"Two\");\n    }\n\n    @Test\n    void testOptionsDebugTrue() throws Exception {\n        // Debug mode enabled does not crash and produces output\n        var options = MAPPER.readValue(\"{\\\"debug\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Debug test</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Debug test\"), \"expected to contain: \" + \"Debug test\");\n    }\n\n    @Test\n    void testOptionsDefaultTitleTrue() throws Exception {\n        // Links without title get empty title attribute when defaultTitle is true\n        var options = MAPPER.readValue(\"{\\\"default_title\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p><a href='https://example.com'>Link</a></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Link\"), \"expected to contain: \" + \"Link\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testOptionsEncodingUtf8() throws Exception {\n        // UTF-8 encoding hint for special characters\n        var options = MAPPER.readValue(\"{\\\"encoding\\\":\\\"utf-8\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Café naïve résumé</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n    }\n\n    @Test\n    void testOptionsEscapeAsciiEnabled() throws Exception {\n        // ASCII Markdown characters are escaped when escapeAscii is true\n        var options = MAPPER.readValue(\"{\\\"escape_ascii\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text with # hash and [brackets] and * star</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Text\"), \"expected to contain: \" + \"Text\");\n        assertTrue(result.content().orElse(\"\").contains(\"hash\"), \"expected to contain: \" + \"hash\");\n        assertTrue(result.content().orElse(\"\").contains(\"brackets\"), \"expected to contain: \" + \"brackets\");\n        assertTrue(result.content().orElse(\"\").contains(\"star\"), \"expected to contain: \" + \"star\");\n    }\n\n    @Test\n    void testOptionsEscapeAsterisks() throws Exception {\n        // escape_asterisks option escapes asterisks in plain text\n        var options = MAPPER.readValue(\"{\\\"escape_asterisks\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Use 2*3 = 6 in math.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"3\"), \"expected to contain: \" + \"3\");\n        assertTrue(result.content().orElse(\"\").contains(\"6\"), \"expected to contain: \" + \"6\");\n    }\n\n    @Test\n    void testOptionsEscapeMisc() throws Exception {\n        // escape_misc option escapes miscellaneous markdown characters\n        var options = MAPPER.readValue(\"{\\\"escape_misc\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Use # and | and ~ in text.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Use\"), \"expected to contain: \" + \"Use\");\n        assertTrue(result.content().orElse(\"\").contains(\"and\"), \"expected to contain: \" + \"and\");\n        assertTrue(result.content().orElse(\"\").contains(\"in text.\"), \"expected to contain: \" + \"in text.\");\n    }\n\n    @Test\n    void testOptionsEscapeUnderscores() throws Exception {\n        // escape_underscores option escapes underscores in plain text\n        var options = MAPPER.readValue(\"{\\\"escape_underscores\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>The variable_name is defined.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"variable\"), \"expected to contain: \" + \"variable\");\n        assertTrue(result.content().orElse(\"\").contains(\"name\"), \"expected to contain: \" + \"name\");\n        assertTrue(result.content().orElse(\"\").contains(\"defined.\"), \"expected to contain: \" + \"defined.\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsAttribute() throws Exception {\n        // Elements matching CSS attribute selector are excluded entirely\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\"[role='complementary']\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><div role=\\\"complementary\\\">Sidebar</div><p>Primary text</p></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Primary text\"), \"expected to contain: \" + \"Primary text\");\n        assertFalse(result.content().orElse(\"\").contains(\"Sidebar\"), \"expected NOT to contain: \" + \"Sidebar\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsClass() throws Exception {\n        // Elements matching CSS class selector are excluded entirely\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\".cookie-banner\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><div class=\\\"cookie-banner\\\">Accept cookies</div><p>Main content</p></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Main content\"), \"expected to contain: \" + \"Main content\");\n        assertFalse(result.content().orElse(\"\").contains(\"cookies\"), \"expected NOT to contain: \" + \"cookies\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsEmptyNoop() throws Exception {\n        // Empty exclude_selectors list does not affect output\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Hello world</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Hello world\"), \"expected to contain: \" + \"Hello world\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsId() throws Exception {\n        // Elements matching CSS id selector are excluded entirely\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\"#ad-container\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><div id=\\\"ad-container\\\">Buy stuff</div><p>Article text</p></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Article text\"), \"expected to contain: \" + \"Article text\");\n        assertFalse(result.content().orElse(\"\").contains(\"Buy stuff\"), \"expected NOT to contain: \" + \"Buy stuff\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsMultiple() throws Exception {\n        // Multiple CSS selectors each exclude their matched elements\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\".nav\\\",\\\"footer\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><nav class=\\\"nav\\\">Menu</nav><p>Content</p><footer>Footer</footer></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Content\"), \"expected to contain: \" + \"Content\");\n        assertFalse(result.content().orElse(\"\").contains(\"Menu\"), \"expected NOT to contain: \" + \"Menu\");\n        assertFalse(result.content().orElse(\"\").contains(\"Footer\"), \"expected NOT to contain: \" + \"Footer\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsNestedContentDropped() throws Exception {\n        // All descendants of excluded elements are dropped\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\".sidebar\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><aside class=\\\"sidebar\\\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Main text\"), \"expected to contain: \" + \"Main text\");\n        assertFalse(result.content().orElse(\"\").contains(\"Related\"), \"expected NOT to contain: \" + \"Related\");\n        assertFalse(result.content().orElse(\"\").contains(\"Sidebar text\"), \"expected NOT to contain: \" + \"Sidebar text\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsPlainTextMode() throws Exception {\n        // Exclude selectors work in plain text output mode\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\".nav\\\"],\\\"output_format\\\":\\\"Plain\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><div class=\\\"nav\\\">Navigation</div><p>Article body</p></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Article body\"), \"expected to contain: \" + \"Article body\");\n        assertFalse(result.content().orElse(\"\").contains(\"Navigation\"), \"expected NOT to contain: \" + \"Navigation\");\n    }\n\n    @Test\n    void testOptionsExcludeSelectorsVsStripTags() throws Exception {\n        // exclude_selectors drops entire subtree unlike strip_tags which keeps children\n        var options = MAPPER.readValue(\"{\\\"exclude_selectors\\\":[\\\".wrapper\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<body><div class=\\\"wrapper\\\"><p>Inner paragraph</p></div><p>Outer text</p></body>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Outer text\"), \"expected to contain: \" + \"Outer text\");\n        assertFalse(result.content().orElse(\"\").contains(\"Inner paragraph\"), \"expected NOT to contain: \" + \"Inner paragraph\");\n    }\n\n    @Test\n    void testOptionsExtractMetadataTrue() throws Exception {\n        // Extract metadata returns document metadata when enabled\n        var options = MAPPER.readValue(\"{\\\"extract_metadata\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(\"Test Page\", result.metadata().document().title().orElse(\"\").trim());\n        assertEquals(\"A test page\", result.metadata().document().description().orElse(\"\").trim());\n    }\n\n    @Test\n    void testOptionsHeadingStyleAtx() throws Exception {\n        // ATX heading style produces hash-prefixed headings\n        var options = MAPPER.readValue(\"{\\\"heading_style\\\":\\\"Atx\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><h2>Subtitle</h2>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"# Title\"), \"expected to contain: \" + \"# Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"## Subtitle\"), \"expected to contain: \" + \"## Subtitle\");\n    }\n\n    @Test\n    void testOptionsHeadingStyleAtxClosed() throws Exception {\n        // ATX closed heading style adds closing hashes\n        var options = MAPPER.readValue(\"{\\\"heading_style\\\":\\\"AtxClosed\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Closed Heading</h1>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"# Closed Heading #\"), \"expected to contain: \" + \"# Closed Heading #\");\n    }\n\n    @Test\n    void testOptionsHeadingStyleUnderlined() throws Exception {\n        // Underlined heading style produces setext-style headings for h1 and h2\n        var options = MAPPER.readValue(\"{\\\"heading_style\\\":\\\"Underlined\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Main Title</h1>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Main Title\"), \"expected to contain: \" + \"Main Title\");\n    }\n\n    @Test\n    void testOptionsHighlightBold() throws Exception {\n        // Mark tag rendered as bold\n        var options = MAPPER.readValue(\"{\\\"highlight_style\\\":\\\"Bold\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text with <mark>highlighted</mark> text.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"**highlighted**\"), \"expected to contain: \" + \"**highlighted**\");\n    }\n\n    @Test\n    void testOptionsHighlightDoubleEqual() throws Exception {\n        // Mark tag with double equal highlight style\n        var options = MAPPER.readValue(\"{\\\"highlight_style\\\":\\\"DoubleEqual\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text with <mark>highlighted</mark> here.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"==highlighted==\"), \"expected to contain: \" + \"==highlighted==\");\n    }\n\n    @Test\n    void testOptionsHighlightNone() throws Exception {\n        // Mark tag with no highlight style strips the mark\n        var options = MAPPER.readValue(\"{\\\"highlight_style\\\":\\\"None\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text with <mark>plain</mark> content.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"plain\"), \"expected to contain: \" + \"plain\");\n        assertFalse(result.content().orElse(\"\").contains(\"==\"), \"expected NOT to contain: \" + \"==\");\n    }\n\n    @Test\n    void testOptionsKeepInlineImagesInParagraph() throws Exception {\n        // Images inside specified tags stay inline\n        var options = MAPPER.readValue(\"{\\\"keep_inline_images_in\\\":[\\\"p\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text <img src='icon.png' alt='icon'> more text</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Text\"), \"expected to contain: \" + \"Text\");\n        assertTrue(result.content().orElse(\"\").contains(\"more text\"), \"expected to contain: \" + \"more text\");\n    }\n\n    @Test\n    void testOptionsLinkStyleReference() throws Exception {\n        // Links use reference-style formatting\n        var options = MAPPER.readValue(\"{\\\"link_style\\\":\\\"Reference\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Example\"), \"expected to contain: \" + \"Example\");\n        assertTrue(result.content().orElse(\"\").contains(\"Other\"), \"expected to contain: \" + \"Other\");\n        assertTrue(result.content().orElse(\"\").contains(\"example.com\"), \"expected to contain: \" + \"example.com\");\n    }\n\n    @Test\n    void testOptionsListCustomBullets() throws Exception {\n        // Custom bullet character for unordered lists\n        var options = MAPPER.readValue(\"{\\\"bullets\\\":\\\"*\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item A</li><li>Item B</li></ul>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"* Item A\"), \"expected to contain: \" + \"* Item A\");\n        assertTrue(result.content().orElse(\"\").contains(\"* Item B\"), \"expected to contain: \" + \"* Item B\");\n    }\n\n    @Test\n    void testOptionsListIndentTabs() throws Exception {\n        // Tab indentation type for nested list items\n        var options = MAPPER.readValue(\"{\\\"list_indent_type\\\":\\\"Tabs\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Parent\"), \"expected to contain: \" + \"Parent\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child\"), \"expected to contain: \" + \"Child\");\n    }\n\n    @Test\n    void testOptionsListIndentWidthFour() throws Exception {\n        // Nested lists indented with 4 spaces per level\n        var options = MAPPER.readValue(\"{\\\"list_indent_width\\\":4}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Outer\"), \"expected to contain: \" + \"Outer\");\n        assertTrue(result.content().orElse(\"\").contains(\"Inner\"), \"expected to contain: \" + \"Inner\");\n    }\n\n    @Test\n    void testOptionsMaxDepthDefaultUnlimited() throws Exception {\n        // Default max_depth (null) converts deeply nested content fully\n        var result = HtmlToMarkdown.convert(\"<div><div><div><div><p>Deep content</p></div></div></div></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Deep content\"), \"expected to contain: \" + \"Deep content\");\n    }\n\n    @Test\n    void testOptionsMaxDepthTruncates() throws Exception {\n        // max_depth truncates content beyond the specified depth\n        var options = MAPPER.readValue(\"{\\\"max_depth\\\":3}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Shallow\"), \"expected to contain: \" + \"Shallow\");\n        assertFalse(result.content().orElse(\"\").contains(\"Too deep\"), \"expected NOT to contain: \" + \"Too deep\");\n    }\n\n    @Test\n    void testOptionsMaxDepthZeroEmpty() throws Exception {\n        // max_depth of 0 produces empty output\n        var options = MAPPER.readValue(\"{\\\"max_depth\\\":0}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Hello</p>\", options);\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testOptionsNewlineBackslash() throws Exception {\n        // Hard line breaks rendered with backslash\n        var options = MAPPER.readValue(\"{\\\"newline_style\\\":\\\"Backslash\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Line one<br>Line two</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Line one\"), \"expected to contain: \" + \"Line one\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line two\"), \"expected to contain: \" + \"Line two\");\n    }\n\n    @Test\n    void testOptionsNewlineSpaces() throws Exception {\n        // Hard line breaks rendered with trailing spaces\n        var options = MAPPER.readValue(\"{\\\"newline_style\\\":\\\"Spaces\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>First<br>Second</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"First\"), \"expected to contain: \" + \"First\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second\"), \"expected to contain: \" + \"Second\");\n    }\n\n    @Test\n    void testOptionsOutputFormatDjot() throws Exception {\n        // Djot output format produces djot-compatible markup\n        var options = MAPPER.readValue(\"{\\\"output_format\\\":\\\"Djot\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Simple paragraph.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Simple paragraph.\"), \"expected to contain: \" + \"Simple paragraph.\");\n    }\n\n    @Test\n    void testOptionsOutputFormatMarkdown() throws Exception {\n        // Default markdown output format produces standard markdown\n        var options = MAPPER.readValue(\"{\\\"heading_style\\\":\\\"Atx\\\",\\\"output_format\\\":\\\"Markdown\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>Some text.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"# Title\"), \"expected to contain: \" + \"# Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Some text.\"), \"expected to contain: \" + \"Some text.\");\n    }\n\n    @Test\n    void testOptionsOutputFormatPlain() throws Exception {\n        // Plain text output format strips markdown syntax\n        var options = MAPPER.readValue(\"{\\\"output_format\\\":\\\"Plain\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Title\"), \"expected to contain: \" + \"Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"bold\"), \"expected to contain: \" + \"bold\");\n        assertTrue(result.content().orElse(\"\").contains(\"text.\"), \"expected to contain: \" + \"text.\");\n    }\n\n    @Test\n    void testOptionsPreprocessingAggressive() throws Exception {\n        // Aggressive preset removes nav, footer, aside unconditionally\n        var options = MAPPER.readValue(\"{\\\"preprocessing\\\":{\\\"preset\\\":\\\"Aggressive\\\"}}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Title\"), \"expected to contain: \" + \"Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Content\"), \"expected to contain: \" + \"Content\");\n        assertFalse(result.content().orElse(\"\").contains(\"Menu\"), \"expected NOT to contain: \" + \"Menu\");\n    }\n\n    @Test\n    void testOptionsPreprocessingMinimal() throws Exception {\n        // Minimal preset preserves nav, footer, aside\n        var options = MAPPER.readValue(\"{\\\"preprocessing\\\":{\\\"preset\\\":\\\"Minimal\\\"}}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Navigation\"), \"expected to contain: \" + \"Navigation\");\n        assertTrue(result.content().orElse(\"\").contains(\"Content\"), \"expected to contain: \" + \"Content\");\n        assertTrue(result.content().orElse(\"\").contains(\"Footer\"), \"expected to contain: \" + \"Footer\");\n    }\n\n    @Test\n    void testOptionsPreprocessingRemoveForms() throws Exception {\n        // Forms are removed when remove_forms is true\n        var options = MAPPER.readValue(\"{\\\"preprocessing\\\":{\\\"remove_forms\\\":true}}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Before\"), \"expected to contain: \" + \"Before\");\n        assertTrue(result.content().orElse(\"\").contains(\"After\"), \"expected to contain: \" + \"After\");\n        assertFalse(result.content().orElse(\"\").contains(\"Submit\"), \"expected NOT to contain: \" + \"Submit\");\n    }\n\n    @Test\n    void testOptionsPreserveTagsIframe() throws Exception {\n        // Iframe tags preserved as raw HTML in output\n        var options = MAPPER.readValue(\"{\\\"preserve_tags\\\":[\\\"iframe\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Before\"), \"expected to contain: \" + \"Before\");\n        assertTrue(result.content().orElse(\"\").contains(\"After\"), \"expected to contain: \" + \"After\");\n        assertTrue(result.content().orElse(\"\").contains(\"<iframe\"), \"expected to contain: \" + \"<iframe\");\n    }\n\n    @Test\n    void testOptionsSkipImagesTrue() throws Exception {\n        // Images are omitted from output when skipImages is true\n        var options = MAPPER.readValue(\"{\\\"skip_images\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Before <img src='test.jpg' alt='photo'> After</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Before\"), \"expected to contain: \" + \"Before\");\n        assertTrue(result.content().orElse(\"\").contains(\"After\"), \"expected to contain: \" + \"After\");\n        assertFalse(result.content().orElse(\"\").contains(\"photo\"), \"expected NOT to contain: \" + \"photo\");\n    }\n\n    @Test\n    void testOptionsStripNewlines() throws Exception {\n        // Strip newlines produces single-line paragraphs\n        var options = MAPPER.readValue(\"{\\\"strip_newlines\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph.\"), \"expected to contain: \" + \"First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph.\"), \"expected to contain: \" + \"Second paragraph.\");\n    }\n\n    @Test\n    void testOptionsStripTagsDivSpan() throws Exception {\n        // Div and span tags stripped but content preserved\n        var options = MAPPER.readValue(\"{\\\"strip_tags\\\":[\\\"div\\\",\\\"span\\\"]}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"Inside div\"), \"expected to contain: \" + \"Inside div\");\n        assertTrue(result.content().orElse(\"\").contains(\"span text\"), \"expected to contain: \" + \"span text\");\n    }\n\n    @Test\n    void testOptionsStrongEmUnderscore() throws Exception {\n        // Strong and em tags use underscore symbol instead of asterisk\n        var options = MAPPER.readValue(\"{\\\"strong_em_symbol\\\":\\\"_\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p><strong>bold</strong> and <em>italic</em></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"__bold__\"), \"expected to contain: \" + \"__bold__\");\n        assertTrue(result.content().orElse(\"\").contains(\"_italic_\"), \"expected to contain: \" + \"_italic_\");\n    }\n\n    @Test\n    void testOptionsSubSymbolTilde() throws Exception {\n        // Subscript rendered with tilde symbol\n        var options = MAPPER.readValue(\"{\\\"sub_symbol\\\":\\\"~\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"~2~\"), \"expected to contain: \" + \"~2~\");\n    }\n\n    @Test\n    void testOptionsSupSymbolCaret() throws Exception {\n        // Superscript rendered with caret symbol\n        var options = MAPPER.readValue(\"{\\\"sup_symbol\\\":\\\"^\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>x<sup>2</sup></p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"^2^\"), \"expected to contain: \" + \"^2^\");\n    }\n\n    @Test\n    void testOptionsWhitespaceNormalized() throws Exception {\n        // Normalized whitespace mode collapses multiple spaces\n        var options = MAPPER.readValue(\"{\\\"whitespace_mode\\\":\\\"Normalized\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Text   with    extra   spaces.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Text\"), \"expected to contain: \" + \"Text\");\n        assertTrue(result.content().orElse(\"\").contains(\"with\"), \"expected to contain: \" + \"with\");\n        assertTrue(result.content().orElse(\"\").contains(\"extra\"), \"expected to contain: \" + \"extra\");\n        assertTrue(result.content().orElse(\"\").contains(\"spaces.\"), \"expected to contain: \" + \"spaces.\");\n    }\n\n    @Test\n    void testOptionsWhitespaceStrict() throws Exception {\n        // Strict whitespace mode preserves whitespace as-is\n        var options = MAPPER.readValue(\"{\\\"whitespace_mode\\\":\\\"Strict\\\"}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Preserved   spacing.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Preserved\"), \"expected to contain: \" + \"Preserved\");\n        assertTrue(result.content().orElse(\"\").contains(\"spacing.\"), \"expected to contain: \" + \"spacing.\");\n    }\n\n    @Test\n    void testOptionsWrapDisabled() throws Exception {\n        // Wrap option disabled preserves long lines without breaking\n        var options = MAPPER.readValue(\"{\\\"wrap\\\":false}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\", options);\n        assertTrue(result.content().orElse(\"\").contains(\"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\"), \"expected to contain: \" + \"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\");\n    }\n\n    @Test\n    void testOptionsWrapEnabled() throws Exception {\n        // Wrap option enabled with custom width wraps long lines\n        var options = MAPPER.readValue(\"{\\\"wrap\\\":true,\\\"wrap_width\\\":40}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"This is a long paragraph\"), \"expected to contain: \" + \"This is a long paragraph\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/RealWorldTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0a58bf719c39794429ff44126797d30bc034c314e1c4bd93ce38ea6394a4db50\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: real-world. */\nclass RealWorldTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testRealWorldBlogPost() throws Exception {\n        // Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\n        var result = HtmlToMarkdown.convert(\"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"# Getting Started with Rust\"), \"expected to contain: \" + \"# Getting Started with Rust\");\n        assertTrue(result.content().orElse(\"\").contains(\"## Installation\"), \"expected to contain: \" + \"## Installation\");\n        assertTrue(result.content().orElse(\"\").contains(\"## Hello World\"), \"expected to contain: \" + \"## Hello World\");\n        assertTrue(result.content().orElse(\"\").contains(\"## Key Concepts\"), \"expected to contain: \" + \"## Key Concepts\");\n        assertTrue(result.content().orElse(\"\").contains(\"cargo run\"), \"expected to contain: \" + \"cargo run\");\n        assertTrue(result.content().orElse(\"\").contains(\"[Mozilla](https://www.mozilla.org)\"), \"expected to contain: \" + \"[Mozilla](https://www.mozilla.org)\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Ownership and borrowing\"), \"expected to contain: \" + \"- Ownership and borrowing\");\n    }\n\n    @Test\n    void testRealWorldDocumentationPage() throws Exception {\n        // Documentation page with nested lists, code examples, and blockquotes converts correctly\n        var result = HtmlToMarkdown.convert(\"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"# Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration values.</p></blockquote></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"# API Reference\"), \"expected to contain: \" + \"# API Reference\");\n        assertTrue(result.content().orElse(\"\").contains(\"## convert_html\"), \"expected to contain: \" + \"## convert_html\");\n        assertTrue(result.content().orElse(\"\").contains(\"### Parameters\"), \"expected to contain: \" + \"### Parameters\");\n        assertTrue(result.content().orElse(\"\").contains(\"### Returns\"), \"expected to contain: \" + \"### Returns\");\n        assertTrue(result.content().orElse(\"\").contains(\"### Example\"), \"expected to contain: \" + \"### Example\");\n        assertTrue(result.content().orElse(\"\").contains(\"## ConversionOptions\"), \"expected to contain: \" + \"## ConversionOptions\");\n        assertTrue(result.content().orElse(\"\").contains(\"> \"), \"expected to contain: \" + \"> \");\n        assertTrue(result.content().orElse(\"\").contains(\"thread-safe\"), \"expected to contain: \" + \"thread-safe\");\n        assertTrue(result.content().orElse(\"\").contains(\"convert_html\"), \"expected to contain: \" + \"convert_html\");\n        assertTrue(result.content().orElse(\"\").contains(\"ConversionOptions\"), \"expected to contain: \" + \"ConversionOptions\");\n    }\n\n    @Test\n    void testRealWorldProductPage() throws Exception {\n        // Product page with table, images, and lists converts correctly\n        var result = HtmlToMarkdown.convert(\"<div class=\\\"product\\\"><h1>Wireless Keyboard Pro</h1><img src=\\\"https://example.com/keyboard.jpg\\\" alt=\\\"Wireless Keyboard Pro\\\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"# Wireless Keyboard Pro\"), \"expected to contain: \" + \"# Wireless Keyboard Pro\");\n        assertTrue(result.content().orElse(\"\").contains(\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\"), \"expected to contain: \" + \"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\");\n        assertTrue(result.content().orElse(\"\").contains(\"## Specifications\"), \"expected to contain: \" + \"## Specifications\");\n        assertTrue(result.content().orElse(\"\").contains(\"Battery Life\"), \"expected to contain: \" + \"Battery Life\");\n        assertTrue(result.content().orElse(\"\").contains(\"12 months\"), \"expected to contain: \" + \"12 months\");\n        assertTrue(result.content().orElse(\"\").contains(\"Bluetooth 5.0\"), \"expected to contain: \" + \"Bluetooth 5.0\");\n        assertTrue(result.content().orElse(\"\").contains(\"## What's in the Box\"), \"expected to contain: \" + \"## What's in the Box\");\n        assertTrue(result.content().orElse(\"\").contains(\"USB-C charging cable\"), \"expected to contain: \" + \"USB-C charging cable\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n        assertTrue(result.content().orElse(\"\").contains(\"---\"), \"expected to contain: \" + \"---\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/ResultTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:336438db04230025a1baaa0f040c0cf4494631e2734e4e64d4a83f2e2ed6b474\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: result. */\nclass ResultTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testResultTablesEmptyWhenNoTables() throws Exception {\n        // Result tables array is empty when input has no tables\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>No tables here</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(0, result.tables().size(), \"expected exactly 0 elements\");\n    }\n\n    @Test\n    void testResultTablesMultiple() throws Exception {\n        // Multiple tables each appear in the tables array\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\", options);\n        assertTrue(result.tables().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testResultTablesSimple() throws Exception {\n        // Simple table populates the tables array in result\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.tables().size() >= 1, \"expected at least 1 elements\");\n    }\n\n    @Test\n    void testResultTablesWithoutStructureFlag() throws Exception {\n        // Tables array is empty when includeDocumentStructure is false\n        var result = HtmlToMarkdown.convert(\"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(0, result.tables().size(), \"expected exactly 0 elements\");\n    }\n\n    @Test\n    void testResultWarningsEmptyForCleanInput() throws Exception {\n        // Warnings array is empty for well-formed HTML without problematic content\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(0, result.warnings().size(), \"expected exactly 0 elements\");\n    }\n\n    @Test\n    void testResultWarningsEmptyForComplexInput() throws Exception {\n        // Warnings array is empty for complex but valid HTML\n        var result = HtmlToMarkdown.convert(\"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(0, result.warnings().size(), \"expected exactly 0 elements\");\n    }\n\n    @Test\n    void testResultWarningsEmptyForMalformedHtml() throws Exception {\n        // Warnings array is empty even for malformed HTML (parser is lenient)\n        var result = HtmlToMarkdown.convert(\"<p>Unclosed paragraph<div>Mixed nesting</p></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertEquals(0, result.warnings().size(), \"expected exactly 0 elements\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/SmokeTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c41e92871f45c45893c452814fdcaaa6b4a74dc56d5d5575bcc407b16561c37f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: smoke. */\nclass SmokeTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testSmokeEmptyString() throws Exception {\n        // Empty string produces empty output\n        var result = HtmlToMarkdown.convert(\"\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testSmokeSimpleHeading() throws Exception {\n        // H1 heading converts to ATX markdown\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"# Title\"), \"expected to contain: \" + \"# Title\");\n    }\n\n    @Test\n    void testSmokeSimpleParagraph() throws Exception {\n        // Simple paragraph converts correctly\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/StructureTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:923e0d84d2652f7bd5ff3a69a7ebe823064b2e04477a597fdaa2c31e0cae57e0\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: structure. */\nclass StructureTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testStructureCodeBlock() throws Exception {\n        // Fenced code block produces Code node\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Example code:</p><pre><code class=\\\"language-rust\\\">fn main() { println!(\\\"Hello\\\"); }</code></pre>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testStructureDeepNestingH1H2H3() throws Exception {\n        // H1 > H2 > H3 creates three levels of heading nesting\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 5, \"expected at least 5 elements\");\n    }\n\n    @Test\n    void testStructureH1H2NestedGroup() throws Exception {\n        // H1 followed by H2 creates a nested group under the H1\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 3, \"expected at least 3 elements\");\n    }\n\n    @Test\n    void testStructureHeadingParagraph() throws Exception {\n        // Simple heading followed by paragraph produces Heading and Paragraph nodes\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>A paragraph of text.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testStructureList() throws Exception {\n        // Unordered list produces List and ListItem nodes\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 2, \"expected at least 2 elements\");\n    }\n\n    @Test\n    void testStructureMultipleHeadings() throws Exception {\n        // Multiple headings create multiple Heading nodes with correct levels\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 4, \"expected at least 4 elements\");\n    }\n\n    @Test\n    void testStructureSiblingH1Groups() throws Exception {\n        // H1, H2, then another H1 creates two sibling top-level groups\n        var options = MAPPER.readValue(\"{\\\"include_document_structure\\\":true}\", ConversionOptions.class);\n        var result = HtmlToMarkdown.convert(\"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\", options);\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertFalse(result.document().orElseThrow().nodes().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.document().orElseThrow().nodes().size() >= 4, \"expected at least 4 elements\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/VisitorTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b7aa1a287e24dfe728f830b97a8272bac6d7d04bd782b76cfb437a9f03842ede\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.TestVisitor;\nimport dev.kreuzberg.htmltomarkdown.VisitContext;\nimport dev.kreuzberg.htmltomarkdown.VisitResult;\n\n/** E2e tests for category: visitor. */\nclass VisitorTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testVisitorAudioCustom() throws Exception {\n        // Visitor replaces audio element with custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitAudio(VisitContext ctx, String src) {\n                return VisitResult.custom(\"[AUDIO: podcast.mp3]\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Listen to this: <audio src=\\\"podcast.mp3\\\" controls></audio></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[AUDIO: podcast.mp3]\"), \"expected to contain: \" + \"[AUDIO: podcast.mp3]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Listen to this:\"), \"expected to contain: \" + \"Listen to this:\");\n    }\n\n    @Test\n    void testVisitorAudioSkip() throws Exception {\n        // Visitor removes audio elements from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitAudio(VisitContext ctx, String src) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Background music:</p><audio src=\\\"music.ogg\\\" autoplay></audio><p>Enjoy!</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Background music:\"), \"expected to contain: \" + \"Background music:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Enjoy!\"), \"expected to contain: \" + \"Enjoy!\");\n        assertFalse(result.content().orElse(\"\").contains(\"music.ogg\"), \"expected NOT to contain: \" + \"music.ogg\");\n    }\n\n    @Test\n    void testVisitorButtonCustom() throws Exception {\n        // Visitor replaces button with bracketed text\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitButton(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"[BTN:%s]\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Confirm action: <button type=\\\"submit\\\">Click me</button> or <button type=\\\"reset\\\">Cancel</button></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[BTN:Click me]\"), \"expected to contain: \" + \"[BTN:Click me]\");\n        assertTrue(result.content().orElse(\"\").contains(\"[BTN:Cancel]\"), \"expected to contain: \" + \"[BTN:Cancel]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Confirm action:\"), \"expected to contain: \" + \"Confirm action:\");\n    }\n\n    @Test\n    void testVisitorButtonSkip() throws Exception {\n        // Visitor removes all buttons from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitButton(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Actions available:\"), \"expected to contain: \" + \"Actions available:\");\n        assertFalse(result.content().orElse(\"\").contains(\"Save\"), \"expected NOT to contain: \" + \"Save\");\n        assertFalse(result.content().orElse(\"\").contains(\"Delete\"), \"expected NOT to contain: \" + \"Delete\");\n        assertFalse(result.content().orElse(\"\").contains(\"Export\"), \"expected NOT to contain: \" + \"Export\");\n    }\n\n    @Test\n    void testVisitorContinueDefault() throws Exception {\n        // Visitor continue action preserves default conversion\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitStrong(VisitContext ctx, String text) {\n                return VisitResult.continue_();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Hello <strong>World</strong></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"**World**\"), \"expected to contain: \" + \"**World**\");\n    }\n\n    @Test\n    void testVisitorCustomBlockquote() throws Exception {\n        // Visitor replaces blockquote with custom format\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitBlockquote(VisitContext ctx, String content, long depth) {\n                return VisitResult.custom(String.format(\"QUOTE: \\\"%s\\\"\", content));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>A wise quote.</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"QUOTE:\"), \"expected to contain: \" + \"QUOTE:\");\n        assertTrue(result.content().orElse(\"\").contains(\"A wise quote.\"), \"expected to contain: \" + \"A wise quote.\");\n    }\n\n    @Test\n    void testVisitorCustomEmphasis() throws Exception {\n        // Visitor replaces emphasis with custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitEmphasis(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\">>>%s<<<\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>This is <em>important</em> text.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\">>>important<<<\"), \"expected to contain: \" + \">>>important<<<\");\n        assertFalse(result.content().orElse(\"\").contains(\"*important*\"), \"expected NOT to contain: \" + \"*important*\");\n    }\n\n    @Test\n    void testVisitorCustomHeading() throws Exception {\n        // Visitor replaces heading with custom format\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHeading(VisitContext ctx, int level, String text, String id) {\n                return VisitResult.custom(String.format(\"--- %s ---\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h2>Section Title</h2><p>Content below heading.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"--- Section Title ---\"), \"expected to contain: \" + \"--- Section Title ---\");\n        assertFalse(result.content().orElse(\"\").contains(\"## Section Title\"), \"expected NOT to contain: \" + \"## Section Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Content below heading.\"), \"expected to contain: \" + \"Content below heading.\");\n    }\n\n    @Test\n    void testVisitorCustomImage() throws Exception {\n        // Visitor replaces image with custom output using template\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitImage(VisitContext ctx, String src, String alt, String title) {\n                return VisitResult.custom(String.format(\"[Image: %s]\", alt));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"banner.png\\\" alt=\\\"Banner\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[Image: Banner]\"), \"expected to contain: \" + \"[Image: Banner]\");\n        assertFalse(result.content().orElse(\"\").contains(\"banner.png\"), \"expected NOT to contain: \" + \"banner.png\");\n    }\n\n    @Test\n    void testVisitorCustomLinkFormat() throws Exception {\n        // Visitor reformats links using template interpolation\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitLink(VisitContext ctx, String href, String text, String title) {\n                return VisitResult.custom(String.format(\"%s (%s)\", text, href));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Visit <a href=\\\"https://example.com\\\">Example</a> for more info.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Example (https://example.com)\"), \"expected to contain: \" + \"Example (https://example.com)\");\n        assertFalse(result.content().orElse(\"\").contains(\"[Example]\"), \"expected NOT to contain: \" + \"[Example]\");\n    }\n\n    @Test\n    void testVisitorCustomLinkStatic() throws Exception {\n        // Visitor replaces link with static custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitLink(VisitContext ctx, String href, String text, String title) {\n                return VisitResult.custom(\"[REDACTED LINK]\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\">Click here</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[REDACTED LINK]\"), \"expected to contain: \" + \"[REDACTED LINK]\");\n        assertFalse(result.content().orElse(\"\").contains(\"example.com\"), \"expected NOT to contain: \" + \"example.com\");\n    }\n\n    @Test\n    void testVisitorCustomOutput() throws Exception {\n        // Visitor custom action replaces element output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHeading(VisitContext ctx, int level, String text, String id) {\n                return VisitResult.custom(\"## REPLACED HEADING\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h1>Original Heading</h1>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"## REPLACED HEADING\"), \"expected to contain: \" + \"## REPLACED HEADING\");\n        assertFalse(result.content().orElse(\"\").contains(\"# Original Heading\"), \"expected NOT to contain: \" + \"# Original Heading\");\n    }\n\n    @Test\n    void testVisitorDefinitionListCustom() throws Exception {\n        // Visitor customizes definition list items\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitDefinitionTerm(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"**%s**\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"**HTML**\"), \"expected to contain: \" + \"**HTML**\");\n        assertTrue(result.content().orElse(\"\").contains(\"**CSS**\"), \"expected to contain: \" + \"**CSS**\");\n        assertTrue(result.content().orElse(\"\").contains(\"HyperText Markup Language\"), \"expected to contain: \" + \"HyperText Markup Language\");\n    }\n\n    @Test\n    void testVisitorDefinitionListCustomFormat() throws Exception {\n        // Visitor formats definition lists with custom templates\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitDefinitionTerm(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"### %s\", text));\n            }\n            @Override public VisitResult visitDefinitionDescription(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"> %s\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"### Python\"), \"expected to contain: \" + \"### Python\");\n        assertTrue(result.content().orElse(\"\").contains(\"### JavaScript\"), \"expected to contain: \" + \"### JavaScript\");\n        assertTrue(result.content().orElse(\"\").contains(\"> A high-level programming language\"), \"expected to contain: \" + \"> A high-level programming language\");\n        assertTrue(result.content().orElse(\"\").contains(\"> A scripting language for web browsers\"), \"expected to contain: \" + \"> A scripting language for web browsers\");\n    }\n\n    @Test\n    void testVisitorDefinitionListSkip() throws Exception {\n        // Visitor skips definition list items from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitDefinitionDescription(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n            @Override public VisitResult visitDefinitionTerm(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Glossary:\"), \"expected to contain: \" + \"Glossary:\");\n        assertTrue(result.content().orElse(\"\").contains(\"End of glossary\"), \"expected to contain: \" + \"End of glossary\");\n        assertFalse(result.content().orElse(\"\").contains(\"Term A\"), \"expected NOT to contain: \" + \"Term A\");\n        assertFalse(result.content().orElse(\"\").contains(\"Definition\"), \"expected NOT to contain: \" + \"Definition\");\n    }\n\n    @Test\n    void testVisitorDetailsSummaryCustom() throws Exception {\n        // Visitor customizes details/summary disclosure elements\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitSummary(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"[EXPANDABLE] %s\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[EXPANDABLE] Click to expand\"), \"expected to contain: \" + \"[EXPANDABLE] Click to expand\");\n        assertTrue(result.content().orElse(\"\").contains(\"This content is initially hidden.\"), \"expected to contain: \" + \"This content is initially hidden.\");\n    }\n\n    @Test\n    void testVisitorDetailsSummarySkip() throws Exception {\n        // Visitor removes details/summary elements entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitDetails(VisitContext ctx, boolean isOpen) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Main content here.\"), \"expected to contain: \" + \"Main content here.\");\n        assertTrue(result.content().orElse(\"\").contains(\"More main content.\"), \"expected to contain: \" + \"More main content.\");\n        assertFalse(result.content().orElse(\"\").contains(\"Hidden section\"), \"expected NOT to contain: \" + \"Hidden section\");\n        assertFalse(result.content().orElse(\"\").contains(\"Secret details\"), \"expected NOT to contain: \" + \"Secret details\");\n    }\n\n    @Test\n    void testVisitorFigureCustom() throws Exception {\n        // Visitor customizes figure and figcaption elements\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitFigcaption(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"*%s*\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\\\"diagram.png\\\" alt=\\\"System architecture diagram\\\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Article Title\"), \"expected to contain: \" + \"Article Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"*Figure 1: System Architecture*\"), \"expected to contain: \" + \"*Figure 1: System Architecture*\");\n        assertTrue(result.content().orElse(\"\").contains(\"Explanation of the figure.\"), \"expected to contain: \" + \"Explanation of the figure.\");\n    }\n\n    @Test\n    void testVisitorFigureCustomWrap() throws Exception {\n        // Visitor wraps figure content with custom formatting\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitFigureStart(VisitContext ctx) {\n                return VisitResult.custom(\"\\n[FIGURE]\\n\");\n            }\n            @Override public VisitResult visitFigureEnd(VisitContext ctx, String output) {\n                return VisitResult.custom(String.format(\"%s\\n[/FIGURE]\\n\", output));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<section><h2>Gallery</h2><figure><img src=\\\"photo1.jpg\\\" alt=\\\"Photo\\\"><figcaption>Beautiful sunset</figcaption></figure></section>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[FIGURE]\"), \"expected to contain: \" + \"[FIGURE]\");\n        assertTrue(result.content().orElse(\"\").contains(\"[/FIGURE]\"), \"expected to contain: \" + \"[/FIGURE]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Gallery\"), \"expected to contain: \" + \"Gallery\");\n    }\n\n    @Test\n    void testVisitorFigureSkip() throws Exception {\n        // Visitor removes figure elements with their captions\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitFigureStart(VisitContext ctx) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>See the chart below:</p><figure><img src=\\\"chart.svg\\\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"See the chart below:\"), \"expected to contain: \" + \"See the chart below:\");\n        assertTrue(result.content().orElse(\"\").contains(\"As shown in the chart above.\"), \"expected to contain: \" + \"As shown in the chart above.\");\n        assertFalse(result.content().orElse(\"\").contains(\"Revenue Trends\"), \"expected NOT to contain: \" + \"Revenue Trends\");\n        assertFalse(result.content().orElse(\"\").contains(\"chart.svg\"), \"expected NOT to contain: \" + \"chart.svg\");\n    }\n\n    @Test\n    void testVisitorFormCustom() throws Exception {\n        // Visitor replaces form with custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitForm(VisitContext ctx, String actionUrl, String method) {\n                return VisitResult.custom(\"[FORM PLACEHOLDER]\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<div><form action=\\\"/submit\\\" method=\\\"POST\\\"><label>Name: <input type=\\\"text\\\" name=\\\"name\\\"></label><button type=\\\"submit\\\">Submit</button></form></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[FORM PLACEHOLDER]\"), \"expected to contain: \" + \"[FORM PLACEHOLDER]\");\n        assertFalse(result.content().orElse(\"\").contains(\"submit\"), \"expected NOT to contain: \" + \"submit\");\n        assertFalse(result.content().orElse(\"\").contains(\"input\"), \"expected NOT to contain: \" + \"input\");\n    }\n\n    @Test\n    void testVisitorFormSkip() throws Exception {\n        // Visitor skips form elements entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitForm(VisitContext ctx, String actionUrl, String method) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Before form</p><form><input type=\\\"email\\\" name=\\\"email\\\"></form><p>After form</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Before form\"), \"expected to contain: \" + \"Before form\");\n        assertTrue(result.content().orElse(\"\").contains(\"After form\"), \"expected to contain: \" + \"After form\");\n        assertFalse(result.content().orElse(\"\").contains(\"email\"), \"expected NOT to contain: \" + \"email\");\n    }\n\n    @Test\n    void testVisitorHorizontalRuleCustom() throws Exception {\n        // Visitor replaces horizontal rule with custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHorizontalRule(VisitContext ctx) {\n                return VisitResult.custom(\"\\n[DIVIDER]\\n\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[DIVIDER]\"), \"expected to contain: \" + \"[DIVIDER]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section A\"), \"expected to contain: \" + \"Section A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section B\"), \"expected to contain: \" + \"Section B\");\n        assertFalse(result.content().orElse(\"\").contains(\"---\"), \"expected NOT to contain: \" + \"---\");\n    }\n\n    @Test\n    void testVisitorHorizontalRuleSkip() throws Exception {\n        // Visitor removes all horizontal rules\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHorizontalRule(VisitContext ctx) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Part 1\"), \"expected to contain: \" + \"Part 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Part 2\"), \"expected to contain: \" + \"Part 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Part 3\"), \"expected to contain: \" + \"Part 3\");\n        assertFalse(result.content().orElse(\"\").contains(\"---\"), \"expected NOT to contain: \" + \"---\");\n    }\n\n    @Test\n    void testVisitorIframeCustom() throws Exception {\n        // Visitor replaces embedded iframe with custom text\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitIframe(VisitContext ctx, String src) {\n                return VisitResult.custom(\"[EMBEDDED: https://maps.example.com/embed]\");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Embedded map:</p><iframe src=\\\"https://maps.example.com/embed\\\" width=\\\"400\\\" height=\\\"300\\\"></iframe><p>End of map</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[EMBEDDED: https://maps.example.com/embed]\"), \"expected to contain: \" + \"[EMBEDDED: https://maps.example.com/embed]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Embedded map:\"), \"expected to contain: \" + \"Embedded map:\");\n        assertTrue(result.content().orElse(\"\").contains(\"End of map\"), \"expected to contain: \" + \"End of map\");\n    }\n\n    @Test\n    void testVisitorIframeSkip() throws Exception {\n        // Visitor removes embedded iframes\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitIframe(VisitContext ctx, String src) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h3>Reviews</h3><iframe src=\\\"https://widget.example.com/reviews\\\"></iframe><p>See reviews from our partners.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Reviews\"), \"expected to contain: \" + \"Reviews\");\n        assertTrue(result.content().orElse(\"\").contains(\"See reviews from our partners.\"), \"expected to contain: \" + \"See reviews from our partners.\");\n        assertFalse(result.content().orElse(\"\").contains(\"widget.example.com\"), \"expected NOT to contain: \" + \"widget.example.com\");\n    }\n\n    @Test\n    void testVisitorInputCustom() throws Exception {\n        // Visitor replaces input with labeled output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitInput(VisitContext ctx, String inputType, String name, String value) {\n                return VisitResult.custom(String.format(\"[INPUT:%s]\", inputType));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<form><label>Username: <input type=\\\"text\\\" name=\\\"username\\\" value=\\\"\\\"></label><label>Password: <input type=\\\"password\\\" name=\\\"password\\\"></label></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[INPUT:text]\"), \"expected to contain: \" + \"[INPUT:text]\");\n        assertTrue(result.content().orElse(\"\").contains(\"[INPUT:password]\"), \"expected to contain: \" + \"[INPUT:password]\");\n    }\n\n    @Test\n    void testVisitorInputSkip() throws Exception {\n        // Visitor skips all input elements\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitInput(VisitContext ctx, String inputType, String name, String value) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Sign up:</p><input type=\\\"text\\\" name=\\\"email\\\" placeholder=\\\"your@email.com\\\"><input type=\\\"checkbox\\\" name=\\\"agree\\\"><p>Continue</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Sign up:\"), \"expected to contain: \" + \"Sign up:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Continue\"), \"expected to contain: \" + \"Continue\");\n        assertFalse(result.content().orElse(\"\").contains(\"email\"), \"expected NOT to contain: \" + \"email\");\n    }\n\n    @Test\n    void testVisitorLineBreakCustom() throws Exception {\n        // Visitor replaces line break with custom output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitLineBreak(VisitContext ctx) {\n                return VisitResult.custom(\" | \");\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>First line<br>Second line<br>Third line</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"First line | Second line | Third line\"), \"expected to contain: \" + \"First line | Second line | Third line\");\n        assertFalse(result.content().orElse(\"\").contains(\"\\n\\n\"), \"expected NOT to contain: \" + \"\\n\\n\");\n    }\n\n    @Test\n    void testVisitorLineBreakSkip() throws Exception {\n        // Visitor removes all line breaks\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitLineBreak(VisitContext ctx) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Address Line 1Address Line 2Address Line 3\"), \"expected to contain: \" + \"Address Line 1Address Line 2Address Line 3\");\n    }\n\n    @Test\n    void testVisitorMarkCustom() throws Exception {\n        // Visitor replaces highlight/mark with custom template\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitMark(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"==%s==\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>This is a <mark>highlighted passage</mark> in the text.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"==highlighted passage==\"), \"expected to contain: \" + \"==highlighted passage==\");\n        assertTrue(result.content().orElse(\"\").contains(\"This is a\"), \"expected to contain: \" + \"This is a\");\n    }\n\n    @Test\n    void testVisitorMarkSkip() throws Exception {\n        // Visitor skips mark elements entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitMark(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Key insight: <mark>always validate input</mark> for security.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().orElse(\"\").contains(\"always validate input\"), \"expected NOT to contain: \" + \"always validate input\");\n        assertTrue(result.content().orElse(\"\").contains(\"Key insight:\"), \"expected to contain: \" + \"Key insight:\");\n        assertTrue(result.content().orElse(\"\").contains(\"for security.\"), \"expected to contain: \" + \"for security.\");\n    }\n\n    @Test\n    void testVisitorPreserveHtml() throws Exception {\n        // Visitor preserve_html action includes raw HTML in output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitCustomElement(VisitContext ctx, String tagName, String html) {\n                return VisitResult.preserveHtml();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<div><custom-tag>Custom content</custom-tag></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"<custom-tag>\"), \"expected to contain: \" + \"<custom-tag>\");\n    }\n\n    @Test\n    void testVisitorSkipAllHeadings() throws Exception {\n        // Visitor skips all headings from document\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHeading(VisitContext ctx, int level, String text, String id) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>Body text remains.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().orElse(\"\").contains(\"Title\"), \"expected NOT to contain: \" + \"Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Body text remains.\"), \"expected to contain: \" + \"Body text remains.\");\n    }\n\n    @Test\n    void testVisitorSkipCodeBlocks() throws Exception {\n        // Visitor skips code blocks from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitCodeBlock(VisitContext ctx, String lang, String code) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Intro text\"), \"expected to contain: \" + \"Intro text\");\n        assertTrue(result.content().orElse(\"\").contains(\"Outro text\"), \"expected to contain: \" + \"Outro text\");\n        assertFalse(result.content().orElse(\"\").contains(\"let x = 42\"), \"expected NOT to contain: \" + \"let x = 42\");\n    }\n\n    @Test\n    void testVisitorSkipHeading() throws Exception {\n        // Visitor skip action omits all headings from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitHeading(VisitContext ctx, int level, String text, String id) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1><p>Body text remains.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().orElse(\"\").contains(\"Title\"), \"expected NOT to contain: \" + \"Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Body text remains.\"), \"expected to contain: \" + \"Body text remains.\");\n    }\n\n    @Test\n    void testVisitorSkipImages() throws Exception {\n        // Visitor skips all images from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitImage(VisitContext ctx, String src, String alt, String title) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Before image</p><img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\"><p>After image</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Before image\"), \"expected to contain: \" + \"Before image\");\n        assertTrue(result.content().orElse(\"\").contains(\"After image\"), \"expected to contain: \" + \"After image\");\n        assertFalse(result.content().orElse(\"\").contains(\"photo.jpg\"), \"expected NOT to contain: \" + \"photo.jpg\");\n        assertFalse(result.content().orElse(\"\").contains(\"A photo\"), \"expected NOT to contain: \" + \"A photo\");\n    }\n\n    @Test\n    void testVisitorSkipLinks() throws Exception {\n        // Visitor skips all links entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitLink(VisitContext ctx, String href, String text, String title) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Before <a href=\\\"https://example.com\\\">link text</a> after</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().orElse(\"\").contains(\"link text\"), \"expected NOT to contain: \" + \"link text\");\n        assertFalse(result.content().orElse(\"\").contains(\"example.com\"), \"expected NOT to contain: \" + \"example.com\");\n    }\n\n    @Test\n    void testVisitorSkipStrong() throws Exception {\n        // Visitor skips bold/strong elements\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitStrong(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Normal <strong>bold text</strong> normal</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertFalse(result.content().orElse(\"\").contains(\"bold text\"), \"expected NOT to contain: \" + \"bold text\");\n        assertTrue(result.content().orElse(\"\").contains(\"Normal\"), \"expected to contain: \" + \"Normal\");\n    }\n\n    @Test\n    void testVisitorSubscriptCustom() throws Exception {\n        // Visitor replaces subscript with custom template\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitSubscript(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"~%s~\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O is water.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"H~2~O\"), \"expected to contain: \" + \"H~2~O\");\n        assertTrue(result.content().orElse(\"\").contains(\"is water\"), \"expected to contain: \" + \"is water\");\n    }\n\n    @Test\n    void testVisitorSubscriptSkip() throws Exception {\n        // Visitor skips subscript elements entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitSubscript(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"The formula CHO is sugar.\"), \"expected to contain: \" + \"The formula CHO is sugar.\");\n    }\n\n    @Test\n    void testVisitorSuperscriptCustom() throws Exception {\n        // Visitor replaces superscript with custom template\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitSuperscript(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"^%s^\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"E=mc^2^\"), \"expected to contain: \" + \"E=mc^2^\");\n        assertTrue(result.content().orElse(\"\").contains(\"revolutionized physics\"), \"expected to contain: \" + \"revolutionized physics\");\n    }\n\n    @Test\n    void testVisitorSuperscriptSkip() throws Exception {\n        // Visitor skips superscript from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitSuperscript(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"The equation x + y = z has no solutions.\"), \"expected to contain: \" + \"The equation x + y = z has no solutions.\");\n    }\n\n    @Test\n    void testVisitorUnderlineCustom() throws Exception {\n        // Visitor replaces underline with custom markup\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitUnderline(VisitContext ctx, String text) {\n                return VisitResult.custom(String.format(\"_%s_\", text));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>This is <u>very important</u> text.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"_very important_\"), \"expected to contain: \" + \"_very important_\");\n        assertFalse(result.content().orElse(\"\").contains(\"**\"), \"expected NOT to contain: \" + \"**\");\n    }\n\n    @Test\n    void testVisitorUnderlineSkip() throws Exception {\n        // Visitor skips underline elements from output\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitUnderline(VisitContext ctx, String text) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Normal text with <u>underlined part</u> and more text.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Normal text with\"), \"expected to contain: \" + \"Normal text with\");\n        assertTrue(result.content().orElse(\"\").contains(\"and more text.\"), \"expected to contain: \" + \"and more text.\");\n        assertFalse(result.content().orElse(\"\").contains(\"underlined part\"), \"expected NOT to contain: \" + \"underlined part\");\n    }\n\n    @Test\n    void testVisitorVideoCustom() throws Exception {\n        // Visitor replaces video with custom link\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitVideo(VisitContext ctx, String src) {\n                return VisitResult.custom(String.format(\"[VIDEO: %s]\", src));\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<p>Watch our tutorial:</p><video src=\\\"tutorial.mp4\\\" width=\\\"320\\\" height=\\\"240\\\" controls></video><p>Great content!</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"[VIDEO: tutorial.mp4]\"), \"expected to contain: \" + \"[VIDEO: tutorial.mp4]\");\n        assertTrue(result.content().orElse(\"\").contains(\"Watch our tutorial:\"), \"expected to contain: \" + \"Watch our tutorial:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Great content!\"), \"expected to contain: \" + \"Great content!\");\n    }\n\n    @Test\n    void testVisitorVideoSkip() throws Exception {\n        // Visitor removes video elements entirely\n        class _TestVisitor implements TestVisitor {\n            @Override public VisitResult visitVideo(VisitContext ctx, String src) {\n                return VisitResult.skip();\n            }\n        }\n        var visitor = new _TestVisitor();\n        var result = HtmlToMarkdown.convert(\"<h2>Demo</h2><video src=\\\"demo.webm\\\"></video><p>See the demo above.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class), visitor);\n        assertTrue(result.content().orElse(\"\").contains(\"Demo\"), \"expected to contain: \" + \"Demo\");\n        assertTrue(result.content().orElse(\"\").contains(\"See the demo above.\"), \"expected to contain: \" + \"See the demo above.\");\n        assertFalse(result.content().orElse(\"\").contains(\"demo.webm\"), \"expected NOT to contain: \" + \"demo.webm\");\n    }\n\n}\n"
  },
  {
    "path": "e2e/kotlin/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    kotlin(\"jvm\") version \"2.1.10\"\n}\n\ngroup = \"dev.kreuzberg\"\nversion = \"0.1.0\"\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_21\n    targetCompatibility = JavaVersion.VERSION_21\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget.set(JvmTarget.JVM_21)\n    }\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    testImplementation(files(\"../../packages/kotlin/build/libs/html-to-markdown-rs-0.1.0.jar\"))\n    testImplementation(\"org.junit.jupiter:junit-jupiter-api:5.11.4\")\n    testImplementation(\"org.junit.jupiter:junit-jupiter-engine:5.11.4\")\n    testImplementation(\"com.fasterxml.jackson.core:jackson-databind:2.18.2\")\n    testImplementation(\"com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n    environment(\"java.library.path\", \"../../target/release\")\n}\n"
  },
  {
    "path": "e2e/node/package.json",
    "content": "{\n  \"name\": \"html-to-markdown-e2e-typescript\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"html-to-markdown\": \"workspace:*\",\n    \"vitest\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "e2e/node/tests/conversion.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1233450ba5824fa8accd11ed4b0b6bd578af81c6e9c2bc0a9bfdf8656ae7ea7a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"conversion\", () => {\n  it(\"blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed\", () => {\n    const result = convert(\n      \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"> First paragraph.\");\n    expect(result.content).toContain(\"> Second paragraph.\");\n  });\n\n  it(\"blockquote_nested: Nested blockquote produces double-prefixed lines\", () => {\n    const result = convert(\n      \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Outer quote.\");\n    expect(result.content).toContain(\"Inner quote.\");\n  });\n\n  it(\"blockquote_simple: Simple blockquote\", () => {\n    const result = convert(\n      \"<blockquote><p>Quote text</p></blockquote>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"> Quote text\");\n  });\n\n  it(\"blockquote_with_list: Blockquote containing a list preserves list items inside quote\", () => {\n    const result = convert(\n      \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Quote intro:\");\n    expect(result.content).toContain(\"Point one\");\n    expect(result.content).toContain(\"Point two\");\n  });\n\n  it(\"bold_and_italic: Nested bold and italic\", () => {\n    const result = convert(\n      \"<p><strong><em>both</em></strong></p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"***both***\");\n  });\n\n  it(\"bold_strong: Strong tag converts to bold\", () => {\n    const result = convert(\"<p><strong>bold</strong></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"**bold**\");\n  });\n\n  it(\"code_block: Code block with language preserves content\", () => {\n    const result = convert(\n      \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"print('hello')\");\n  });\n\n  it(\"code_block_no_language: Code block without a language class preserves content\", () => {\n    const result = convert(\n      \"<pre><code>plain code here</code></pre>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"plain code here\");\n  });\n\n  it(\"code_inline_in_paragraph: Inline code element nested inside a paragraph\", () => {\n    const result = convert(\n      \"<p>Call the <code>initialize()</code> method first.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"`initialize()`\");\n  });\n\n  it(\"code_with_backticks_in_content: Inline code containing backtick characters is properly escaped\", () => {\n    const result = convert(\n      \"<p>Use <code>`backtick` here</code> carefully.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"backtick\");\n  });\n\n  it(\"emphasis_mark_highlight: mark tag produces highlighted output\", () => {\n    const result = convert(\"<p><mark>highlighted</mark></p>\", {} as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted\");\n  });\n\n  it(\"emphasis_strikethrough_del: del tag converts to GFM strikethrough\", () => {\n    const result = convert(\"<p><del>deleted text</del></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"~~deleted text~~\");\n  });\n\n  it(\"emphasis_strikethrough_s: s tag converts to GFM strikethrough\", () => {\n    const result = convert(\"<p><s>strikethrough</s></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"~~strikethrough~~\");\n  });\n\n  it(\"emphasis_subscript: sub tag content is preserved\", () => {\n    const result = convert(\"<p>H<sub>2</sub>O</p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n  });\n\n  it(\"emphasis_superscript: sup tag content is preserved\", () => {\n    const result = convert(\"<p>x<sup>2</sup></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"x\");\n    expect(result.content).toContain(\"2\");\n  });\n\n  it(\"emphasis_underline_u: u tag content is preserved in output\", () => {\n    const result = convert(\"<p><u>underlined</u></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"underlined\");\n  });\n\n  it(\"form_input_elements: Form input elements produce readable output without form mechanics\", () => {\n    const result = convert(\n      '<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Name\");\n  });\n\n  it(\"form_select_options: Select element with options produces readable output\", () => {\n    const result = convert(\n      '<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Color\");\n  });\n\n  it(\"form_textarea: Textarea element produces readable output\", () => {\n    const result = convert(\n      \"<form><label>Message:</label><textarea>Default text content</textarea></form>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Message\");\n  });\n\n  it(\"heading_h1: H1 heading\", () => {\n    const result = convert(\"<h1>Heading 1</h1>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"# Heading 1\");\n  });\n\n  it(\"heading_h2: H2 heading\", () => {\n    const result = convert(\"<h2>Heading 2</h2>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"## Heading 2\");\n  });\n\n  it(\"heading_h3: H3 heading\", () => {\n    const result = convert(\"<h3>Heading 3</h3>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"### Heading 3\");\n  });\n\n  it(\"heading_h4: H4 heading\", () => {\n    const result = convert(\"<h4>Heading 4</h4>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"#### Heading 4\");\n  });\n\n  it(\"heading_h5: H5 heading\", () => {\n    const result = convert(\"<h5>Heading 5</h5>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"##### Heading 5\");\n  });\n\n  it(\"heading_h6: H6 heading\", () => {\n    const result = convert(\"<h6>Heading 6</h6>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"###### Heading 6\");\n  });\n\n  it(\"image_figure_figcaption: Figure with figcaption preserves both image and caption\", () => {\n    const result = convert(\n      '<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"![A sunset](sunset.jpg)\");\n    expect(result.content).toContain(\"Beautiful sunset over the ocean\");\n  });\n\n  it(\"image_linked: Image inside an anchor produces a linked image\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"![Icon](icon.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"image_no_alt: Image without alt text produces image markdown\", () => {\n    const result = convert('<img src=\"banner.jpg\">', {} as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"banner.jpg\");\n  });\n\n  it(\"image_simple: Image with alt text\", () => {\n    const result = convert(\n      '<img src=\"photo.jpg\" alt=\"A photo\">',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"![A photo](photo.jpg)\");\n  });\n\n  it(\"image_with_title: Image with title attribute includes title in output\", () => {\n    const result = convert(\n      '<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"![Sales chart](chart.png\");\n    expect(result.content).toContain(\"Q3 Sales\");\n  });\n\n  it(\"inline_code: Inline code\", () => {\n    const result = convert(\n      \"<p>Use <code>console.log()</code> to debug</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"`console.log()`\");\n  });\n\n  it(\"italic_em: Em tag converts to italic\", () => {\n    const result = convert(\"<p><em>italic</em></p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"*italic*\");\n  });\n\n  it(\"line_break_br_tag: Single br tag produces a line break in output\", () => {\n    const result = convert(\n      \"<p>First line.<br>Second line.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"First line.\");\n    expect(result.content).toContain(\"Second line.\");\n  });\n\n  it(\"line_break_hr_tag: hr tag produces a horizontal separator between content\", () => {\n    const result = convert(\n      \"<p>Before rule.</p><hr><p>After rule.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Before rule.\");\n    expect(result.content).toContain(\"After rule.\");\n  });\n\n  it(\"line_break_multiple_br: Multiple consecutive br tags in sequence\", () => {\n    const result = convert(\"<p>Start.<br><br>End.</p>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Start.\");\n    expect(result.content).toContain(\"End.\");\n  });\n\n  it(\"link_anchor_fragment: Fragment-only anchor link is preserved\", () => {\n    const result = convert(\n      '<a href=\"#section\">Jump to section</a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"[Jump to section](#section)\");\n  });\n\n  it(\"link_empty_href: Link with empty href produces output with the link text\", () => {\n    const result = convert('<a href=\"\">No destination</a>', {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"No destination\");\n  });\n\n  it(\"link_image_inside: Image inside a link produces a linked image\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"![Logo](logo.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"link_mailto: Mailto link is preserved with mailto: scheme\", () => {\n    const result = convert(\n      '<a href=\"mailto:user@example.com\">Email us</a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"mailto:user@example.com\");\n  });\n\n  it(\"link_simple: Simple link\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\">Example</a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"[Example](https://example.com)\");\n  });\n\n  it(\"link_with_bold_text: Link containing bold text preserves formatting\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><strong>Bold link</strong></a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"**Bold link**\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"link_with_title: Link with title attribute\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\" title=\"Example Site\">Example</a>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"[Example](https://example.com\");\n    expect(result.content).toContain(\"Example Site\");\n  });\n\n  it(\"list_definition_dl: Definition list with dt and dd elements\", () => {\n    const result = convert(\n      \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Term One\");\n    expect(result.content).toContain(\"Definition of term one.\");\n    expect(result.content).toContain(\"Term Two\");\n    expect(result.content).toContain(\"Definition of term two.\");\n  });\n\n  it(\"list_item_multiple_paragraphs: List item containing multiple paragraphs\", () => {\n    const result = convert(\n      \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"First paragraph in item.\");\n    expect(result.content).toContain(\"Second paragraph in item.\");\n    expect(result.content).toContain(\"Simple item\");\n  });\n\n  it(\"list_mixed_nested: Mixed list: ordered list nested inside unordered list\", () => {\n    const result = convert(\n      \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Item A\");\n    expect(result.content).toContain(\"Sub 1\");\n    expect(result.content).toContain(\"Sub 2\");\n    expect(result.content).toContain(\"Item B\");\n  });\n\n  it(\"list_nested_ordered: Nested ordered list with two levels of depth\", () => {\n    const result = convert(\n      \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Step 1\");\n    expect(result.content).toContain(\"Step 1a\");\n    expect(result.content).toContain(\"Step 1b\");\n    expect(result.content).toContain(\"Step 2\");\n  });\n\n  it(\"list_nested_unordered: Nested unordered list with two levels of depth\", () => {\n    const result = convert(\n      \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Parent A\");\n    expect(result.content).toContain(\"Child A1\");\n    expect(result.content).toContain(\"Child A2\");\n    expect(result.content).toContain(\"Parent B\");\n  });\n\n  it(\"list_task_checkboxes: Task list with checked and unchecked checkboxes\", () => {\n    const result = convert(\n      '<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Done task\");\n    expect(result.content).toContain(\"Pending task\");\n  });\n\n  it(\"ordered_list: Ordered list\", () => {\n    const result = convert(\n      \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"1. First\");\n    expect(result.content).toContain(\"2. Second\");\n    expect(result.content).toContain(\"3. Third\");\n  });\n\n  it(\"paragraph_multiple: Multiple paragraphs are separated by a blank line\", () => {\n    const result = convert(\n      \"<p>First paragraph.</p><p>Second paragraph.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"First paragraph.\");\n    expect(result.content).toContain(\"Second paragraph.\");\n  });\n\n  it(\"paragraph_nested_divs: Text nested inside divs is extracted correctly\", () => {\n    const result = convert(\n      \"<div><div><p>Nested text</p></div></div>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Nested text\");\n  });\n\n  it(\"paragraph_simple: Simple paragraph converts to plain text\", () => {\n    const result = convert(\"<p>Hello World</p>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n  });\n\n  it(\"paragraph_with_inline_formatting: Paragraph with bold, italic, and a link\", () => {\n    const result = convert(\n      '<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"**bold**\");\n    expect(result.content).toContain(\"*italic*\");\n    expect(result.content).toContain(\"[link](https://example.com)\");\n  });\n\n  it(\"paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output\", () => {\n    const result = convert(\n      \"<p>Line one.<br>Line two.<br>Line three.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Line one.\");\n    expect(result.content).toContain(\"Line two.\");\n    expect(result.content).toContain(\"Line three.\");\n  });\n\n  it(\"semantic_abbr: Abbreviation element text is preserved\", () => {\n    const result = convert(\n      '<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"WWW\");\n  });\n\n  it(\"semantic_article: Article element wrapping content preserves inner content\", () => {\n    const result = convert(\n      \"<article><h2>Article Title</h2><p>Article body.</p></article>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Article Title\");\n    expect(result.content).toContain(\"Article body.\");\n  });\n\n  it(\"semantic_definition_list: Definition list with term and description\", () => {\n    const result = convert(\n      \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"HTML\");\n    expect(result.content).toContain(\"HyperText Markup Language\");\n    expect(result.content).toContain(\"CSS\");\n    expect(result.content).toContain(\"Cascading Style Sheets\");\n  });\n\n  it(\"semantic_details_summary: Details and summary elements produce readable output\", () => {\n    const result = convert(\n      \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Click to expand\");\n  });\n\n  it(\"semantic_hr: Horizontal rule produces a separator in output\", () => {\n    const result = convert(\"<p>Above</p><hr><p>Below</p>\", {} as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Above\");\n    expect(result.content).toContain(\"Below\");\n  });\n\n  it(\"semantic_mark_highlight: Mark tag produces highlighted output\", () => {\n    const result = convert(\n      \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted text\");\n  });\n\n  it(\"semantic_section_with_heading: Section element with heading preserves structure\", () => {\n    const result = convert(\n      \"<section><h3>Section Heading</h3><p>Section content.</p></section>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Section Heading\");\n    expect(result.content).toContain(\"Section content.\");\n  });\n\n  it(\"semantic_sub_superscript: Subscript and superscript elements are preserved in output\", () => {\n    const result = convert(\n      \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n    expect(result.content).toContain(\"E=mc\");\n  });\n\n  it(\"simple_table: Simple table with header\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Name\");\n    expect(result.content).toContain(\"Age\");\n    expect(result.content).toContain(\"Alice\");\n    expect(result.content).toContain(\"30\");\n    expect(result.content).toContain(\"|\");\n    expect(result.content).toContain(\"---\");\n  });\n\n  it(\"table_empty: Empty table produces no output or minimal output\", () => {\n    const result = convert(\"<table></table>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"table_no_thead: Table without thead uses first row as implied header\", () => {\n    const result = convert(\n      \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Product\");\n    expect(result.content).toContain(\"Price\");\n    expect(result.content).toContain(\"Apple\");\n    expect(result.content).toContain(\"1.00\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it(\"table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Expression\");\n    expect(result.content).toContain(\"Result\");\n    expect(result.content).toContain(\"true\");\n  });\n\n  it(\"table_with_alignment: Table with column alignment attributes\", () => {\n    const result = convert(\n      '<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Left\");\n    expect(result.content).toContain(\"Center\");\n    expect(result.content).toContain(\"Right\");\n    expect(result.content).toContain(\"L\");\n    expect(result.content).toContain(\"C\");\n    expect(result.content).toContain(\"R\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it(\"table_with_colspan: Table with colspan attribute in a header cell\", () => {\n    const result = convert(\n      '<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Full Name\");\n    expect(result.content).toContain(\"John\");\n    expect(result.content).toContain(\"Doe\");\n  });\n\n  it(\"unordered_list: Unordered list\", () => {\n    const result = convert(\n      \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"- Item 1\");\n    expect(result.content).toContain(\"- Item 2\");\n    expect(result.content).toContain(\"- Item 3\");\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/edge_cases.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:be8e6bd3d3cfeb6c841e28fc2b761e7d326f1dcec5082e46af8b8e9b14e8cb6a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"edge-cases\", () => {\n  it(\"empty_html: Empty HTML document\", () => {\n    const result = convert(\n      \"<html><head></head><body></body></html>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"encoding_cjk_characters: CJK (Chinese, Japanese, Korean) characters are preserved\", () => {\n    const result = convert(\n      \"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"中文内容\");\n    expect(result.content).toContain(\"日本語テキスト\");\n    expect(result.content).toContain(\"한국어 텍스트\");\n  });\n\n  it(\"encoding_html_entities: Common HTML entities are decoded in output\", () => {\n    const result = convert(\n      \"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"&\");\n    expect(result.content).toContain(\"<\");\n    expect(result.content).toContain(\">\");\n  });\n\n  it(\"encoding_named_entities: Named HTML entities like &mdash; and &hellip; are decoded\", () => {\n    const result = convert(\n      \"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"—\");\n    expect(result.content).toContain(\"…\");\n  });\n\n  it(\"encoding_numeric_entities: Numeric HTML entities (decimal and hex) are decoded\", () => {\n    const result = convert(\n      \"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"©\");\n    expect(result.content).toContain(\"®\");\n    expect(result.content).toContain(\"€\");\n  });\n\n  it(\"encoding_unicode_emoji: Emoji and Unicode characters are preserved\", () => {\n    const result = convert(\n      \"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"🌍\");\n    expect(result.content).toContain(\"🚀\");\n    expect(result.content).toContain(\"⭐\");\n  });\n\n  it(\"html_comments_only: Document containing only HTML comments produces empty output\", () => {\n    const result = convert(\n      \"<!-- This is a comment --><!-- Another comment -->\",\n      {} as unknown as ConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"just_whitespace_input: Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\", () => {\n    const result = convert(\"   \", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"malformed_deeply_nested_elements: Deeply nested elements (100 levels) are handled without stack overflow\", () => {\n    const result = convert(\n      \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Deeply nested content\");\n  });\n\n  it(\"malformed_missing_block_closing_tags: Missing closing tags on block elements are auto-closed by parser\", () => {\n    const result = convert(\n      \"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"First paragraph\");\n    expect(result.content).toContain(\"Second paragraph\");\n  });\n\n  it(\"malformed_overlapping_tags: Overlapping bold/italic tags are recovered by the HTML parser without panic\", () => {\n    const result = convert(\n      \"<p><b><i>bold and italic</b></i></p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"bold and italic\");\n  });\n\n  it(\"malformed_unclosed_paragraph: Unclosed <p> tag is recovered gracefully and content is preserved\", () => {\n    const result = convert(\"<p>This paragraph is never closed\", {} as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"This paragraph is never closed\");\n  });\n\n  it(\"script_tags_only: Document with only script tags produces empty output (scripts are stripped)\", () => {\n    const result = convert(\n      \"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"style_tags_only: Document with only style tags produces empty output (styles are stripped)\", () => {\n    const result = convert(\n      \"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"visitor_custom_element_with_nesting: Visitor handles custom elements with nested content\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return { custom: \"[CUSTOM WIDGET]\" };\n      },\n    };\n\n    const result = convert(\n      '<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[CUSTOM WIDGET]\");\n    expect(result.content).not.toContain(\"Widget content here\");\n  });\n\n  it(\"visitor_deeply_nested_skip: Visitor skips deeply nested elements\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Outer\");\n    expect(result.content ?? \"\").toContain(\"text\");\n    expect(result.content).not.toContain(\"highlight\");\n  });\n\n  it(\"visitor_element_end_modification: Visitor modifies element at end after children processed\", () => {\n    const _testVisitor = {\n      visitElementEnd(ctx, output): string | { custom: string } {\n        return { custom: \"MODIFIED OUTPUT\" };\n      },\n    };\n\n    const result = convert(\n      \"<blockquote><p>Original quote</p></blockquote>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"visitor_element_start_skip_entire_subtree: Visitor skips at element_start level removes entire subtree\", () => {\n    const _testVisitor = {\n      visitElementStart(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<div><h1>Title</h1><p>Content</p></div>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"visitor_unknown_tag_preservation: Visitor preserves unknown HTML tags as raw HTML\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return \"preserve_html\";\n      },\n    };\n\n    const result = convert(\n      \"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Article text\");\n    expect(result.content ?? \"\").toContain(\"More article text\");\n    expect(result.content ?? \"\").toContain(\"<x-custom>\");\n  });\n\n  it(\"whitespace_only: Whitespace-only content\", () => {\n    const result = convert(\"<p>   </p>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"xss_onclick_handler_removed: onclick and other on* event handlers are removed from elements\", () => {\n    const result = convert(\n      '<p><a href=\"https://example.com\" onclick=\"alert(\\'xss\\')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Click me\");\n  });\n\n  it(\"xss_script_tag_stripped: Script tag content is stripped and does not appear in output\", () => {\n    const result = convert(\n      \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Safe content\");\n    expect(result.content).toContain(\"More safe content\");\n  });\n\n  it(\"xss_svg_nested_script_stripped: Script tags nested inside SVG are stripped\", () => {\n    const result = convert(\n      \"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Before SVG\");\n    expect(result.content).toContain(\"After SVG\");\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/metadata.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:85454798ca2f2148a0da49f3fcdfa7acad965b3bf41083e2aae40214a0b7c22f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"metadata\", () => {\n  it(\"metadata_author_meta: Extract author from <meta name='author'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.author ?? \"\").trim()).toBe(\"Jane Doe\");\n  });\n\n  it(\"metadata_canonical_url: Extract canonical URL from <link rel='canonical'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.canonicalUrl ?? \"\").trim()).toBe(\n      \"https://example.com/canonical-page\",\n    );\n  });\n\n  it(\"metadata_description_meta: Extract description from <meta name='description'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.description ?? \"\").trim()).toBe(\n      \"This is the page description.\",\n    );\n  });\n\n  it(\"metadata_dublin_core: Extract Dublin Core metadata tags\", () => {\n    const result = convert(\n      '<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"scholarly article\");\n  });\n\n  it(\"metadata_extract_all_images: Extract all images from a document into metadata\", () => {\n    const result = convert(\n      '<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.images.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"metadata_extract_all_links: Extract all links from a document into metadata\", () => {\n    const result = convert(\n      '<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.links.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"metadata_headers_hierarchy: Extract heading hierarchy from document into metadata\", () => {\n    const result = convert(\n      \"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.headers.length).toBeGreaterThanOrEqual(5);\n  });\n\n  it(\"metadata_keywords_meta: Extract keywords from <meta name='keywords'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.document.keywords.length).toBeGreaterThanOrEqual(1);\n  });\n\n  it(\"metadata_lang_attribute: Extract language from html lang attribute\", () => {\n    const result = convert(\n      '<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Hola Mundo\");\n  });\n\n  it(\"metadata_microdata_schema_article: Extract schema.org microdata for Article\", () => {\n    const result = convert(\n      '<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Breaking News Today\");\n    expect(result.content ?? \"\").toContain(\"Jane Reporter\");\n    expect(result.content ?? \"\").toContain(\"important information\");\n  });\n\n  it(\"metadata_microdata_schema_breadcrumb: Extract schema.org breadcrumb navigation microdata\", () => {\n    const result = convert(\n      '<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Home\");\n    expect(result.content ?? \"\").toContain(\"Products\");\n    expect(result.content ?? \"\").toContain(\"Current Page\");\n  });\n\n  it(\"metadata_microdata_schema_organization: Extract schema.org microdata for Organization\", () => {\n    const result = convert(\n      '<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Acme Corp\");\n    expect(result.content ?? \"\").toContain(\"2020\");\n  });\n\n  it(\"metadata_microdata_schema_person: Extract schema.org microdata for Person\", () => {\n    const result = convert(\n      '<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"John Smith\");\n    expect(result.content ?? \"\").toContain(\"john@example.com\");\n  });\n\n  it(\"metadata_microdata_schema_product: Extract schema.org microdata for Product\", () => {\n    const result = convert(\n      '<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Awesome Widget\");\n    expect(result.content ?? \"\").toContain(\"best widget\");\n    expect(result.content ?? \"\").toContain(\"29.99\");\n  });\n\n  it(\"metadata_text_direction_ltr: Extract text direction from lang attribute on html element\", () => {\n    const result = convert(\n      '<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"left-to-right text\");\n  });\n\n  it(\"metadata_text_direction_rtl: Extract right-to-left text direction\", () => {\n    const result = convert(\n      '<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"right-to-left text\");\n  });\n\n  it(\"metadata_title_tag: Extract title from <title> tag\", () => {\n    const result = convert(\n      \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.title ?? \"\").trim()).toBe(\"My Page\");\n  });\n\n  it(\"og_basic_tags: Extract og:title, og:description, and og:image from Open Graph meta tags\", () => {\n    const result = convert(\n      '<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.openGraph[\"title\"] ?? \"\").trim()).toBe(\"OG Title\");\n    expect((result.metadata.document.openGraph[\"description\"] ?? \"\").trim()).toBe(\n      \"OG description text.\",\n    );\n    expect((result.metadata.document.openGraph[\"image\"] ?? \"\").trim()).toBe(\n      \"https://example.com/image.jpg\",\n    );\n  });\n\n  it(\"og_multiple_tags: Extract multiple Open Graph tags including type, url, and site_name\", () => {\n    const result = convert(\n      '<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.openGraph[\"title\"] ?? \"\").trim()).toBe(\"Article Title\");\n    expect((result.metadata.document.openGraph[\"type\"] ?? \"\").trim()).toBe(\"article\");\n    expect((result.metadata.document.openGraph[\"url\"] ?? \"\").trim()).toBe(\n      \"https://example.com/article\",\n    );\n    expect((result.metadata.document.openGraph[\"site_name\"] ?? \"\").trim()).toBe(\"Example Site\");\n  });\n\n  it(\"structured_data_json_ld: JSON-LD script tag is stripped from output (security) but metadata may be extracted\", () => {\n    const result = convert(\n      '<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"My Article\");\n  });\n\n  it(\"structured_data_multiple_json_ld: Multiple JSON-LD blocks are all stripped from output\", () => {\n    const result = convert(\n      '<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Widget\");\n  });\n\n  it(\"twitter_card_tags: Extract Twitter card meta tags\", () => {\n    const result = convert(\n      '<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.twitterCard[\"card\"] ?? \"\").trim()).toBe(\"summary_large_image\");\n    expect((result.metadata.document.twitterCard[\"title\"] ?? \"\").trim()).toBe(\"Twitter Card Title\");\n    expect((result.metadata.document.twitterCard[\"description\"] ?? \"\").trim()).toBe(\n      \"Twitter card description.\",\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/options.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ebbcaf3c661b05fbe3451d4f4358a9266cd3e3c1226d4af1448e53cf6e1c705b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"options\", () => {\n  it(\"options_autolinks_false: Bare URL links rendered as regular markdown links when autolinks disabled\", () => {\n    const result = convert(\"<p><a href='https://example.com'>https://example.com</a></p>\", {\n      autolinks: false,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"example.com\");\n  });\n\n  it(\"options_br_in_tables_false: BR elements in table cells are stripped when disabled\", () => {\n    const result = convert(\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", {\n      brInTables: false,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Col\");\n  });\n\n  it(\"options_br_in_tables_true: BR elements in table cells render as line breaks\", () => {\n    const result = convert(\n      \"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\",\n      { brInTables: true } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Header\");\n    expect(result.content).toContain(\"Line 1\");\n    expect(result.content).toContain(\"Line 2\");\n  });\n\n  it(\"options_code_block_backticks: Backticks code block style uses triple backtick fences\", () => {\n    const result = convert(\"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", {\n      codeBlockStyle: \"Backticks\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"```\");\n    expect(result.content).toContain(\"console.log('hi');\");\n  });\n\n  it(\"options_code_block_indented: Code blocks use 4-space indentation\", () => {\n    const result = convert(\"<pre><code>print('hello')</code></pre>\", {\n      codeBlockStyle: \"Indented\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"print('hello')\");\n    expect(result.content).not.toContain(\"```\");\n  });\n\n  it(\"options_code_block_tildes: Code blocks use tilde fences\", () => {\n    const result = convert(\"<pre><code>let x = 1;</code></pre>\", {\n      codeBlockStyle: \"Tildes\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"~~~\");\n    expect(result.content).toContain(\"let x = 1;\");\n  });\n\n  it(\"options_code_block_tildes_style: Tildes code block style uses triple tilde fences\", () => {\n    const result = convert(\"<pre><code>some code</code></pre>\", {\n      codeBlockStyle: \"Tildes\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"~~~\");\n    expect(result.content).toContain(\"some code\");\n  });\n\n  it(\"options_code_language_python: Default code language annotation on blocks without lang attribute\", () => {\n    const result = convert(\"<pre><code>def hello(): pass</code></pre>\", {\n      codeLanguage: \"python\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"```python\");\n    expect(result.content).toContain(\"def hello\");\n  });\n\n  it(\"options_convert_as_inline: Block elements treated as inline\", () => {\n    const result = convert(\"<p>One</p><p>Two</p>\", {\n      convertAsInline: true,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"One\");\n    expect(result.content).toContain(\"Two\");\n  });\n\n  it(\"options_debug_true: Debug mode enabled does not crash and produces output\", () => {\n    const result = convert(\"<p>Debug test</p>\", { debug: true } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Debug test\");\n  });\n\n  it(\"options_default_title_true: Links without title get empty title attribute when defaultTitle is true\", () => {\n    const result = convert(\"<p><a href='https://example.com'>Link</a></p>\", {\n      defaultTitle: true,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Link\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"options_encoding_utf8: UTF-8 encoding hint for special characters\", () => {\n    const result = convert(\"<p>Café naïve résumé</p>\", {\n      encoding: \"utf-8\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"options_escape_ascii_enabled: ASCII Markdown characters are escaped when escapeAscii is true\", () => {\n    const result = convert(\"<p>Text with # hash and [brackets] and * star</p>\", {\n      escapeAscii: true,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"hash\");\n    expect(result.content).toContain(\"brackets\");\n    expect(result.content).toContain(\"star\");\n  });\n\n  it(\"options_escape_asterisks: escape_asterisks option escapes asterisks in plain text\", () => {\n    const result = convert(\"<p>Use 2*3 = 6 in math.</p>\", {\n      escapeAsterisks: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"3\");\n    expect(result.content).toContain(\"6\");\n  });\n\n  it(\"options_escape_misc: escape_misc option escapes miscellaneous markdown characters\", () => {\n    const result = convert(\"<p>Use # and | and ~ in text.</p>\", {\n      escapeMisc: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Use\");\n    expect(result.content).toContain(\"and\");\n    expect(result.content).toContain(\"in text.\");\n  });\n\n  it(\"options_escape_underscores: escape_underscores option escapes underscores in plain text\", () => {\n    const result = convert(\"<p>The variable_name is defined.</p>\", {\n      escapeUnderscores: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"variable\");\n    expect(result.content).toContain(\"name\");\n    expect(result.content).toContain(\"defined.\");\n  });\n\n  it(\"options_exclude_selectors_attribute: Elements matching CSS attribute selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>',\n      { excludeSelectors: [\"[role='complementary']\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Primary text\");\n    expect(result.content).not.toContain(\"Sidebar\");\n  });\n\n  it(\"options_exclude_selectors_class: Elements matching CSS class selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>',\n      { excludeSelectors: [\".cookie-banner\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Main content\");\n    expect(result.content).not.toContain(\"cookies\");\n  });\n\n  it(\"options_exclude_selectors_empty_noop: Empty exclude_selectors list does not affect output\", () => {\n    const result = convert(\"<p>Hello world</p>\", {\n      excludeSelectors: [],\n    } as unknown as ConversionOptions);\n    expect(result.content ?? \"\").toContain(\"Hello world\");\n  });\n\n  it(\"options_exclude_selectors_id: Elements matching CSS id selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>',\n      { excludeSelectors: [\"#ad-container\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Article text\");\n    expect(result.content).not.toContain(\"Buy stuff\");\n  });\n\n  it(\"options_exclude_selectors_multiple: Multiple CSS selectors each exclude their matched elements\", () => {\n    const result = convert(\n      '<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>',\n      { excludeSelectors: [\".nav\", \"footer\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Content\");\n    expect(result.content).not.toContain(\"Menu\");\n    expect(result.content).not.toContain(\"Footer\");\n  });\n\n  it(\"options_exclude_selectors_nested_content_dropped: All descendants of excluded elements are dropped\", () => {\n    const result = convert(\n      '<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>',\n      { excludeSelectors: [\".sidebar\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Main text\");\n    expect(result.content).not.toContain(\"Related\");\n    expect(result.content).not.toContain(\"Sidebar text\");\n  });\n\n  it(\"options_exclude_selectors_plain_text_mode: Exclude selectors work in plain text output mode\", () => {\n    const result = convert('<body><div class=\"nav\">Navigation</div><p>Article body</p></body>', {\n      excludeSelectors: [\".nav\"],\n      outputFormat: \"Plain\",\n    } as unknown as ConversionOptions);\n    expect(result.content ?? \"\").toContain(\"Article body\");\n    expect(result.content).not.toContain(\"Navigation\");\n  });\n\n  it(\"options_exclude_selectors_vs_strip_tags: exclude_selectors drops entire subtree unlike strip_tags which keeps children\", () => {\n    const result = convert(\n      '<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>',\n      { excludeSelectors: [\".wrapper\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Outer text\");\n    expect(result.content).not.toContain(\"Inner paragraph\");\n  });\n\n  it(\"options_extract_metadata_true: Extract metadata returns document metadata when enabled\", () => {\n    const result = convert(\n      \"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\",\n      { extractMetadata: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.title ?? \"\").trim()).toBe(\"Test Page\");\n    expect((result.metadata.document.description ?? \"\").trim()).toBe(\"A test page\");\n  });\n\n  it(\"options_heading_style_atx: ATX heading style produces hash-prefixed headings\", () => {\n    const result = convert(\"<h1>Title</h1><h2>Subtitle</h2>\", {\n      headingStyle: \"Atx\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n    expect(result.content).toContain(\"## Subtitle\");\n  });\n\n  it(\"options_heading_style_atx_closed: ATX closed heading style adds closing hashes\", () => {\n    const result = convert(\"<h1>Closed Heading</h1>\", {\n      headingStyle: \"AtxClosed\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"# Closed Heading #\");\n  });\n\n  it(\"options_heading_style_underlined: Underlined heading style produces setext-style headings for h1 and h2\", () => {\n    const result = convert(\"<h1>Main Title</h1>\", {\n      headingStyle: \"Underlined\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Main Title\");\n  });\n\n  it(\"options_highlight_bold: Mark tag rendered as bold\", () => {\n    const result = convert(\"<p>Text with <mark>highlighted</mark> text.</p>\", {\n      highlightStyle: \"Bold\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"**highlighted**\");\n  });\n\n  it(\"options_highlight_double_equal: Mark tag with double equal highlight style\", () => {\n    const result = convert(\"<p>Text with <mark>highlighted</mark> here.</p>\", {\n      highlightStyle: \"DoubleEqual\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"==highlighted==\");\n  });\n\n  it(\"options_highlight_none: Mark tag with no highlight style strips the mark\", () => {\n    const result = convert(\"<p>Text with <mark>plain</mark> content.</p>\", {\n      highlightStyle: \"None\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"plain\");\n    expect(result.content).not.toContain(\"==\");\n  });\n\n  it(\"options_keep_inline_images_in_paragraph: Images inside specified tags stay inline\", () => {\n    const result = convert(\"<p>Text <img src='icon.png' alt='icon'> more text</p>\", {\n      keepInlineImagesIn: [\"p\"],\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"more text\");\n  });\n\n  it(\"options_link_style_reference: Links use reference-style formatting\", () => {\n    const result = convert(\n      \"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\",\n      { linkStyle: \"Reference\" } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Example\");\n    expect(result.content).toContain(\"Other\");\n    expect(result.content).toContain(\"example.com\");\n  });\n\n  it(\"options_list_custom_bullets: Custom bullet character for unordered lists\", () => {\n    const result = convert(\"<ul><li>Item A</li><li>Item B</li></ul>\", {\n      bullets: \"*\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"* Item A\");\n    expect(result.content).toContain(\"* Item B\");\n  });\n\n  it(\"options_list_indent_tabs: Tab indentation type for nested list items\", () => {\n    const result = convert(\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", {\n      listIndentType: \"Tabs\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Parent\");\n    expect(result.content).toContain(\"Child\");\n  });\n\n  it(\"options_list_indent_width_four: Nested lists indented with 4 spaces per level\", () => {\n    const result = convert(\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", {\n      listIndentWidth: 4,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Outer\");\n    expect(result.content).toContain(\"Inner\");\n  });\n\n  it(\"options_max_depth_default_unlimited: Default max_depth (null) converts deeply nested content fully\", () => {\n    const result = convert(\n      \"<div><div><div><div><p>Deep content</p></div></div></div></div>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Deep content\");\n  });\n\n  it(\"options_max_depth_truncates: max_depth truncates content beyond the specified depth\", () => {\n    const result = convert(\n      \"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\",\n      { maxDepth: 3 } as unknown as ConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Shallow\");\n    expect(result.content).not.toContain(\"Too deep\");\n  });\n\n  it(\"options_max_depth_zero_empty: max_depth of 0 produces empty output\", () => {\n    const result = convert(\"<p>Hello</p>\", { maxDepth: 0 } as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"options_newline_backslash: Hard line breaks rendered with backslash\", () => {\n    const result = convert(\"<p>Line one<br>Line two</p>\", {\n      newlineStyle: \"Backslash\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Line one\");\n    expect(result.content).toContain(\"Line two\");\n  });\n\n  it(\"options_newline_spaces: Hard line breaks rendered with trailing spaces\", () => {\n    const result = convert(\"<p>First<br>Second</p>\", {\n      newlineStyle: \"Spaces\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"First\");\n    expect(result.content).toContain(\"Second\");\n  });\n\n  it(\"options_output_format_djot: Djot output format produces djot-compatible markup\", () => {\n    const result = convert(\"<p>Simple paragraph.</p>\", {\n      outputFormat: \"Djot\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Simple paragraph.\");\n  });\n\n  it(\"options_output_format_markdown: Default markdown output format produces standard markdown\", () => {\n    const result = convert(\"<h1>Title</h1><p>Some text.</p>\", {\n      headingStyle: \"Atx\",\n      outputFormat: \"Markdown\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n    expect(result.content).toContain(\"Some text.\");\n  });\n\n  it(\"options_output_format_plain: Plain text output format strips markdown syntax\", () => {\n    const result = convert(\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", {\n      outputFormat: \"Plain\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"bold\");\n    expect(result.content).toContain(\"text.\");\n  });\n\n  it(\"options_preprocessing_aggressive: Aggressive preset removes nav, footer, aside unconditionally\", () => {\n    const result = convert(\n      \"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\",\n      { preprocessing: { preset: \"Aggressive\" } } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"Content\");\n    expect(result.content).not.toContain(\"Menu\");\n  });\n\n  it(\"options_preprocessing_minimal: Minimal preset preserves nav, footer, aside\", () => {\n    const result = convert(\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", {\n      preprocessing: { preset: \"Minimal\" },\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Navigation\");\n    expect(result.content).toContain(\"Content\");\n    expect(result.content).toContain(\"Footer\");\n  });\n\n  it(\"options_preprocessing_remove_forms: Forms are removed when remove_forms is true\", () => {\n    const result = convert(\n      \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\",\n      { preprocessing: { removeForms: true } } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).not.toContain(\"Submit\");\n  });\n\n  it(\"options_preserve_tags_iframe: Iframe tags preserved as raw HTML in output\", () => {\n    const result = convert(\n      \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\",\n      { preserveTags: [\"iframe\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).toContain(\"<iframe\");\n  });\n\n  it(\"options_skip_images_true: Images are omitted from output when skipImages is true\", () => {\n    const result = convert(\"<p>Before <img src='test.jpg' alt='photo'> After</p>\", {\n      skipImages: true,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).not.toContain(\"photo\");\n  });\n\n  it(\"options_strip_newlines: Strip newlines produces single-line paragraphs\", () => {\n    const result = convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", {\n      stripNewlines: true,\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"First paragraph.\");\n    expect(result.content).toContain(\"Second paragraph.\");\n  });\n\n  it(\"options_strip_tags_div_span: Div and span tags stripped but content preserved\", () => {\n    const result = convert(\n      \"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\",\n      { stripTags: [\"div\", \"span\"] } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\"Inside div\");\n    expect(result.content).toContain(\"span text\");\n  });\n\n  it(\"options_strong_em_underscore: Strong and em tags use underscore symbol instead of asterisk\", () => {\n    const result = convert(\"<p><strong>bold</strong> and <em>italic</em></p>\", {\n      strongEmSymbol: \"_\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"__bold__\");\n    expect(result.content).toContain(\"_italic_\");\n  });\n\n  it(\"options_sub_symbol_tilde: Subscript rendered with tilde symbol\", () => {\n    const result = convert(\"<p>H<sub>2</sub>O</p>\", {\n      subSymbol: \"~\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"~2~\");\n  });\n\n  it(\"options_sup_symbol_caret: Superscript rendered with caret symbol\", () => {\n    const result = convert(\"<p>x<sup>2</sup></p>\", {\n      supSymbol: \"^\",\n    } as unknown as ConversionOptions);\n    expect(result.content).toContain(\"^2^\");\n  });\n\n  it(\"options_whitespace_normalized: Normalized whitespace mode collapses multiple spaces\", () => {\n    const result = convert(\"<p>Text   with    extra   spaces.</p>\", {\n      whitespaceMode: \"Normalized\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"with\");\n    expect(result.content).toContain(\"extra\");\n    expect(result.content).toContain(\"spaces.\");\n  });\n\n  it(\"options_whitespace_strict: Strict whitespace mode preserves whitespace as-is\", () => {\n    const result = convert(\"<p>Preserved   spacing.</p>\", {\n      whitespaceMode: \"Strict\",\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Preserved\");\n    expect(result.content).toContain(\"spacing.\");\n  });\n\n  it(\"options_wrap_disabled: Wrap option disabled preserves long lines without breaking\", () => {\n    const result = convert(\n      \"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\",\n      { wrap: false } as unknown as ConversionOptions,\n    );\n    expect(result.content).toContain(\n      \"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\",\n    );\n  });\n\n  it(\"options_wrap_enabled: Wrap option enabled with custom width wraps long lines\", () => {\n    const result = convert(\n      \"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\",\n      { wrap: true, wrapWidth: 40 } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"This is a long paragraph\");\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/real_world.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:84c0aa1e545fadf58bbdd2b58e78c5b527151efe3cb812da604e958568b77e98\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"real-world\", () => {\n  it(\"real_world_blog_post: Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\", () => {\n    const result = convert(\n      '<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\"https://www.mozilla.org\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\"language-bash\">curl --proto \\'=https\\' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\"language-rust\">fn main() {\\n    println!(\"Hello, world!\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\"https://doc.rust-lang.org/book/\">Rust Book</a>.</p></article>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# Getting Started with Rust\");\n    expect(result.content).toContain(\"## Installation\");\n    expect(result.content).toContain(\"## Hello World\");\n    expect(result.content).toContain(\"## Key Concepts\");\n    expect(result.content).toContain(\"cargo run\");\n    expect(result.content).toContain(\"[Mozilla](https://www.mozilla.org)\");\n    expect(result.content).toContain(\"- Ownership and borrowing\");\n  });\n\n  it(\"real_world_documentation_page: Documentation page with nested lists, code examples, and blockquotes converts correctly\", () => {\n    const result = convert(\n      '<div class=\"docs\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\"language-rust\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\"language-rust\">let markdown = convert_html(\"&lt;h1&gt;Hello&lt;/h1&gt;\").unwrap();\\nassert_eq!(markdown, \"# Hello\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\"language-rust\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\"/docs/options\">options reference</a> for a full list of configuration values.</p></blockquote></div>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# API Reference\");\n    expect(result.content).toContain(\"## convert_html\");\n    expect(result.content).toContain(\"### Parameters\");\n    expect(result.content).toContain(\"### Returns\");\n    expect(result.content).toContain(\"### Example\");\n    expect(result.content).toContain(\"## ConversionOptions\");\n    expect(result.content).toContain(\"> \");\n    expect(result.content).toContain(\"thread-safe\");\n    expect(result.content).toContain(\"convert_html\");\n    expect(result.content).toContain(\"ConversionOptions\");\n  });\n\n  it(\"real_world_product_page: Product page with table, images, and lists converts correctly\", () => {\n    const result = convert(\n      '<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What\\'s in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>',\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# Wireless Keyboard Pro\");\n    expect(result.content).toContain(\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\");\n    expect(result.content).toContain(\"## Specifications\");\n    expect(result.content).toContain(\"Battery Life\");\n    expect(result.content).toContain(\"12 months\");\n    expect(result.content).toContain(\"Bluetooth 5.0\");\n    expect(result.content).toContain(\"## What's in the Box\");\n    expect(result.content).toContain(\"USB-C charging cable\");\n    expect(result.content).toContain(\"|\");\n    expect(result.content).toContain(\"---\");\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/result.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bfaff38650ca34c3d1b97208bcd9563b7df75e9aecdac8a933f1fb9f3b508be6\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"result\", () => {\n  it(\"result_tables_empty_when_no_tables: Result tables array is empty when input has no tables\", () => {\n    const result = convert(\"<p>No tables here</p>\", {\n      includeDocumentStructure: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBe(0);\n  });\n\n  it(\"result_tables_multiple: Multiple tables each appear in the tables array\", () => {\n    const result = convert(\n      \"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(result.tables.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"result_tables_simple: Simple table populates the tables array in result\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBeGreaterThanOrEqual(1);\n  });\n\n  it(\"result_tables_without_structure_flag: Tables array is empty when includeDocumentStructure is false\", () => {\n    const result = convert(\n      \"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_clean_input: Warnings array is empty for well-formed HTML without problematic content\", () => {\n    const result = convert(\n      \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_complex_input: Warnings array is empty for complex but valid HTML\", () => {\n    const result = convert(\n      \"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_malformed_html: Warnings array is empty even for malformed HTML (parser is lenient)\", () => {\n    const result = convert(\n      \"<p>Unclosed paragraph<div>Mixed nesting</p></div>\",\n      {} as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/smoke.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2e2248bf8fbc3e82e284a3e91e31a8351372752b7d3fae03c57e891988ab5162\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"smoke\", () => {\n  it(\"smoke_empty_string: Empty string produces empty output\", () => {\n    const result = convert(\"\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"smoke_simple_heading: H1 heading converts to ATX markdown\", () => {\n    const result = convert(\"<h1>Title</h1>\", {} as unknown as ConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n  });\n\n  it(\"smoke_simple_paragraph: Simple paragraph converts correctly\", () => {\n    const result = convert(\"<p>Hello World</p>\", {} as unknown as ConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/structure.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d293f3609865cb1f02f096be3817517300845b418d9ac48d2b68fa7bcb63f7f4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"structure\", () => {\n  it(\"structure_code_block: Fenced code block produces Code node\", () => {\n    const result = convert(\n      '<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>',\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_deep_nesting_h1_h2_h3: H1 > H2 > H3 creates three levels of heading nesting\", () => {\n    const result = convert(\n      \"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(5);\n  });\n\n  it(\"structure_h1_h2_nested_group: H1 followed by H2 creates a nested group under the H1\", () => {\n    const result = convert(\n      \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(3);\n  });\n\n  it(\"structure_heading_paragraph: Simple heading followed by paragraph produces Heading and Paragraph nodes\", () => {\n    const result = convert(\"<h1>Title</h1><p>A paragraph of text.</p>\", {\n      includeDocumentStructure: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_list: Unordered list produces List and ListItem nodes\", () => {\n    const result = convert(\"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", {\n      includeDocumentStructure: true,\n    } as unknown as ConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_multiple_headings: Multiple headings create multiple Heading nodes with correct levels\", () => {\n    const result = convert(\n      \"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(4);\n  });\n\n  it(\"structure_sibling_h1_groups: H1, H2, then another H1 creates two sibling top-level groups\", () => {\n    const result = convert(\n      \"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\",\n      { includeDocumentStructure: true } as unknown as ConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(4);\n  });\n});\n"
  },
  {
    "path": "e2e/node/tests/visitor.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a750f84eb033d42a3d505eeb783ec3a0d40d33aa68b4efda389721f2155bada2\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type ConversionOptions } from \"html-to-markdown\";\n\ndescribe(\"visitor\", () => {\n  it(\"visitor_audio_custom: Visitor replaces audio element with custom output\", () => {\n    const _testVisitor = {\n      visitAudio(ctx, src): string | { custom: string } {\n        return { custom: \"[AUDIO: podcast.mp3]\" };\n      },\n    };\n\n    const result = convert(\n      '<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[AUDIO: podcast.mp3]\");\n    expect(result.content ?? \"\").toContain(\"Listen to this:\");\n  });\n\n  it(\"visitor_audio_skip: Visitor removes audio elements from output\", () => {\n    const _testVisitor = {\n      visitAudio(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Background music:\");\n    expect(result.content ?? \"\").toContain(\"Enjoy!\");\n    expect(result.content).not.toContain(\"music.ogg\");\n  });\n\n  it(\"visitor_button_custom: Visitor replaces button with bracketed text\", () => {\n    const _testVisitor = {\n      visitButton(ctx, text): string | { custom: string } {\n        return { custom: `[BTN:{text}]` };\n      },\n    };\n\n    const result = convert(\n      '<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[BTN:Click me]\");\n    expect(result.content ?? \"\").toContain(\"[BTN:Cancel]\");\n    expect(result.content ?? \"\").toContain(\"Confirm action:\");\n  });\n\n  it(\"visitor_button_skip: Visitor removes all buttons from output\", () => {\n    const _testVisitor = {\n      visitButton(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Actions available:\");\n    expect(result.content).not.toContain(\"Save\");\n    expect(result.content).not.toContain(\"Delete\");\n    expect(result.content).not.toContain(\"Export\");\n  });\n\n  it(\"visitor_continue_default: Visitor continue action preserves default conversion\", () => {\n    const _testVisitor = {\n      visitStrong(ctx, text): string | { custom: string } {\n        return \"continue\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Hello <strong>World</strong></p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"**World**\");\n  });\n\n  it(\"visitor_custom_blockquote: Visitor replaces blockquote with custom format\", () => {\n    const _testVisitor = {\n      visitBlockquote(ctx, content, depth): string | { custom: string } {\n        return { custom: `QUOTE: \"{content}\"` };\n      },\n    };\n\n    const result = convert(\n      \"<blockquote><p>A wise quote.</p></blockquote>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"QUOTE:\");\n    expect(result.content ?? \"\").toContain(\"A wise quote.\");\n  });\n\n  it(\"visitor_custom_emphasis: Visitor replaces emphasis with custom output\", () => {\n    const _testVisitor = {\n      visitEmphasis(ctx, text): string | { custom: string } {\n        return { custom: `>>>{text}<<<` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is <em>important</em> text.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\">>>important<<<\");\n    expect(result.content).not.toContain(\"*important*\");\n  });\n\n  it(\"visitor_custom_heading: Visitor replaces heading with custom format\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return { custom: `--- {text} ---` };\n      },\n    };\n\n    const result = convert(\n      \"<h2>Section Title</h2><p>Content below heading.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"--- Section Title ---\");\n    expect(result.content).not.toContain(\"## Section Title\");\n    expect(result.content ?? \"\").toContain(\"Content below heading.\");\n  });\n\n  it(\"visitor_custom_image: Visitor replaces image with custom output using template\", () => {\n    const _testVisitor = {\n      visitImage(ctx, src, alt, title): string | { custom: string } {\n        return { custom: `[Image: {alt}]` };\n      },\n    };\n\n    const result = convert(\n      '<img src=\"banner.png\" alt=\"Banner\">',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[Image: Banner]\");\n    expect(result.content).not.toContain(\"banner.png\");\n  });\n\n  it(\"visitor_custom_link_format: Visitor reformats links using template interpolation\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return { custom: `{text} ({href})` };\n      },\n    };\n\n    const result = convert(\n      '<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Example (https://example.com)\");\n    expect(result.content).not.toContain(\"[Example]\");\n  });\n\n  it(\"visitor_custom_link_static: Visitor replaces link with static custom output\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return { custom: \"[REDACTED LINK]\" };\n      },\n    };\n\n    const result = convert(\n      '<a href=\"https://example.com\">Click here</a>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[REDACTED LINK]\");\n    expect(result.content).not.toContain(\"example.com\");\n  });\n\n  it(\"visitor_custom_output: Visitor custom action replaces element output\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return { custom: \"## REPLACED HEADING\" };\n      },\n    };\n\n    const result = convert(\"<h1>Original Heading</h1>\", {} as unknown as ConversionOptions, {\n      visitor: _testVisitor,\n    });\n    expect(result.content ?? \"\").toContain(\"## REPLACED HEADING\");\n    expect(result.content).not.toContain(\"# Original Heading\");\n  });\n\n  it(\"visitor_definition_list_custom: Visitor customizes definition list items\", () => {\n    const _testVisitor = {\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return { custom: `**{text}**` };\n      },\n    };\n\n    const result = convert(\n      \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"**HTML**\");\n    expect(result.content ?? \"\").toContain(\"**CSS**\");\n    expect(result.content ?? \"\").toContain(\"HyperText Markup Language\");\n  });\n\n  it(\"visitor_definition_list_custom_format: Visitor formats definition lists with custom templates\", () => {\n    const _testVisitor = {\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return { custom: `### {text}` };\n      },\n      visitDefinitionDescription(ctx, text): string | { custom: string } {\n        return { custom: `> {text}` };\n      },\n    };\n\n    const result = convert(\n      \"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"### Python\");\n    expect(result.content ?? \"\").toContain(\"### JavaScript\");\n    expect(result.content ?? \"\").toContain(\"> A high-level programming language\");\n    expect(result.content ?? \"\").toContain(\"> A scripting language for web browsers\");\n  });\n\n  it(\"visitor_definition_list_skip: Visitor skips definition list items from output\", () => {\n    const _testVisitor = {\n      visitDefinitionDescription(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Glossary:\");\n    expect(result.content ?? \"\").toContain(\"End of glossary\");\n    expect(result.content).not.toContain(\"Term A\");\n    expect(result.content).not.toContain(\"Definition\");\n  });\n\n  it(\"visitor_details_summary_custom: Visitor customizes details/summary disclosure elements\", () => {\n    const _testVisitor = {\n      visitSummary(ctx, text): string | { custom: string } {\n        return { custom: `[EXPANDABLE] {text}` };\n      },\n    };\n\n    const result = convert(\n      \"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[EXPANDABLE] Click to expand\");\n    expect(result.content ?? \"\").toContain(\"This content is initially hidden.\");\n  });\n\n  it(\"visitor_details_summary_skip: Visitor removes details/summary elements entirely\", () => {\n    const _testVisitor = {\n      visitDetails(ctx, isOpen): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Main content here.\");\n    expect(result.content ?? \"\").toContain(\"More main content.\");\n    expect(result.content).not.toContain(\"Hidden section\");\n    expect(result.content).not.toContain(\"Secret details\");\n  });\n\n  it(\"visitor_figure_custom: Visitor customizes figure and figcaption elements\", () => {\n    const _testVisitor = {\n      visitFigcaption(ctx, text): string | { custom: string } {\n        return { custom: `*{text}*` };\n      },\n    };\n\n    const result = convert(\n      '<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Article Title\");\n    expect(result.content ?? \"\").toContain(\"*Figure 1: System Architecture*\");\n    expect(result.content ?? \"\").toContain(\"Explanation of the figure.\");\n  });\n\n  it(\"visitor_figure_custom_wrap: Visitor wraps figure content with custom formatting\", () => {\n    const _testVisitor = {\n      visitFigureStart(ctx): string | { custom: string } {\n        return { custom: \"\\n[FIGURE]\\n\" };\n      },\n      visitFigureEnd(ctx, output): string | { custom: string } {\n        return {\n          custom: `{output}\n[/FIGURE]\n`,\n        };\n      },\n    };\n\n    const result = convert(\n      '<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[FIGURE]\");\n    expect(result.content ?? \"\").toContain(\"[/FIGURE]\");\n    expect(result.content ?? \"\").toContain(\"Gallery\");\n  });\n\n  it(\"visitor_figure_skip: Visitor removes figure elements with their captions\", () => {\n    const _testVisitor = {\n      visitFigureStart(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"See the chart below:\");\n    expect(result.content ?? \"\").toContain(\"As shown in the chart above.\");\n    expect(result.content).not.toContain(\"Revenue Trends\");\n    expect(result.content).not.toContain(\"chart.svg\");\n  });\n\n  it(\"visitor_form_custom: Visitor replaces form with custom output\", () => {\n    const _testVisitor = {\n      visitForm(ctx, actionUrl, method): string | { custom: string } {\n        return { custom: \"[FORM PLACEHOLDER]\" };\n      },\n    };\n\n    const result = convert(\n      '<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[FORM PLACEHOLDER]\");\n    expect(result.content).not.toContain(\"submit\");\n    expect(result.content).not.toContain(\"input\");\n  });\n\n  it(\"visitor_form_skip: Visitor skips form elements entirely\", () => {\n    const _testVisitor = {\n      visitForm(ctx, actionUrl, method): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Before form\");\n    expect(result.content ?? \"\").toContain(\"After form\");\n    expect(result.content).not.toContain(\"email\");\n  });\n\n  it(\"visitor_horizontal_rule_custom: Visitor replaces horizontal rule with custom output\", () => {\n    const _testVisitor = {\n      visitHorizontalRule(ctx): string | { custom: string } {\n        return { custom: \"\\n[DIVIDER]\\n\" };\n      },\n    };\n\n    const result = convert(\n      \"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[DIVIDER]\");\n    expect(result.content ?? \"\").toContain(\"Section A\");\n    expect(result.content ?? \"\").toContain(\"Section B\");\n    expect(result.content).not.toContain(\"---\");\n  });\n\n  it(\"visitor_horizontal_rule_skip: Visitor removes all horizontal rules\", () => {\n    const _testVisitor = {\n      visitHorizontalRule(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Part 1\");\n    expect(result.content ?? \"\").toContain(\"Part 2\");\n    expect(result.content ?? \"\").toContain(\"Part 3\");\n    expect(result.content).not.toContain(\"---\");\n  });\n\n  it(\"visitor_iframe_custom: Visitor replaces embedded iframe with custom text\", () => {\n    const _testVisitor = {\n      visitIframe(ctx, src): string | { custom: string } {\n        return { custom: \"[EMBEDDED: https://maps.example.com/embed]\" };\n      },\n    };\n\n    const result = convert(\n      '<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[EMBEDDED: https://maps.example.com/embed]\");\n    expect(result.content ?? \"\").toContain(\"Embedded map:\");\n    expect(result.content ?? \"\").toContain(\"End of map\");\n  });\n\n  it(\"visitor_iframe_skip: Visitor removes embedded iframes\", () => {\n    const _testVisitor = {\n      visitIframe(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Reviews\");\n    expect(result.content ?? \"\").toContain(\"See reviews from our partners.\");\n    expect(result.content).not.toContain(\"widget.example.com\");\n  });\n\n  it(\"visitor_input_custom: Visitor replaces input with labeled output\", () => {\n    const _testVisitor = {\n      visitInput(ctx, inputType, name, value): string | { custom: string } {\n        return { custom: `[INPUT:{input_type}]` };\n      },\n    };\n\n    const result = convert(\n      '<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[INPUT:text]\");\n    expect(result.content ?? \"\").toContain(\"[INPUT:password]\");\n  });\n\n  it(\"visitor_input_skip: Visitor skips all input elements\", () => {\n    const _testVisitor = {\n      visitInput(ctx, inputType, name, value): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Sign up:\");\n    expect(result.content ?? \"\").toContain(\"Continue\");\n    expect(result.content).not.toContain(\"email\");\n  });\n\n  it(\"visitor_line_break_custom: Visitor replaces line break with custom output\", () => {\n    const _testVisitor = {\n      visitLineBreak(ctx): string | { custom: string } {\n        return { custom: \" | \" };\n      },\n    };\n\n    const result = convert(\n      \"<p>First line<br>Second line<br>Third line</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"First line | Second line | Third line\");\n    expect(result.content).not.toContain(\"\\n\\n\");\n  });\n\n  it(\"visitor_line_break_skip: Visitor removes all line breaks\", () => {\n    const _testVisitor = {\n      visitLineBreak(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Address Line 1Address Line 2Address Line 3\");\n  });\n\n  it(\"visitor_mark_custom: Visitor replaces highlight/mark with custom template\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return { custom: `=={text}==` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is a <mark>highlighted passage</mark> in the text.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"==highlighted passage==\");\n    expect(result.content ?? \"\").toContain(\"This is a\");\n  });\n\n  it(\"visitor_mark_skip: Visitor skips mark elements entirely\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Key insight: <mark>always validate input</mark> for security.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"always validate input\");\n    expect(result.content ?? \"\").toContain(\"Key insight:\");\n    expect(result.content ?? \"\").toContain(\"for security.\");\n  });\n\n  it(\"visitor_preserve_html: Visitor preserve_html action includes raw HTML in output\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return \"preserve_html\";\n      },\n    };\n\n    const result = convert(\n      \"<div><custom-tag>Custom content</custom-tag></div>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"<custom-tag>\");\n  });\n\n  it(\"visitor_skip_all_headings: Visitor skips all headings from document\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<h1>Title</h1><p>Body text remains.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"Title\");\n    expect(result.content ?? \"\").toContain(\"Body text remains.\");\n  });\n\n  it(\"visitor_skip_code_blocks: Visitor skips code blocks from output\", () => {\n    const _testVisitor = {\n      visitCodeBlock(ctx, lang, code): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Intro text\");\n    expect(result.content ?? \"\").toContain(\"Outro text\");\n    expect(result.content).not.toContain(\"let x = 42\");\n  });\n\n  it(\"visitor_skip_heading: Visitor skip action omits all headings from output\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<h1>Title</h1><p>Body text remains.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"Title\");\n    expect(result.content ?? \"\").toContain(\"Body text remains.\");\n  });\n\n  it(\"visitor_skip_images: Visitor skips all images from output\", () => {\n    const _testVisitor = {\n      visitImage(ctx, src, alt, title): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Before image\");\n    expect(result.content ?? \"\").toContain(\"After image\");\n    expect(result.content).not.toContain(\"photo.jpg\");\n    expect(result.content).not.toContain(\"A photo\");\n  });\n\n  it(\"visitor_skip_links: Visitor skips all links entirely\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before <a href=\"https://example.com\">link text</a> after</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"link text\");\n    expect(result.content).not.toContain(\"example.com\");\n  });\n\n  it(\"visitor_skip_strong: Visitor skips bold/strong elements\", () => {\n    const _testVisitor = {\n      visitStrong(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Normal <strong>bold text</strong> normal</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"bold text\");\n    expect(result.content ?? \"\").toContain(\"Normal\");\n  });\n\n  it(\"visitor_subscript_custom: Visitor replaces subscript with custom template\", () => {\n    const _testVisitor = {\n      visitSubscript(ctx, text): string | { custom: string } {\n        return { custom: `~{text}~` };\n      },\n    };\n\n    const result = convert(\"<p>H<sub>2</sub>O is water.</p>\", {} as unknown as ConversionOptions, {\n      visitor: _testVisitor,\n    });\n    expect(result.content ?? \"\").toContain(\"H~2~O\");\n    expect(result.content ?? \"\").toContain(\"is water\");\n  });\n\n  it(\"visitor_subscript_skip: Visitor skips subscript elements entirely\", () => {\n    const _testVisitor = {\n      visitSubscript(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"The formula CHO is sugar.\");\n  });\n\n  it(\"visitor_superscript_custom: Visitor replaces superscript with custom template\", () => {\n    const _testVisitor = {\n      visitSuperscript(ctx, text): string | { custom: string } {\n        return { custom: `^{text}^` };\n      },\n    };\n\n    const result = convert(\n      \"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"E=mc^2^\");\n    expect(result.content ?? \"\").toContain(\"revolutionized physics\");\n  });\n\n  it(\"visitor_superscript_skip: Visitor skips superscript from output\", () => {\n    const _testVisitor = {\n      visitSuperscript(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"The equation x + y = z has no solutions.\");\n  });\n\n  it(\"visitor_underline_custom: Visitor replaces underline with custom markup\", () => {\n    const _testVisitor = {\n      visitUnderline(ctx, text): string | { custom: string } {\n        return { custom: `_{text}_` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is <u>very important</u> text.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"_very important_\");\n    expect(result.content).not.toContain(\"**\");\n  });\n\n  it(\"visitor_underline_skip: Visitor skips underline elements from output\", () => {\n    const _testVisitor = {\n      visitUnderline(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Normal text with <u>underlined part</u> and more text.</p>\",\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Normal text with\");\n    expect(result.content ?? \"\").toContain(\"and more text.\");\n    expect(result.content).not.toContain(\"underlined part\");\n  });\n\n  it(\"visitor_video_custom: Visitor replaces video with custom link\", () => {\n    const _testVisitor = {\n      visitVideo(ctx, src): string | { custom: string } {\n        return { custom: `[VIDEO: {src}]` };\n      },\n    };\n\n    const result = convert(\n      '<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[VIDEO: tutorial.mp4]\");\n    expect(result.content ?? \"\").toContain(\"Watch our tutorial:\");\n    expect(result.content ?? \"\").toContain(\"Great content!\");\n  });\n\n  it(\"visitor_video_skip: Visitor removes video elements entirely\", () => {\n    const _testVisitor = {\n      visitVideo(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>',\n      {} as unknown as ConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Demo\");\n    expect(result.content ?? \"\").toContain(\"See the demo above.\");\n    expect(result.content).not.toContain(\"demo.webm\");\n  });\n});\n"
  },
  {
    "path": "e2e/node/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"strictNullChecks\": false,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"tests/**/*.ts\", \"vitest.config.ts\"]\n}\n"
  },
  {
    "path": "e2e/node/vitest.config.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:75c0a1b3cb3150511b7963c9f0765d70b73ea54764a80883d358ade8c28fdb7b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    include: [\"tests/**/*.test.ts\"],\n  },\n});\n"
  },
  {
    "path": "e2e/php/bootstrap.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d3d179a92cd405cca99e78df40674a83d8c0d0a4d67f3c5a7b4611f2c0388c32\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\n// Load the e2e project autoloader (PHPUnit, test helpers).\nrequire_once __DIR__ . '/vendor/autoload.php';\n\n// Load the PHP binding package classes via its Composer autoloader.\n// The package's autoloader is separate from the e2e project's autoloader\n// since the php-ext type prevents direct composer path dependency.\n$pkgAutoloader = __DIR__ . '/../../packages/php/vendor/autoload.php';\nif (file_exists($pkgAutoloader)) {\n    require_once $pkgAutoloader;\n}\n"
  },
  {
    "path": "e2e/php/composer.json",
    "content": "{\n  \"name\": \"kreuzberg-dev/e2e-php\",\n  \"description\": \"E2e tests for PHP bindings\",\n  \"type\": \"project\",\n  \"require-dev\": {\n    \"phpunit/phpunit\": \"^13.1\",\n    \"guzzlehttp/guzzle\": \"^7.0\"\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"HtmlToMarkdown\\\\E2e\\\\\": \"tests/\"\n    }\n  }\n}\n"
  },
  {
    "path": "e2e/php/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/13.1/phpunit.xsd\"\n         bootstrap=\"bootstrap.php\"\n         colors=\"true\"\n         failOnRisky=\"true\"\n         failOnWarning=\"true\">\n    <testsuites>\n        <testsuite name=\"e2e\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "e2e/php/tests/ConversionTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1a8fd89d2df0a15496f75119403a551bf518df4126de541e4f49fa22361c770c\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: conversion. */\nfinal class ConversionTest extends TestCase\n{\n    /** Blockquote with multiple paragraphs has each paragraph prefixed */\n    public function test_blockquote_multiple_paragraphs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\");\n        $this->assertStringContainsString(\"> First paragraph.\", $result);\n        $this->assertStringContainsString(\"> Second paragraph.\", $result);\n    }\n\n    /** Nested blockquote produces double-prefixed lines */\n    public function test_blockquote_nested(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Outer quote.\", $result);\n        $this->assertStringContainsString(\"Inner quote.\", $result);\n    }\n\n    /** Simple blockquote */\n    public function test_blockquote_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Quote text</p></blockquote>\");\n        $this->assertStringContainsString(\"> Quote text\", $result);\n    }\n\n    /** Blockquote containing a list preserves list items inside quote */\n    public function test_blockquote_with_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Quote intro:\", $result);\n        $this->assertStringContainsString(\"Point one\", $result);\n        $this->assertStringContainsString(\"Point two\", $result);\n    }\n\n    /** Nested bold and italic */\n    public function test_bold_and_italic(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><strong><em>both</em></strong></p>\");\n        $this->assertStringContainsString(\"***both***\", $result);\n    }\n\n    /** Strong tag converts to bold */\n    public function test_bold_strong(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><strong>bold</strong></p>\");\n        $this->assertStringContainsString(\"**bold**\", $result);\n    }\n\n    /** Code block with language preserves content */\n    public function test_code_block(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"print('hello')\", $result);\n    }\n\n    /** Code block without a language class preserves content */\n    public function test_code_block_no_language(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>plain code here</code></pre>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"plain code here\", $result);\n    }\n\n    /** Inline code element nested inside a paragraph */\n    public function test_code_inline_in_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Call the <code>initialize()</code> method first.</p>\");\n        $this->assertStringContainsString(\"`initialize()`\", $result);\n    }\n\n    /** Inline code containing backtick characters is properly escaped */\n    public function test_code_with_backticks_in_content(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"backtick\", $result);\n    }\n\n    /** mark tag produces highlighted output */\n    public function test_emphasis_mark_highlight(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><mark>highlighted</mark></p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"highlighted\", $result);\n    }\n\n    /** del tag converts to GFM strikethrough */\n    public function test_emphasis_strikethrough_del(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><del>deleted text</del></p>\");\n        $this->assertStringContainsString(\"~~deleted text~~\", $result);\n    }\n\n    /** s tag converts to GFM strikethrough */\n    public function test_emphasis_strikethrough_s(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><s>strikethrough</s></p>\");\n        $this->assertStringContainsString(\"~~strikethrough~~\", $result);\n    }\n\n    /** sub tag content is preserved */\n    public function test_emphasis_subscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O</p>\");\n        $this->assertStringContainsString(\"H\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n        $this->assertStringContainsString(\"O\", $result);\n    }\n\n    /** sup tag content is preserved */\n    public function test_emphasis_superscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>x<sup>2</sup></p>\");\n        $this->assertStringContainsString(\"x\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n    }\n\n    /** u tag content is preserved in output */\n    public function test_emphasis_underline_u(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><u>underlined</u></p>\");\n        $this->assertStringContainsString(\"underlined\", $result);\n    }\n\n    /** Form input elements produce readable output without form mechanics */\n    public function test_form_input_elements(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Name\", $result);\n    }\n\n    /** Select element with options produces readable output */\n    public function test_form_select_options(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Color\", $result);\n    }\n\n    /** Textarea element produces readable output */\n    public function test_form_textarea(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Message\", $result);\n    }\n\n    /** H1 heading */\n    public function test_heading_h1(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Heading 1</h1>\");\n        $this->assertEquals(\"# Heading 1\", trim($result));\n    }\n\n    /** H2 heading */\n    public function test_heading_h2(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h2>Heading 2</h2>\");\n        $this->assertEquals(\"## Heading 2\", trim($result));\n    }\n\n    /** H3 heading */\n    public function test_heading_h3(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h3>Heading 3</h3>\");\n        $this->assertEquals(\"### Heading 3\", trim($result));\n    }\n\n    /** H4 heading */\n    public function test_heading_h4(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h4>Heading 4</h4>\");\n        $this->assertEquals(\"#### Heading 4\", trim($result));\n    }\n\n    /** H5 heading */\n    public function test_heading_h5(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h5>Heading 5</h5>\");\n        $this->assertEquals(\"##### Heading 5\", trim($result));\n    }\n\n    /** H6 heading */\n    public function test_heading_h6(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h6>Heading 6</h6>\");\n        $this->assertEquals(\"###### Heading 6\", trim($result));\n    }\n\n    /** Figure with figcaption preserves both image and caption */\n    public function test_image_figure_figcaption(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\");\n        $this->assertStringContainsString(\"![A sunset](sunset.jpg)\", $result);\n        $this->assertStringContainsString(\"Beautiful sunset over the ocean\", $result);\n    }\n\n    /** Image inside an anchor produces a linked image */\n    public function test_image_linked(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\");\n        $this->assertStringContainsString(\"![Icon](icon.png)\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Image without alt text produces image markdown */\n    public function test_image_no_alt(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"banner.jpg\\\">\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"banner.jpg\", $result);\n    }\n\n    /** Image with alt text */\n    public function test_image_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\");\n        $this->assertStringContainsString(\"![A photo](photo.jpg)\", $result);\n    }\n\n    /** Image with title attribute includes title in output */\n    public function test_image_with_title(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\");\n        $this->assertStringContainsString(\"![Sales chart](chart.png\", $result);\n        $this->assertStringContainsString(\"Q3 Sales\", $result);\n    }\n\n    /** Inline code */\n    public function test_inline_code(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use <code>console.log()</code> to debug</p>\");\n        $this->assertStringContainsString(\"`console.log()`\", $result);\n    }\n\n    /** Em tag converts to italic */\n    public function test_italic_em(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><em>italic</em></p>\");\n        $this->assertStringContainsString(\"*italic*\", $result);\n    }\n\n    /** Single br tag produces a line break in output */\n    public function test_line_break_br_tag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First line.<br>Second line.</p>\");\n        $this->assertStringContainsString(\"First line.\", $result);\n        $this->assertStringContainsString(\"Second line.\", $result);\n    }\n\n    /** hr tag produces a horizontal separator between content */\n    public function test_line_break_hr_tag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before rule.</p><hr><p>After rule.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Before rule.\", $result);\n        $this->assertStringContainsString(\"After rule.\", $result);\n    }\n\n    /** Multiple consecutive br tags in sequence */\n    public function test_line_break_multiple_br(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Start.<br><br>End.</p>\");\n        $this->assertStringContainsString(\"Start.\", $result);\n        $this->assertStringContainsString(\"End.\", $result);\n    }\n\n    /** Fragment-only anchor link is preserved */\n    public function test_link_anchor_fragment(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"#section\\\">Jump to section</a>\");\n        $this->assertStringContainsString(\"[Jump to section](#section)\", $result);\n    }\n\n    /** Link with empty href produces output with the link text */\n    public function test_link_empty_href(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"\\\">No destination</a>\");\n        $this->assertStringContainsString(\"No destination\", $result);\n    }\n\n    /** Image inside a link produces a linked image */\n    public function test_link_image_inside(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\");\n        $this->assertStringContainsString(\"![Logo](logo.png)\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Mailto link is preserved with mailto: scheme */\n    public function test_link_mailto(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\");\n        $this->assertStringContainsString(\"mailto:user@example.com\", $result);\n    }\n\n    /** Simple link */\n    public function test_link_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\">Example</a>\");\n        $this->assertStringContainsString(\"[Example](https://example.com)\", $result);\n    }\n\n    /** Link containing bold text preserves formatting */\n    public function test_link_with_bold_text(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\");\n        $this->assertStringContainsString(\"**Bold link**\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Link with title attribute */\n    public function test_link_with_title(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\");\n        $this->assertStringContainsString(\"[Example](https://example.com\", $result);\n        $this->assertStringContainsString(\"Example Site\", $result);\n    }\n\n    /** Definition list with dt and dd elements */\n    public function test_list_definition_dl(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\");\n        $this->assertStringContainsString(\"Term One\", $result);\n        $this->assertStringContainsString(\"Definition of term one.\", $result);\n        $this->assertStringContainsString(\"Term Two\", $result);\n        $this->assertStringContainsString(\"Definition of term two.\", $result);\n    }\n\n    /** List item containing multiple paragraphs */\n    public function test_list_item_multiple_paragraphs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\");\n        $this->assertStringContainsString(\"First paragraph in item.\", $result);\n        $this->assertStringContainsString(\"Second paragraph in item.\", $result);\n        $this->assertStringContainsString(\"Simple item\", $result);\n    }\n\n    /** Mixed list: ordered list nested inside unordered list */\n    public function test_list_mixed_nested(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\");\n        $this->assertStringContainsString(\"Item A\", $result);\n        $this->assertStringContainsString(\"Sub 1\", $result);\n        $this->assertStringContainsString(\"Sub 2\", $result);\n        $this->assertStringContainsString(\"Item B\", $result);\n    }\n\n    /** Nested ordered list with two levels of depth */\n    public function test_list_nested_ordered(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\");\n        $this->assertStringContainsString(\"Step 1\", $result);\n        $this->assertStringContainsString(\"Step 1a\", $result);\n        $this->assertStringContainsString(\"Step 1b\", $result);\n        $this->assertStringContainsString(\"Step 2\", $result);\n    }\n\n    /** Nested unordered list with two levels of depth */\n    public function test_list_nested_unordered(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\");\n        $this->assertStringContainsString(\"Parent A\", $result);\n        $this->assertStringContainsString(\"Child A1\", $result);\n        $this->assertStringContainsString(\"Child A2\", $result);\n        $this->assertStringContainsString(\"Parent B\", $result);\n    }\n\n    /** Task list with checked and unchecked checkboxes */\n    public function test_list_task_checkboxes(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Done task\", $result);\n        $this->assertStringContainsString(\"Pending task\", $result);\n    }\n\n    /** Ordered list */\n    public function test_ordered_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\");\n        $this->assertStringContainsString(\"1. First\", $result);\n        $this->assertStringContainsString(\"2. Second\", $result);\n        $this->assertStringContainsString(\"3. Third\", $result);\n    }\n\n    /** Multiple paragraphs are separated by a blank line */\n    public function test_paragraph_multiple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\");\n        $this->assertStringContainsString(\"First paragraph.\", $result);\n        $this->assertStringContainsString(\"Second paragraph.\", $result);\n    }\n\n    /** Text nested inside divs is extracted correctly */\n    public function test_paragraph_nested_divs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><div><p>Nested text</p></div></div>\");\n        $this->assertStringContainsString(\"Nested text\", $result);\n    }\n\n    /** Simple paragraph converts to plain text */\n    public function test_paragraph_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello World</p>\");\n        $this->assertEquals(\"Hello World\", trim($result));\n    }\n\n    /** Paragraph with bold, italic, and a link */\n    public function test_paragraph_with_inline_formatting(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\");\n        $this->assertStringContainsString(\"**bold**\", $result);\n        $this->assertStringContainsString(\"*italic*\", $result);\n        $this->assertStringContainsString(\"[link](https://example.com)\", $result);\n    }\n\n    /** Paragraph with br tags produces line breaks in output */\n    public function test_paragraph_with_line_breaks(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Line one.\", $result);\n        $this->assertStringContainsString(\"Line two.\", $result);\n        $this->assertStringContainsString(\"Line three.\", $result);\n    }\n\n    /** Abbreviation element text is preserved */\n    public function test_semantic_abbr(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\");\n        $this->assertStringContainsString(\"WWW\", $result);\n    }\n\n    /** Article element wrapping content preserves inner content */\n    public function test_semantic_article(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\");\n        $this->assertStringContainsString(\"Article Title\", $result);\n        $this->assertStringContainsString(\"Article body.\", $result);\n    }\n\n    /** Definition list with term and description */\n    public function test_semantic_definition_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\");\n        $this->assertStringContainsString(\"HTML\", $result);\n        $this->assertStringContainsString(\"HyperText Markup Language\", $result);\n        $this->assertStringContainsString(\"CSS\", $result);\n        $this->assertStringContainsString(\"Cascading Style Sheets\", $result);\n    }\n\n    /** Details and summary elements produce readable output */\n    public function test_semantic_details_summary(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Click to expand\", $result);\n    }\n\n    /** Horizontal rule produces a separator in output */\n    public function test_semantic_hr(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Above</p><hr><p>Below</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Above\", $result);\n        $this->assertStringContainsString(\"Below\", $result);\n    }\n\n    /** Mark tag produces highlighted output */\n    public function test_semantic_mark_highlight(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"highlighted text\", $result);\n    }\n\n    /** Section element with heading preserves structure */\n    public function test_semantic_section_with_heading(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\");\n        $this->assertStringContainsString(\"Section Heading\", $result);\n        $this->assertStringContainsString(\"Section content.\", $result);\n    }\n\n    /** Subscript and superscript elements are preserved in output */\n    public function test_semantic_sub_superscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"H\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n        $this->assertStringContainsString(\"O\", $result);\n        $this->assertStringContainsString(\"E=mc\", $result);\n    }\n\n    /** Simple table with header */\n    public function test_simple_table(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\");\n        $this->assertStringContainsString(\"Name\", $result);\n        $this->assertStringContainsString(\"Age\", $result);\n        $this->assertStringContainsString(\"Alice\", $result);\n        $this->assertStringContainsString(\"30\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n        $this->assertStringContainsString(\"---\", $result);\n    }\n\n    /** Empty table produces no output or minimal output */\n    public function test_table_empty(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table></table>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Table without thead uses first row as implied header */\n    public function test_table_no_thead(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Product\", $result);\n        $this->assertStringContainsString(\"Price\", $result);\n        $this->assertStringContainsString(\"Apple\", $result);\n        $this->assertStringContainsString(\"1.00\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n    }\n\n    /** Table cells containing pipe characters are escaped in output */\n    public function test_table_pipe_chars_in_content(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Expression\", $result);\n        $this->assertStringContainsString(\"Result\", $result);\n        $this->assertStringContainsString(\"true\", $result);\n    }\n\n    /** Table with column alignment attributes */\n    public function test_table_with_alignment(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Left\", $result);\n        $this->assertStringContainsString(\"Center\", $result);\n        $this->assertStringContainsString(\"Right\", $result);\n        $this->assertStringContainsString(\"L\", $result);\n        $this->assertStringContainsString(\"C\", $result);\n        $this->assertStringContainsString(\"R\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n    }\n\n    /** Table with colspan attribute in a header cell */\n    public function test_table_with_colspan(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Full Name\", $result);\n        $this->assertStringContainsString(\"John\", $result);\n        $this->assertStringContainsString(\"Doe\", $result);\n    }\n\n    /** Unordered list */\n    public function test_unordered_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\");\n        $this->assertStringContainsString(\"- Item 1\", $result);\n        $this->assertStringContainsString(\"- Item 2\", $result);\n        $this->assertStringContainsString(\"- Item 3\", $result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/EdgeCasesTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b54445535a31a9c4c6af37925434d232d79d504d2914ebc46822b639b33dba92\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: edge-cases. */\nfinal class EdgeCasesTest extends TestCase\n{\n    /** Empty HTML document */\n    public function test_empty_html(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head></head><body></body></html>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** CJK (Chinese, Japanese, Korean) characters are preserved */\n    public function test_encoding_cjk_characters(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"中文内容\", $result);\n        $this->assertStringContainsString(\"日本語テキスト\", $result);\n        $this->assertStringContainsString(\"한국어 텍스트\", $result);\n    }\n\n    /** Common HTML entities are decoded in output */\n    public function test_encoding_html_entities(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"&\", $result);\n        $this->assertStringContainsString(\"<\", $result);\n        $this->assertStringContainsString(\">\", $result);\n    }\n\n    /** Named HTML entities like &mdash; and &hellip; are decoded */\n    public function test_encoding_named_entities(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"—\", $result);\n        $this->assertStringContainsString(\"…\", $result);\n    }\n\n    /** Numeric HTML entities (decimal and hex) are decoded */\n    public function test_encoding_numeric_entities(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"©\", $result);\n        $this->assertStringContainsString(\"®\", $result);\n        $this->assertStringContainsString(\"€\", $result);\n    }\n\n    /** Emoji and Unicode characters are preserved */\n    public function test_encoding_unicode_emoji(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"🌍\", $result);\n        $this->assertStringContainsString(\"🚀\", $result);\n        $this->assertStringContainsString(\"⭐\", $result);\n    }\n\n    /** Document containing only HTML comments produces empty output */\n    public function test_html_comments_only(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<!-- This is a comment --><!-- Another comment -->\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Input that is only whitespace characters (spaces, tabs, newlines) produces empty output */\n    public function test_just_whitespace_input(): void\n    {\n        $result = HtmlToMarkdown::convert(\"   \");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Deeply nested elements (100 levels) are handled without stack overflow */\n    public function test_malformed_deeply_nested_elements(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Deeply nested content\", $result);\n    }\n\n    /** Missing closing tags on block elements are auto-closed by parser */\n    public function test_malformed_missing_block_closing_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Title\", $result);\n        $this->assertStringContainsString(\"First paragraph\", $result);\n        $this->assertStringContainsString(\"Second paragraph\", $result);\n    }\n\n    /** Overlapping bold/italic tags are recovered by the HTML parser without panic */\n    public function test_malformed_overlapping_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><b><i>bold and italic</b></i></p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"bold and italic\", $result);\n    }\n\n    /** Unclosed <p> tag is recovered gracefully and content is preserved */\n    public function test_malformed_unclosed_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This paragraph is never closed\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"This paragraph is never closed\", $result);\n    }\n\n    /** Document with only script tags produces empty output (scripts are stripped) */\n    public function test_script_tags_only(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Document with only style tags produces empty output (styles are stripped) */\n    public function test_style_tags_only(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Visitor handles custom elements with nested content */\n    public function test_visitor_custom_element_with_nesting(): void\n    {\n        $visitor = new class {\n            public function visit_custom_element($ctx, $tagName, $html) {\n                return ['custom' => \"[CUSTOM WIDGET]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<div><custom-widget data-value=\\\"123\\\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[CUSTOM WIDGET]\", $result);\n        $this->assertStringNotContainsString(\"Widget content here\", $result);\n    }\n\n    /** Visitor skips deeply nested elements */\n    public function test_visitor_deeply_nested_skip(): void\n    {\n        $visitor = new class {\n            public function visit_mark($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Outer\", $result);\n        $this->assertStringContainsString(\"text\", $result);\n        $this->assertStringNotContainsString(\"highlight\", $result);\n    }\n\n    /** Visitor modifies element at end after children processed */\n    public function test_visitor_element_end_modification(): void\n    {\n        $visitor = new class {\n            public function visit_element_end($ctx, $output) {\n                return ['custom' => \"MODIFIED OUTPUT\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Original quote</p></blockquote>\", (object)['visitor' => $visitor]);\n        $this->assertNotEmpty($result);\n    }\n\n    /** Visitor skips at element_start level removes entire subtree */\n    public function test_visitor_element_start_skip_entire_subtree(): void\n    {\n        $visitor = new class {\n            public function visit_element_start($ctx) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<div><h1>Title</h1><p>Content</p></div>\", (object)['visitor' => $visitor]);\n        $this->assertNotEmpty($result);\n    }\n\n    /** Visitor preserves unknown HTML tags as raw HTML */\n    public function test_visitor_unknown_tag_preservation(): void\n    {\n        $visitor = new class {\n            public function visit_custom_element($ctx, $tagName, $html) {\n                return 'preserve_html';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Article text\", $result);\n        $this->assertStringContainsString(\"More article text\", $result);\n        $this->assertStringContainsString(\"<x-custom>\", $result);\n    }\n\n    /** Whitespace-only content */\n    public function test_whitespace_only(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>   </p>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** onclick and other on* event handlers are removed from elements */\n    public function test_xss_onclick_handler_removed(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><a href=\\\"https://example.com\\\" onclick=\\\"alert('xss')\\\">Click me</a></p><button onmouseover=\\\"steal_data()\\\">Hover me</button>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Click me\", $result);\n    }\n\n    /** Script tag content is stripped and does not appear in output */\n    public function test_xss_script_tag_stripped(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Safe content\", $result);\n        $this->assertStringContainsString(\"More safe content\", $result);\n    }\n\n    /** Script tags nested inside SVG are stripped */\n    public function test_xss_svg_nested_script_stripped(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Before SVG\", $result);\n        $this->assertStringContainsString(\"After SVG\", $result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/MetadataTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bc6719b77baa33186a882219f5155e20c18455a6ec0831ab9e906c031fce542c\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: metadata. */\nfinal class MetadataTest extends TestCase\n{\n    /** Extract author from <meta name='author'> tag */\n    public function test_metadata_author_meta(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Page</title><meta name=\\\"author\\\" content=\\\"Jane Doe\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.author' not on simple result type\n    }\n\n    /** Extract canonical URL from <link rel='canonical'> tag */\n    public function test_metadata_canonical_url(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Page</title><link rel=\\\"canonical\\\" href=\\\"https://example.com/canonical-page\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.canonical_url' not on simple result type\n    }\n\n    /** Extract description from <meta name='description'> tag */\n    public function test_metadata_description_meta(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Page</title><meta name=\\\"description\\\" content=\\\"This is the page description.\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.description' not on simple result type\n    }\n\n    /** Extract Dublin Core metadata tags */\n    public function test_metadata_dublin_core(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Scholarly Work</title><meta name=\\\"DC.title\\\" content=\\\"Principles of Knowledge Management\\\"><meta name=\\\"DC.creator\\\" content=\\\"Dr. Alice Johnson\\\"><meta name=\\\"DC.date\\\" content=\\\"2023-06-15\\\"><meta name=\\\"DC.subject\\\" content=\\\"Knowledge Management\\\"><meta name=\\\"DC.publisher\\\" content=\\\"Academic Press\\\"></head><body><p>This is a scholarly article.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"scholarly article\", $result);\n    }\n\n    /** Extract all images from a document into metadata */\n    public function test_metadata_extract_all_images(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Gallery</title></head><body><img src=\\\"https://example.com/photo1.jpg\\\" alt=\\\"Photo 1\\\"><img src=\\\"https://example.com/photo2.png\\\" alt=\\\"Photo 2\\\"><img src=\\\"/local/image.webp\\\" alt=\\\"Local image\\\"></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.images' not on simple result type\n    }\n\n    /** Extract all links from a document into metadata */\n    public function test_metadata_extract_all_links(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Links Page</title></head><body><p>Visit <a href=\\\"https://example.com\\\">Example</a> or <a href=\\\"https://docs.example.com\\\">Docs</a>.</p><p>Also see <a href=\\\"/relative/path\\\">relative link</a> and <a href=\\\"mailto:hello@example.com\\\">email us</a>.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.links' not on simple result type\n    }\n\n    /** Extract heading hierarchy from document into metadata */\n    public function test_metadata_headers_hierarchy(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.headings' not on simple result type\n    }\n\n    /** Extract keywords from <meta name='keywords'> tag */\n    public function test_metadata_keywords_meta(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Page</title><meta name=\\\"keywords\\\" content=\\\"rust, markdown, html, converter\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.document.keywords' not on simple result type\n    }\n\n    /** Extract language from html lang attribute */\n    public function test_metadata_lang_attribute(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html lang=\\\"es\\\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Hola Mundo\", $result);\n    }\n\n    /** Extract schema.org microdata for Article */\n    public function test_metadata_microdata_schema_article(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Article</title></head><body><article itemscope itemtype=\\\"https://schema.org/Article\\\"><h1 itemprop=\\\"headline\\\">Breaking News Today</h1><span itemprop=\\\"author\\\">Jane Reporter</span><span itemprop=\\\"datePublished\\\">2024-04-22</span><div itemprop=\\\"articleBody\\\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Breaking News Today\", $result);\n        $this->assertStringContainsString(\"Jane Reporter\", $result);\n        $this->assertStringContainsString(\"important information\", $result);\n    }\n\n    /** Extract schema.org breadcrumb navigation microdata */\n    public function test_metadata_microdata_schema_breadcrumb(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\\\"https://schema.org/BreadcrumbList\\\"><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com\\\"><span itemprop=\\\"name\\\">Home</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com/products\\\"><span itemprop=\\\"name\\\">Products</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><span itemprop=\\\"name\\\">Current Page</span></span></nav></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Home\", $result);\n        $this->assertStringContainsString(\"Products\", $result);\n        $this->assertStringContainsString(\"Current Page\", $result);\n    }\n\n    /** Extract schema.org microdata for Organization */\n    public function test_metadata_microdata_schema_organization(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Company</title></head><body><div itemscope itemtype=\\\"https://schema.org/Organization\\\"><span itemprop=\\\"name\\\">Acme Corp</span><span itemprop=\\\"foundingDate\\\">2020</span><span itemprop=\\\"url\\\">https://acmecorp.example.com</span><span itemprop=\\\"logo\\\">https://acmecorp.example.com/logo.png</span></div></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Acme Corp\", $result);\n        $this->assertStringContainsString(\"2020\", $result);\n    }\n\n    /** Extract schema.org microdata for Person */\n    public function test_metadata_microdata_schema_person(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Contact</title></head><body><div itemscope itemtype=\\\"https://schema.org/Person\\\"><span itemprop=\\\"name\\\">John Smith</span><span itemprop=\\\"email\\\">john@example.com</span><span itemprop=\\\"telephone\\\">+1-555-0100</span></div></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"John Smith\", $result);\n        $this->assertStringContainsString(\"john@example.com\", $result);\n    }\n\n    /** Extract schema.org microdata for Product */\n    public function test_metadata_microdata_schema_product(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Product</title></head><body><div itemscope itemtype=\\\"https://schema.org/Product\\\"><h1 itemprop=\\\"name\\\">Awesome Widget</h1><span itemprop=\\\"description\\\">The best widget on the market</span><span itemprop=\\\"price\\\">29.99</span><span itemprop=\\\"priceCurrency\\\">USD</span><img itemprop=\\\"image\\\" src=\\\"widget.jpg\\\" alt=\\\"Widget\\\"><span itemprop=\\\"ratingValue\\\">4.5</span></div></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Awesome Widget\", $result);\n        $this->assertStringContainsString(\"best widget\", $result);\n        $this->assertStringContainsString(\"29.99\", $result);\n    }\n\n    /** Extract text direction from lang attribute on html element */\n    public function test_metadata_text_direction_ltr(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html lang=\\\"en\\\" dir=\\\"ltr\\\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"left-to-right text\", $result);\n    }\n\n    /** Extract right-to-left text direction */\n    public function test_metadata_text_direction_rtl(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html lang=\\\"ar\\\" dir=\\\"rtl\\\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"right-to-left text\", $result);\n    }\n\n    /** Extract title from <title> tag */\n    public function test_metadata_title_tag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.title' not on simple result type\n    }\n\n    /** Extract og:title, og:description, and og:image from Open Graph meta tags */\n    public function test_og_basic_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Fallback Title</title><meta property=\\\"og:title\\\" content=\\\"OG Title\\\"><meta property=\\\"og:description\\\" content=\\\"OG description text.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/image.jpg\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.open_graph.title' not on simple result type\n        // skipped: result_is_simple, field 'metadata.open_graph.description' not on simple result type\n        // skipped: result_is_simple, field 'metadata.open_graph.image' not on simple result type\n    }\n\n    /** Extract multiple Open Graph tags including type, url, and site_name */\n    public function test_og_multiple_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><meta property=\\\"og:title\\\" content=\\\"Article Title\\\"><meta property=\\\"og:type\\\" content=\\\"article\\\"><meta property=\\\"og:url\\\" content=\\\"https://example.com/article\\\"><meta property=\\\"og:site_name\\\" content=\\\"Example Site\\\"><meta property=\\\"og:description\\\" content=\\\"An interesting article.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/article.jpg\\\"></head><body><article><p>Article content here.</p></article></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.open_graph.title' not on simple result type\n        // skipped: result_is_simple, field 'metadata.open_graph.type' not on simple result type\n        // skipped: result_is_simple, field 'metadata.open_graph.url' not on simple result type\n        // skipped: result_is_simple, field 'metadata.open_graph.site_name' not on simple result type\n    }\n\n    /** JSON-LD script tag is stripped from output (security) but metadata may be extracted */\n    public function test_structured_data_json_ld(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Article</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Article\\\",\\\"headline\\\":\\\"My Article\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Jane Doe\\\"},\\\"datePublished\\\":\\\"2024-01-15\\\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"My Article\", $result);\n    }\n\n    /** Multiple JSON-LD blocks are all stripped from output */\n    public function test_structured_data_multiple_json_ld(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Shop Page</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Product\\\",\\\"name\\\":\\\"Widget\\\",\\\"price\\\":\\\"9.99\\\"}</script><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"BreadcrumbList\\\",\\\"itemListElement\\\":[{\\\"@type\\\":\\\"ListItem\\\",\\\"position\\\":1,\\\"name\\\":\\\"Home\\\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Widget\", $result);\n    }\n\n    /** Extract Twitter card meta tags */\n    public function test_twitter_card_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><meta name=\\\"twitter:card\\\" content=\\\"summary_large_image\\\"><meta name=\\\"twitter:site\\\" content=\\\"@examplesite\\\"><meta name=\\\"twitter:title\\\" content=\\\"Twitter Card Title\\\"><meta name=\\\"twitter:description\\\" content=\\\"Twitter card description.\\\"><meta name=\\\"twitter:image\\\" content=\\\"https://example.com/twitter-image.jpg\\\"></head><body><p>Content</p></body></html>\");\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.twitter.card' not on simple result type\n        // skipped: result_is_simple, field 'metadata.twitter.title' not on simple result type\n        // skipped: result_is_simple, field 'metadata.twitter.description' not on simple result type\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/OptionsTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:af8bc716177f0ef7cf426bdc4e4dd54acb5fce772727996ded482980464cc9f1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: options. */\nfinal class OptionsTest extends TestCase\n{\n    /** Bare URL links rendered as regular markdown links when autolinks disabled */\n    public function test_options_autolinks_false(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><a href='https://example.com'>https://example.com</a></p>\", [\"autolinks\" => false]);\n        $this->assertStringContainsString(\"example.com\", $result);\n    }\n\n    /** BR elements in table cells are stripped when disabled */\n    public function test_options_br_in_tables_false(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", [\"br_in_tables\" => false]);\n        $this->assertStringContainsString(\"Col\", $result);\n    }\n\n    /** BR elements in table cells render as line breaks */\n    public function test_options_br_in_tables_true(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\", [\"br_in_tables\" => true]);\n        $this->assertStringContainsString(\"Header\", $result);\n        $this->assertStringContainsString(\"Line 1\", $result);\n        $this->assertStringContainsString(\"Line 2\", $result);\n    }\n\n    /** Backticks code block style uses triple backtick fences */\n    public function test_options_code_block_backticks(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", [\"code_block_style\" => \"backticks\"]);\n        $this->assertStringContainsString(\"```\", $result);\n        $this->assertStringContainsString(\"console.log('hi');\", $result);\n    }\n\n    /** Code blocks use 4-space indentation */\n    public function test_options_code_block_indented(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>print('hello')</code></pre>\", [\"code_block_style\" => \"indented\"]);\n        $this->assertStringContainsString(\"print('hello')\", $result);\n        $this->assertStringNotContainsString(\"```\", $result);\n    }\n\n    /** Code blocks use tilde fences */\n    public function test_options_code_block_tildes(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>let x = 1;</code></pre>\", [\"code_block_style\" => \"tildes\"]);\n        $this->assertStringContainsString(\"~~~\", $result);\n        $this->assertStringContainsString(\"let x = 1;\", $result);\n    }\n\n    /** Tildes code block style uses triple tilde fences */\n    public function test_options_code_block_tildes_style(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>some code</code></pre>\", [\"code_block_style\" => \"tildes\"]);\n        $this->assertStringContainsString(\"~~~\", $result);\n        $this->assertStringContainsString(\"some code\", $result);\n    }\n\n    /** Default code language annotation on blocks without lang attribute */\n    public function test_options_code_language_python(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>def hello(): pass</code></pre>\", [\"code_language\" => \"python\"]);\n        $this->assertStringContainsString(\"```python\", $result);\n        $this->assertStringContainsString(\"def hello\", $result);\n    }\n\n    /** Block elements treated as inline */\n    public function test_options_convert_as_inline(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>One</p><p>Two</p>\", [\"convert_as_inline\" => true]);\n        $this->assertStringContainsString(\"One\", $result);\n        $this->assertStringContainsString(\"Two\", $result);\n    }\n\n    /** Debug mode enabled does not crash and produces output */\n    public function test_options_debug_true(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Debug test</p>\", [\"debug\" => true]);\n        $this->assertStringContainsString(\"Debug test\", $result);\n    }\n\n    /** Links without title get empty title attribute when defaultTitle is true */\n    public function test_options_default_title_true(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><a href='https://example.com'>Link</a></p>\", [\"default_title\" => true]);\n        $this->assertStringContainsString(\"Link\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** UTF-8 encoding hint for special characters */\n    public function test_options_encoding_utf8(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Café naïve résumé</p>\", [\"encoding\" => \"utf-8\"]);\n        $this->assertNotEmpty($result);\n    }\n\n    /** ASCII Markdown characters are escaped when escapeAscii is true */\n    public function test_options_escape_ascii_enabled(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text with # hash and [brackets] and * star</p>\", [\"escape_ascii\" => true]);\n        $this->assertStringContainsString(\"Text\", $result);\n        $this->assertStringContainsString(\"hash\", $result);\n        $this->assertStringContainsString(\"brackets\", $result);\n        $this->assertStringContainsString(\"star\", $result);\n    }\n\n    /** escape_asterisks option escapes asterisks in plain text */\n    public function test_options_escape_asterisks(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use 2*3 = 6 in math.</p>\", [\"escape_asterisks\" => true]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"2\", $result);\n        $this->assertStringContainsString(\"3\", $result);\n        $this->assertStringContainsString(\"6\", $result);\n    }\n\n    /** escape_misc option escapes miscellaneous markdown characters */\n    public function test_options_escape_misc(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use # and | and ~ in text.</p>\", [\"escape_misc\" => true]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Use\", $result);\n        $this->assertStringContainsString(\"and\", $result);\n        $this->assertStringContainsString(\"in text.\", $result);\n    }\n\n    /** escape_underscores option escapes underscores in plain text */\n    public function test_options_escape_underscores(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>The variable_name is defined.</p>\", [\"escape_underscores\" => true]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"variable\", $result);\n        $this->assertStringContainsString(\"name\", $result);\n        $this->assertStringContainsString(\"defined.\", $result);\n    }\n\n    /** Elements matching CSS attribute selector are excluded entirely */\n    public function test_options_exclude_selectors_attribute(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><div role=\\\"complementary\\\">Sidebar</div><p>Primary text</p></body>\", [\"exclude_selectors\" => [\"[role='complementary']\"]]);\n        $this->assertStringContainsString(\"Primary text\", $result);\n        $this->assertStringNotContainsString(\"Sidebar\", $result);\n    }\n\n    /** Elements matching CSS class selector are excluded entirely */\n    public function test_options_exclude_selectors_class(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><div class=\\\"cookie-banner\\\">Accept cookies</div><p>Main content</p></body>\", [\"exclude_selectors\" => [\".cookie-banner\"]]);\n        $this->assertStringContainsString(\"Main content\", $result);\n        $this->assertStringNotContainsString(\"cookies\", $result);\n    }\n\n    /** Empty exclude_selectors list does not affect output */\n    public function test_options_exclude_selectors_empty_noop(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello world</p>\", [\"exclude_selectors\" => []]);\n        $this->assertStringContainsString(\"Hello world\", $result);\n    }\n\n    /** Elements matching CSS id selector are excluded entirely */\n    public function test_options_exclude_selectors_id(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><div id=\\\"ad-container\\\">Buy stuff</div><p>Article text</p></body>\", [\"exclude_selectors\" => [\"#ad-container\"]]);\n        $this->assertStringContainsString(\"Article text\", $result);\n        $this->assertStringNotContainsString(\"Buy stuff\", $result);\n    }\n\n    /** Multiple CSS selectors each exclude their matched elements */\n    public function test_options_exclude_selectors_multiple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><nav class=\\\"nav\\\">Menu</nav><p>Content</p><footer>Footer</footer></body>\", [\"exclude_selectors\" => [\".nav\", \"footer\"]]);\n        $this->assertStringContainsString(\"Content\", $result);\n        $this->assertStringNotContainsString(\"Menu\", $result);\n        $this->assertStringNotContainsString(\"Footer\", $result);\n    }\n\n    /** All descendants of excluded elements are dropped */\n    public function test_options_exclude_selectors_nested_content_dropped(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><aside class=\\\"sidebar\\\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>\", [\"exclude_selectors\" => [\".sidebar\"]]);\n        $this->assertStringContainsString(\"Main text\", $result);\n        $this->assertStringNotContainsString(\"Related\", $result);\n        $this->assertStringNotContainsString(\"Sidebar text\", $result);\n    }\n\n    /** Exclude selectors work in plain text output mode */\n    public function test_options_exclude_selectors_plain_text_mode(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><div class=\\\"nav\\\">Navigation</div><p>Article body</p></body>\", [\"exclude_selectors\" => [\".nav\"], \"output_format\" => \"plain\"]);\n        $this->assertStringContainsString(\"Article body\", $result);\n        $this->assertStringNotContainsString(\"Navigation\", $result);\n    }\n\n    /** exclude_selectors drops entire subtree unlike strip_tags which keeps children */\n    public function test_options_exclude_selectors_vs_strip_tags(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<body><div class=\\\"wrapper\\\"><p>Inner paragraph</p></div><p>Outer text</p></body>\", [\"exclude_selectors\" => [\".wrapper\"]]);\n        $this->assertStringContainsString(\"Outer text\", $result);\n        $this->assertStringNotContainsString(\"Inner paragraph\", $result);\n    }\n\n    /** Extract metadata returns document metadata when enabled */\n    public function test_options_extract_metadata_true(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\", [\"extract_metadata\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'metadata.title' not on simple result type\n        // skipped: result_is_simple, field 'metadata.description' not on simple result type\n    }\n\n    /** ATX heading style produces hash-prefixed headings */\n    public function test_options_heading_style_atx(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><h2>Subtitle</h2>\", [\"heading_style\" => \"atx\"]);\n        $this->assertStringContainsString(\"# Title\", $result);\n        $this->assertStringContainsString(\"## Subtitle\", $result);\n    }\n\n    /** ATX closed heading style adds closing hashes */\n    public function test_options_heading_style_atx_closed(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Closed Heading</h1>\", [\"heading_style\" => \"atx_closed\"]);\n        $this->assertStringContainsString(\"# Closed Heading #\", $result);\n    }\n\n    /** Underlined heading style produces setext-style headings for h1 and h2 */\n    public function test_options_heading_style_underlined(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Main Title</h1>\", [\"heading_style\" => \"underlined\"]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Main Title\", $result);\n    }\n\n    /** Mark tag rendered as bold */\n    public function test_options_highlight_bold(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text with <mark>highlighted</mark> text.</p>\", [\"highlight_style\" => \"bold\"]);\n        $this->assertStringContainsString(\"**highlighted**\", $result);\n    }\n\n    /** Mark tag with double equal highlight style */\n    public function test_options_highlight_double_equal(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text with <mark>highlighted</mark> here.</p>\", [\"highlight_style\" => \"double_equal\"]);\n        $this->assertStringContainsString(\"==highlighted==\", $result);\n    }\n\n    /** Mark tag with no highlight style strips the mark */\n    public function test_options_highlight_none(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text with <mark>plain</mark> content.</p>\", [\"highlight_style\" => \"none\"]);\n        $this->assertStringContainsString(\"plain\", $result);\n        $this->assertStringNotContainsString(\"==\", $result);\n    }\n\n    /** Images inside specified tags stay inline */\n    public function test_options_keep_inline_images_in_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text <img src='icon.png' alt='icon'> more text</p>\", [\"keep_inline_images_in\" => [\"p\"]]);\n        $this->assertStringContainsString(\"Text\", $result);\n        $this->assertStringContainsString(\"more text\", $result);\n    }\n\n    /** Links use reference-style formatting */\n    public function test_options_link_style_reference(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\", [\"link_style\" => \"reference\"]);\n        $this->assertStringContainsString(\"Example\", $result);\n        $this->assertStringContainsString(\"Other\", $result);\n        $this->assertStringContainsString(\"example.com\", $result);\n    }\n\n    /** Custom bullet character for unordered lists */\n    public function test_options_list_custom_bullets(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Item A</li><li>Item B</li></ul>\", [\"bullets\" => \"*\"]);\n        $this->assertStringContainsString(\"* Item A\", $result);\n        $this->assertStringContainsString(\"* Item B\", $result);\n    }\n\n    /** Tab indentation type for nested list items */\n    public function test_options_list_indent_tabs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", [\"list_indent_type\" => \"tabs\"]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Parent\", $result);\n        $this->assertStringContainsString(\"Child\", $result);\n    }\n\n    /** Nested lists indented with 4 spaces per level */\n    public function test_options_list_indent_width_four(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", [\"list_indent_width\" => 4]);\n        $this->assertStringContainsString(\"Outer\", $result);\n        $this->assertStringContainsString(\"Inner\", $result);\n    }\n\n    /** Default max_depth (null) converts deeply nested content fully */\n    public function test_options_max_depth_default_unlimited(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><div><div><div><p>Deep content</p></div></div></div></div>\");\n        $this->assertStringContainsString(\"Deep content\", $result);\n    }\n\n    /** max_depth truncates content beyond the specified depth */\n    public function test_options_max_depth_truncates(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\", [\"max_depth\" => 3]);\n        $this->assertStringContainsString(\"Shallow\", $result);\n        $this->assertStringNotContainsString(\"Too deep\", $result);\n    }\n\n    /** max_depth of 0 produces empty output */\n    public function test_options_max_depth_zero_empty(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello</p>\", [\"max_depth\" => 0]);\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Hard line breaks rendered with backslash */\n    public function test_options_newline_backslash(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Line one<br>Line two</p>\", [\"newline_style\" => \"backslash\"]);\n        $this->assertStringContainsString(\"Line one\", $result);\n        $this->assertStringContainsString(\"Line two\", $result);\n    }\n\n    /** Hard line breaks rendered with trailing spaces */\n    public function test_options_newline_spaces(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First<br>Second</p>\", [\"newline_style\" => \"spaces\"]);\n        $this->assertStringContainsString(\"First\", $result);\n        $this->assertStringContainsString(\"Second\", $result);\n    }\n\n    /** Djot output format produces djot-compatible markup */\n    public function test_options_output_format_djot(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Simple paragraph.</p>\", [\"output_format\" => \"djot\"]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Simple paragraph.\", $result);\n    }\n\n    /** Default markdown output format produces standard markdown */\n    public function test_options_output_format_markdown(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>Some text.</p>\", [\"heading_style\" => \"atx\", \"output_format\" => \"markdown\"]);\n        $this->assertStringContainsString(\"# Title\", $result);\n        $this->assertStringContainsString(\"Some text.\", $result);\n    }\n\n    /** Plain text output format strips markdown syntax */\n    public function test_options_output_format_plain(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", [\"output_format\" => \"plain\"]);\n        $this->assertStringContainsString(\"Title\", $result);\n        $this->assertStringContainsString(\"bold\", $result);\n        $this->assertStringContainsString(\"text.\", $result);\n    }\n\n    /** Aggressive preset removes nav, footer, aside unconditionally */\n    public function test_options_preprocessing_aggressive(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\", [\"preprocessing\" => [\"preset\" => \"Aggressive\"]]);\n        $this->assertStringContainsString(\"Title\", $result);\n        $this->assertStringContainsString(\"Content\", $result);\n        $this->assertStringNotContainsString(\"Menu\", $result);\n    }\n\n    /** Minimal preset preserves nav, footer, aside */\n    public function test_options_preprocessing_minimal(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", [\"preprocessing\" => [\"preset\" => \"Minimal\"]]);\n        $this->assertStringContainsString(\"Navigation\", $result);\n        $this->assertStringContainsString(\"Content\", $result);\n        $this->assertStringContainsString(\"Footer\", $result);\n    }\n\n    /** Forms are removed when remove_forms is true */\n    public function test_options_preprocessing_remove_forms(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\", [\"preprocessing\" => [\"removeForms\" => true]]);\n        $this->assertStringContainsString(\"Before\", $result);\n        $this->assertStringContainsString(\"After\", $result);\n        $this->assertStringNotContainsString(\"Submit\", $result);\n    }\n\n    /** Iframe tags preserved as raw HTML in output */\n    public function test_options_preserve_tags_iframe(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\", [\"preserve_tags\" => [\"iframe\"]]);\n        $this->assertStringContainsString(\"Before\", $result);\n        $this->assertStringContainsString(\"After\", $result);\n        $this->assertStringContainsString(\"<iframe\", $result);\n    }\n\n    /** Images are omitted from output when skipImages is true */\n    public function test_options_skip_images_true(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before <img src='test.jpg' alt='photo'> After</p>\", [\"skip_images\" => true]);\n        $this->assertStringContainsString(\"Before\", $result);\n        $this->assertStringContainsString(\"After\", $result);\n        $this->assertStringNotContainsString(\"photo\", $result);\n    }\n\n    /** Strip newlines produces single-line paragraphs */\n    public function test_options_strip_newlines(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", [\"strip_newlines\" => true]);\n        $this->assertStringContainsString(\"First paragraph.\", $result);\n        $this->assertStringContainsString(\"Second paragraph.\", $result);\n    }\n\n    /** Div and span tags stripped but content preserved */\n    public function test_options_strip_tags_div_span(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\", [\"strip_tags\" => [\"div\", \"span\"]]);\n        $this->assertStringContainsString(\"Inside div\", $result);\n        $this->assertStringContainsString(\"span text\", $result);\n    }\n\n    /** Strong and em tags use underscore symbol instead of asterisk */\n    public function test_options_strong_em_underscore(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><strong>bold</strong> and <em>italic</em></p>\", [\"strong_em_symbol\" => \"_\"]);\n        $this->assertStringContainsString(\"__bold__\", $result);\n        $this->assertStringContainsString(\"_italic_\", $result);\n    }\n\n    /** Subscript rendered with tilde symbol */\n    public function test_options_sub_symbol_tilde(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O</p>\", [\"sub_symbol\" => \"~\"]);\n        $this->assertStringContainsString(\"~2~\", $result);\n    }\n\n    /** Superscript rendered with caret symbol */\n    public function test_options_sup_symbol_caret(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>x<sup>2</sup></p>\", [\"sup_symbol\" => \"^\"]);\n        $this->assertStringContainsString(\"^2^\", $result);\n    }\n\n    /** Normalized whitespace mode collapses multiple spaces */\n    public function test_options_whitespace_normalized(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Text   with    extra   spaces.</p>\", [\"whitespace_mode\" => \"normalized\"]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Text\", $result);\n        $this->assertStringContainsString(\"with\", $result);\n        $this->assertStringContainsString(\"extra\", $result);\n        $this->assertStringContainsString(\"spaces.\", $result);\n    }\n\n    /** Strict whitespace mode preserves whitespace as-is */\n    public function test_options_whitespace_strict(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Preserved   spacing.</p>\", [\"whitespace_mode\" => \"strict\"]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Preserved\", $result);\n        $this->assertStringContainsString(\"spacing.\", $result);\n    }\n\n    /** Wrap option disabled preserves long lines without breaking */\n    public function test_options_wrap_disabled(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\", [\"wrap\" => false]);\n        $this->assertStringContainsString(\"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\", $result);\n    }\n\n    /** Wrap option enabled with custom width wraps long lines */\n    public function test_options_wrap_enabled(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\", [\"wrap\" => true, \"wrap_width\" => 40]);\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"This is a long paragraph\", $result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/RealWorldTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d33041f3794ff35ddef3715e6873401f730385880791d34ef0d4c9edce7ffdb8\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: real-world. */\nfinal class RealWorldTest extends TestCase\n{\n    /** Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown */\n    public function test_real_world_blog_post(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"# Getting Started with Rust\", $result);\n        $this->assertStringContainsString(\"## Installation\", $result);\n        $this->assertStringContainsString(\"## Hello World\", $result);\n        $this->assertStringContainsString(\"## Key Concepts\", $result);\n        $this->assertStringContainsString(\"cargo run\", $result);\n        $this->assertStringContainsString(\"[Mozilla](https://www.mozilla.org)\", $result);\n        $this->assertStringContainsString(\"- Ownership and borrowing\", $result);\n    }\n\n    /** Documentation page with nested lists, code examples, and blockquotes converts correctly */\n    public function test_real_world_documentation_page(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"# Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration values.</p></blockquote></div>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"# API Reference\", $result);\n        $this->assertStringContainsString(\"## convert_html\", $result);\n        $this->assertStringContainsString(\"### Parameters\", $result);\n        $this->assertStringContainsString(\"### Returns\", $result);\n        $this->assertStringContainsString(\"### Example\", $result);\n        $this->assertStringContainsString(\"## ConversionOptions\", $result);\n        $this->assertStringContainsString(\"> \", $result);\n        $this->assertStringContainsString(\"thread-safe\", $result);\n        $this->assertStringContainsString(\"convert_html\", $result);\n        $this->assertStringContainsString(\"ConversionOptions\", $result);\n    }\n\n    /** Product page with table, images, and lists converts correctly */\n    public function test_real_world_product_page(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div class=\\\"product\\\"><h1>Wireless Keyboard Pro</h1><img src=\\\"https://example.com/keyboard.jpg\\\" alt=\\\"Wireless Keyboard Pro\\\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"# Wireless Keyboard Pro\", $result);\n        $this->assertStringContainsString(\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\", $result);\n        $this->assertStringContainsString(\"## Specifications\", $result);\n        $this->assertStringContainsString(\"Battery Life\", $result);\n        $this->assertStringContainsString(\"12 months\", $result);\n        $this->assertStringContainsString(\"Bluetooth 5.0\", $result);\n        $this->assertStringContainsString(\"## What's in the Box\", $result);\n        $this->assertStringContainsString(\"USB-C charging cable\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n        $this->assertStringContainsString(\"---\", $result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/ResultTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6f2d61778ec14ae85872331e60d9c932831d353702d71266b5b0c7d93a94a4f6\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: result. */\nfinal class ResultTest extends TestCase\n{\n    /** Result tables array is empty when input has no tables */\n    public function test_result_tables_empty_when_no_tables(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>No tables here</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        $this->assertCount(0, $result);\n    }\n\n    /** Multiple tables each appear in the tables array */\n    public function test_result_tables_multiple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\", [\"include_document_structure\" => true]);\n        $this->assertGreaterThanOrEqual(2, count($result));\n    }\n\n    /** Simple table populates the tables array in result */\n    public function test_result_tables_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        $this->assertGreaterThanOrEqual(1, count($result));\n    }\n\n    /** Tables array is empty when includeDocumentStructure is false */\n    public function test_result_tables_without_structure_flag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertCount(0, $result);\n    }\n\n    /** Warnings array is empty for well-formed HTML without problematic content */\n    public function test_result_warnings_empty_for_clean_input(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertCount(0, $result);\n    }\n\n    /** Warnings array is empty for complex but valid HTML */\n    public function test_result_warnings_empty_for_complex_input(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\");\n        $this->assertNotEmpty($result);\n        $this->assertCount(0, $result);\n    }\n\n    /** Warnings array is empty even for malformed HTML (parser is lenient) */\n    public function test_result_warnings_empty_for_malformed_html(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Unclosed paragraph<div>Mixed nesting</p></div>\");\n        $this->assertNotEmpty($result);\n        $this->assertCount(0, $result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/SmokeTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:827559b85d5203d5c396c19f8f4786849fe41c2f159637d59e0aa90bf740f43f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: smoke. */\nfinal class SmokeTest extends TestCase\n{\n    /** Empty string produces empty output */\n    public function test_smoke_empty_string(): void\n    {\n        $result = HtmlToMarkdown::convert(\"\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** H1 heading converts to ATX markdown */\n    public function test_smoke_simple_heading(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1>\");\n        $this->assertStringContainsString(\"# Title\", $result);\n    }\n\n    /** Simple paragraph converts correctly */\n    public function test_smoke_simple_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello World</p>\");\n        $this->assertEquals(\"Hello World\", trim($result));\n        $this->assertNotEmpty($result);\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/StructureTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:506caaf1842d551699406cdc7f5cb9f639305f38281d94ce9df0089fe488ad1f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: structure. */\nfinal class StructureTest extends TestCase\n{\n    /** Fenced code block produces Code node */\n    public function test_structure_code_block(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Example code:</p><pre><code class=\\\"language-rust\\\">fn main() { println!(\\\"Hello\\\"); }</code></pre>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** H1 > H2 > H3 creates three levels of heading nesting */\n    public function test_structure_deep_nesting_h1_h2_h3(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** H1 followed by H2 creates a nested group under the H1 */\n    public function test_structure_h1_h2_nested_group(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** Simple heading followed by paragraph produces Heading and Paragraph nodes */\n    public function test_structure_heading_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>A paragraph of text.</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** Unordered list produces List and ListItem nodes */\n    public function test_structure_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** Multiple headings create multiple Heading nodes with correct levels */\n    public function test_structure_multiple_headings(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n\n    /** H1, H2, then another H1 creates two sibling top-level groups */\n    public function test_structure_sibling_h1_groups(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\", [\"include_document_structure\" => true]);\n        $this->assertNotEmpty($result);\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n        // skipped: result_is_simple, field 'document.nodes' not on simple result type\n    }\n}\n"
  },
  {
    "path": "e2e/php/tests/VisitorTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:fef2e7a67e25ee7f82242de000e472bb96cf64970a6d9d5e601db8bdcab4ea75\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: visitor. */\nfinal class VisitorTest extends TestCase\n{\n    /** Visitor replaces audio element with custom output */\n    public function test_visitor_audio_custom(): void\n    {\n        $visitor = new class {\n            public function visit_audio($ctx, $src) {\n                return ['custom' => \"[AUDIO: podcast.mp3]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Listen to this: <audio src=\\\"podcast.mp3\\\" controls></audio></p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[AUDIO: podcast.mp3]\", $result);\n        $this->assertStringContainsString(\"Listen to this:\", $result);\n    }\n\n    /** Visitor removes audio elements from output */\n    public function test_visitor_audio_skip(): void\n    {\n        $visitor = new class {\n            public function visit_audio($ctx, $src) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Background music:</p><audio src=\\\"music.ogg\\\" autoplay></audio><p>Enjoy!</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Background music:\", $result);\n        $this->assertStringContainsString(\"Enjoy!\", $result);\n        $this->assertStringNotContainsString(\"music.ogg\", $result);\n    }\n\n    /** Visitor replaces button with bracketed text */\n    public function test_visitor_button_custom(): void\n    {\n        $visitor = new class {\n            public function visit_button($ctx, $text) {\n                return ['custom' => \"[BTN:{text}]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Confirm action: <button type=\\\"submit\\\">Click me</button> or <button type=\\\"reset\\\">Cancel</button></p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[BTN:Click me]\", $result);\n        $this->assertStringContainsString(\"[BTN:Cancel]\", $result);\n        $this->assertStringContainsString(\"Confirm action:\", $result);\n    }\n\n    /** Visitor removes all buttons from output */\n    public function test_visitor_button_skip(): void\n    {\n        $visitor = new class {\n            public function visit_button($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Actions available:\", $result);\n        $this->assertStringNotContainsString(\"Save\", $result);\n        $this->assertStringNotContainsString(\"Delete\", $result);\n        $this->assertStringNotContainsString(\"Export\", $result);\n    }\n\n    /** Visitor continue action preserves default conversion */\n    public function test_visitor_continue_default(): void\n    {\n        $visitor = new class {\n            public function visit_strong($ctx, $text) {\n                return 'continue';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Hello <strong>World</strong></p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"**World**\", $result);\n    }\n\n    /** Visitor replaces blockquote with custom format */\n    public function test_visitor_custom_blockquote(): void\n    {\n        $visitor = new class {\n            public function visit_blockquote($ctx, $content, $depth) {\n                return ['custom' => \"QUOTE: \\\"{content}\\\"\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>A wise quote.</p></blockquote>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"QUOTE:\", $result);\n        $this->assertStringContainsString(\"A wise quote.\", $result);\n    }\n\n    /** Visitor replaces emphasis with custom output */\n    public function test_visitor_custom_emphasis(): void\n    {\n        $visitor = new class {\n            public function visit_emphasis($ctx, $text) {\n                return ['custom' => \">>>{text}<<<\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>This is <em>important</em> text.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\">>>important<<<\", $result);\n        $this->assertStringNotContainsString(\"*important*\", $result);\n    }\n\n    /** Visitor replaces heading with custom format */\n    public function test_visitor_custom_heading(): void\n    {\n        $visitor = new class {\n            public function visit_heading($ctx, $level, $text, $id) {\n                return ['custom' => \"--- {text} ---\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h2>Section Title</h2><p>Content below heading.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"--- Section Title ---\", $result);\n        $this->assertStringNotContainsString(\"## Section Title\", $result);\n        $this->assertStringContainsString(\"Content below heading.\", $result);\n    }\n\n    /** Visitor replaces image with custom output using template */\n    public function test_visitor_custom_image(): void\n    {\n        $visitor = new class {\n            public function visit_image($ctx, $src, $alt, $title) {\n                return ['custom' => \"[Image: {alt}]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"banner.png\\\" alt=\\\"Banner\\\">\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[Image: Banner]\", $result);\n        $this->assertStringNotContainsString(\"banner.png\", $result);\n    }\n\n    /** Visitor reformats links using template interpolation */\n    public function test_visitor_custom_link_format(): void\n    {\n        $visitor = new class {\n            public function visit_link($ctx, $href, $text, $title) {\n                return ['custom' => \"{text} ({href})\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Visit <a href=\\\"https://example.com\\\">Example</a> for more info.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Example (https://example.com)\", $result);\n        $this->assertStringNotContainsString(\"[Example]\", $result);\n    }\n\n    /** Visitor replaces link with static custom output */\n    public function test_visitor_custom_link_static(): void\n    {\n        $visitor = new class {\n            public function visit_link($ctx, $href, $text, $title) {\n                return ['custom' => \"[REDACTED LINK]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\">Click here</a>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[REDACTED LINK]\", $result);\n        $this->assertStringNotContainsString(\"example.com\", $result);\n    }\n\n    /** Visitor custom action replaces element output */\n    public function test_visitor_custom_output(): void\n    {\n        $visitor = new class {\n            public function visit_heading($ctx, $level, $text, $id) {\n                return ['custom' => \"## REPLACED HEADING\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h1>Original Heading</h1>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"## REPLACED HEADING\", $result);\n        $this->assertStringNotContainsString(\"# Original Heading\", $result);\n    }\n\n    /** Visitor customizes definition list items */\n    public function test_visitor_definition_list_custom(): void\n    {\n        $visitor = new class {\n            public function visit_definition_term($ctx, $text) {\n                return ['custom' => \"**{text}**\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"**HTML**\", $result);\n        $this->assertStringContainsString(\"**CSS**\", $result);\n        $this->assertStringContainsString(\"HyperText Markup Language\", $result);\n    }\n\n    /** Visitor formats definition lists with custom templates */\n    public function test_visitor_definition_list_custom_format(): void\n    {\n        $visitor = new class {\n            public function visit_definition_term($ctx, $text) {\n                return ['custom' => \"### {text}\"];\n            }\n            public function visit_definition_description($ctx, $text) {\n                return ['custom' => \"> {text}\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"### Python\", $result);\n        $this->assertStringContainsString(\"### JavaScript\", $result);\n        $this->assertStringContainsString(\"> A high-level programming language\", $result);\n        $this->assertStringContainsString(\"> A scripting language for web browsers\", $result);\n    }\n\n    /** Visitor skips definition list items from output */\n    public function test_visitor_definition_list_skip(): void\n    {\n        $visitor = new class {\n            public function visit_definition_description($ctx, $text) {\n                return 'skip';\n            }\n            public function visit_definition_term($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Glossary:\", $result);\n        $this->assertStringContainsString(\"End of glossary\", $result);\n        $this->assertStringNotContainsString(\"Term A\", $result);\n        $this->assertStringNotContainsString(\"Definition\", $result);\n    }\n\n    /** Visitor customizes details/summary disclosure elements */\n    public function test_visitor_details_summary_custom(): void\n    {\n        $visitor = new class {\n            public function visit_summary($ctx, $text) {\n                return ['custom' => \"[EXPANDABLE] {text}\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[EXPANDABLE] Click to expand\", $result);\n        $this->assertStringContainsString(\"This content is initially hidden.\", $result);\n    }\n\n    /** Visitor removes details/summary elements entirely */\n    public function test_visitor_details_summary_skip(): void\n    {\n        $visitor = new class {\n            public function visit_details($ctx, $isOpen) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Main content here.\", $result);\n        $this->assertStringContainsString(\"More main content.\", $result);\n        $this->assertStringNotContainsString(\"Hidden section\", $result);\n        $this->assertStringNotContainsString(\"Secret details\", $result);\n    }\n\n    /** Visitor customizes figure and figcaption elements */\n    public function test_visitor_figure_custom(): void\n    {\n        $visitor = new class {\n            public function visit_figcaption($ctx, $text) {\n                return ['custom' => \"*{text}*\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\\\"diagram.png\\\" alt=\\\"System architecture diagram\\\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Article Title\", $result);\n        $this->assertStringContainsString(\"*Figure 1: System Architecture*\", $result);\n        $this->assertStringContainsString(\"Explanation of the figure.\", $result);\n    }\n\n    /** Visitor wraps figure content with custom formatting */\n    public function test_visitor_figure_custom_wrap(): void\n    {\n        $visitor = new class {\n            public function visit_figure_start($ctx) {\n                return ['custom' => \"\\n[FIGURE]\\n\"];\n            }\n            public function visit_figure_end($ctx, $output) {\n                return ['custom' => \"{output}\\n[/FIGURE]\\n\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<section><h2>Gallery</h2><figure><img src=\\\"photo1.jpg\\\" alt=\\\"Photo\\\"><figcaption>Beautiful sunset</figcaption></figure></section>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[FIGURE]\", $result);\n        $this->assertStringContainsString(\"[/FIGURE]\", $result);\n        $this->assertStringContainsString(\"Gallery\", $result);\n    }\n\n    /** Visitor removes figure elements with their captions */\n    public function test_visitor_figure_skip(): void\n    {\n        $visitor = new class {\n            public function visit_figure_start($ctx) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>See the chart below:</p><figure><img src=\\\"chart.svg\\\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"See the chart below:\", $result);\n        $this->assertStringContainsString(\"As shown in the chart above.\", $result);\n        $this->assertStringNotContainsString(\"Revenue Trends\", $result);\n        $this->assertStringNotContainsString(\"chart.svg\", $result);\n    }\n\n    /** Visitor replaces form with custom output */\n    public function test_visitor_form_custom(): void\n    {\n        $visitor = new class {\n            public function visit_form($ctx, $actionUrl, $method) {\n                return ['custom' => \"[FORM PLACEHOLDER]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<div><form action=\\\"/submit\\\" method=\\\"POST\\\"><label>Name: <input type=\\\"text\\\" name=\\\"name\\\"></label><button type=\\\"submit\\\">Submit</button></form></div>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[FORM PLACEHOLDER]\", $result);\n        $this->assertStringNotContainsString(\"submit\", $result);\n        $this->assertStringNotContainsString(\"input\", $result);\n    }\n\n    /** Visitor skips form elements entirely */\n    public function test_visitor_form_skip(): void\n    {\n        $visitor = new class {\n            public function visit_form($ctx, $actionUrl, $method) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Before form</p><form><input type=\\\"email\\\" name=\\\"email\\\"></form><p>After form</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Before form\", $result);\n        $this->assertStringContainsString(\"After form\", $result);\n        $this->assertStringNotContainsString(\"email\", $result);\n    }\n\n    /** Visitor replaces horizontal rule with custom output */\n    public function test_visitor_horizontal_rule_custom(): void\n    {\n        $visitor = new class {\n            public function visit_horizontal_rule($ctx) {\n                return ['custom' => \"\\n[DIVIDER]\\n\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[DIVIDER]\", $result);\n        $this->assertStringContainsString(\"Section A\", $result);\n        $this->assertStringContainsString(\"Section B\", $result);\n        $this->assertStringNotContainsString(\"---\", $result);\n    }\n\n    /** Visitor removes all horizontal rules */\n    public function test_visitor_horizontal_rule_skip(): void\n    {\n        $visitor = new class {\n            public function visit_horizontal_rule($ctx) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Part 1\", $result);\n        $this->assertStringContainsString(\"Part 2\", $result);\n        $this->assertStringContainsString(\"Part 3\", $result);\n        $this->assertStringNotContainsString(\"---\", $result);\n    }\n\n    /** Visitor replaces embedded iframe with custom text */\n    public function test_visitor_iframe_custom(): void\n    {\n        $visitor = new class {\n            public function visit_iframe($ctx, $src) {\n                return ['custom' => \"[EMBEDDED: https://maps.example.com/embed]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Embedded map:</p><iframe src=\\\"https://maps.example.com/embed\\\" width=\\\"400\\\" height=\\\"300\\\"></iframe><p>End of map</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[EMBEDDED: https://maps.example.com/embed]\", $result);\n        $this->assertStringContainsString(\"Embedded map:\", $result);\n        $this->assertStringContainsString(\"End of map\", $result);\n    }\n\n    /** Visitor removes embedded iframes */\n    public function test_visitor_iframe_skip(): void\n    {\n        $visitor = new class {\n            public function visit_iframe($ctx, $src) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h3>Reviews</h3><iframe src=\\\"https://widget.example.com/reviews\\\"></iframe><p>See reviews from our partners.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Reviews\", $result);\n        $this->assertStringContainsString(\"See reviews from our partners.\", $result);\n        $this->assertStringNotContainsString(\"widget.example.com\", $result);\n    }\n\n    /** Visitor replaces input with labeled output */\n    public function test_visitor_input_custom(): void\n    {\n        $visitor = new class {\n            public function visit_input($ctx, $inputType, $name, $value) {\n                return ['custom' => \"[INPUT:{input_type}]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<form><label>Username: <input type=\\\"text\\\" name=\\\"username\\\" value=\\\"\\\"></label><label>Password: <input type=\\\"password\\\" name=\\\"password\\\"></label></form>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[INPUT:text]\", $result);\n        $this->assertStringContainsString(\"[INPUT:password]\", $result);\n    }\n\n    /** Visitor skips all input elements */\n    public function test_visitor_input_skip(): void\n    {\n        $visitor = new class {\n            public function visit_input($ctx, $inputType, $name, $value) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Sign up:</p><input type=\\\"text\\\" name=\\\"email\\\" placeholder=\\\"your@email.com\\\"><input type=\\\"checkbox\\\" name=\\\"agree\\\"><p>Continue</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Sign up:\", $result);\n        $this->assertStringContainsString(\"Continue\", $result);\n        $this->assertStringNotContainsString(\"email\", $result);\n    }\n\n    /** Visitor replaces line break with custom output */\n    public function test_visitor_line_break_custom(): void\n    {\n        $visitor = new class {\n            public function visit_line_break($ctx) {\n                return ['custom' => \" | \"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>First line<br>Second line<br>Third line</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"First line | Second line | Third line\", $result);\n        $this->assertStringNotContainsString(\"\\n\\n\", $result);\n    }\n\n    /** Visitor removes all line breaks */\n    public function test_visitor_line_break_skip(): void\n    {\n        $visitor = new class {\n            public function visit_line_break($ctx) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Address Line 1Address Line 2Address Line 3\", $result);\n    }\n\n    /** Visitor replaces highlight/mark with custom template */\n    public function test_visitor_mark_custom(): void\n    {\n        $visitor = new class {\n            public function visit_mark($ctx, $text) {\n                return ['custom' => \"=={text}==\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>This is a <mark>highlighted passage</mark> in the text.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"==highlighted passage==\", $result);\n        $this->assertStringContainsString(\"This is a\", $result);\n    }\n\n    /** Visitor skips mark elements entirely */\n    public function test_visitor_mark_skip(): void\n    {\n        $visitor = new class {\n            public function visit_mark($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Key insight: <mark>always validate input</mark> for security.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringNotContainsString(\"always validate input\", $result);\n        $this->assertStringContainsString(\"Key insight:\", $result);\n        $this->assertStringContainsString(\"for security.\", $result);\n    }\n\n    /** Visitor preserve_html action includes raw HTML in output */\n    public function test_visitor_preserve_html(): void\n    {\n        $visitor = new class {\n            public function visit_custom_element($ctx, $tagName, $html) {\n                return 'preserve_html';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<div><custom-tag>Custom content</custom-tag></div>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"<custom-tag>\", $result);\n    }\n\n    /** Visitor skips all headings from document */\n    public function test_visitor_skip_all_headings(): void\n    {\n        $visitor = new class {\n            public function visit_heading($ctx, $level, $text, $id) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>Body text remains.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringNotContainsString(\"Title\", $result);\n        $this->assertStringContainsString(\"Body text remains.\", $result);\n    }\n\n    /** Visitor skips code blocks from output */\n    public function test_visitor_skip_code_blocks(): void\n    {\n        $visitor = new class {\n            public function visit_code_block($ctx, $lang, $code) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Intro text\", $result);\n        $this->assertStringContainsString(\"Outro text\", $result);\n        $this->assertStringNotContainsString(\"let x = 42\", $result);\n    }\n\n    /** Visitor skip action omits all headings from output */\n    public function test_visitor_skip_heading(): void\n    {\n        $visitor = new class {\n            public function visit_heading($ctx, $level, $text, $id) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1><p>Body text remains.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringNotContainsString(\"Title\", $result);\n        $this->assertStringContainsString(\"Body text remains.\", $result);\n    }\n\n    /** Visitor skips all images from output */\n    public function test_visitor_skip_images(): void\n    {\n        $visitor = new class {\n            public function visit_image($ctx, $src, $alt, $title) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Before image</p><img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\"><p>After image</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Before image\", $result);\n        $this->assertStringContainsString(\"After image\", $result);\n        $this->assertStringNotContainsString(\"photo.jpg\", $result);\n        $this->assertStringNotContainsString(\"A photo\", $result);\n    }\n\n    /** Visitor skips all links entirely */\n    public function test_visitor_skip_links(): void\n    {\n        $visitor = new class {\n            public function visit_link($ctx, $href, $text, $title) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Before <a href=\\\"https://example.com\\\">link text</a> after</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringNotContainsString(\"link text\", $result);\n        $this->assertStringNotContainsString(\"example.com\", $result);\n    }\n\n    /** Visitor skips bold/strong elements */\n    public function test_visitor_skip_strong(): void\n    {\n        $visitor = new class {\n            public function visit_strong($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Normal <strong>bold text</strong> normal</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringNotContainsString(\"bold text\", $result);\n        $this->assertStringContainsString(\"Normal\", $result);\n    }\n\n    /** Visitor replaces subscript with custom template */\n    public function test_visitor_subscript_custom(): void\n    {\n        $visitor = new class {\n            public function visit_subscript($ctx, $text) {\n                return ['custom' => \"~{text}~\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O is water.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"H~2~O\", $result);\n        $this->assertStringContainsString(\"is water\", $result);\n    }\n\n    /** Visitor skips subscript elements entirely */\n    public function test_visitor_subscript_skip(): void\n    {\n        $visitor = new class {\n            public function visit_subscript($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"The formula CHO is sugar.\", $result);\n    }\n\n    /** Visitor replaces superscript with custom template */\n    public function test_visitor_superscript_custom(): void\n    {\n        $visitor = new class {\n            public function visit_superscript($ctx, $text) {\n                return ['custom' => \"^{text}^\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"E=mc^2^\", $result);\n        $this->assertStringContainsString(\"revolutionized physics\", $result);\n    }\n\n    /** Visitor skips superscript from output */\n    public function test_visitor_superscript_skip(): void\n    {\n        $visitor = new class {\n            public function visit_superscript($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"The equation x + y = z has no solutions.\", $result);\n    }\n\n    /** Visitor replaces underline with custom markup */\n    public function test_visitor_underline_custom(): void\n    {\n        $visitor = new class {\n            public function visit_underline($ctx, $text) {\n                return ['custom' => \"_{text}_\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>This is <u>very important</u> text.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"_very important_\", $result);\n        $this->assertStringNotContainsString(\"**\", $result);\n    }\n\n    /** Visitor skips underline elements from output */\n    public function test_visitor_underline_skip(): void\n    {\n        $visitor = new class {\n            public function visit_underline($ctx, $text) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Normal text with <u>underlined part</u> and more text.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Normal text with\", $result);\n        $this->assertStringContainsString(\"and more text.\", $result);\n        $this->assertStringNotContainsString(\"underlined part\", $result);\n    }\n\n    /** Visitor replaces video with custom link */\n    public function test_visitor_video_custom(): void\n    {\n        $visitor = new class {\n            public function visit_video($ctx, $src) {\n                return ['custom' => \"[VIDEO: {src}]\"];\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<p>Watch our tutorial:</p><video src=\\\"tutorial.mp4\\\" width=\\\"320\\\" height=\\\"240\\\" controls></video><p>Great content!</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"[VIDEO: tutorial.mp4]\", $result);\n        $this->assertStringContainsString(\"Watch our tutorial:\", $result);\n        $this->assertStringContainsString(\"Great content!\", $result);\n    }\n\n    /** Visitor removes video elements entirely */\n    public function test_visitor_video_skip(): void\n    {\n        $visitor = new class {\n            public function visit_video($ctx, $src) {\n                return 'skip';\n            }\n        };\n        $result = HtmlToMarkdown::convert(\"<h2>Demo</h2><video src=\\\"demo.webm\\\"></video><p>See the demo above.</p>\", (object)['visitor' => $visitor]);\n        $this->assertStringContainsString(\"Demo\", $result);\n        $this->assertStringContainsString(\"See the demo above.\", $result);\n        $this->assertStringNotContainsString(\"demo.webm\", $result);\n    }\n}\n"
  },
  {
    "path": "e2e/python/__init__.py",
    "content": ""
  },
  {
    "path": "e2e/python/conftest.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:52e73aa2948c5f1cb9ef4e47e5e646aec4311b1cf1076c17ee05f36b10b0bae9\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Pytest configuration for e2e tests.\"\"\"\n# Ensure the package is importable.\n# The html_to_markdown package is expected to be installed in the current environment.\n"
  },
  {
    "path": "e2e/python/pyproject.toml",
    "content": "[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [ \"setuptools>=68\", \"wheel\" ]\n\n[project]\nname = \"html-to-markdown-e2e-tests\"\nversion = \"0.0.0\"\ndescription = \"End-to-end tests\"\nrequires-python = \">=3.10\"\nclassifiers = [\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n]\ndependencies = [ \"html-to-markdown\", \"pytest>=7.4\", \"pytest-asyncio>=0.23\", \"pytest-timeout>=2.1\" ]\n\n[tool.setuptools]\npackages = []\n\n[tool.uv]\nsources.html-to-markdown = { path = \"../../packages/python\" }\n\n[tool.ruff]\nlint.ignore = [ \"PLR2004\" ]\nlint.per-file-ignores.\"tests/**\" = [ \"B017\", \"PT011\", \"S101\", \"S108\" ]\n\n[tool.pytest]\nini_options.asyncio_mode = \"auto\"\nini_options.testpaths = [ \"tests\" ]\nini_options.python_files = \"test_*.py\"\nini_options.python_functions = \"test_*\"\nini_options.addopts = \"-v --strict-markers --tb=short\"\nini_options.timeout = 300\n"
  },
  {
    "path": "e2e/python/tests/__init__.py",
    "content": ""
  },
  {
    "path": "e2e/python/tests/test_conversion.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:7a072f3b8efbac71e97cb92a43184b26496b36da7db485186b406e7ec9175e4e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: conversion.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_blockquote_multiple_paragraphs() -> None:\n    \"\"\"Blockquote with multiple paragraphs has each paragraph prefixed.\"\"\"\n    html = \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"> First paragraph.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"> Second paragraph.\" in result.content  # noqa: S101\n\n\ndef test_blockquote_nested() -> None:\n    \"\"\"Nested blockquote produces double-prefixed lines.\"\"\"\n    html = \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Outer quote.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Inner quote.\" in result.content  # noqa: S101\n\n\ndef test_blockquote_simple() -> None:\n    \"\"\"Simple blockquote.\"\"\"\n    html = \"<blockquote><p>Quote text</p></blockquote>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"> Quote text\" in result.content  # noqa: S101\n\n\ndef test_blockquote_with_list() -> None:\n    \"\"\"Blockquote containing a list preserves list items inside quote.\"\"\"\n    html = \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Quote intro:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Point one\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Point two\" in result.content  # noqa: S101\n\n\ndef test_bold_and_italic() -> None:\n    \"\"\"Nested bold and italic.\"\"\"\n    html = \"<p><strong><em>both</em></strong></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"***both***\" in result.content  # noqa: S101\n\n\ndef test_bold_strong() -> None:\n    \"\"\"Strong tag converts to bold.\"\"\"\n    html = \"<p><strong>bold</strong></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**bold**\" in result.content  # noqa: S101\n\n\ndef test_code_block() -> None:\n    \"\"\"Code block with language preserves content.\"\"\"\n    html = \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"print('hello')\" in result.content  # noqa: S101\n\n\ndef test_code_block_no_language() -> None:\n    \"\"\"Code block without a language class preserves content.\"\"\"\n    html = \"<pre><code>plain code here</code></pre>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"plain code here\" in result.content  # noqa: S101\n\n\ndef test_code_inline_in_paragraph() -> None:\n    \"\"\"Inline code element nested inside a paragraph.\"\"\"\n    html = \"<p>Call the <code>initialize()</code> method first.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"`initialize()`\" in result.content  # noqa: S101\n\n\ndef test_code_with_backticks_in_content() -> None:\n    \"\"\"Inline code containing backtick characters is properly escaped.\"\"\"\n    html = \"<p>Use <code>`backtick` here</code> carefully.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"backtick\" in result.content  # noqa: S101\n\n\ndef test_emphasis_mark_highlight() -> None:\n    \"\"\"mark tag produces highlighted output.\"\"\"\n    html = \"<p><mark>highlighted</mark></p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"highlighted\" in result.content  # noqa: S101\n\n\ndef test_emphasis_strikethrough_del() -> None:\n    \"\"\"del tag converts to GFM strikethrough.\"\"\"\n    html = \"<p><del>deleted text</del></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"~~deleted text~~\" in result.content  # noqa: S101\n\n\ndef test_emphasis_strikethrough_s() -> None:\n    \"\"\"s tag converts to GFM strikethrough.\"\"\"\n    html = \"<p><s>strikethrough</s></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"~~strikethrough~~\" in result.content  # noqa: S101\n\n\ndef test_emphasis_subscript() -> None:\n    \"\"\"sub tag content is preserved.\"\"\"\n    html = \"<p>H<sub>2</sub>O</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"H\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"O\" in result.content  # noqa: S101\n\n\ndef test_emphasis_superscript() -> None:\n    \"\"\"sup tag content is preserved.\"\"\"\n    html = \"<p>x<sup>2</sup></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"x\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n\n\ndef test_emphasis_underline_u() -> None:\n    \"\"\"u tag content is preserved in output.\"\"\"\n    html = \"<p><u>underlined</u></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"underlined\" in result.content  # noqa: S101\n\n\ndef test_form_input_elements() -> None:\n    \"\"\"Form input elements produce readable output without form mechanics.\"\"\"\n    html = '<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Name\" in result.content  # noqa: S101\n\n\ndef test_form_select_options() -> None:\n    \"\"\"Select element with options produces readable output.\"\"\"\n    html = '<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Color\" in result.content  # noqa: S101\n\n\ndef test_form_textarea() -> None:\n    \"\"\"Textarea element produces readable output.\"\"\"\n    html = (\n        \"<form><label>Message:</label><textarea>Default text content</textarea></form>\"\n    )\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Message\" in result.content  # noqa: S101\n\n\ndef test_heading_h1() -> None:\n    \"\"\"H1 heading.\"\"\"\n    html = \"<h1>Heading 1</h1>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"# Heading 1\"  # noqa: S101\n\n\ndef test_heading_h2() -> None:\n    \"\"\"H2 heading.\"\"\"\n    html = \"<h2>Heading 2</h2>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"## Heading 2\"  # noqa: S101\n\n\ndef test_heading_h3() -> None:\n    \"\"\"H3 heading.\"\"\"\n    html = \"<h3>Heading 3</h3>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"### Heading 3\"  # noqa: S101\n\n\ndef test_heading_h4() -> None:\n    \"\"\"H4 heading.\"\"\"\n    html = \"<h4>Heading 4</h4>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"#### Heading 4\"  # noqa: S101\n\n\ndef test_heading_h5() -> None:\n    \"\"\"H5 heading.\"\"\"\n    html = \"<h5>Heading 5</h5>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"##### Heading 5\"  # noqa: S101\n\n\ndef test_heading_h6() -> None:\n    \"\"\"H6 heading.\"\"\"\n    html = \"<h6>Heading 6</h6>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"###### Heading 6\"  # noqa: S101\n\n\ndef test_image_figure_figcaption() -> None:\n    \"\"\"Figure with figcaption preserves both image and caption.\"\"\"\n    html = '<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![A sunset](sunset.jpg)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Beautiful sunset over the ocean\" in result.content  # noqa: S101\n\n\ndef test_image_linked() -> None:\n    \"\"\"Image inside an anchor produces a linked image.\"\"\"\n    html = '<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Icon](icon.png)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_image_no_alt() -> None:\n    \"\"\"Image without alt text produces image markdown.\"\"\"\n    html = '<img src=\"banner.jpg\">'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"banner.jpg\" in result.content  # noqa: S101\n\n\ndef test_image_simple() -> None:\n    \"\"\"Image with alt text.\"\"\"\n    html = '<img src=\"photo.jpg\" alt=\"A photo\">'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![A photo](photo.jpg)\" in result.content  # noqa: S101\n\n\ndef test_image_with_title() -> None:\n    \"\"\"Image with title attribute includes title in output.\"\"\"\n    html = '<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Sales chart](chart.png\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Q3 Sales\" in result.content  # noqa: S101\n\n\ndef test_inline_code() -> None:\n    \"\"\"Inline code.\"\"\"\n    html = \"<p>Use <code>console.log()</code> to debug</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"`console.log()`\" in result.content  # noqa: S101\n\n\ndef test_italic_em() -> None:\n    \"\"\"Em tag converts to italic.\"\"\"\n    html = \"<p><em>italic</em></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"*italic*\" in result.content  # noqa: S101\n\n\ndef test_line_break_br_tag() -> None:\n    \"\"\"Single br tag produces a line break in output.\"\"\"\n    html = \"<p>First line.<br>Second line.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First line.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second line.\" in result.content  # noqa: S101\n\n\ndef test_line_break_hr_tag() -> None:\n    \"\"\"hr tag produces a horizontal separator between content.\"\"\"\n    html = \"<p>Before rule.</p><hr><p>After rule.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Before rule.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After rule.\" in result.content  # noqa: S101\n\n\ndef test_line_break_multiple_br() -> None:\n    \"\"\"Multiple consecutive br tags in sequence.\"\"\"\n    html = \"<p>Start.<br><br>End.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Start.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"End.\" in result.content  # noqa: S101\n\n\ndef test_link_anchor_fragment() -> None:\n    \"\"\"Fragment-only anchor link is preserved.\"\"\"\n    html = '<a href=\"#section\">Jump to section</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Jump to section](#section)\" in result.content  # noqa: S101\n\n\ndef test_link_empty_href() -> None:\n    \"\"\"Link with empty href produces output with the link text.\"\"\"\n    html = '<a href=\"\">No destination</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"No destination\" in result.content  # noqa: S101\n\n\ndef test_link_image_inside() -> None:\n    \"\"\"Image inside a link produces a linked image.\"\"\"\n    html = '<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Logo](logo.png)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_link_mailto() -> None:\n    \"\"\"Mailto link is preserved with mailto: scheme.\"\"\"\n    html = '<a href=\"mailto:user@example.com\">Email us</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"mailto:user@example.com\" in result.content  # noqa: S101\n\n\ndef test_link_simple() -> None:\n    \"\"\"Simple link.\"\"\"\n    html = '<a href=\"https://example.com\">Example</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Example](https://example.com)\" in result.content  # noqa: S101\n\n\ndef test_link_with_bold_text() -> None:\n    \"\"\"Link containing bold text preserves formatting.\"\"\"\n    html = '<a href=\"https://example.com\"><strong>Bold link</strong></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**Bold link**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_link_with_title() -> None:\n    \"\"\"Link with title attribute.\"\"\"\n    html = '<a href=\"https://example.com\" title=\"Example Site\">Example</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Example](https://example.com\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Example Site\" in result.content  # noqa: S101\n\n\ndef test_list_definition_dl() -> None:\n    \"\"\"Definition list with dt and dd elements.\"\"\"\n    html = \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Term One\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Definition of term one.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Term Two\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Definition of term two.\" in result.content  # noqa: S101\n\n\ndef test_list_item_multiple_paragraphs() -> None:\n    \"\"\"List item containing multiple paragraphs.\"\"\"\n    html = \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph in item.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph in item.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Simple item\" in result.content  # noqa: S101\n\n\ndef test_list_mixed_nested() -> None:\n    \"\"\"Mixed list: ordered list nested inside unordered list.\"\"\"\n    html = (\n        \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\"\n    )\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Item A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Sub 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Sub 2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Item B\" in result.content  # noqa: S101\n\n\ndef test_list_nested_ordered() -> None:\n    \"\"\"Nested ordered list with two levels of depth.\"\"\"\n    html = \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1a\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1b\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 2\" in result.content  # noqa: S101\n\n\ndef test_list_nested_unordered() -> None:\n    \"\"\"Nested unordered list with two levels of depth.\"\"\"\n    html = \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Parent A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Child A1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Child A2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Parent B\" in result.content  # noqa: S101\n\n\ndef test_list_task_checkboxes() -> None:\n    \"\"\"Task list with checked and unchecked checkboxes.\"\"\"\n    html = '<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Done task\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Pending task\" in result.content  # noqa: S101\n\n\ndef test_ordered_list() -> None:\n    \"\"\"Ordered list.\"\"\"\n    html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"1. First\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2. Second\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"3. Third\" in result.content  # noqa: S101\n\n\ndef test_paragraph_multiple() -> None:\n    \"\"\"Multiple paragraphs are separated by a blank line.\"\"\"\n    html = \"<p>First paragraph.</p><p>Second paragraph.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph.\" in result.content  # noqa: S101\n\n\ndef test_paragraph_nested_divs() -> None:\n    \"\"\"Text nested inside divs is extracted correctly.\"\"\"\n    html = \"<div><div><p>Nested text</p></div></div>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Nested text\" in result.content  # noqa: S101\n\n\ndef test_paragraph_simple() -> None:\n    \"\"\"Simple paragraph converts to plain text.\"\"\"\n    html = \"<p>Hello World</p>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"Hello World\"  # noqa: S101\n\n\ndef test_paragraph_with_inline_formatting() -> None:\n    \"\"\"Paragraph with bold, italic, and a link.\"\"\"\n    html = '<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**bold**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"*italic*\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[link](https://example.com)\" in result.content  # noqa: S101\n\n\ndef test_paragraph_with_line_breaks() -> None:\n    \"\"\"Paragraph with br tags produces line breaks in output.\"\"\"\n    html = \"<p>Line one.<br>Line two.<br>Line three.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line one.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line two.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line three.\" in result.content  # noqa: S101\n\n\ndef test_semantic_abbr() -> None:\n    \"\"\"Abbreviation element text is preserved.\"\"\"\n    html = '<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"WWW\" in result.content  # noqa: S101\n\n\ndef test_semantic_article() -> None:\n    \"\"\"Article element wrapping content preserves inner content.\"\"\"\n    html = \"<article><h2>Article Title</h2><p>Article body.</p></article>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Article Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Article body.\" in result.content  # noqa: S101\n\n\ndef test_semantic_definition_list() -> None:\n    \"\"\"Definition list with term and description.\"\"\"\n    html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"HTML\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"HyperText Markup Language\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"CSS\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Cascading Style Sheets\" in result.content  # noqa: S101\n\n\ndef test_semantic_details_summary() -> None:\n    \"\"\"Details and summary elements produce readable output.\"\"\"\n    html = \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Click to expand\" in result.content  # noqa: S101\n\n\ndef test_semantic_hr() -> None:\n    \"\"\"Horizontal rule produces a separator in output.\"\"\"\n    html = \"<p>Above</p><hr><p>Below</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Above\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Below\" in result.content  # noqa: S101\n\n\ndef test_semantic_mark_highlight() -> None:\n    \"\"\"Mark tag produces highlighted output.\"\"\"\n    html = \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"highlighted text\" in result.content  # noqa: S101\n\n\ndef test_semantic_section_with_heading() -> None:\n    \"\"\"Section element with heading preserves structure.\"\"\"\n    html = \"<section><h3>Section Heading</h3><p>Section content.</p></section>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Section Heading\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Section content.\" in result.content  # noqa: S101\n\n\ndef test_semantic_sub_superscript() -> None:\n    \"\"\"Subscript and superscript elements are preserved in output.\"\"\"\n    html = \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"H\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"O\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"E=mc\" in result.content  # noqa: S101\n\n\ndef test_simple_table() -> None:\n    \"\"\"Simple table with header.\"\"\"\n    html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Name\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Age\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Alice\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"30\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"---\" in result.content  # noqa: S101\n\n\ndef test_table_empty() -> None:\n    \"\"\"Empty table produces no output or minimal output.\"\"\"\n    html = \"<table></table>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_table_no_thead() -> None:\n    \"\"\"Table without thead uses first row as implied header.\"\"\"\n    html = \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Product\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Price\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Apple\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"1.00\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n\n\ndef test_table_pipe_chars_in_content() -> None:\n    \"\"\"Table cells containing pipe characters are escaped in output.\"\"\"\n    html = \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Expression\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Result\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"true\" in result.content  # noqa: S101\n\n\ndef test_table_with_alignment() -> None:\n    \"\"\"Table with column alignment attributes.\"\"\"\n    html = '<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Left\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Center\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Right\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"L\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"C\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"R\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n\n\ndef test_table_with_colspan() -> None:\n    \"\"\"Table with colspan attribute in a header cell.\"\"\"\n    html = '<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Full Name\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"John\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Doe\" in result.content  # noqa: S101\n\n\ndef test_unordered_list() -> None:\n    \"\"\"Unordered list.\"\"\"\n    html = \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 3\" in result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_edge_cases.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:1af89794b914b036942c1c23d71bbf58a93285472495bd2f38fa539fcb8f0d73\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: edge-cases.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_empty_html() -> None:\n    \"\"\"Empty HTML document.\"\"\"\n    html = \"<html><head></head><body></body></html>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_encoding_cjk_characters() -> None:\n    \"\"\"CJK (Chinese, Japanese, Korean) characters are preserved.\"\"\"\n    html = \"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"中文内容\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"日本語テキスト\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"한국어 텍스트\" in result.content  # noqa: S101\n\n\ndef test_encoding_html_entities() -> None:\n    \"\"\"Common HTML entities are decoded in output.\"\"\"\n    html = \"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"&\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"<\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \">\" in result.content  # noqa: S101\n\n\ndef test_encoding_named_entities() -> None:\n    \"\"\"Named HTML entities like &mdash; and &hellip; are decoded.\"\"\"\n    html = \"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"—\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"…\" in result.content  # noqa: S101\n\n\ndef test_encoding_numeric_entities() -> None:\n    \"\"\"Numeric HTML entities (decimal and hex) are decoded.\"\"\"\n    html = \"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"©\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"®\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"€\" in result.content  # noqa: S101\n\n\ndef test_encoding_unicode_emoji() -> None:\n    \"\"\"Emoji and Unicode characters are preserved.\"\"\"\n    html = \"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"🌍\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"🚀\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"⭐\" in result.content  # noqa: S101\n\n\ndef test_html_comments_only() -> None:\n    \"\"\"Document containing only HTML comments produces empty output.\"\"\"\n    html = \"<!-- This is a comment --><!-- Another comment -->\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_just_whitespace_input() -> None:\n    \"\"\"Input that is only whitespace characters (spaces, tabs, newlines) produces empty output.\"\"\"\n    html = \"   \"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_malformed_deeply_nested_elements() -> None:\n    \"\"\"Deeply nested elements (100 levels) are handled without stack overflow.\"\"\"\n    html = \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Deeply nested content\" in result.content  # noqa: S101\n\n\ndef test_malformed_missing_block_closing_tags() -> None:\n    \"\"\"Missing closing tags on block elements are auto-closed by parser.\"\"\"\n    html = \"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph\" in result.content  # noqa: S101\n\n\ndef test_malformed_overlapping_tags() -> None:\n    \"\"\"Overlapping bold/italic tags are recovered by the HTML parser without panic.\"\"\"\n    html = \"<p><b><i>bold and italic</b></i></p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"bold and italic\" in result.content  # noqa: S101\n\n\ndef test_malformed_unclosed_paragraph() -> None:\n    \"\"\"Unclosed <p> tag is recovered gracefully and content is preserved.\"\"\"\n    html = \"<p>This paragraph is never closed\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"This paragraph is never closed\" in result.content  # noqa: S101\n\n\ndef test_script_tags_only() -> None:\n    \"\"\"Document with only script tags produces empty output (scripts are stripped).\"\"\"\n    html = \"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_style_tags_only() -> None:\n    \"\"\"Document with only style tags produces empty output (styles are stripped).\"\"\"\n    html = \"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_visitor_custom_element_with_nesting() -> None:\n    \"\"\"Visitor handles custom elements with nested content.\"\"\"\n\n    class _TestVisitor:\n        def visit_custom_element(self, ctx, tag_name, html):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"[CUSTOM WIDGET]\"}\n\n    html = '<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[CUSTOM WIDGET]\" in result.content  # noqa: S101\n    assert result.content is None or \"Widget content here\" not in result.content  # noqa: S101\n\n\ndef test_visitor_deeply_nested_skip() -> None:\n    \"\"\"Visitor skips deeply nested elements.\"\"\"\n\n    class _TestVisitor:\n        def visit_mark(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Outer\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"text\" in result.content  # noqa: S101\n    assert result.content is None or \"highlight\" not in result.content  # noqa: S101\n\n\ndef test_visitor_element_end_modification() -> None:\n    \"\"\"Visitor modifies element at end after children processed.\"\"\"\n\n    class _TestVisitor:\n        def visit_element_end(self, ctx, output, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"MODIFIED OUTPUT\"}\n\n    html = \"<blockquote><p>Original quote</p></blockquote>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content  # noqa: S101\n\n\ndef test_visitor_element_start_skip_entire_subtree() -> None:\n    \"\"\"Visitor skips at element_start level removes entire subtree.\"\"\"\n\n    class _TestVisitor:\n        def visit_element_start(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<div><h1>Title</h1><p>Content</p></div>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content  # noqa: S101\n\n\ndef test_visitor_unknown_tag_preservation() -> None:\n    \"\"\"Visitor preserves unknown HTML tags as raw HTML.\"\"\"\n\n    class _TestVisitor:\n        def visit_custom_element(self, ctx, tag_name, html):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"preserve_html\"\n\n    html = \"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Article text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"More article text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"<x-custom>\" in result.content  # noqa: S101\n\n\ndef test_whitespace_only() -> None:\n    \"\"\"Whitespace-only content.\"\"\"\n    html = \"<p>   </p>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_xss_onclick_handler_removed() -> None:\n    \"\"\"onclick and other on* event handlers are removed from elements.\"\"\"\n    html = '<p><a href=\"https://example.com\" onclick=\"alert(\\'xss\\')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Click me\" in result.content  # noqa: S101\n\n\ndef test_xss_script_tag_stripped() -> None:\n    \"\"\"Script tag content is stripped and does not appear in output.\"\"\"\n    html = \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Safe content\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"More safe content\" in result.content  # noqa: S101\n\n\ndef test_xss_svg_nested_script_stripped() -> None:\n    \"\"\"Script tags nested inside SVG are stripped.\"\"\"\n    html = \"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Before SVG\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After SVG\" in result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_metadata.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e3dd8e86c21640cac753babf1568a323ef1bac644fc62edabe05ed10fa9813b5\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: metadata.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_metadata_author_meta() -> None:\n    \"\"\"Extract author from <meta name='author'> tag.\"\"\"\n    html = '<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.metadata.document.author.strip() == \"Jane Doe\"  # noqa: S101\n\n\ndef test_metadata_canonical_url() -> None:\n    \"\"\"Extract canonical URL from <link rel='canonical'> tag.\"\"\"\n    html = '<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert (\n        result.metadata.document.canonical_url.strip()\n        == \"https://example.com/canonical-page\"\n    )  # noqa: S101\n\n\ndef test_metadata_description_meta() -> None:\n    \"\"\"Extract description from <meta name='description'> tag.\"\"\"\n    html = '<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert (\n        result.metadata.document.description.strip() == \"This is the page description.\"\n    )  # noqa: S101\n\n\ndef test_metadata_dublin_core() -> None:\n    \"\"\"Extract Dublin Core metadata tags.\"\"\"\n    html = '<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"scholarly article\" in result.content  # noqa: S101\n\n\ndef test_metadata_extract_all_images() -> None:\n    \"\"\"Extract all images from a document into metadata.\"\"\"\n    html = '<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.metadata.images) >= 2  # noqa: S101\n\n\ndef test_metadata_extract_all_links() -> None:\n    \"\"\"Extract all links from a document into metadata.\"\"\"\n    html = '<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.metadata.links) >= 2  # noqa: S101\n\n\ndef test_metadata_headers_hierarchy() -> None:\n    \"\"\"Extract heading hierarchy from document into metadata.\"\"\"\n    html = \"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.metadata.headers) >= 5  # noqa: S101\n\n\ndef test_metadata_keywords_meta() -> None:\n    \"\"\"Extract keywords from <meta name='keywords'> tag.\"\"\"\n    html = '<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.metadata.document.keywords) >= 1  # noqa: S101\n\n\ndef test_metadata_lang_attribute() -> None:\n    \"\"\"Extract language from html lang attribute.\"\"\"\n    html = '<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Hola Mundo\" in result.content  # noqa: S101\n\n\ndef test_metadata_microdata_schema_article() -> None:\n    \"\"\"Extract schema.org microdata for Article.\"\"\"\n    html = '<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Breaking News Today\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Jane Reporter\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"important information\" in result.content  # noqa: S101\n\n\ndef test_metadata_microdata_schema_breadcrumb() -> None:\n    \"\"\"Extract schema.org breadcrumb navigation microdata.\"\"\"\n    html = '<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Home\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Products\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Current Page\" in result.content  # noqa: S101\n\n\ndef test_metadata_microdata_schema_organization() -> None:\n    \"\"\"Extract schema.org microdata for Organization.\"\"\"\n    html = '<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Acme Corp\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2020\" in result.content  # noqa: S101\n\n\ndef test_metadata_microdata_schema_person() -> None:\n    \"\"\"Extract schema.org microdata for Person.\"\"\"\n    html = '<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"John Smith\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"john@example.com\" in result.content  # noqa: S101\n\n\ndef test_metadata_microdata_schema_product() -> None:\n    \"\"\"Extract schema.org microdata for Product.\"\"\"\n    html = '<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Awesome Widget\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"best widget\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"29.99\" in result.content  # noqa: S101\n\n\ndef test_metadata_text_direction_ltr() -> None:\n    \"\"\"Extract text direction from lang attribute on html element.\"\"\"\n    html = '<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"left-to-right text\" in result.content  # noqa: S101\n\n\ndef test_metadata_text_direction_rtl() -> None:\n    \"\"\"Extract right-to-left text direction.\"\"\"\n    html = '<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"right-to-left text\" in result.content  # noqa: S101\n\n\ndef test_metadata_title_tag() -> None:\n    \"\"\"Extract title from <title> tag.\"\"\"\n    html = \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.metadata.document.title.strip() == \"My Page\"  # noqa: S101\n\n\ndef test_og_basic_tags() -> None:\n    \"\"\"Extract og:title, og:description, and og:image from Open Graph meta tags.\"\"\"\n    html = '<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.metadata.document.open_graph.get(\"title\").strip() == \"OG Title\"  # noqa: S101\n    assert (\n        result.metadata.document.open_graph.get(\"description\").strip()\n        == \"OG description text.\"\n    )  # noqa: S101\n    assert (\n        result.metadata.document.open_graph.get(\"image\").strip()\n        == \"https://example.com/image.jpg\"\n    )  # noqa: S101\n\n\ndef test_og_multiple_tags() -> None:\n    \"\"\"Extract multiple Open Graph tags including type, url, and site_name.\"\"\"\n    html = '<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.metadata.document.open_graph.get(\"title\").strip() == \"Article Title\"  # noqa: S101\n    assert result.metadata.document.open_graph.get(\"type\").strip() == \"article\"  # noqa: S101\n    assert (\n        result.metadata.document.open_graph.get(\"url\").strip()\n        == \"https://example.com/article\"\n    )  # noqa: S101\n    assert (\n        result.metadata.document.open_graph.get(\"site_name\").strip() == \"Example Site\"\n    )  # noqa: S101\n\n\ndef test_structured_data_json_ld() -> None:\n    \"\"\"JSON-LD script tag is stripped from output (security) but metadata may be extracted.\"\"\"\n    html = '<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"My Article\" in result.content  # noqa: S101\n\n\ndef test_structured_data_multiple_json_ld() -> None:\n    \"\"\"Multiple JSON-LD blocks are all stripped from output.\"\"\"\n    html = '<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Widget\" in result.content  # noqa: S101\n\n\ndef test_twitter_card_tags() -> None:\n    \"\"\"Extract Twitter card meta tags.\"\"\"\n    html = '<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert (\n        result.metadata.document.twitter_card.get(\"card\").strip()\n        == \"summary_large_image\"\n    )  # noqa: S101\n    assert (\n        result.metadata.document.twitter_card.get(\"title\").strip()\n        == \"Twitter Card Title\"\n    )  # noqa: S101\n    assert (\n        result.metadata.document.twitter_card.get(\"description\").strip()\n        == \"Twitter card description.\"\n    )  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_options.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6ce1ab1356513811b673bd6e06620910639df524a0df56094e2938e600f459c8\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: options.\"\"\"\n\nfrom html_to_markdown import convert, ConversionOptions\nfrom html_to_markdown.options import (\n    CodeBlockStyle,\n    HeadingStyle,\n    HighlightStyle,\n    LinkStyle,\n    ListIndentType,\n    NewlineStyle,\n    OutputFormat,\n    WhitespaceMode,\n)\n\n\ndef test_options_autolinks_false() -> None:\n    \"\"\"Bare URL links rendered as regular markdown links when autolinks disabled.\"\"\"\n    html = \"<p><a href='https://example.com'>https://example.com</a></p>\"\n    options = ConversionOptions(autolinks=False)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"example.com\" in result.content  # noqa: S101\n\n\ndef test_options_br_in_tables_false() -> None:\n    \"\"\"BR elements in table cells are stripped when disabled.\"\"\"\n    html = \"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\"\n    options = ConversionOptions(br_in_tables=False)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Col\" in result.content  # noqa: S101\n\n\ndef test_options_br_in_tables_true() -> None:\n    \"\"\"BR elements in table cells render as line breaks.\"\"\"\n    html = \"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\"\n    options = ConversionOptions(br_in_tables=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Header\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line 2\" in result.content  # noqa: S101\n\n\ndef test_options_code_block_backticks() -> None:\n    \"\"\"Backticks code block style uses triple backtick fences.\"\"\"\n    html = \"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\"\n    options = ConversionOptions(code_block_style=CodeBlockStyle.BACKTICKS)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"```\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"console.log('hi');\" in result.content  # noqa: S101\n\n\ndef test_options_code_block_indented() -> None:\n    \"\"\"Code blocks use 4-space indentation.\"\"\"\n    html = \"<pre><code>print('hello')</code></pre>\"\n    options = ConversionOptions(code_block_style=CodeBlockStyle.INDENTED)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"print('hello')\" in result.content  # noqa: S101\n    assert result.content is None or \"```\" not in result.content  # noqa: S101\n\n\ndef test_options_code_block_tildes() -> None:\n    \"\"\"Code blocks use tilde fences.\"\"\"\n    html = \"<pre><code>let x = 1;</code></pre>\"\n    options = ConversionOptions(code_block_style=CodeBlockStyle.TILDES)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"~~~\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"let x = 1;\" in result.content  # noqa: S101\n\n\ndef test_options_code_block_tildes_style() -> None:\n    \"\"\"Tildes code block style uses triple tilde fences.\"\"\"\n    html = \"<pre><code>some code</code></pre>\"\n    options = ConversionOptions(code_block_style=CodeBlockStyle.TILDES)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"~~~\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"some code\" in result.content  # noqa: S101\n\n\ndef test_options_code_language_python() -> None:\n    \"\"\"Default code language annotation on blocks without lang attribute.\"\"\"\n    html = \"<pre><code>def hello(): pass</code></pre>\"\n    options = ConversionOptions(code_language=\"python\")\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"```python\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"def hello\" in result.content  # noqa: S101\n\n\ndef test_options_convert_as_inline() -> None:\n    \"\"\"Block elements treated as inline.\"\"\"\n    html = \"<p>One</p><p>Two</p>\"\n    options = ConversionOptions(convert_as_inline=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"One\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Two\" in result.content  # noqa: S101\n\n\ndef test_options_debug_true() -> None:\n    \"\"\"Debug mode enabled does not crash and produces output.\"\"\"\n    html = \"<p>Debug test</p>\"\n    options = ConversionOptions(debug=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Debug test\" in result.content  # noqa: S101\n\n\ndef test_options_default_title_true() -> None:\n    \"\"\"Links without title get empty title attribute when defaultTitle is true.\"\"\"\n    html = \"<p><a href='https://example.com'>Link</a></p>\"\n    options = ConversionOptions(default_title=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Link\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_options_encoding_utf8() -> None:\n    \"\"\"UTF-8 encoding hint for special characters.\"\"\"\n    html = \"<p>Café naïve résumé</p>\"\n    options = ConversionOptions(encoding=\"utf-8\")\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n\n\ndef test_options_escape_ascii_enabled() -> None:\n    \"\"\"ASCII Markdown characters are escaped when escapeAscii is true.\"\"\"\n    html = \"<p>Text with # hash and [brackets] and * star</p>\"\n    options = ConversionOptions(escape_ascii=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"hash\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"brackets\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"star\" in result.content  # noqa: S101\n\n\ndef test_options_escape_asterisks() -> None:\n    \"\"\"escape_asterisks option escapes asterisks in plain text.\"\"\"\n    html = \"<p>Use 2*3 = 6 in math.</p>\"\n    options = ConversionOptions(escape_asterisks=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"3\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"6\" in result.content  # noqa: S101\n\n\ndef test_options_escape_misc() -> None:\n    \"\"\"escape_misc option escapes miscellaneous markdown characters.\"\"\"\n    html = \"<p>Use # and | and ~ in text.</p>\"\n    options = ConversionOptions(escape_misc=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Use\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"and\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"in text.\" in result.content  # noqa: S101\n\n\ndef test_options_escape_underscores() -> None:\n    \"\"\"escape_underscores option escapes underscores in plain text.\"\"\"\n    html = \"<p>The variable_name is defined.</p>\"\n    options = ConversionOptions(escape_underscores=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"variable\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"name\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"defined.\" in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_attribute() -> None:\n    \"\"\"Elements matching CSS attribute selector are excluded entirely.\"\"\"\n    html = '<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>'\n    options = ConversionOptions(exclude_selectors=[\"[role='complementary']\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Primary text\" in result.content  # noqa: S101\n    assert result.content is None or \"Sidebar\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_class() -> None:\n    \"\"\"Elements matching CSS class selector are excluded entirely.\"\"\"\n    html = '<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>'\n    options = ConversionOptions(exclude_selectors=[\".cookie-banner\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Main content\" in result.content  # noqa: S101\n    assert result.content is None or \"cookies\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_empty_noop() -> None:\n    \"\"\"Empty exclude_selectors list does not affect output.\"\"\"\n    html = \"<p>Hello world</p>\"\n    options = ConversionOptions(exclude_selectors=[])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Hello world\" in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_id() -> None:\n    \"\"\"Elements matching CSS id selector are excluded entirely.\"\"\"\n    html = '<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>'\n    options = ConversionOptions(exclude_selectors=[\"#ad-container\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Article text\" in result.content  # noqa: S101\n    assert result.content is None or \"Buy stuff\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_multiple() -> None:\n    \"\"\"Multiple CSS selectors each exclude their matched elements.\"\"\"\n    html = (\n        '<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>'\n    )\n    options = ConversionOptions(exclude_selectors=[\".nav\", \"footer\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Content\" in result.content  # noqa: S101\n    assert result.content is None or \"Menu\" not in result.content  # noqa: S101\n    assert result.content is None or \"Footer\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_nested_content_dropped() -> None:\n    \"\"\"All descendants of excluded elements are dropped.\"\"\"\n    html = '<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>'\n    options = ConversionOptions(exclude_selectors=[\".sidebar\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Main text\" in result.content  # noqa: S101\n    assert result.content is None or \"Related\" not in result.content  # noqa: S101\n    assert result.content is None or \"Sidebar text\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_plain_text_mode() -> None:\n    \"\"\"Exclude selectors work in plain text output mode.\"\"\"\n    html = '<body><div class=\"nav\">Navigation</div><p>Article body</p></body>'\n    options = ConversionOptions(\n        exclude_selectors=[\".nav\"], output_format=OutputFormat.PLAIN\n    )\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Article body\" in result.content  # noqa: S101\n    assert result.content is None or \"Navigation\" not in result.content  # noqa: S101\n\n\ndef test_options_exclude_selectors_vs_strip_tags() -> None:\n    \"\"\"exclude_selectors drops entire subtree unlike strip_tags which keeps children.\"\"\"\n    html = '<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>'\n    options = ConversionOptions(exclude_selectors=[\".wrapper\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Outer text\" in result.content  # noqa: S101\n    assert result.content is None or \"Inner paragraph\" not in result.content  # noqa: S101\n\n\ndef test_options_extract_metadata_true() -> None:\n    \"\"\"Extract metadata returns document metadata when enabled.\"\"\"\n    html = \"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\"\n    options = ConversionOptions(extract_metadata=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.metadata.document.title.strip() == \"Test Page\"  # noqa: S101\n    assert result.metadata.document.description.strip() == \"A test page\"  # noqa: S101\n\n\ndef test_options_heading_style_atx() -> None:\n    \"\"\"ATX heading style produces hash-prefixed headings.\"\"\"\n    html = \"<h1>Title</h1><h2>Subtitle</h2>\"\n    options = ConversionOptions(heading_style=HeadingStyle.ATX)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"# Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## Subtitle\" in result.content  # noqa: S101\n\n\ndef test_options_heading_style_atx_closed() -> None:\n    \"\"\"ATX closed heading style adds closing hashes.\"\"\"\n    html = \"<h1>Closed Heading</h1>\"\n    options = ConversionOptions(heading_style=HeadingStyle.ATX_CLOSED)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"# Closed Heading #\" in result.content  # noqa: S101\n\n\ndef test_options_heading_style_underlined() -> None:\n    \"\"\"Underlined heading style produces setext-style headings for h1 and h2.\"\"\"\n    html = \"<h1>Main Title</h1>\"\n    options = ConversionOptions(heading_style=HeadingStyle.UNDERLINED)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Main Title\" in result.content  # noqa: S101\n\n\ndef test_options_highlight_bold() -> None:\n    \"\"\"Mark tag rendered as bold.\"\"\"\n    html = \"<p>Text with <mark>highlighted</mark> text.</p>\"\n    options = ConversionOptions(highlight_style=HighlightStyle.BOLD)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"**highlighted**\" in result.content  # noqa: S101\n\n\ndef test_options_highlight_double_equal() -> None:\n    \"\"\"Mark tag with double equal highlight style.\"\"\"\n    html = \"<p>Text with <mark>highlighted</mark> here.</p>\"\n    options = ConversionOptions(highlight_style=HighlightStyle.DOUBLE_EQUAL)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"==highlighted==\" in result.content  # noqa: S101\n\n\ndef test_options_highlight_none() -> None:\n    \"\"\"Mark tag with no highlight style strips the mark.\"\"\"\n    html = \"<p>Text with <mark>plain</mark> content.</p>\"\n    options = ConversionOptions(highlight_style=HighlightStyle.NONE)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"plain\" in result.content  # noqa: S101\n    assert result.content is None or \"==\" not in result.content  # noqa: S101\n\n\ndef test_options_keep_inline_images_in_paragraph() -> None:\n    \"\"\"Images inside specified tags stay inline.\"\"\"\n    html = \"<p>Text <img src='icon.png' alt='icon'> more text</p>\"\n    options = ConversionOptions(keep_inline_images_in=[\"p\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"more text\" in result.content  # noqa: S101\n\n\ndef test_options_link_style_reference() -> None:\n    \"\"\"Links use reference-style formatting.\"\"\"\n    html = \"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\"\n    options = ConversionOptions(link_style=LinkStyle.REFERENCE)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Example\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Other\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"example.com\" in result.content  # noqa: S101\n\n\ndef test_options_list_custom_bullets() -> None:\n    \"\"\"Custom bullet character for unordered lists.\"\"\"\n    html = \"<ul><li>Item A</li><li>Item B</li></ul>\"\n    options = ConversionOptions(bullets=\"*\")\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"* Item A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"* Item B\" in result.content  # noqa: S101\n\n\ndef test_options_list_indent_tabs() -> None:\n    \"\"\"Tab indentation type for nested list items.\"\"\"\n    html = \"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\"\n    options = ConversionOptions(list_indent_type=ListIndentType.TABS)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Parent\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Child\" in result.content  # noqa: S101\n\n\ndef test_options_list_indent_width_four() -> None:\n    \"\"\"Nested lists indented with 4 spaces per level.\"\"\"\n    html = \"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\"\n    options = ConversionOptions(list_indent_width=4)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Outer\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Inner\" in result.content  # noqa: S101\n\n\ndef test_options_max_depth_default_unlimited() -> None:\n    \"\"\"Default max_depth (null) converts deeply nested content fully.\"\"\"\n    html = \"<div><div><div><div><p>Deep content</p></div></div></div></div>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Deep content\" in result.content  # noqa: S101\n\n\ndef test_options_max_depth_truncates() -> None:\n    \"\"\"max_depth truncates content beyond the specified depth.\"\"\"\n    html = \"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\"\n    options = ConversionOptions(max_depth=3)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Shallow\" in result.content  # noqa: S101\n    assert result.content is None or \"Too deep\" not in result.content  # noqa: S101\n\n\ndef test_options_max_depth_zero_empty() -> None:\n    \"\"\"max_depth of 0 produces empty output.\"\"\"\n    html = \"<p>Hello</p>\"\n    options = ConversionOptions(max_depth=0)\n    result = convert(html=html, options=options)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_options_newline_backslash() -> None:\n    \"\"\"Hard line breaks rendered with backslash.\"\"\"\n    html = \"<p>Line one<br>Line two</p>\"\n    options = ConversionOptions(newline_style=NewlineStyle.BACKSLASH)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Line one\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line two\" in result.content  # noqa: S101\n\n\ndef test_options_newline_spaces() -> None:\n    \"\"\"Hard line breaks rendered with trailing spaces.\"\"\"\n    html = \"<p>First<br>Second</p>\"\n    options = ConversionOptions(newline_style=NewlineStyle.SPACES)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"First\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second\" in result.content  # noqa: S101\n\n\ndef test_options_output_format_djot() -> None:\n    \"\"\"Djot output format produces djot-compatible markup.\"\"\"\n    html = \"<p>Simple paragraph.</p>\"\n    options = ConversionOptions(output_format=OutputFormat.DJOT)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Simple paragraph.\" in result.content  # noqa: S101\n\n\ndef test_options_output_format_markdown() -> None:\n    \"\"\"Default markdown output format produces standard markdown.\"\"\"\n    html = \"<h1>Title</h1><p>Some text.</p>\"\n    options = ConversionOptions(\n        heading_style=HeadingStyle.ATX, output_format=OutputFormat.MARKDOWN\n    )\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"# Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Some text.\" in result.content  # noqa: S101\n\n\ndef test_options_output_format_plain() -> None:\n    \"\"\"Plain text output format strips markdown syntax.\"\"\"\n    html = \"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\"\n    options = ConversionOptions(output_format=OutputFormat.PLAIN)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"bold\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"text.\" in result.content  # noqa: S101\n\n\ndef test_options_preprocessing_aggressive() -> None:\n    \"\"\"Aggressive preset removes nav, footer, aside unconditionally.\"\"\"\n    html = \"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\"\n    options = ConversionOptions(preprocessing={\"preset\": \"Aggressive\"})\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Content\" in result.content  # noqa: S101\n    assert result.content is None or \"Menu\" not in result.content  # noqa: S101\n\n\ndef test_options_preprocessing_minimal() -> None:\n    \"\"\"Minimal preset preserves nav, footer, aside.\"\"\"\n    html = \"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\"\n    options = ConversionOptions(preprocessing={\"preset\": \"Minimal\"})\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Navigation\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Content\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Footer\" in result.content  # noqa: S101\n\n\ndef test_options_preprocessing_remove_forms() -> None:\n    \"\"\"Forms are removed when remove_forms is true.\"\"\"\n    html = \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\"\n    options = ConversionOptions(preprocessing={\"removeForms\": True})\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Before\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After\" in result.content  # noqa: S101\n    assert result.content is None or \"Submit\" not in result.content  # noqa: S101\n\n\ndef test_options_preserve_tags_iframe() -> None:\n    \"\"\"Iframe tags preserved as raw HTML in output.\"\"\"\n    html = \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\"\n    options = ConversionOptions(preserve_tags=[\"iframe\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Before\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"<iframe\" in result.content  # noqa: S101\n\n\ndef test_options_skip_images_true() -> None:\n    \"\"\"Images are omitted from output when skipImages is true.\"\"\"\n    html = \"<p>Before <img src='test.jpg' alt='photo'> After</p>\"\n    options = ConversionOptions(skip_images=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Before\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After\" in result.content  # noqa: S101\n    assert result.content is None or \"photo\" not in result.content  # noqa: S101\n\n\ndef test_options_strip_newlines() -> None:\n    \"\"\"Strip newlines produces single-line paragraphs.\"\"\"\n    html = \"<p>First paragraph.</p><p>Second paragraph.</p>\"\n    options = ConversionOptions(strip_newlines=True)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph.\" in result.content  # noqa: S101\n\n\ndef test_options_strip_tags_div_span() -> None:\n    \"\"\"Div and span tags stripped but content preserved.\"\"\"\n    html = \"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\"\n    options = ConversionOptions(strip_tags=[\"div\", \"span\"])\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"Inside div\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"span text\" in result.content  # noqa: S101\n\n\ndef test_options_strong_em_underscore() -> None:\n    \"\"\"Strong and em tags use underscore symbol instead of asterisk.\"\"\"\n    html = \"<p><strong>bold</strong> and <em>italic</em></p>\"\n    options = ConversionOptions(strong_em_symbol=\"_\")\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"__bold__\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"_italic_\" in result.content  # noqa: S101\n\n\ndef test_options_sub_symbol_tilde() -> None:\n    \"\"\"Subscript rendered with tilde symbol.\"\"\"\n    html = \"<p>H<sub>2</sub>O</p>\"\n    options = ConversionOptions(sub_symbol=\"~\")\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"~2~\" in result.content  # noqa: S101\n\n\ndef test_options_sup_symbol_caret() -> None:\n    \"\"\"Superscript rendered with caret symbol.\"\"\"\n    html = \"<p>x<sup>2</sup></p>\"\n    options = ConversionOptions(sup_symbol=\"^\")\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert \"^2^\" in result.content  # noqa: S101\n\n\ndef test_options_whitespace_normalized() -> None:\n    \"\"\"Normalized whitespace mode collapses multiple spaces.\"\"\"\n    html = \"<p>Text   with    extra   spaces.</p>\"\n    options = ConversionOptions(whitespace_mode=WhitespaceMode.NORMALIZED)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"with\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"extra\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"spaces.\" in result.content  # noqa: S101\n\n\ndef test_options_whitespace_strict() -> None:\n    \"\"\"Strict whitespace mode preserves whitespace as-is.\"\"\"\n    html = \"<p>Preserved   spacing.</p>\"\n    options = ConversionOptions(whitespace_mode=WhitespaceMode.STRICT)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Preserved\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"spacing.\" in result.content  # noqa: S101\n\n\ndef test_options_wrap_disabled() -> None:\n    \"\"\"Wrap option disabled preserves long lines without breaking.\"\"\"\n    html = \"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\"\n    options = ConversionOptions(wrap=False)\n    result = convert(html=html, options=options)\n    assert result.content is not None  # noqa: S101\n    assert (\n        \"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\"\n        in result.content\n    )  # noqa: S101\n\n\ndef test_options_wrap_enabled() -> None:\n    \"\"\"Wrap option enabled with custom width wraps long lines.\"\"\"\n    html = \"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\"\n    options = ConversionOptions(wrap=True, wrap_width=40)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"This is a long paragraph\" in result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_real_world.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:12af1285b0cce275a3ec0f32d3323d3698c14353d150e6a872d57950d2cc300c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: real-world.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_real_world_blog_post() -> None:\n    \"\"\"Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown.\"\"\"\n    html = '<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\"https://www.mozilla.org\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\"language-bash\">curl --proto \\'=https\\' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\"language-rust\">fn main() {\\n    println!(\"Hello, world!\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\"https://doc.rust-lang.org/book/\">Rust Book</a>.</p></article>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"# Getting Started with Rust\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## Installation\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## Hello World\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## Key Concepts\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"cargo run\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[Mozilla](https://www.mozilla.org)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"- Ownership and borrowing\" in result.content  # noqa: S101\n\n\ndef test_real_world_documentation_page() -> None:\n    \"\"\"Documentation page with nested lists, code examples, and blockquotes converts correctly.\"\"\"\n    html = '<div class=\"docs\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\"language-rust\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\"language-rust\">let markdown = convert_html(\"&lt;h1&gt;Hello&lt;/h1&gt;\").unwrap();\\nassert_eq!(markdown, \"# Hello\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\"language-rust\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\"/docs/options\">options reference</a> for a full list of configuration values.</p></blockquote></div>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"# API Reference\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## convert_html\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"### Parameters\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"### Returns\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"### Example\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## ConversionOptions\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"> \" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"thread-safe\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"convert_html\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"ConversionOptions\" in result.content  # noqa: S101\n\n\ndef test_real_world_product_page() -> None:\n    \"\"\"Product page with table, images, and lists converts correctly.\"\"\"\n    html = '<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What\\'s in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"# Wireless Keyboard Pro\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert (\n        \"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\" in result.content\n    )  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## Specifications\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Battery Life\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"12 months\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Bluetooth 5.0\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"## What's in the Box\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"USB-C charging cable\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"---\" in result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_result.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:13c9800f9ed16ca810597bfbb70687d031ea7d796cebf0d4095fadcdebb93ed7\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: result.\"\"\"\n\nfrom html_to_markdown import convert, ConversionOptions\n\n\ndef test_result_tables_empty_when_no_tables() -> None:\n    \"\"\"Result tables array is empty when input has no tables.\"\"\"\n    html = \"<p>No tables here</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert len(result.tables) == 0  # noqa: S101\n\n\ndef test_result_tables_multiple() -> None:\n    \"\"\"Multiple tables each appear in the tables array.\"\"\"\n    html = \"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert len(result.tables) >= 2  # noqa: S101\n\n\ndef test_result_tables_simple() -> None:\n    \"\"\"Simple table populates the tables array in result.\"\"\"\n    html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert len(result.tables) >= 1  # noqa: S101\n\n\ndef test_result_tables_without_structure_flag() -> None:\n    \"\"\"Tables array is empty when includeDocumentStructure is false.\"\"\"\n    html = \"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.tables) == 0  # noqa: S101\n\n\ndef test_result_warnings_empty_for_clean_input() -> None:\n    \"\"\"Warnings array is empty for well-formed HTML without problematic content.\"\"\"\n    html = \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.warnings) == 0  # noqa: S101\n\n\ndef test_result_warnings_empty_for_complex_input() -> None:\n    \"\"\"Warnings array is empty for complex but valid HTML.\"\"\"\n    html = \"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.warnings) == 0  # noqa: S101\n\n\ndef test_result_warnings_empty_for_malformed_html() -> None:\n    \"\"\"Warnings array is empty even for malformed HTML (parser is lenient).\"\"\"\n    html = \"<p>Unclosed paragraph<div>Mixed nesting</p></div>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert len(result.warnings) == 0  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_smoke.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:15d92fb861279fb3c4522f118eb734426513e25bab584fb6f0c865a541c4e56d\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: smoke.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_smoke_empty_string() -> None:\n    \"\"\"Empty string produces empty output.\"\"\"\n    html = \"\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_smoke_simple_heading() -> None:\n    \"\"\"H1 heading converts to ATX markdown.\"\"\"\n    html = \"<h1>Title</h1>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"# Title\" in result.content  # noqa: S101\n\n\ndef test_smoke_simple_paragraph() -> None:\n    \"\"\"Simple paragraph converts correctly.\"\"\"\n    html = \"<p>Hello World</p>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"Hello World\"  # noqa: S101\n    assert result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_structure.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:3084b908d502ba6dafe3d6242a605baf6c1fdf985a0a0cdfd7dbf13029438f61\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: structure.\"\"\"\n\nfrom html_to_markdown import convert, ConversionOptions\n\n\ndef test_structure_code_block() -> None:\n    \"\"\"Fenced code block produces Code node.\"\"\"\n    html = '<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>'\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 2  # noqa: S101\n\n\ndef test_structure_deep_nesting_h1_h2_h3() -> None:\n    \"\"\"H1 > H2 > H3 creates three levels of heading nesting.\"\"\"\n    html = \"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 5  # noqa: S101\n\n\ndef test_structure_h1_h2_nested_group() -> None:\n    \"\"\"H1 followed by H2 creates a nested group under the H1.\"\"\"\n    html = \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 3  # noqa: S101\n\n\ndef test_structure_heading_paragraph() -> None:\n    \"\"\"Simple heading followed by paragraph produces Heading and Paragraph nodes.\"\"\"\n    html = \"<h1>Title</h1><p>A paragraph of text.</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 2  # noqa: S101\n\n\ndef test_structure_list() -> None:\n    \"\"\"Unordered list produces List and ListItem nodes.\"\"\"\n    html = \"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 2  # noqa: S101\n\n\ndef test_structure_multiple_headings() -> None:\n    \"\"\"Multiple headings create multiple Heading nodes with correct levels.\"\"\"\n    html = \"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 4  # noqa: S101\n\n\ndef test_structure_sibling_h1_groups() -> None:\n    \"\"\"H1, H2, then another H1 creates two sibling top-level groups.\"\"\"\n    html = \"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\"\n    options = ConversionOptions(include_document_structure=True)\n    result = convert(html=html, options=options)\n    assert result.content  # noqa: S101\n    assert result.document.nodes  # noqa: S101\n    assert len(result.document.nodes) >= 4  # noqa: S101\n"
  },
  {
    "path": "e2e/python/tests/test_visitor.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:2804b2507fc672949154ee1512674ea5cef5adae9dce80e4922b002bf08f1934\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: visitor.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_visitor_audio_custom() -> None:\n    \"\"\"Visitor replaces audio element with custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_audio(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"[AUDIO: podcast.mp3]\"}\n\n    html = '<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[AUDIO: podcast.mp3]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Listen to this:\" in result.content  # noqa: S101\n\n\ndef test_visitor_audio_skip() -> None:\n    \"\"\"Visitor removes audio elements from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_audio(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = (\n        '<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>'\n    )\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Background music:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Enjoy!\" in result.content  # noqa: S101\n    assert result.content is None or \"music.ogg\" not in result.content  # noqa: S101\n\n\ndef test_visitor_button_custom() -> None:\n    \"\"\"Visitor replaces button with bracketed text.\"\"\"\n\n    class _TestVisitor:\n        def visit_button(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"[BTN:{text}]\"}\n\n    html = '<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[BTN:Click me]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[BTN:Cancel]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Confirm action:\" in result.content  # noqa: S101\n\n\ndef test_visitor_button_skip() -> None:\n    \"\"\"Visitor removes all buttons from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_button(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Actions available:\" in result.content  # noqa: S101\n    assert result.content is None or \"Save\" not in result.content  # noqa: S101\n    assert result.content is None or \"Delete\" not in result.content  # noqa: S101\n    assert result.content is None or \"Export\" not in result.content  # noqa: S101\n\n\ndef test_visitor_continue_default() -> None:\n    \"\"\"Visitor continue action preserves default conversion.\"\"\"\n\n    class _TestVisitor:\n        def visit_strong(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"continue\"\n\n    html = \"<p>Hello <strong>World</strong></p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"**World**\" in result.content  # noqa: S101\n\n\ndef test_visitor_custom_blockquote() -> None:\n    \"\"\"Visitor replaces blockquote with custom format.\"\"\"\n\n    class _TestVisitor:\n        def visit_blockquote(self, ctx, content, depth):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f'QUOTE: \"{content}\"'}\n\n    html = \"<blockquote><p>A wise quote.</p></blockquote>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"QUOTE:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"A wise quote.\" in result.content  # noqa: S101\n\n\ndef test_visitor_custom_emphasis() -> None:\n    \"\"\"Visitor replaces emphasis with custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_emphasis(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\">>>{text}<<<\"}\n\n    html = \"<p>This is <em>important</em> text.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \">>>important<<<\" in result.content  # noqa: S101\n    assert result.content is None or \"*important*\" not in result.content  # noqa: S101\n\n\ndef test_visitor_custom_heading() -> None:\n    \"\"\"Visitor replaces heading with custom format.\"\"\"\n\n    class _TestVisitor:\n        def visit_heading(self, ctx, level, text, id):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"--- {text} ---\"}\n\n    html = \"<h2>Section Title</h2><p>Content below heading.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"--- Section Title ---\" in result.content  # noqa: S101\n    assert result.content is None or \"## Section Title\" not in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Content below heading.\" in result.content  # noqa: S101\n\n\ndef test_visitor_custom_image() -> None:\n    \"\"\"Visitor replaces image with custom output using template.\"\"\"\n\n    class _TestVisitor:\n        def visit_image(self, ctx, src, alt, title):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"[Image: {alt}]\"}\n\n    html = '<img src=\"banner.png\" alt=\"Banner\">'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[Image: Banner]\" in result.content  # noqa: S101\n    assert result.content is None or \"banner.png\" not in result.content  # noqa: S101\n\n\ndef test_visitor_custom_link_format() -> None:\n    \"\"\"Visitor reformats links using template interpolation.\"\"\"\n\n    class _TestVisitor:\n        def visit_link(self, ctx, href, text, title):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"{text} ({href})\"}\n\n    html = '<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Example (https://example.com)\" in result.content  # noqa: S101\n    assert result.content is None or \"[Example]\" not in result.content  # noqa: S101\n\n\ndef test_visitor_custom_link_static() -> None:\n    \"\"\"Visitor replaces link with static custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_link(self, ctx, href, text, title):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"[REDACTED LINK]\"}\n\n    html = '<a href=\"https://example.com\">Click here</a>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[REDACTED LINK]\" in result.content  # noqa: S101\n    assert result.content is None or \"example.com\" not in result.content  # noqa: S101\n\n\ndef test_visitor_custom_output() -> None:\n    \"\"\"Visitor custom action replaces element output.\"\"\"\n\n    class _TestVisitor:\n        def visit_heading(self, ctx, level, text, id):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"## REPLACED HEADING\"}\n\n    html = \"<h1>Original Heading</h1>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"## REPLACED HEADING\" in result.content  # noqa: S101\n    assert result.content is None or \"# Original Heading\" not in result.content  # noqa: S101\n\n\ndef test_visitor_definition_list_custom() -> None:\n    \"\"\"Visitor customizes definition list items.\"\"\"\n\n    class _TestVisitor:\n        def visit_definition_term(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"**{text}**\"}\n\n    html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"**HTML**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"**CSS**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"HyperText Markup Language\" in result.content  # noqa: S101\n\n\ndef test_visitor_definition_list_custom_format() -> None:\n    \"\"\"Visitor formats definition lists with custom templates.\"\"\"\n\n    class _TestVisitor:\n        def visit_definition_term(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"### {text}\"}\n\n        def visit_definition_description(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"> {text}\"}\n\n    html = \"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"### Python\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"### JavaScript\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"> A high-level programming language\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"> A scripting language for web browsers\" in result.content  # noqa: S101\n\n\ndef test_visitor_definition_list_skip() -> None:\n    \"\"\"Visitor skips definition list items from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_definition_description(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n        def visit_definition_term(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Glossary:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"End of glossary\" in result.content  # noqa: S101\n    assert result.content is None or \"Term A\" not in result.content  # noqa: S101\n    assert result.content is None or \"Definition\" not in result.content  # noqa: S101\n\n\ndef test_visitor_details_summary_custom() -> None:\n    \"\"\"Visitor customizes details/summary disclosure elements.\"\"\"\n\n    class _TestVisitor:\n        def visit_summary(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"[EXPANDABLE] {text}\"}\n\n    html = \"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[EXPANDABLE] Click to expand\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"This content is initially hidden.\" in result.content  # noqa: S101\n\n\ndef test_visitor_details_summary_skip() -> None:\n    \"\"\"Visitor removes details/summary elements entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_details(self, ctx, is_open):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Main content here.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"More main content.\" in result.content  # noqa: S101\n    assert result.content is None or \"Hidden section\" not in result.content  # noqa: S101\n    assert result.content is None or \"Secret details\" not in result.content  # noqa: S101\n\n\ndef test_visitor_figure_custom() -> None:\n    \"\"\"Visitor customizes figure and figcaption elements.\"\"\"\n\n    class _TestVisitor:\n        def visit_figcaption(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"*{text}*\"}\n\n    html = '<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Article Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"*Figure 1: System Architecture*\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Explanation of the figure.\" in result.content  # noqa: S101\n\n\ndef test_visitor_figure_custom_wrap() -> None:\n    \"\"\"Visitor wraps figure content with custom formatting.\"\"\"\n\n    class _TestVisitor:\n        def visit_figure_start(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"\\n[FIGURE]\\n\"}\n\n        def visit_figure_end(self, ctx, output, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"{output}\\n[/FIGURE]\\n\"}\n\n    html = '<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[FIGURE]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[/FIGURE]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Gallery\" in result.content  # noqa: S101\n\n\ndef test_visitor_figure_skip() -> None:\n    \"\"\"Visitor removes figure elements with their captions.\"\"\"\n\n    class _TestVisitor:\n        def visit_figure_start(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"See the chart below:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"As shown in the chart above.\" in result.content  # noqa: S101\n    assert result.content is None or \"Revenue Trends\" not in result.content  # noqa: S101\n    assert result.content is None or \"chart.svg\" not in result.content  # noqa: S101\n\n\ndef test_visitor_form_custom() -> None:\n    \"\"\"Visitor replaces form with custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_form(self, ctx, action_url, method):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"[FORM PLACEHOLDER]\"}\n\n    html = '<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[FORM PLACEHOLDER]\" in result.content  # noqa: S101\n    assert result.content is None or \"submit\" not in result.content  # noqa: S101\n    assert result.content is None or \"input\" not in result.content  # noqa: S101\n\n\ndef test_visitor_form_skip() -> None:\n    \"\"\"Visitor skips form elements entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_form(self, ctx, action_url, method):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Before form\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After form\" in result.content  # noqa: S101\n    assert result.content is None or \"email\" not in result.content  # noqa: S101\n\n\ndef test_visitor_horizontal_rule_custom() -> None:\n    \"\"\"Visitor replaces horizontal rule with custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_horizontal_rule(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"\\n[DIVIDER]\\n\"}\n\n    html = \"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[DIVIDER]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Section A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Section B\" in result.content  # noqa: S101\n    assert result.content is None or \"---\" not in result.content  # noqa: S101\n\n\ndef test_visitor_horizontal_rule_skip() -> None:\n    \"\"\"Visitor removes all horizontal rules.\"\"\"\n\n    class _TestVisitor:\n        def visit_horizontal_rule(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Part 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Part 2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Part 3\" in result.content  # noqa: S101\n    assert result.content is None or \"---\" not in result.content  # noqa: S101\n\n\ndef test_visitor_iframe_custom() -> None:\n    \"\"\"Visitor replaces embedded iframe with custom text.\"\"\"\n\n    class _TestVisitor:\n        def visit_iframe(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \"[EMBEDDED: https://maps.example.com/embed]\"}\n\n    html = '<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[EMBEDDED: https://maps.example.com/embed]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Embedded map:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"End of map\" in result.content  # noqa: S101\n\n\ndef test_visitor_iframe_skip() -> None:\n    \"\"\"Visitor removes embedded iframes.\"\"\"\n\n    class _TestVisitor:\n        def visit_iframe(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Reviews\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"See reviews from our partners.\" in result.content  # noqa: S101\n    assert result.content is None or \"widget.example.com\" not in result.content  # noqa: S101\n\n\ndef test_visitor_input_custom() -> None:\n    \"\"\"Visitor replaces input with labeled output.\"\"\"\n\n    class _TestVisitor:\n        def visit_input(self, ctx, input_type, name, value):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"[INPUT:{input_type}]\"}\n\n    html = '<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[INPUT:text]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[INPUT:password]\" in result.content  # noqa: S101\n\n\ndef test_visitor_input_skip() -> None:\n    \"\"\"Visitor skips all input elements.\"\"\"\n\n    class _TestVisitor:\n        def visit_input(self, ctx, input_type, name, value):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Sign up:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Continue\" in result.content  # noqa: S101\n    assert result.content is None or \"email\" not in result.content  # noqa: S101\n\n\ndef test_visitor_line_break_custom() -> None:\n    \"\"\"Visitor replaces line break with custom output.\"\"\"\n\n    class _TestVisitor:\n        def visit_line_break(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": \" | \"}\n\n    html = \"<p>First line<br>Second line<br>Third line</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"First line | Second line | Third line\" in result.content  # noqa: S101\n    assert result.content is None or \"\\n\\n\" not in result.content  # noqa: S101\n\n\ndef test_visitor_line_break_skip() -> None:\n    \"\"\"Visitor removes all line breaks.\"\"\"\n\n    class _TestVisitor:\n        def visit_line_break(self, ctx, *args):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Address Line 1Address Line 2Address Line 3\" in result.content  # noqa: S101\n\n\ndef test_visitor_mark_custom() -> None:\n    \"\"\"Visitor replaces highlight/mark with custom template.\"\"\"\n\n    class _TestVisitor:\n        def visit_mark(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"=={text}==\"}\n\n    html = \"<p>This is a <mark>highlighted passage</mark> in the text.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"==highlighted passage==\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"This is a\" in result.content  # noqa: S101\n\n\ndef test_visitor_mark_skip() -> None:\n    \"\"\"Visitor skips mark elements entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_mark(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Key insight: <mark>always validate input</mark> for security.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is None or \"always validate input\" not in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Key insight:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"for security.\" in result.content  # noqa: S101\n\n\ndef test_visitor_preserve_html() -> None:\n    \"\"\"Visitor preserve_html action includes raw HTML in output.\"\"\"\n\n    class _TestVisitor:\n        def visit_custom_element(self, ctx, tag_name, html):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"preserve_html\"\n\n    html = \"<div><custom-tag>Custom content</custom-tag></div>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"<custom-tag>\" in result.content  # noqa: S101\n\n\ndef test_visitor_skip_all_headings() -> None:\n    \"\"\"Visitor skips all headings from document.\"\"\"\n\n    class _TestVisitor:\n        def visit_heading(self, ctx, level, text, id):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<h1>Title</h1><p>Body text remains.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is None or \"Title\" not in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Body text remains.\" in result.content  # noqa: S101\n\n\ndef test_visitor_skip_code_blocks() -> None:\n    \"\"\"Visitor skips code blocks from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_code_block(self, ctx, lang, code):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Intro text\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Outro text\" in result.content  # noqa: S101\n    assert result.content is None or \"let x = 42\" not in result.content  # noqa: S101\n\n\ndef test_visitor_skip_heading() -> None:\n    \"\"\"Visitor skip action omits all headings from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_heading(self, ctx, level, text, id):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<h1>Title</h1><p>Body text remains.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is None or \"Title\" not in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Body text remains.\" in result.content  # noqa: S101\n\n\ndef test_visitor_skip_images() -> None:\n    \"\"\"Visitor skips all images from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_image(self, ctx, src, alt, title):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Before image\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After image\" in result.content  # noqa: S101\n    assert result.content is None or \"photo.jpg\" not in result.content  # noqa: S101\n    assert result.content is None or \"A photo\" not in result.content  # noqa: S101\n\n\ndef test_visitor_skip_links() -> None:\n    \"\"\"Visitor skips all links entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_link(self, ctx, href, text, title):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<p>Before <a href=\"https://example.com\">link text</a> after</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is None or \"link text\" not in result.content  # noqa: S101\n    assert result.content is None or \"example.com\" not in result.content  # noqa: S101\n\n\ndef test_visitor_skip_strong() -> None:\n    \"\"\"Visitor skips bold/strong elements.\"\"\"\n\n    class _TestVisitor:\n        def visit_strong(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Normal <strong>bold text</strong> normal</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is None or \"bold text\" not in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Normal\" in result.content  # noqa: S101\n\n\ndef test_visitor_subscript_custom() -> None:\n    \"\"\"Visitor replaces subscript with custom template.\"\"\"\n\n    class _TestVisitor:\n        def visit_subscript(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"~{text}~\"}\n\n    html = \"<p>H<sub>2</sub>O is water.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"H~2~O\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"is water\" in result.content  # noqa: S101\n\n\ndef test_visitor_subscript_skip() -> None:\n    \"\"\"Visitor skips subscript elements entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_subscript(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"The formula CHO is sugar.\" in result.content  # noqa: S101\n\n\ndef test_visitor_superscript_custom() -> None:\n    \"\"\"Visitor replaces superscript with custom template.\"\"\"\n\n    class _TestVisitor:\n        def visit_superscript(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"^{text}^\"}\n\n    html = \"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"E=mc^2^\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"revolutionized physics\" in result.content  # noqa: S101\n\n\ndef test_visitor_superscript_skip() -> None:\n    \"\"\"Visitor skips superscript from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_superscript(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"The equation x + y = z has no solutions.\" in result.content  # noqa: S101\n\n\ndef test_visitor_underline_custom() -> None:\n    \"\"\"Visitor replaces underline with custom markup.\"\"\"\n\n    class _TestVisitor:\n        def visit_underline(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"_{text}_\"}\n\n    html = \"<p>This is <u>very important</u> text.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"_very important_\" in result.content  # noqa: S101\n    assert result.content is None or \"**\" not in result.content  # noqa: S101\n\n\ndef test_visitor_underline_skip() -> None:\n    \"\"\"Visitor skips underline elements from output.\"\"\"\n\n    class _TestVisitor:\n        def visit_underline(self, ctx, text):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = \"<p>Normal text with <u>underlined part</u> and more text.</p>\"\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Normal text with\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"and more text.\" in result.content  # noqa: S101\n    assert result.content is None or \"underlined part\" not in result.content  # noqa: S101\n\n\ndef test_visitor_video_custom() -> None:\n    \"\"\"Visitor replaces video with custom link.\"\"\"\n\n    class _TestVisitor:\n        def visit_video(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return {\"custom\": f\"[VIDEO: {src}]\"}\n\n    html = '<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"[VIDEO: tutorial.mp4]\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Watch our tutorial:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Great content!\" in result.content  # noqa: S101\n\n\ndef test_visitor_video_skip() -> None:\n    \"\"\"Visitor removes video elements entirely.\"\"\"\n\n    class _TestVisitor:\n        def visit_video(self, ctx, src):  # noqa: A002, ANN001, ANN202, ARG002\n            return \"skip\"\n\n    html = '<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>'\n    result = convert(html=html, visitor=_TestVisitor())\n    assert result.content is not None  # noqa: S101\n    assert \"Demo\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"See the demo above.\" in result.content  # noqa: S101\n    assert result.content is None or \"demo.webm\" not in result.content  # noqa: S101\n"
  },
  {
    "path": "e2e/r/DESCRIPTION",
    "content": "Package: e2e.r\nTitle: E2E Tests for htmltomarkdown\nVersion: 0.1.0\nDescription: End-to-end test suite.\nSuggests: testthat (>= 3.0.0)\nConfig/testthat/edition: 3\n"
  },
  {
    "path": "e2e/r/run_tests.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:508777dfdd00fdee2fe74caca07bddd68a7d9eebd03c9d40f88d5fc183f6644c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nlibrary(testthat)\ndevtools::load_all(\"../../packages/r\")\n\ntest_dir(\"tests\")\n"
  },
  {
    "path": "e2e/r/tests/test_conversion.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:a2858c4699aa4662f39464b1e423723ce372b1e965050be2904d0b4cfe27be07\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: conversion\n\ntest_that(\"blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed\", {\n  result <- convert(html = \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\")\n  expect_true(grepl(\"> First paragraph.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"> Second paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_nested: Nested blockquote produces double-prefixed lines\", {\n  result <- convert(html = \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Outer quote.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Inner quote.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_simple: Simple blockquote\", {\n  result <- convert(html = \"<blockquote><p>Quote text</p></blockquote>\")\n  expect_true(grepl(\"> Quote text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_with_list: Blockquote containing a list preserves list items inside quote\", {\n  result <- convert(html = \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Quote intro:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Point one\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Point two\", result$content, fixed = TRUE))\n})\n\ntest_that(\"bold_and_italic: Nested bold and italic\", {\n  result <- convert(html = \"<p><strong><em>both</em></strong></p>\")\n  expect_true(grepl(\"***both***\", result$content, fixed = TRUE))\n})\n\ntest_that(\"bold_strong: Strong tag converts to bold\", {\n  result <- convert(html = \"<p><strong>bold</strong></p>\")\n  expect_true(grepl(\"**bold**\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_block: Code block with language preserves content\", {\n  result <- convert(html = \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"print('hello')\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_block_no_language: Code block without a language class preserves content\", {\n  result <- convert(html = \"<pre><code>plain code here</code></pre>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"plain code here\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_inline_in_paragraph: Inline code element nested inside a paragraph\", {\n  result <- convert(html = \"<p>Call the <code>initialize()</code> method first.</p>\")\n  expect_true(grepl(\"`initialize()`\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_with_backticks_in_content: Inline code containing backtick characters is properly escaped\", {\n  result <- convert(html = \"<p>Use <code>`backtick` here</code> carefully.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"backtick\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_mark_highlight: mark tag produces highlighted output\", {\n  result <- convert(html = \"<p><mark>highlighted</mark></p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"highlighted\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_strikethrough_del: del tag converts to GFM strikethrough\", {\n  result <- convert(html = \"<p><del>deleted text</del></p>\")\n  expect_true(grepl(\"~~deleted text~~\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_strikethrough_s: s tag converts to GFM strikethrough\", {\n  result <- convert(html = \"<p><s>strikethrough</s></p>\")\n  expect_true(grepl(\"~~strikethrough~~\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_subscript: sub tag content is preserved\", {\n  result <- convert(html = \"<p>H<sub>2</sub>O</p>\")\n  expect_true(grepl(\"H\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"O\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_superscript: sup tag content is preserved\", {\n  result <- convert(html = \"<p>x<sup>2</sup></p>\")\n  expect_true(grepl(\"x\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_underline_u: u tag content is preserved in output\", {\n  result <- convert(html = \"<p><u>underlined</u></p>\")\n  expect_true(grepl(\"underlined\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_input_elements: Form input elements produce readable output without form mechanics\", {\n  result <- convert(html = \"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Name\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_select_options: Select element with options produces readable output\", {\n  result <- convert(html = \"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Color\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_textarea: Textarea element produces readable output\", {\n  result <- convert(html = \"<form><label>Message:</label><textarea>Default text content</textarea></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Message\", result$content, fixed = TRUE))\n})\n\ntest_that(\"heading_h1: H1 heading\", {\n  result <- convert(html = \"<h1>Heading 1</h1>\")\n  expect_equal(trimws(result$content), \"# Heading 1\")\n})\n\ntest_that(\"heading_h2: H2 heading\", {\n  result <- convert(html = \"<h2>Heading 2</h2>\")\n  expect_equal(trimws(result$content), \"## Heading 2\")\n})\n\ntest_that(\"heading_h3: H3 heading\", {\n  result <- convert(html = \"<h3>Heading 3</h3>\")\n  expect_equal(trimws(result$content), \"### Heading 3\")\n})\n\ntest_that(\"heading_h4: H4 heading\", {\n  result <- convert(html = \"<h4>Heading 4</h4>\")\n  expect_equal(trimws(result$content), \"#### Heading 4\")\n})\n\ntest_that(\"heading_h5: H5 heading\", {\n  result <- convert(html = \"<h5>Heading 5</h5>\")\n  expect_equal(trimws(result$content), \"##### Heading 5\")\n})\n\ntest_that(\"heading_h6: H6 heading\", {\n  result <- convert(html = \"<h6>Heading 6</h6>\")\n  expect_equal(trimws(result$content), \"###### Heading 6\")\n})\n\ntest_that(\"image_figure_figcaption: Figure with figcaption preserves both image and caption\", {\n  result <- convert(html = \"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\")\n  expect_true(grepl(\"![A sunset](sunset.jpg)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Beautiful sunset over the ocean\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_linked: Image inside an anchor produces a linked image\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\")\n  expect_true(grepl(\"![Icon](icon.png)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_no_alt: Image without alt text produces image markdown\", {\n  result <- convert(html = \"<img src=\\\"banner.jpg\\\">\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"banner.jpg\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_simple: Image with alt text\", {\n  result <- convert(html = \"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\")\n  expect_true(grepl(\"![A photo](photo.jpg)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_with_title: Image with title attribute includes title in output\", {\n  result <- convert(html = \"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\")\n  expect_true(grepl(\"![Sales chart](chart.png\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Q3 Sales\", result$content, fixed = TRUE))\n})\n\ntest_that(\"inline_code: Inline code\", {\n  result <- convert(html = \"<p>Use <code>console.log()</code> to debug</p>\")\n  expect_true(grepl(\"`console.log()`\", result$content, fixed = TRUE))\n})\n\ntest_that(\"italic_em: Em tag converts to italic\", {\n  result <- convert(html = \"<p><em>italic</em></p>\")\n  expect_true(grepl(\"*italic*\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_br_tag: Single br tag produces a line break in output\", {\n  result <- convert(html = \"<p>First line.<br>Second line.</p>\")\n  expect_true(grepl(\"First line.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second line.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_hr_tag: hr tag produces a horizontal separator between content\", {\n  result <- convert(html = \"<p>Before rule.</p><hr><p>After rule.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Before rule.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After rule.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_multiple_br: Multiple consecutive br tags in sequence\", {\n  result <- convert(html = \"<p>Start.<br><br>End.</p>\")\n  expect_true(grepl(\"Start.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"End.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_anchor_fragment: Fragment-only anchor link is preserved\", {\n  result <- convert(html = \"<a href=\\\"#section\\\">Jump to section</a>\")\n  expect_true(grepl(\"[Jump to section](#section)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_empty_href: Link with empty href produces output with the link text\", {\n  result <- convert(html = \"<a href=\\\"\\\">No destination</a>\")\n  expect_true(grepl(\"No destination\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_image_inside: Image inside a link produces a linked image\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\")\n  expect_true(grepl(\"![Logo](logo.png)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_mailto: Mailto link is preserved with mailto: scheme\", {\n  result <- convert(html = \"<a href=\\\"mailto:user@example.com\\\">Email us</a>\")\n  expect_true(grepl(\"mailto:user@example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_simple: Simple link\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\">Example</a>\")\n  expect_true(grepl(\"[Example](https://example.com)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_with_bold_text: Link containing bold text preserves formatting\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\")\n  expect_true(grepl(\"**Bold link**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_with_title: Link with title attribute\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\")\n  expect_true(grepl(\"[Example](https://example.com\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Example Site\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_definition_dl: Definition list with dt and dd elements\", {\n  result <- convert(html = \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\")\n  expect_true(grepl(\"Term One\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Definition of term one.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Term Two\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Definition of term two.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_item_multiple_paragraphs: List item containing multiple paragraphs\", {\n  result <- convert(html = \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\")\n  expect_true(grepl(\"First paragraph in item.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph in item.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Simple item\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_mixed_nested: Mixed list: ordered list nested inside unordered list\", {\n  result <- convert(html = \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\")\n  expect_true(grepl(\"Item A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Sub 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Sub 2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Item B\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_nested_ordered: Nested ordered list with two levels of depth\", {\n  result <- convert(html = \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\")\n  expect_true(grepl(\"Step 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 1a\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 1b\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 2\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_nested_unordered: Nested unordered list with two levels of depth\", {\n  result <- convert(html = \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\")\n  expect_true(grepl(\"Parent A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Child A1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Child A2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Parent B\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_task_checkboxes: Task list with checked and unchecked checkboxes\", {\n  result <- convert(html = \"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Done task\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Pending task\", result$content, fixed = TRUE))\n})\n\ntest_that(\"ordered_list: Ordered list\", {\n  result <- convert(html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\")\n  expect_true(grepl(\"1. First\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2. Second\", result$content, fixed = TRUE))\n  expect_true(grepl(\"3. Third\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_multiple: Multiple paragraphs are separated by a blank line\", {\n  result <- convert(html = \"<p>First paragraph.</p><p>Second paragraph.</p>\")\n  expect_true(grepl(\"First paragraph.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_nested_divs: Text nested inside divs is extracted correctly\", {\n  result <- convert(html = \"<div><div><p>Nested text</p></div></div>\")\n  expect_true(grepl(\"Nested text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_simple: Simple paragraph converts to plain text\", {\n  result <- convert(html = \"<p>Hello World</p>\")\n  expect_equal(trimws(result$content), \"Hello World\")\n})\n\ntest_that(\"paragraph_with_inline_formatting: Paragraph with bold, italic, and a link\", {\n  result <- convert(html = \"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\")\n  expect_true(grepl(\"**bold**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"*italic*\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[link](https://example.com)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output\", {\n  result <- convert(html = \"<p>Line one.<br>Line two.<br>Line three.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Line one.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line two.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line three.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_abbr: Abbreviation element text is preserved\", {\n  result <- convert(html = \"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\")\n  expect_true(grepl(\"WWW\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_article: Article element wrapping content preserves inner content\", {\n  result <- convert(html = \"<article><h2>Article Title</h2><p>Article body.</p></article>\")\n  expect_true(grepl(\"Article Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Article body.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_definition_list: Definition list with term and description\", {\n  result <- convert(html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\")\n  expect_true(grepl(\"HTML\", result$content, fixed = TRUE))\n  expect_true(grepl(\"HyperText Markup Language\", result$content, fixed = TRUE))\n  expect_true(grepl(\"CSS\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Cascading Style Sheets\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_details_summary: Details and summary elements produce readable output\", {\n  result <- convert(html = \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Click to expand\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_hr: Horizontal rule produces a separator in output\", {\n  result <- convert(html = \"<p>Above</p><hr><p>Below</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Above\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Below\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_mark_highlight: Mark tag produces highlighted output\", {\n  result <- convert(html = \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"highlighted text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_section_with_heading: Section element with heading preserves structure\", {\n  result <- convert(html = \"<section><h3>Section Heading</h3><p>Section content.</p></section>\")\n  expect_true(grepl(\"Section Heading\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Section content.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_sub_superscript: Subscript and superscript elements are preserved in output\", {\n  result <- convert(html = \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"H\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"O\", result$content, fixed = TRUE))\n  expect_true(grepl(\"E=mc\", result$content, fixed = TRUE))\n})\n\ntest_that(\"simple_table: Simple table with header\", {\n  result <- convert(html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\")\n  expect_true(grepl(\"Name\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Age\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Alice\", result$content, fixed = TRUE))\n  expect_true(grepl(\"30\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n  expect_true(grepl(\"---\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_empty: Empty table produces no output or minimal output\", {\n  result <- convert(html = \"<table></table>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"table_no_thead: Table without thead uses first row as implied header\", {\n  result <- convert(html = \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Product\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Price\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Apple\", result$content, fixed = TRUE))\n  expect_true(grepl(\"1.00\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output\", {\n  result <- convert(html = \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Expression\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Result\", result$content, fixed = TRUE))\n  expect_true(grepl(\"true\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_with_alignment: Table with column alignment attributes\", {\n  result <- convert(html = \"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Left\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Center\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Right\", result$content, fixed = TRUE))\n  expect_true(grepl(\"L\", result$content, fixed = TRUE))\n  expect_true(grepl(\"C\", result$content, fixed = TRUE))\n  expect_true(grepl(\"R\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_with_colspan: Table with colspan attribute in a header cell\", {\n  result <- convert(html = \"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Full Name\", result$content, fixed = TRUE))\n  expect_true(grepl(\"John\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Doe\", result$content, fixed = TRUE))\n})\n\ntest_that(\"unordered_list: Unordered list\", {\n  result <- convert(html = \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\")\n  expect_true(grepl(\"- Item 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"- Item 2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"- Item 3\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "e2e/r/tests/test_edge_cases.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:7dfac95f1c73c404c530137751ed9d329be35e60701448824f9c671f899cfda0\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: edge-cases\n\ntest_that(\"empty_html: Empty HTML document\", {\n  result <- convert(html = \"<html><head></head><body></body></html>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"encoding_cjk_characters: CJK (Chinese, Japanese, Korean) characters are preserved\", {\n  result <- convert(html = \"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"中文内容\", result$content, fixed = TRUE))\n  expect_true(grepl(\"日本語テキスト\", result$content, fixed = TRUE))\n  expect_true(grepl(\"한국어 텍스트\", result$content, fixed = TRUE))\n})\n\ntest_that(\"encoding_html_entities: Common HTML entities are decoded in output\", {\n  result <- convert(html = \"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"&\", result$content, fixed = TRUE))\n  expect_true(grepl(\"<\", result$content, fixed = TRUE))\n  expect_true(grepl(\">\", result$content, fixed = TRUE))\n})\n\ntest_that(\"encoding_named_entities: Named HTML entities like &mdash; and &hellip; are decoded\", {\n  result <- convert(html = \"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"—\", result$content, fixed = TRUE))\n  expect_true(grepl(\"…\", result$content, fixed = TRUE))\n})\n\ntest_that(\"encoding_numeric_entities: Numeric HTML entities (decimal and hex) are decoded\", {\n  result <- convert(html = \"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"©\", result$content, fixed = TRUE))\n  expect_true(grepl(\"®\", result$content, fixed = TRUE))\n  expect_true(grepl(\"€\", result$content, fixed = TRUE))\n})\n\ntest_that(\"encoding_unicode_emoji: Emoji and Unicode characters are preserved\", {\n  result <- convert(html = \"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"🌍\", result$content, fixed = TRUE))\n  expect_true(grepl(\"🚀\", result$content, fixed = TRUE))\n  expect_true(grepl(\"⭐\", result$content, fixed = TRUE))\n})\n\ntest_that(\"html_comments_only: Document containing only HTML comments produces empty output\", {\n  result <- convert(html = \"<!-- This is a comment --><!-- Another comment -->\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"just_whitespace_input: Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\", {\n  result <- convert(html = \"   \")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"malformed_deeply_nested_elements: Deeply nested elements (100 levels) are handled without stack overflow\", {\n  result <- convert(html = \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Deeply nested content\", result$content, fixed = TRUE))\n})\n\ntest_that(\"malformed_missing_block_closing_tags: Missing closing tags on block elements are auto-closed by parser\", {\n  result <- convert(html = \"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"First paragraph\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph\", result$content, fixed = TRUE))\n})\n\ntest_that(\"malformed_overlapping_tags: Overlapping bold/italic tags are recovered by the HTML parser without panic\", {\n  result <- convert(html = \"<p><b><i>bold and italic</b></i></p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"bold and italic\", result$content, fixed = TRUE))\n})\n\ntest_that(\"malformed_unclosed_paragraph: Unclosed <p> tag is recovered gracefully and content is preserved\", {\n  result <- convert(html = \"<p>This paragraph is never closed\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"This paragraph is never closed\", result$content, fixed = TRUE))\n})\n\ntest_that(\"script_tags_only: Document with only script tags produces empty output (scripts are stripped)\", {\n  result <- convert(html = \"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"style_tags_only: Document with only style tags produces empty output (styles are stripped)\", {\n  result <- convert(html = \"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"visitor_custom_element_with_nesting: Visitor handles custom elements with nested content\", {\n  visitor <- list(\n    visit_custom_element = function(ctx, tag_name, html) {\n      list(custom = [CUSTOM WIDGET])\n    },\n  )\n\n  result <- convert(html = \"<div><custom-widget data-value=\\\"123\\\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>\", visitor = visitor)\n  expect_true(grepl(\"[CUSTOM WIDGET]\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Widget content here\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_deeply_nested_skip: Visitor skips deeply nested elements\", {\n  visitor <- list(\n    visit_mark = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\", visitor = visitor)\n  expect_true(grepl(\"Outer\", result$content, fixed = TRUE))\n  expect_true(grepl(\"text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"highlight\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_element_end_modification: Visitor modifies element at end after children processed\", {\n  visitor <- list(\n    visit_element_end = function(ctx, output) {\n      list(custom = MODIFIED OUTPUT)\n    },\n  )\n\n  result <- convert(html = \"<blockquote><p>Original quote</p></blockquote>\", visitor = visitor)\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n})\n\ntest_that(\"visitor_element_start_skip_entire_subtree: Visitor skips at element_start level removes entire subtree\", {\n  visitor <- list(\n    visit_element_start = function(ctx) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<div><h1>Title</h1><p>Content</p></div>\", visitor = visitor)\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n})\n\ntest_that(\"visitor_unknown_tag_preservation: Visitor preserves unknown HTML tags as raw HTML\", {\n  visitor <- list(\n    visit_custom_element = function(ctx, tag_name, html) {\n      \"preserve_html\"\n    },\n  )\n\n  result <- convert(html = \"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\", visitor = visitor)\n  expect_true(grepl(\"Article text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"More article text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"<x-custom>\", result$content, fixed = TRUE))\n})\n\ntest_that(\"whitespace_only: Whitespace-only content\", {\n  result <- convert(html = \"<p>   </p>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"xss_onclick_handler_removed: onclick and other on* event handlers are removed from elements\", {\n  result <- convert(html = \"<p><a href=\\\"https://example.com\\\" onclick=\\\"alert('xss')\\\">Click me</a></p><button onmouseover=\\\"steal_data()\\\">Hover me</button>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Click me\", result$content, fixed = TRUE))\n})\n\ntest_that(\"xss_script_tag_stripped: Script tag content is stripped and does not appear in output\", {\n  result <- convert(html = \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Safe content\", result$content, fixed = TRUE))\n  expect_true(grepl(\"More safe content\", result$content, fixed = TRUE))\n})\n\ntest_that(\"xss_svg_nested_script_stripped: Script tags nested inside SVG are stripped\", {\n  result <- convert(html = \"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Before SVG\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After SVG\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "e2e/r/tests/test_metadata.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:78d28a709b10b2e3ae84efc329b879f57bf8f930427a9405f6027d4f4d8f6557\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: metadata\n\ntest_that(\"metadata_author_meta: Extract author from <meta name='author'> tag\", {\n  result <- convert(html = \"<html><head><title>Page</title><meta name=\\\"author\\\" content=\\\"Jane Doe\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$author), \"Jane Doe\")\n})\n\ntest_that(\"metadata_canonical_url: Extract canonical URL from <link rel='canonical'> tag\", {\n  result <- convert(html = \"<html><head><title>Page</title><link rel=\\\"canonical\\\" href=\\\"https://example.com/canonical-page\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$canonical_url), \"https://example.com/canonical-page\")\n})\n\ntest_that(\"metadata_description_meta: Extract description from <meta name='description'> tag\", {\n  result <- convert(html = \"<html><head><title>Page</title><meta name=\\\"description\\\" content=\\\"This is the page description.\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$description), \"This is the page description.\")\n})\n\ntest_that(\"metadata_dublin_core: Extract Dublin Core metadata tags\", {\n  result <- convert(html = \"<html><head><title>Scholarly Work</title><meta name=\\\"DC.title\\\" content=\\\"Principles of Knowledge Management\\\"><meta name=\\\"DC.creator\\\" content=\\\"Dr. Alice Johnson\\\"><meta name=\\\"DC.date\\\" content=\\\"2023-06-15\\\"><meta name=\\\"DC.subject\\\" content=\\\"Knowledge Management\\\"><meta name=\\\"DC.publisher\\\" content=\\\"Academic Press\\\"></head><body><p>This is a scholarly article.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"scholarly article\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_extract_all_images: Extract all images from a document into metadata\", {\n  result <- convert(html = \"<html><head><title>Gallery</title></head><body><img src=\\\"https://example.com/photo1.jpg\\\" alt=\\\"Photo 1\\\"><img src=\\\"https://example.com/photo2.png\\\" alt=\\\"Photo 2\\\"><img src=\\\"/local/image.webp\\\" alt=\\\"Local image\\\"></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(length(result$metadata$images) >= 2)\n})\n\ntest_that(\"metadata_extract_all_links: Extract all links from a document into metadata\", {\n  result <- convert(html = \"<html><head><title>Links Page</title></head><body><p>Visit <a href=\\\"https://example.com\\\">Example</a> or <a href=\\\"https://docs.example.com\\\">Docs</a>.</p><p>Also see <a href=\\\"/relative/path\\\">relative link</a> and <a href=\\\"mailto:hello@example.com\\\">email us</a>.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(length(result$metadata$links) >= 2)\n})\n\ntest_that(\"metadata_headers_hierarchy: Extract heading hierarchy from document into metadata\", {\n  result <- convert(html = \"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(length(result$metadata$headers) >= 5)\n})\n\ntest_that(\"metadata_keywords_meta: Extract keywords from <meta name='keywords'> tag\", {\n  result <- convert(html = \"<html><head><title>Page</title><meta name=\\\"keywords\\\" content=\\\"rust, markdown, html, converter\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(length(result$metadata$document$keywords) >= 1)\n})\n\ntest_that(\"metadata_lang_attribute: Extract language from html lang attribute\", {\n  result <- convert(html = \"<html lang=\\\"es\\\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Hola Mundo\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_microdata_schema_article: Extract schema.org microdata for Article\", {\n  result <- convert(html = \"<html><head><title>Article</title></head><body><article itemscope itemtype=\\\"https://schema.org/Article\\\"><h1 itemprop=\\\"headline\\\">Breaking News Today</h1><span itemprop=\\\"author\\\">Jane Reporter</span><span itemprop=\\\"datePublished\\\">2024-04-22</span><div itemprop=\\\"articleBody\\\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Breaking News Today\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Jane Reporter\", result$content, fixed = TRUE))\n  expect_true(grepl(\"important information\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_microdata_schema_breadcrumb: Extract schema.org breadcrumb navigation microdata\", {\n  result <- convert(html = \"<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\\\"https://schema.org/BreadcrumbList\\\"><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com\\\"><span itemprop=\\\"name\\\">Home</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com/products\\\"><span itemprop=\\\"name\\\">Products</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><span itemprop=\\\"name\\\">Current Page</span></span></nav></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Home\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Products\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Current Page\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_microdata_schema_organization: Extract schema.org microdata for Organization\", {\n  result <- convert(html = \"<html><head><title>Company</title></head><body><div itemscope itemtype=\\\"https://schema.org/Organization\\\"><span itemprop=\\\"name\\\">Acme Corp</span><span itemprop=\\\"foundingDate\\\">2020</span><span itemprop=\\\"url\\\">https://acmecorp.example.com</span><span itemprop=\\\"logo\\\">https://acmecorp.example.com/logo.png</span></div></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Acme Corp\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2020\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_microdata_schema_person: Extract schema.org microdata for Person\", {\n  result <- convert(html = \"<html><head><title>Contact</title></head><body><div itemscope itemtype=\\\"https://schema.org/Person\\\"><span itemprop=\\\"name\\\">John Smith</span><span itemprop=\\\"email\\\">john@example.com</span><span itemprop=\\\"telephone\\\">+1-555-0100</span></div></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"John Smith\", result$content, fixed = TRUE))\n  expect_true(grepl(\"john@example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_microdata_schema_product: Extract schema.org microdata for Product\", {\n  result <- convert(html = \"<html><head><title>Product</title></head><body><div itemscope itemtype=\\\"https://schema.org/Product\\\"><h1 itemprop=\\\"name\\\">Awesome Widget</h1><span itemprop=\\\"description\\\">The best widget on the market</span><span itemprop=\\\"price\\\">29.99</span><span itemprop=\\\"priceCurrency\\\">USD</span><img itemprop=\\\"image\\\" src=\\\"widget.jpg\\\" alt=\\\"Widget\\\"><span itemprop=\\\"ratingValue\\\">4.5</span></div></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Awesome Widget\", result$content, fixed = TRUE))\n  expect_true(grepl(\"best widget\", result$content, fixed = TRUE))\n  expect_true(grepl(\"29.99\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_text_direction_ltr: Extract text direction from lang attribute on html element\", {\n  result <- convert(html = \"<html lang=\\\"en\\\" dir=\\\"ltr\\\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"left-to-right text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_text_direction_rtl: Extract right-to-left text direction\", {\n  result <- convert(html = \"<html lang=\\\"ar\\\" dir=\\\"rtl\\\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"right-to-left text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"metadata_title_tag: Extract title from <title> tag\", {\n  result <- convert(html = \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$title), \"My Page\")\n})\n\ntest_that(\"og_basic_tags: Extract og:title, og:description, and og:image from Open Graph meta tags\", {\n  result <- convert(html = \"<html><head><title>Fallback Title</title><meta property=\\\"og:title\\\" content=\\\"OG Title\\\"><meta property=\\\"og:description\\\" content=\\\"OG description text.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/image.jpg\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$open_graph[[\"title\"]]), \"OG Title\")\n  expect_equal(trimws(result$metadata$document$open_graph[[\"description\"]]), \"OG description text.\")\n  expect_equal(trimws(result$metadata$document$open_graph[[\"image\"]]), \"https://example.com/image.jpg\")\n})\n\ntest_that(\"og_multiple_tags: Extract multiple Open Graph tags including type, url, and site_name\", {\n  result <- convert(html = \"<html><head><meta property=\\\"og:title\\\" content=\\\"Article Title\\\"><meta property=\\\"og:type\\\" content=\\\"article\\\"><meta property=\\\"og:url\\\" content=\\\"https://example.com/article\\\"><meta property=\\\"og:site_name\\\" content=\\\"Example Site\\\"><meta property=\\\"og:description\\\" content=\\\"An interesting article.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/article.jpg\\\"></head><body><article><p>Article content here.</p></article></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$open_graph[[\"title\"]]), \"Article Title\")\n  expect_equal(trimws(result$metadata$document$open_graph[[\"type\"]]), \"article\")\n  expect_equal(trimws(result$metadata$document$open_graph[[\"url\"]]), \"https://example.com/article\")\n  expect_equal(trimws(result$metadata$document$open_graph[[\"site_name\"]]), \"Example Site\")\n})\n\ntest_that(\"structured_data_json_ld: JSON-LD script tag is stripped from output (security) but metadata may be extracted\", {\n  result <- convert(html = \"<html><head><title>Article</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Article\\\",\\\"headline\\\":\\\"My Article\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Jane Doe\\\"},\\\"datePublished\\\":\\\"2024-01-15\\\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"My Article\", result$content, fixed = TRUE))\n})\n\ntest_that(\"structured_data_multiple_json_ld: Multiple JSON-LD blocks are all stripped from output\", {\n  result <- convert(html = \"<html><head><title>Shop Page</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Product\\\",\\\"name\\\":\\\"Widget\\\",\\\"price\\\":\\\"9.99\\\"}</script><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"BreadcrumbList\\\",\\\"itemListElement\\\":[{\\\"@type\\\":\\\"ListItem\\\",\\\"position\\\":1,\\\"name\\\":\\\"Home\\\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Widget\", result$content, fixed = TRUE))\n})\n\ntest_that(\"twitter_card_tags: Extract Twitter card meta tags\", {\n  result <- convert(html = \"<html><head><meta name=\\\"twitter:card\\\" content=\\\"summary_large_image\\\"><meta name=\\\"twitter:site\\\" content=\\\"@examplesite\\\"><meta name=\\\"twitter:title\\\" content=\\\"Twitter Card Title\\\"><meta name=\\\"twitter:description\\\" content=\\\"Twitter card description.\\\"><meta name=\\\"twitter:image\\\" content=\\\"https://example.com/twitter-image.jpg\\\"></head><body><p>Content</p></body></html>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$twitter_card[[\"card\"]]), \"summary_large_image\")\n  expect_equal(trimws(result$metadata$document$twitter_card[[\"title\"]]), \"Twitter Card Title\")\n  expect_equal(trimws(result$metadata$document$twitter_card[[\"description\"]]), \"Twitter card description.\")\n})\n"
  },
  {
    "path": "e2e/r/tests/test_options.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:edb92452edc2352b6b3e34013e83a8acc3856a1617d9a6610b0132c580a8875e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: options\n\ntest_that(\"options_autolinks_false: Bare URL links rendered as regular markdown links when autolinks disabled\", {\n  result <- convert(html = \"<p><a href='https://example.com'>https://example.com</a></p>\", options = list(\"autolinks\" = FALSE))\n  expect_true(grepl(\"example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_br_in_tables_false: BR elements in table cells are stripped when disabled\", {\n  result <- convert(html = \"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", options = list(\"brInTables\" = FALSE))\n  expect_true(grepl(\"Col\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_br_in_tables_true: BR elements in table cells render as line breaks\", {\n  result <- convert(html = \"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\", options = list(\"brInTables\" = TRUE))\n  expect_true(grepl(\"Header\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line 2\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_code_block_backticks: Backticks code block style uses triple backtick fences\", {\n  result <- convert(html = \"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", options = list(\"codeBlockStyle\" = \"backticks\"))\n  expect_true(grepl(\"```\", result$content, fixed = TRUE))\n  expect_true(grepl(\"console.log('hi');\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_code_block_indented: Code blocks use 4-space indentation\", {\n  result <- convert(html = \"<pre><code>print('hello')</code></pre>\", options = list(\"codeBlockStyle\" = \"indented\"))\n  expect_true(grepl(\"print('hello')\", result$content, fixed = TRUE))\n  expect_false(grepl(\"```\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_code_block_tildes: Code blocks use tilde fences\", {\n  result <- convert(html = \"<pre><code>let x = 1;</code></pre>\", options = list(\"codeBlockStyle\" = \"tildes\"))\n  expect_true(grepl(\"~~~\", result$content, fixed = TRUE))\n  expect_true(grepl(\"let x = 1;\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_code_block_tildes_style: Tildes code block style uses triple tilde fences\", {\n  result <- convert(html = \"<pre><code>some code</code></pre>\", options = list(\"codeBlockStyle\" = \"tildes\"))\n  expect_true(grepl(\"~~~\", result$content, fixed = TRUE))\n  expect_true(grepl(\"some code\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_code_language_python: Default code language annotation on blocks without lang attribute\", {\n  result <- convert(html = \"<pre><code>def hello(): pass</code></pre>\", options = list(\"codeLanguage\" = \"python\"))\n  expect_true(grepl(\"```python\", result$content, fixed = TRUE))\n  expect_true(grepl(\"def hello\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_convert_as_inline: Block elements treated as inline\", {\n  result <- convert(html = \"<p>One</p><p>Two</p>\", options = list(\"convertAsInline\" = TRUE))\n  expect_true(grepl(\"One\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Two\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_debug_true: Debug mode enabled does not crash and produces output\", {\n  result <- convert(html = \"<p>Debug test</p>\", options = list(\"debug\" = TRUE))\n  expect_true(grepl(\"Debug test\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_default_title_true: Links without title get empty title attribute when defaultTitle is true\", {\n  result <- convert(html = \"<p><a href='https://example.com'>Link</a></p>\", options = list(\"defaultTitle\" = TRUE))\n  expect_true(grepl(\"Link\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_encoding_utf8: UTF-8 encoding hint for special characters\", {\n  result <- convert(html = \"<p>Café naïve résumé</p>\", options = list(\"encoding\" = \"utf-8\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n})\n\ntest_that(\"options_escape_ascii_enabled: ASCII Markdown characters are escaped when escapeAscii is true\", {\n  result <- convert(html = \"<p>Text with # hash and [brackets] and * star</p>\", options = list(\"escapeAscii\" = TRUE))\n  expect_true(grepl(\"Text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"hash\", result$content, fixed = TRUE))\n  expect_true(grepl(\"brackets\", result$content, fixed = TRUE))\n  expect_true(grepl(\"star\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_escape_asterisks: escape_asterisks option escapes asterisks in plain text\", {\n  result <- convert(html = \"<p>Use 2*3 = 6 in math.</p>\", options = list(\"escapeAsterisks\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"3\", result$content, fixed = TRUE))\n  expect_true(grepl(\"6\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_escape_misc: escape_misc option escapes miscellaneous markdown characters\", {\n  result <- convert(html = \"<p>Use # and | and ~ in text.</p>\", options = list(\"escapeMisc\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Use\", result$content, fixed = TRUE))\n  expect_true(grepl(\"and\", result$content, fixed = TRUE))\n  expect_true(grepl(\"in text.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_escape_underscores: escape_underscores option escapes underscores in plain text\", {\n  result <- convert(html = \"<p>The variable_name is defined.</p>\", options = list(\"escapeUnderscores\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"variable\", result$content, fixed = TRUE))\n  expect_true(grepl(\"name\", result$content, fixed = TRUE))\n  expect_true(grepl(\"defined.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_attribute: Elements matching CSS attribute selector are excluded entirely\", {\n  result <- convert(html = \"<body><div role=\\\"complementary\\\">Sidebar</div><p>Primary text</p></body>\", options = list(\"excludeSelectors\" = c(\"[role='complementary']\")))\n  expect_true(grepl(\"Primary text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Sidebar\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_class: Elements matching CSS class selector are excluded entirely\", {\n  result <- convert(html = \"<body><div class=\\\"cookie-banner\\\">Accept cookies</div><p>Main content</p></body>\", options = list(\"excludeSelectors\" = c(\".cookie-banner\")))\n  expect_true(grepl(\"Main content\", result$content, fixed = TRUE))\n  expect_false(grepl(\"cookies\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_empty_noop: Empty exclude_selectors list does not affect output\", {\n  result <- convert(html = \"<p>Hello world</p>\", options = list(\"excludeSelectors\" = c()))\n  expect_true(grepl(\"Hello world\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_id: Elements matching CSS id selector are excluded entirely\", {\n  result <- convert(html = \"<body><div id=\\\"ad-container\\\">Buy stuff</div><p>Article text</p></body>\", options = list(\"excludeSelectors\" = c(\"#ad-container\")))\n  expect_true(grepl(\"Article text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Buy stuff\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_multiple: Multiple CSS selectors each exclude their matched elements\", {\n  result <- convert(html = \"<body><nav class=\\\"nav\\\">Menu</nav><p>Content</p><footer>Footer</footer></body>\", options = list(\"excludeSelectors\" = c(\".nav\", \"footer\")))\n  expect_true(grepl(\"Content\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Menu\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Footer\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_nested_content_dropped: All descendants of excluded elements are dropped\", {\n  result <- convert(html = \"<body><aside class=\\\"sidebar\\\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>\", options = list(\"excludeSelectors\" = c(\".sidebar\")))\n  expect_true(grepl(\"Main text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Related\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Sidebar text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_plain_text_mode: Exclude selectors work in plain text output mode\", {\n  result <- convert(html = \"<body><div class=\\\"nav\\\">Navigation</div><p>Article body</p></body>\", options = list(\"excludeSelectors\" = c(\".nav\"), \"outputFormat\" = \"plain\"))\n  expect_true(grepl(\"Article body\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Navigation\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_exclude_selectors_vs_strip_tags: exclude_selectors drops entire subtree unlike strip_tags which keeps children\", {\n  result <- convert(html = \"<body><div class=\\\"wrapper\\\"><p>Inner paragraph</p></div><p>Outer text</p></body>\", options = list(\"excludeSelectors\" = c(\".wrapper\")))\n  expect_true(grepl(\"Outer text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Inner paragraph\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_extract_metadata_true: Extract metadata returns document metadata when enabled\", {\n  result <- convert(html = \"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\", options = list(\"extractMetadata\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(trimws(result$metadata$document$title), \"Test Page\")\n  expect_equal(trimws(result$metadata$document$description), \"A test page\")\n})\n\ntest_that(\"options_heading_style_atx: ATX heading style produces hash-prefixed headings\", {\n  result <- convert(html = \"<h1>Title</h1><h2>Subtitle</h2>\", options = list(\"headingStyle\" = \"atx\"))\n  expect_true(grepl(\"# Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## Subtitle\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_heading_style_atx_closed: ATX closed heading style adds closing hashes\", {\n  result <- convert(html = \"<h1>Closed Heading</h1>\", options = list(\"headingStyle\" = \"atxclosed\"))\n  expect_true(grepl(\"# Closed Heading #\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_heading_style_underlined: Underlined heading style produces setext-style headings for h1 and h2\", {\n  result <- convert(html = \"<h1>Main Title</h1>\", options = list(\"headingStyle\" = \"underlined\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Main Title\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_highlight_bold: Mark tag rendered as bold\", {\n  result <- convert(html = \"<p>Text with <mark>highlighted</mark> text.</p>\", options = list(\"highlightStyle\" = \"bold\"))\n  expect_true(grepl(\"**highlighted**\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_highlight_double_equal: Mark tag with double equal highlight style\", {\n  result <- convert(html = \"<p>Text with <mark>highlighted</mark> here.</p>\", options = list(\"highlightStyle\" = \"doubleequal\"))\n  expect_true(grepl(\"==highlighted==\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_highlight_none: Mark tag with no highlight style strips the mark\", {\n  result <- convert(html = \"<p>Text with <mark>plain</mark> content.</p>\", options = list(\"highlightStyle\" = \"none\"))\n  expect_true(grepl(\"plain\", result$content, fixed = TRUE))\n  expect_false(grepl(\"==\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_keep_inline_images_in_paragraph: Images inside specified tags stay inline\", {\n  result <- convert(html = \"<p>Text <img src='icon.png' alt='icon'> more text</p>\", options = list(\"keepInlineImagesIn\" = c(\"p\")))\n  expect_true(grepl(\"Text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"more text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_link_style_reference: Links use reference-style formatting\", {\n  result <- convert(html = \"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\", options = list(\"linkStyle\" = \"reference\"))\n  expect_true(grepl(\"Example\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Other\", result$content, fixed = TRUE))\n  expect_true(grepl(\"example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_list_custom_bullets: Custom bullet character for unordered lists\", {\n  result <- convert(html = \"<ul><li>Item A</li><li>Item B</li></ul>\", options = list(\"bullets\" = \"*\"))\n  expect_true(grepl(\"* Item A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"* Item B\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_list_indent_tabs: Tab indentation type for nested list items\", {\n  result <- convert(html = \"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", options = list(\"listIndentType\" = \"tabs\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Parent\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Child\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_list_indent_width_four: Nested lists indented with 4 spaces per level\", {\n  result <- convert(html = \"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", options = list(\"listIndentWidth\" = 4))\n  expect_true(grepl(\"Outer\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Inner\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_max_depth_default_unlimited: Default max_depth (null) converts deeply nested content fully\", {\n  result <- convert(html = \"<div><div><div><div><p>Deep content</p></div></div></div></div>\")\n  expect_true(grepl(\"Deep content\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_max_depth_truncates: max_depth truncates content beyond the specified depth\", {\n  result <- convert(html = \"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\", options = list(\"maxDepth\" = 3))\n  expect_true(grepl(\"Shallow\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Too deep\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_max_depth_zero_empty: max_depth of 0 produces empty output\", {\n  result <- convert(html = \"<p>Hello</p>\", options = list(\"maxDepth\" = 0))\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"options_newline_backslash: Hard line breaks rendered with backslash\", {\n  result <- convert(html = \"<p>Line one<br>Line two</p>\", options = list(\"newlineStyle\" = \"backslash\"))\n  expect_true(grepl(\"Line one\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line two\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_newline_spaces: Hard line breaks rendered with trailing spaces\", {\n  result <- convert(html = \"<p>First<br>Second</p>\", options = list(\"newlineStyle\" = \"spaces\"))\n  expect_true(grepl(\"First\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_output_format_djot: Djot output format produces djot-compatible markup\", {\n  result <- convert(html = \"<p>Simple paragraph.</p>\", options = list(\"outputFormat\" = \"djot\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Simple paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_output_format_markdown: Default markdown output format produces standard markdown\", {\n  result <- convert(html = \"<h1>Title</h1><p>Some text.</p>\", options = list(\"headingStyle\" = \"atx\", \"outputFormat\" = \"markdown\"))\n  expect_true(grepl(\"# Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Some text.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_output_format_plain: Plain text output format strips markdown syntax\", {\n  result <- convert(html = \"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", options = list(\"outputFormat\" = \"plain\"))\n  expect_true(grepl(\"Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"bold\", result$content, fixed = TRUE))\n  expect_true(grepl(\"text.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_preprocessing_aggressive: Aggressive preset removes nav, footer, aside unconditionally\", {\n  result <- convert(html = \"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\", options = list(\"preprocessing\" = list(\"preset\" = \"aggressive\")))\n  expect_true(grepl(\"Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Content\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Menu\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_preprocessing_minimal: Minimal preset preserves nav, footer, aside\", {\n  result <- convert(html = \"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", options = list(\"preprocessing\" = list(\"preset\" = \"minimal\")))\n  expect_true(grepl(\"Navigation\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Content\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Footer\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_preprocessing_remove_forms: Forms are removed when remove_forms is true\", {\n  result <- convert(html = \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\", options = list(\"preprocessing\" = list(\"removeForms\" = TRUE)))\n  expect_true(grepl(\"Before\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Submit\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_preserve_tags_iframe: Iframe tags preserved as raw HTML in output\", {\n  result <- convert(html = \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\", options = list(\"preserveTags\" = c(\"iframe\")))\n  expect_true(grepl(\"Before\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After\", result$content, fixed = TRUE))\n  expect_true(grepl(\"<iframe\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_skip_images_true: Images are omitted from output when skipImages is true\", {\n  result <- convert(html = \"<p>Before <img src='test.jpg' alt='photo'> After</p>\", options = list(\"skipImages\" = TRUE))\n  expect_true(grepl(\"Before\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After\", result$content, fixed = TRUE))\n  expect_false(grepl(\"photo\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_strip_newlines: Strip newlines produces single-line paragraphs\", {\n  result <- convert(html = \"<p>First paragraph.</p><p>Second paragraph.</p>\", options = list(\"stripNewlines\" = TRUE))\n  expect_true(grepl(\"First paragraph.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_strip_tags_div_span: Div and span tags stripped but content preserved\", {\n  result <- convert(html = \"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\", options = list(\"stripTags\" = c(\"div\", \"span\")))\n  expect_true(grepl(\"Inside div\", result$content, fixed = TRUE))\n  expect_true(grepl(\"span text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_strong_em_underscore: Strong and em tags use underscore symbol instead of asterisk\", {\n  result <- convert(html = \"<p><strong>bold</strong> and <em>italic</em></p>\", options = list(\"strongEmSymbol\" = \"_\"))\n  expect_true(grepl(\"__bold__\", result$content, fixed = TRUE))\n  expect_true(grepl(\"_italic_\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_sub_symbol_tilde: Subscript rendered with tilde symbol\", {\n  result <- convert(html = \"<p>H<sub>2</sub>O</p>\", options = list(\"subSymbol\" = \"~\"))\n  expect_true(grepl(\"~2~\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_sup_symbol_caret: Superscript rendered with caret symbol\", {\n  result <- convert(html = \"<p>x<sup>2</sup></p>\", options = list(\"supSymbol\" = \"^\"))\n  expect_true(grepl(\"^2^\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_whitespace_normalized: Normalized whitespace mode collapses multiple spaces\", {\n  result <- convert(html = \"<p>Text   with    extra   spaces.</p>\", options = list(\"whitespaceMode\" = \"normalized\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"with\", result$content, fixed = TRUE))\n  expect_true(grepl(\"extra\", result$content, fixed = TRUE))\n  expect_true(grepl(\"spaces.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_whitespace_strict: Strict whitespace mode preserves whitespace as-is\", {\n  result <- convert(html = \"<p>Preserved   spacing.</p>\", options = list(\"whitespaceMode\" = \"strict\"))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Preserved\", result$content, fixed = TRUE))\n  expect_true(grepl(\"spacing.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_wrap_disabled: Wrap option disabled preserves long lines without breaking\", {\n  result <- convert(html = \"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\", options = list(\"wrap\" = FALSE))\n  expect_true(grepl(\"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"options_wrap_enabled: Wrap option enabled with custom width wraps long lines\", {\n  result <- convert(html = \"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\", options = list(\"wrap\" = TRUE, \"wrapWidth\" = 40))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"This is a long paragraph\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "e2e/r/tests/test_real_world.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f24247070241b14dd6ed198de4ea97a9867412bad6c14272e85de4e0d7d224cc\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: real-world\n\ntest_that(\"real_world_blog_post: Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\", {\n  result <- convert(html = \"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"# Getting Started with Rust\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## Installation\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## Hello World\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## Key Concepts\", result$content, fixed = TRUE))\n  expect_true(grepl(\"cargo run\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[Mozilla](https://www.mozilla.org)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"- Ownership and borrowing\", result$content, fixed = TRUE))\n})\n\ntest_that(\"real_world_documentation_page: Documentation page with nested lists, code examples, and blockquotes converts correctly\", {\n  result <- convert(html = \"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"# Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration values.</p></blockquote></div>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"# API Reference\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## convert_html\", result$content, fixed = TRUE))\n  expect_true(grepl(\"### Parameters\", result$content, fixed = TRUE))\n  expect_true(grepl(\"### Returns\", result$content, fixed = TRUE))\n  expect_true(grepl(\"### Example\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## ConversionOptions\", result$content, fixed = TRUE))\n  expect_true(grepl(\"> \", result$content, fixed = TRUE))\n  expect_true(grepl(\"thread-safe\", result$content, fixed = TRUE))\n  expect_true(grepl(\"convert_html\", result$content, fixed = TRUE))\n  expect_true(grepl(\"ConversionOptions\", result$content, fixed = TRUE))\n})\n\ntest_that(\"real_world_product_page: Product page with table, images, and lists converts correctly\", {\n  result <- convert(html = \"<div class=\\\"product\\\"><h1>Wireless Keyboard Pro</h1><img src=\\\"https://example.com/keyboard.jpg\\\" alt=\\\"Wireless Keyboard Pro\\\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"# Wireless Keyboard Pro\", result$content, fixed = TRUE))\n  expect_true(grepl(\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## Specifications\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Battery Life\", result$content, fixed = TRUE))\n  expect_true(grepl(\"12 months\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Bluetooth 5.0\", result$content, fixed = TRUE))\n  expect_true(grepl(\"## What's in the Box\", result$content, fixed = TRUE))\n  expect_true(grepl(\"USB-C charging cable\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n  expect_true(grepl(\"---\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "e2e/r/tests/test_result.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:2d6dc4d6949cf841cdcc8794a0b6a50a473b93ece5317b9fa20926c6e6b9ecd4\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: result\n\ntest_that(\"result_tables_empty_when_no_tables: Result tables array is empty when input has no tables\", {\n  result <- convert(html = \"<p>No tables here</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(length(result$tables), 0)\n})\n\ntest_that(\"result_tables_multiple: Multiple tables each appear in the tables array\", {\n  result <- convert(html = \"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(length(result$tables) >= 2)\n})\n\ntest_that(\"result_tables_simple: Simple table populates the tables array in result\", {\n  result <- convert(html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(length(result$tables) >= 1)\n})\n\ntest_that(\"result_tables_without_structure_flag: Tables array is empty when includeDocumentStructure is false\", {\n  result <- convert(html = \"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(length(result$tables), 0)\n})\n\ntest_that(\"result_warnings_empty_for_clean_input: Warnings array is empty for well-formed HTML without problematic content\", {\n  result <- convert(html = \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(length(result$warnings), 0)\n})\n\ntest_that(\"result_warnings_empty_for_complex_input: Warnings array is empty for complex but valid HTML\", {\n  result <- convert(html = \"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(length(result$warnings), 0)\n})\n\ntest_that(\"result_warnings_empty_for_malformed_html: Warnings array is empty even for malformed HTML (parser is lenient)\", {\n  result <- convert(html = \"<p>Unclosed paragraph<div>Mixed nesting</p></div>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_equal(length(result$warnings), 0)\n})\n"
  },
  {
    "path": "e2e/r/tests/test_smoke.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f342ac3f5cc14830465a13591970369b02a7d49fe7be60803f3089a92cd95fc0\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: smoke\n\ntest_that(\"smoke_empty_string: Empty string produces empty output\", {\n  result <- convert(html = \"\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"smoke_simple_heading: H1 heading converts to ATX markdown\", {\n  result <- convert(html = \"<h1>Title</h1>\")\n  expect_true(grepl(\"# Title\", result$content, fixed = TRUE))\n})\n\ntest_that(\"smoke_simple_paragraph: Simple paragraph converts correctly\", {\n  result <- convert(html = \"<p>Hello World</p>\")\n  expect_equal(trimws(result$content), \"Hello World\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n})\n"
  },
  {
    "path": "e2e/r/tests/test_structure.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e1a736e2515f2c9fc18d139a301adeb90307ba18c00b3b55b2cf3d16fd2d17b7\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: structure\n\ntest_that(\"structure_code_block: Fenced code block produces Code node\", {\n  result <- convert(html = \"<p>Example code:</p><pre><code class=\\\"language-rust\\\">fn main() { println!(\\\"Hello\\\"); }</code></pre>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 2)\n})\n\ntest_that(\"structure_deep_nesting_h1_h2_h3: H1 > H2 > H3 creates three levels of heading nesting\", {\n  result <- convert(html = \"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 5)\n})\n\ntest_that(\"structure_h1_h2_nested_group: H1 followed by H2 creates a nested group under the H1\", {\n  result <- convert(html = \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 3)\n})\n\ntest_that(\"structure_heading_paragraph: Simple heading followed by paragraph produces Heading and Paragraph nodes\", {\n  result <- convert(html = \"<h1>Title</h1><p>A paragraph of text.</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 2)\n})\n\ntest_that(\"structure_list: Unordered list produces List and ListItem nodes\", {\n  result <- convert(html = \"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 2)\n})\n\ntest_that(\"structure_multiple_headings: Multiple headings create multiple Heading nodes with correct levels\", {\n  result <- convert(html = \"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 4)\n})\n\ntest_that(\"structure_sibling_h1_groups: H1, H2, then another H1 creates two sibling top-level groups\", {\n  result <- convert(html = \"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\", options = list(\"includeDocumentStructure\" = TRUE))\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(if (is.character(result$document$nodes)) nchar(result$document$nodes) > 0 else length(result$document$nodes) > 0)\n  expect_true(length(result$document$nodes) >= 4)\n})\n"
  },
  {
    "path": "e2e/r/tests/test_visitor.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:09ce3de60ddd4cdf5cefd5b4eb3be53f5596555774b37ddb3e929cc51d6a985c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: visitor\n\ntest_that(\"visitor_audio_custom: Visitor replaces audio element with custom output\", {\n  visitor <- list(\n    visit_audio = function(ctx, src) {\n      list(custom = [AUDIO: podcast.mp3])\n    },\n  )\n\n  result <- convert(html = \"<p>Listen to this: <audio src=\\\"podcast.mp3\\\" controls></audio></p>\", visitor = visitor)\n  expect_true(grepl(\"[AUDIO: podcast.mp3]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Listen to this:\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_audio_skip: Visitor removes audio elements from output\", {\n  visitor <- list(\n    visit_audio = function(ctx, src) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Background music:</p><audio src=\\\"music.ogg\\\" autoplay></audio><p>Enjoy!</p>\", visitor = visitor)\n  expect_true(grepl(\"Background music:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Enjoy!\", result$content, fixed = TRUE))\n  expect_false(grepl(\"music.ogg\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_button_custom: Visitor replaces button with bracketed text\", {\n  visitor <- list(\n    visit_button = function(ctx, text) {\n      list(custom = [BTN:{text}])\n    },\n  )\n\n  result <- convert(html = \"<p>Confirm action: <button type=\\\"submit\\\">Click me</button> or <button type=\\\"reset\\\">Cancel</button></p>\", visitor = visitor)\n  expect_true(grepl(\"[BTN:Click me]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[BTN:Cancel]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Confirm action:\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_button_skip: Visitor removes all buttons from output\", {\n  visitor <- list(\n    visit_button = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\", visitor = visitor)\n  expect_true(grepl(\"Actions available:\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Save\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Delete\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Export\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_continue_default: Visitor continue action preserves default conversion\", {\n  visitor <- list(\n    visit_strong = function(ctx, text) {\n      \"continue\"\n    },\n  )\n\n  result <- convert(html = \"<p>Hello <strong>World</strong></p>\", visitor = visitor)\n  expect_true(grepl(\"**World**\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_blockquote: Visitor replaces blockquote with custom format\", {\n  visitor <- list(\n    visit_blockquote = function(ctx, content, depth) {\n      list(custom = QUOTE: \\\"{content}\\\")\n    },\n  )\n\n  result <- convert(html = \"<blockquote><p>A wise quote.</p></blockquote>\", visitor = visitor)\n  expect_true(grepl(\"QUOTE:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"A wise quote.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_emphasis: Visitor replaces emphasis with custom output\", {\n  visitor <- list(\n    visit_emphasis = function(ctx, text) {\n      list(custom = >>>{text}<<<)\n    },\n  )\n\n  result <- convert(html = \"<p>This is <em>important</em> text.</p>\", visitor = visitor)\n  expect_true(grepl(\">>>important<<<\", result$content, fixed = TRUE))\n  expect_false(grepl(\"*important*\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_heading: Visitor replaces heading with custom format\", {\n  visitor <- list(\n    visit_heading = function(ctx, level, text, id) {\n      list(custom = --- {text} ---)\n    },\n  )\n\n  result <- convert(html = \"<h2>Section Title</h2><p>Content below heading.</p>\", visitor = visitor)\n  expect_true(grepl(\"--- Section Title ---\", result$content, fixed = TRUE))\n  expect_false(grepl(\"## Section Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Content below heading.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_image: Visitor replaces image with custom output using template\", {\n  visitor <- list(\n    visit_image = function(ctx, src, alt, title) {\n      list(custom = [Image: {alt}])\n    },\n  )\n\n  result <- convert(html = \"<img src=\\\"banner.png\\\" alt=\\\"Banner\\\">\", visitor = visitor)\n  expect_true(grepl(\"[Image: Banner]\", result$content, fixed = TRUE))\n  expect_false(grepl(\"banner.png\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_link_format: Visitor reformats links using template interpolation\", {\n  visitor <- list(\n    visit_link = function(ctx, href, text, title) {\n      list(custom = {text} ({href}))\n    },\n  )\n\n  result <- convert(html = \"<p>Visit <a href=\\\"https://example.com\\\">Example</a> for more info.</p>\", visitor = visitor)\n  expect_true(grepl(\"Example (https://example.com)\", result$content, fixed = TRUE))\n  expect_false(grepl(\"[Example]\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_link_static: Visitor replaces link with static custom output\", {\n  visitor <- list(\n    visit_link = function(ctx, href, text, title) {\n      list(custom = [REDACTED LINK])\n    },\n  )\n\n  result <- convert(html = \"<a href=\\\"https://example.com\\\">Click here</a>\", visitor = visitor)\n  expect_true(grepl(\"[REDACTED LINK]\", result$content, fixed = TRUE))\n  expect_false(grepl(\"example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_custom_output: Visitor custom action replaces element output\", {\n  visitor <- list(\n    visit_heading = function(ctx, level, text, id) {\n      list(custom = ## REPLACED HEADING)\n    },\n  )\n\n  result <- convert(html = \"<h1>Original Heading</h1>\", visitor = visitor)\n  expect_true(grepl(\"## REPLACED HEADING\", result$content, fixed = TRUE))\n  expect_false(grepl(\"# Original Heading\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_definition_list_custom: Visitor customizes definition list items\", {\n  visitor <- list(\n    visit_definition_term = function(ctx, text) {\n      list(custom = **{text}**)\n    },\n  )\n\n  result <- convert(html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", visitor = visitor)\n  expect_true(grepl(\"**HTML**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"**CSS**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"HyperText Markup Language\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_definition_list_custom_format: Visitor formats definition lists with custom templates\", {\n  visitor <- list(\n    visit_definition_term = function(ctx, text) {\n      list(custom = ### {text})\n    },\n    visit_definition_description = function(ctx, text) {\n      list(custom = > {text})\n    },\n  )\n\n  result <- convert(html = \"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\", visitor = visitor)\n  expect_true(grepl(\"### Python\", result$content, fixed = TRUE))\n  expect_true(grepl(\"### JavaScript\", result$content, fixed = TRUE))\n  expect_true(grepl(\"> A high-level programming language\", result$content, fixed = TRUE))\n  expect_true(grepl(\"> A scripting language for web browsers\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_definition_list_skip: Visitor skips definition list items from output\", {\n  visitor <- list(\n    visit_definition_description = function(ctx, text) {\n      \"skip\"\n    },\n    visit_definition_term = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\", visitor = visitor)\n  expect_true(grepl(\"Glossary:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"End of glossary\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Term A\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Definition\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_details_summary_custom: Visitor customizes details/summary disclosure elements\", {\n  visitor <- list(\n    visit_summary = function(ctx, text) {\n      list(custom = [EXPANDABLE] {text})\n    },\n  )\n\n  result <- convert(html = \"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\", visitor = visitor)\n  expect_true(grepl(\"[EXPANDABLE] Click to expand\", result$content, fixed = TRUE))\n  expect_true(grepl(\"This content is initially hidden.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_details_summary_skip: Visitor removes details/summary elements entirely\", {\n  visitor <- list(\n    visit_details = function(ctx, is_open) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\", visitor = visitor)\n  expect_true(grepl(\"Main content here.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"More main content.\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Hidden section\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Secret details\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_figure_custom: Visitor customizes figure and figcaption elements\", {\n  visitor <- list(\n    visit_figcaption = function(ctx, text) {\n      list(custom = *{text}*)\n    },\n  )\n\n  result <- convert(html = \"<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\\\"diagram.png\\\" alt=\\\"System architecture diagram\\\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>\", visitor = visitor)\n  expect_true(grepl(\"Article Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"*Figure 1: System Architecture*\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Explanation of the figure.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_figure_custom_wrap: Visitor wraps figure content with custom formatting\", {\n  visitor <- list(\n    visit_figure_start = function(ctx) {\n      list(custom = \\n[FIGURE]\\n)\n    },\n    visit_figure_end = function(ctx, output) {\n      list(custom = {output}\\n[/FIGURE]\\n)\n    },\n  )\n\n  result <- convert(html = \"<section><h2>Gallery</h2><figure><img src=\\\"photo1.jpg\\\" alt=\\\"Photo\\\"><figcaption>Beautiful sunset</figcaption></figure></section>\", visitor = visitor)\n  expect_true(grepl(\"[FIGURE]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[/FIGURE]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Gallery\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_figure_skip: Visitor removes figure elements with their captions\", {\n  visitor <- list(\n    visit_figure_start = function(ctx) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>See the chart below:</p><figure><img src=\\\"chart.svg\\\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>\", visitor = visitor)\n  expect_true(grepl(\"See the chart below:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"As shown in the chart above.\", result$content, fixed = TRUE))\n  expect_false(grepl(\"Revenue Trends\", result$content, fixed = TRUE))\n  expect_false(grepl(\"chart.svg\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_form_custom: Visitor replaces form with custom output\", {\n  visitor <- list(\n    visit_form = function(ctx, action_url, method) {\n      list(custom = [FORM PLACEHOLDER])\n    },\n  )\n\n  result <- convert(html = \"<div><form action=\\\"/submit\\\" method=\\\"POST\\\"><label>Name: <input type=\\\"text\\\" name=\\\"name\\\"></label><button type=\\\"submit\\\">Submit</button></form></div>\", visitor = visitor)\n  expect_true(grepl(\"[FORM PLACEHOLDER]\", result$content, fixed = TRUE))\n  expect_false(grepl(\"submit\", result$content, fixed = TRUE))\n  expect_false(grepl(\"input\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_form_skip: Visitor skips form elements entirely\", {\n  visitor <- list(\n    visit_form = function(ctx, action_url, method) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Before form</p><form><input type=\\\"email\\\" name=\\\"email\\\"></form><p>After form</p>\", visitor = visitor)\n  expect_true(grepl(\"Before form\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After form\", result$content, fixed = TRUE))\n  expect_false(grepl(\"email\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_horizontal_rule_custom: Visitor replaces horizontal rule with custom output\", {\n  visitor <- list(\n    visit_horizontal_rule = function(ctx) {\n      list(custom = \\n[DIVIDER]\\n)\n    },\n  )\n\n  result <- convert(html = \"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\", visitor = visitor)\n  expect_true(grepl(\"[DIVIDER]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Section A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Section B\", result$content, fixed = TRUE))\n  expect_false(grepl(\"---\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_horizontal_rule_skip: Visitor removes all horizontal rules\", {\n  visitor <- list(\n    visit_horizontal_rule = function(ctx) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\", visitor = visitor)\n  expect_true(grepl(\"Part 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Part 2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Part 3\", result$content, fixed = TRUE))\n  expect_false(grepl(\"---\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_iframe_custom: Visitor replaces embedded iframe with custom text\", {\n  visitor <- list(\n    visit_iframe = function(ctx, src) {\n      list(custom = [EMBEDDED: https://maps.example.com/embed])\n    },\n  )\n\n  result <- convert(html = \"<p>Embedded map:</p><iframe src=\\\"https://maps.example.com/embed\\\" width=\\\"400\\\" height=\\\"300\\\"></iframe><p>End of map</p>\", visitor = visitor)\n  expect_true(grepl(\"[EMBEDDED: https://maps.example.com/embed]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Embedded map:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"End of map\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_iframe_skip: Visitor removes embedded iframes\", {\n  visitor <- list(\n    visit_iframe = function(ctx, src) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<h3>Reviews</h3><iframe src=\\\"https://widget.example.com/reviews\\\"></iframe><p>See reviews from our partners.</p>\", visitor = visitor)\n  expect_true(grepl(\"Reviews\", result$content, fixed = TRUE))\n  expect_true(grepl(\"See reviews from our partners.\", result$content, fixed = TRUE))\n  expect_false(grepl(\"widget.example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_input_custom: Visitor replaces input with labeled output\", {\n  visitor <- list(\n    visit_input = function(ctx, input_type, name, value) {\n      list(custom = [INPUT:{input_type}])\n    },\n  )\n\n  result <- convert(html = \"<form><label>Username: <input type=\\\"text\\\" name=\\\"username\\\" value=\\\"\\\"></label><label>Password: <input type=\\\"password\\\" name=\\\"password\\\"></label></form>\", visitor = visitor)\n  expect_true(grepl(\"[INPUT:text]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[INPUT:password]\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_input_skip: Visitor skips all input elements\", {\n  visitor <- list(\n    visit_input = function(ctx, input_type, name, value) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Sign up:</p><input type=\\\"text\\\" name=\\\"email\\\" placeholder=\\\"your@email.com\\\"><input type=\\\"checkbox\\\" name=\\\"agree\\\"><p>Continue</p>\", visitor = visitor)\n  expect_true(grepl(\"Sign up:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Continue\", result$content, fixed = TRUE))\n  expect_false(grepl(\"email\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_line_break_custom: Visitor replaces line break with custom output\", {\n  visitor <- list(\n    visit_line_break = function(ctx) {\n      list(custom =  | )\n    },\n  )\n\n  result <- convert(html = \"<p>First line<br>Second line<br>Third line</p>\", visitor = visitor)\n  expect_true(grepl(\"First line | Second line | Third line\", result$content, fixed = TRUE))\n  expect_false(grepl(\"\\n\\n\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_line_break_skip: Visitor removes all line breaks\", {\n  visitor <- list(\n    visit_line_break = function(ctx) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\", visitor = visitor)\n  expect_true(grepl(\"Address Line 1Address Line 2Address Line 3\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_mark_custom: Visitor replaces highlight/mark with custom template\", {\n  visitor <- list(\n    visit_mark = function(ctx, text) {\n      list(custom = =={text}==)\n    },\n  )\n\n  result <- convert(html = \"<p>This is a <mark>highlighted passage</mark> in the text.</p>\", visitor = visitor)\n  expect_true(grepl(\"==highlighted passage==\", result$content, fixed = TRUE))\n  expect_true(grepl(\"This is a\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_mark_skip: Visitor skips mark elements entirely\", {\n  visitor <- list(\n    visit_mark = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Key insight: <mark>always validate input</mark> for security.</p>\", visitor = visitor)\n  expect_false(grepl(\"always validate input\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Key insight:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"for security.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_preserve_html: Visitor preserve_html action includes raw HTML in output\", {\n  visitor <- list(\n    visit_custom_element = function(ctx, tag_name, html) {\n      \"preserve_html\"\n    },\n  )\n\n  result <- convert(html = \"<div><custom-tag>Custom content</custom-tag></div>\", visitor = visitor)\n  expect_true(grepl(\"<custom-tag>\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_all_headings: Visitor skips all headings from document\", {\n  visitor <- list(\n    visit_heading = function(ctx, level, text, id) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<h1>Title</h1><p>Body text remains.</p>\", visitor = visitor)\n  expect_false(grepl(\"Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Body text remains.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_code_blocks: Visitor skips code blocks from output\", {\n  visitor <- list(\n    visit_code_block = function(ctx, lang, code) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\", visitor = visitor)\n  expect_true(grepl(\"Intro text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Outro text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"let x = 42\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_heading: Visitor skip action omits all headings from output\", {\n  visitor <- list(\n    visit_heading = function(ctx, level, text, id) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<h1>Title</h1><p>Body text remains.</p>\", visitor = visitor)\n  expect_false(grepl(\"Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Body text remains.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_images: Visitor skips all images from output\", {\n  visitor <- list(\n    visit_image = function(ctx, src, alt, title) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Before image</p><img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\"><p>After image</p>\", visitor = visitor)\n  expect_true(grepl(\"Before image\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After image\", result$content, fixed = TRUE))\n  expect_false(grepl(\"photo.jpg\", result$content, fixed = TRUE))\n  expect_false(grepl(\"A photo\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_links: Visitor skips all links entirely\", {\n  visitor <- list(\n    visit_link = function(ctx, href, text, title) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Before <a href=\\\"https://example.com\\\">link text</a> after</p>\", visitor = visitor)\n  expect_false(grepl(\"link text\", result$content, fixed = TRUE))\n  expect_false(grepl(\"example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_skip_strong: Visitor skips bold/strong elements\", {\n  visitor <- list(\n    visit_strong = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Normal <strong>bold text</strong> normal</p>\", visitor = visitor)\n  expect_false(grepl(\"bold text\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Normal\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_subscript_custom: Visitor replaces subscript with custom template\", {\n  visitor <- list(\n    visit_subscript = function(ctx, text) {\n      list(custom = ~{text}~)\n    },\n  )\n\n  result <- convert(html = \"<p>H<sub>2</sub>O is water.</p>\", visitor = visitor)\n  expect_true(grepl(\"H~2~O\", result$content, fixed = TRUE))\n  expect_true(grepl(\"is water\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_subscript_skip: Visitor skips subscript elements entirely\", {\n  visitor <- list(\n    visit_subscript = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\", visitor = visitor)\n  expect_true(grepl(\"The formula CHO is sugar.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_superscript_custom: Visitor replaces superscript with custom template\", {\n  visitor <- list(\n    visit_superscript = function(ctx, text) {\n      list(custom = ^{text}^)\n    },\n  )\n\n  result <- convert(html = \"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\", visitor = visitor)\n  expect_true(grepl(\"E=mc^2^\", result$content, fixed = TRUE))\n  expect_true(grepl(\"revolutionized physics\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_superscript_skip: Visitor skips superscript from output\", {\n  visitor <- list(\n    visit_superscript = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\", visitor = visitor)\n  expect_true(grepl(\"The equation x + y = z has no solutions.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_underline_custom: Visitor replaces underline with custom markup\", {\n  visitor <- list(\n    visit_underline = function(ctx, text) {\n      list(custom = _{text}_)\n    },\n  )\n\n  result <- convert(html = \"<p>This is <u>very important</u> text.</p>\", visitor = visitor)\n  expect_true(grepl(\"_very important_\", result$content, fixed = TRUE))\n  expect_false(grepl(\"**\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_underline_skip: Visitor skips underline elements from output\", {\n  visitor <- list(\n    visit_underline = function(ctx, text) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<p>Normal text with <u>underlined part</u> and more text.</p>\", visitor = visitor)\n  expect_true(grepl(\"Normal text with\", result$content, fixed = TRUE))\n  expect_true(grepl(\"and more text.\", result$content, fixed = TRUE))\n  expect_false(grepl(\"underlined part\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_video_custom: Visitor replaces video with custom link\", {\n  visitor <- list(\n    visit_video = function(ctx, src) {\n      list(custom = [VIDEO: {src}])\n    },\n  )\n\n  result <- convert(html = \"<p>Watch our tutorial:</p><video src=\\\"tutorial.mp4\\\" width=\\\"320\\\" height=\\\"240\\\" controls></video><p>Great content!</p>\", visitor = visitor)\n  expect_true(grepl(\"[VIDEO: tutorial.mp4]\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Watch our tutorial:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Great content!\", result$content, fixed = TRUE))\n})\n\ntest_that(\"visitor_video_skip: Visitor removes video elements entirely\", {\n  visitor <- list(\n    visit_video = function(ctx, src) {\n      \"skip\"\n    },\n  )\n\n  result <- convert(html = \"<h2>Demo</h2><video src=\\\"demo.webm\\\"></video><p>See the demo above.</p>\", visitor = visitor)\n  expect_true(grepl(\"Demo\", result$content, fixed = TRUE))\n  expect_true(grepl(\"See the demo above.\", result$content, fixed = TRUE))\n  expect_false(grepl(\"demo.webm\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "e2e/ruby/.rubocop.yaml",
    "content": "# Generated by alef e2e — do not edit.\nAllCops:\n  NewCops: enable\n  TargetRubyVersion: 3.2\n  SuggestExtensions: false\n\nplugins:\n  - rubocop-rspec\n\n# --- Justified suppressions for generated test code ---\n\n# Generated tests are verbose by nature (setup + multiple assertions).\nMetrics/BlockLength:\n  Enabled: false\nMetrics/MethodLength:\n  Enabled: false\nLayout/LineLength:\n  Enabled: false\n\n# Generated tests use multiple assertions per example for thorough verification.\nRSpec/MultipleExpectations:\n  Enabled: false\nRSpec/ExampleLength:\n  Enabled: false\n\n# Generated tests describe categories as strings, not classes.\nRSpec/DescribeClass:\n  Enabled: false\n\n# Fixture-driven tests may produce identical assertion bodies for different inputs.\nRSpec/RepeatedExample:\n  Enabled: false\n\n# Error-handling tests use bare raise_error (exception type not known at generation time).\nRSpec/UnspecifiedException:\n  Enabled: false\n"
  },
  {
    "path": "e2e/ruby/Gemfile",
    "content": "# frozen_string_literal: true\n\nsource 'https://rubygems.org'\n\ngem 'html-to-markdown', path: '../../packages/ruby'\ngem 'rspec', '~> 3.13'\ngem 'rubocop', '~> 1.86'\ngem 'rubocop-rspec', '~> 3.9'\ngem 'faraday', '~> 2.0'\n"
  },
  {
    "path": "e2e/ruby/spec/conversion_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6d793cb869a5c387c7f197d6ccbf7d0884c3ce8be3cf351cc8749db9e4ac967b\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'conversion' do\n  it 'blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed' do\n    result = HtmlToMarkdown.convert('<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>')\n    expect(result.to_s).to include('> First paragraph.')\n    expect(result.to_s).to include('> Second paragraph.')\n  end\n\n  it 'blockquote_nested: Nested blockquote produces double-prefixed lines' do\n    result = HtmlToMarkdown.convert('<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Outer quote.')\n    expect(result.to_s).to include('Inner quote.')\n  end\n\n  it 'blockquote_simple: Simple blockquote' do\n    result = HtmlToMarkdown.convert('<blockquote><p>Quote text</p></blockquote>')\n    expect(result.to_s).to include('> Quote text')\n  end\n\n  it 'blockquote_with_list: Blockquote containing a list preserves list items inside quote' do\n    result = HtmlToMarkdown.convert('<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Quote intro:')\n    expect(result.to_s).to include('Point one')\n    expect(result.to_s).to include('Point two')\n  end\n\n  it 'bold_and_italic: Nested bold and italic' do\n    result = HtmlToMarkdown.convert('<p><strong><em>both</em></strong></p>')\n    expect(result.to_s).to include('***both***')\n  end\n\n  it 'bold_strong: Strong tag converts to bold' do\n    result = HtmlToMarkdown.convert('<p><strong>bold</strong></p>')\n    expect(result.to_s).to include('**bold**')\n  end\n\n  it 'code_block: Code block with language preserves content' do\n    result = HtmlToMarkdown.convert('<pre><code class=\"language-python\">print(\\'hello\\')</code></pre>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('print(\\'hello\\')')\n  end\n\n  it 'code_block_no_language: Code block without a language class preserves content' do\n    result = HtmlToMarkdown.convert('<pre><code>plain code here</code></pre>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('plain code here')\n  end\n\n  it 'code_inline_in_paragraph: Inline code element nested inside a paragraph' do\n    result = HtmlToMarkdown.convert('<p>Call the <code>initialize()</code> method first.</p>')\n    expect(result.to_s).to include('`initialize()`')\n  end\n\n  it 'code_with_backticks_in_content: Inline code containing backtick characters is properly escaped' do\n    result = HtmlToMarkdown.convert('<p>Use <code>`backtick` here</code> carefully.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('backtick')\n  end\n\n  it 'emphasis_mark_highlight: mark tag produces highlighted output' do\n    result = HtmlToMarkdown.convert('<p><mark>highlighted</mark></p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('highlighted')\n  end\n\n  it 'emphasis_strikethrough_del: del tag converts to GFM strikethrough' do\n    result = HtmlToMarkdown.convert('<p><del>deleted text</del></p>')\n    expect(result.to_s).to include('~~deleted text~~')\n  end\n\n  it 'emphasis_strikethrough_s: s tag converts to GFM strikethrough' do\n    result = HtmlToMarkdown.convert('<p><s>strikethrough</s></p>')\n    expect(result.to_s).to include('~~strikethrough~~')\n  end\n\n  it 'emphasis_subscript: sub tag content is preserved' do\n    result = HtmlToMarkdown.convert('<p>H<sub>2</sub>O</p>')\n    expect(result.to_s).to include('H')\n    expect(result.to_s).to include('2')\n    expect(result.to_s).to include('O')\n  end\n\n  it 'emphasis_superscript: sup tag content is preserved' do\n    result = HtmlToMarkdown.convert('<p>x<sup>2</sup></p>')\n    expect(result.to_s).to include('x')\n    expect(result.to_s).to include('2')\n  end\n\n  it 'emphasis_underline_u: u tag content is preserved in output' do\n    result = HtmlToMarkdown.convert('<p><u>underlined</u></p>')\n    expect(result.to_s).to include('underlined')\n  end\n\n  it 'form_input_elements: Form input elements produce readable output without form mechanics' do\n    result = HtmlToMarkdown.convert('<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Name')\n  end\n\n  it 'form_select_options: Select element with options produces readable output' do\n    result = HtmlToMarkdown.convert('<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Color')\n  end\n\n  it 'form_textarea: Textarea element produces readable output' do\n    result = HtmlToMarkdown.convert('<form><label>Message:</label><textarea>Default text content</textarea></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Message')\n  end\n\n  it 'heading_h1: H1 heading' do\n    result = HtmlToMarkdown.convert('<h1>Heading 1</h1>')\n    expect(result.strip).to eq('# Heading 1')\n  end\n\n  it 'heading_h2: H2 heading' do\n    result = HtmlToMarkdown.convert('<h2>Heading 2</h2>')\n    expect(result.strip).to eq('## Heading 2')\n  end\n\n  it 'heading_h3: H3 heading' do\n    result = HtmlToMarkdown.convert('<h3>Heading 3</h3>')\n    expect(result.strip).to eq('### Heading 3')\n  end\n\n  it 'heading_h4: H4 heading' do\n    result = HtmlToMarkdown.convert('<h4>Heading 4</h4>')\n    expect(result.strip).to eq('#### Heading 4')\n  end\n\n  it 'heading_h5: H5 heading' do\n    result = HtmlToMarkdown.convert('<h5>Heading 5</h5>')\n    expect(result.strip).to eq('##### Heading 5')\n  end\n\n  it 'heading_h6: H6 heading' do\n    result = HtmlToMarkdown.convert('<h6>Heading 6</h6>')\n    expect(result.strip).to eq('###### Heading 6')\n  end\n\n  it 'image_figure_figcaption: Figure with figcaption preserves both image and caption' do\n    result = HtmlToMarkdown.convert('<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>')\n    expect(result.to_s).to include('![A sunset](sunset.jpg)')\n    expect(result.to_s).to include('Beautiful sunset over the ocean')\n  end\n\n  it 'image_linked: Image inside an anchor produces a linked image' do\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>')\n    expect(result.to_s).to include('![Icon](icon.png)')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'image_no_alt: Image without alt text produces image markdown' do\n    result = HtmlToMarkdown.convert('<img src=\"banner.jpg\">')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('banner.jpg')\n  end\n\n  it 'image_simple: Image with alt text' do\n    result = HtmlToMarkdown.convert('<img src=\"photo.jpg\" alt=\"A photo\">')\n    expect(result.to_s).to include('![A photo](photo.jpg)')\n  end\n\n  it 'image_with_title: Image with title attribute includes title in output' do\n    result = HtmlToMarkdown.convert('<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">')\n    expect(result.to_s).to include('![Sales chart](chart.png')\n    expect(result.to_s).to include('Q3 Sales')\n  end\n\n  it 'inline_code: Inline code' do\n    result = HtmlToMarkdown.convert('<p>Use <code>console.log()</code> to debug</p>')\n    expect(result.to_s).to include('`console.log()`')\n  end\n\n  it 'italic_em: Em tag converts to italic' do\n    result = HtmlToMarkdown.convert('<p><em>italic</em></p>')\n    expect(result.to_s).to include('*italic*')\n  end\n\n  it 'line_break_br_tag: Single br tag produces a line break in output' do\n    result = HtmlToMarkdown.convert('<p>First line.<br>Second line.</p>')\n    expect(result.to_s).to include('First line.')\n    expect(result.to_s).to include('Second line.')\n  end\n\n  it 'line_break_hr_tag: hr tag produces a horizontal separator between content' do\n    result = HtmlToMarkdown.convert('<p>Before rule.</p><hr><p>After rule.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Before rule.')\n    expect(result.to_s).to include('After rule.')\n  end\n\n  it 'line_break_multiple_br: Multiple consecutive br tags in sequence' do\n    result = HtmlToMarkdown.convert('<p>Start.<br><br>End.</p>')\n    expect(result.to_s).to include('Start.')\n    expect(result.to_s).to include('End.')\n  end\n\n  it 'link_anchor_fragment: Fragment-only anchor link is preserved' do\n    result = HtmlToMarkdown.convert('<a href=\"#section\">Jump to section</a>')\n    expect(result.to_s).to include('[Jump to section](#section)')\n  end\n\n  it 'link_empty_href: Link with empty href produces output with the link text' do\n    result = HtmlToMarkdown.convert('<a href=\"\">No destination</a>')\n    expect(result.to_s).to include('No destination')\n  end\n\n  it 'link_image_inside: Image inside a link produces a linked image' do\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>')\n    expect(result.to_s).to include('![Logo](logo.png)')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'link_mailto: Mailto link is preserved with mailto: scheme' do\n    result = HtmlToMarkdown.convert('<a href=\"mailto:user@example.com\">Email us</a>')\n    expect(result.to_s).to include('mailto:user@example.com')\n  end\n\n  it 'link_simple: Simple link' do\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\">Example</a>')\n    expect(result.to_s).to include('[Example](https://example.com)')\n  end\n\n  it 'link_with_bold_text: Link containing bold text preserves formatting' do\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\"><strong>Bold link</strong></a>')\n    expect(result.to_s).to include('**Bold link**')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'link_with_title: Link with title attribute' do\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\" title=\"Example Site\">Example</a>')\n    expect(result.to_s).to include('[Example](https://example.com')\n    expect(result.to_s).to include('Example Site')\n  end\n\n  it 'list_definition_dl: Definition list with dt and dd elements' do\n    result = HtmlToMarkdown.convert('<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>')\n    expect(result.to_s).to include('Term One')\n    expect(result.to_s).to include('Definition of term one.')\n    expect(result.to_s).to include('Term Two')\n    expect(result.to_s).to include('Definition of term two.')\n  end\n\n  it 'list_item_multiple_paragraphs: List item containing multiple paragraphs' do\n    result = HtmlToMarkdown.convert('<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>')\n    expect(result.to_s).to include('First paragraph in item.')\n    expect(result.to_s).to include('Second paragraph in item.')\n    expect(result.to_s).to include('Simple item')\n  end\n\n  it 'list_mixed_nested: Mixed list: ordered list nested inside unordered list' do\n    result = HtmlToMarkdown.convert('<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>')\n    expect(result.to_s).to include('Item A')\n    expect(result.to_s).to include('Sub 1')\n    expect(result.to_s).to include('Sub 2')\n    expect(result.to_s).to include('Item B')\n  end\n\n  it 'list_nested_ordered: Nested ordered list with two levels of depth' do\n    result = HtmlToMarkdown.convert('<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>')\n    expect(result.to_s).to include('Step 1')\n    expect(result.to_s).to include('Step 1a')\n    expect(result.to_s).to include('Step 1b')\n    expect(result.to_s).to include('Step 2')\n  end\n\n  it 'list_nested_unordered: Nested unordered list with two levels of depth' do\n    result = HtmlToMarkdown.convert('<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>')\n    expect(result.to_s).to include('Parent A')\n    expect(result.to_s).to include('Child A1')\n    expect(result.to_s).to include('Child A2')\n    expect(result.to_s).to include('Parent B')\n  end\n\n  it 'list_task_checkboxes: Task list with checked and unchecked checkboxes' do\n    result = HtmlToMarkdown.convert('<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Done task')\n    expect(result.to_s).to include('Pending task')\n  end\n\n  it 'ordered_list: Ordered list' do\n    result = HtmlToMarkdown.convert('<ol><li>First</li><li>Second</li><li>Third</li></ol>')\n    expect(result.to_s).to include('1. First')\n    expect(result.to_s).to include('2. Second')\n    expect(result.to_s).to include('3. Third')\n  end\n\n  it 'paragraph_multiple: Multiple paragraphs are separated by a blank line' do\n    result = HtmlToMarkdown.convert('<p>First paragraph.</p><p>Second paragraph.</p>')\n    expect(result.to_s).to include('First paragraph.')\n    expect(result.to_s).to include('Second paragraph.')\n  end\n\n  it 'paragraph_nested_divs: Text nested inside divs is extracted correctly' do\n    result = HtmlToMarkdown.convert('<div><div><p>Nested text</p></div></div>')\n    expect(result.to_s).to include('Nested text')\n  end\n\n  it 'paragraph_simple: Simple paragraph converts to plain text' do\n    result = HtmlToMarkdown.convert('<p>Hello World</p>')\n    expect(result.strip).to eq('Hello World')\n  end\n\n  it 'paragraph_with_inline_formatting: Paragraph with bold, italic, and a link' do\n    result = HtmlToMarkdown.convert('<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>')\n    expect(result.to_s).to include('**bold**')\n    expect(result.to_s).to include('*italic*')\n    expect(result.to_s).to include('[link](https://example.com)')\n  end\n\n  it 'paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output' do\n    result = HtmlToMarkdown.convert('<p>Line one.<br>Line two.<br>Line three.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Line one.')\n    expect(result.to_s).to include('Line two.')\n    expect(result.to_s).to include('Line three.')\n  end\n\n  it 'semantic_abbr: Abbreviation element text is preserved' do\n    result = HtmlToMarkdown.convert('<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>')\n    expect(result.to_s).to include('WWW')\n  end\n\n  it 'semantic_article: Article element wrapping content preserves inner content' do\n    result = HtmlToMarkdown.convert('<article><h2>Article Title</h2><p>Article body.</p></article>')\n    expect(result.to_s).to include('Article Title')\n    expect(result.to_s).to include('Article body.')\n  end\n\n  it 'semantic_definition_list: Definition list with term and description' do\n    result = HtmlToMarkdown.convert('<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>')\n    expect(result.to_s).to include('HTML')\n    expect(result.to_s).to include('HyperText Markup Language')\n    expect(result.to_s).to include('CSS')\n    expect(result.to_s).to include('Cascading Style Sheets')\n  end\n\n  it 'semantic_details_summary: Details and summary elements produce readable output' do\n    result = HtmlToMarkdown.convert('<details><summary>Click to expand</summary><p>Hidden content here.</p></details>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Click to expand')\n  end\n\n  it 'semantic_hr: Horizontal rule produces a separator in output' do\n    result = HtmlToMarkdown.convert('<p>Above</p><hr><p>Below</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Above')\n    expect(result.to_s).to include('Below')\n  end\n\n  it 'semantic_mark_highlight: Mark tag produces highlighted output' do\n    result = HtmlToMarkdown.convert('<p>This is <mark>highlighted text</mark> in a sentence.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('highlighted text')\n  end\n\n  it 'semantic_section_with_heading: Section element with heading preserves structure' do\n    result = HtmlToMarkdown.convert('<section><h3>Section Heading</h3><p>Section content.</p></section>')\n    expect(result.to_s).to include('Section Heading')\n    expect(result.to_s).to include('Section content.')\n  end\n\n  it 'semantic_sub_superscript: Subscript and superscript elements are preserved in output' do\n    result = HtmlToMarkdown.convert('<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('H')\n    expect(result.to_s).to include('2')\n    expect(result.to_s).to include('O')\n    expect(result.to_s).to include('E=mc')\n  end\n\n  it 'simple_table: Simple table with header' do\n    result = HtmlToMarkdown.convert('<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>')\n    expect(result.to_s).to include('Name')\n    expect(result.to_s).to include('Age')\n    expect(result.to_s).to include('Alice')\n    expect(result.to_s).to include('30')\n    expect(result.to_s).to include('|')\n    expect(result.to_s).to include('---')\n  end\n\n  it 'table_empty: Empty table produces no output or minimal output' do\n    result = HtmlToMarkdown.convert('<table></table>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'table_no_thead: Table without thead uses first row as implied header' do\n    result = HtmlToMarkdown.convert('<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Product')\n    expect(result.to_s).to include('Price')\n    expect(result.to_s).to include('Apple')\n    expect(result.to_s).to include('1.00')\n    expect(result.to_s).to include('|')\n  end\n\n  it 'table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output' do\n    result = HtmlToMarkdown.convert('<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Expression')\n    expect(result.to_s).to include('Result')\n    expect(result.to_s).to include('true')\n  end\n\n  it 'table_with_alignment: Table with column alignment attributes' do\n    result = HtmlToMarkdown.convert('<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Left')\n    expect(result.to_s).to include('Center')\n    expect(result.to_s).to include('Right')\n    expect(result.to_s).to include('L')\n    expect(result.to_s).to include('C')\n    expect(result.to_s).to include('R')\n    expect(result.to_s).to include('|')\n  end\n\n  it 'table_with_colspan: Table with colspan attribute in a header cell' do\n    result = HtmlToMarkdown.convert('<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Full Name')\n    expect(result.to_s).to include('John')\n    expect(result.to_s).to include('Doe')\n  end\n\n  it 'unordered_list: Unordered list' do\n    result = HtmlToMarkdown.convert('<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>')\n    expect(result.to_s).to include('- Item 1')\n    expect(result.to_s).to include('- Item 2')\n    expect(result.to_s).to include('- Item 3')\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/edge_cases_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:c5bde8c4d08c07c7de9de588a4893a6658b1965502b978a28610c3e993b8d71c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'edge-cases' do\n  it 'empty_html: Empty HTML document' do\n    result = HtmlToMarkdown.convert('<html><head></head><body></body></html>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'encoding_cjk_characters: CJK (Chinese, Japanese, Korean) characters are preserved' do\n    result = HtmlToMarkdown.convert('<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('中文内容')\n    expect(result.to_s).to include('日本語テキスト')\n    expect(result.to_s).to include('한국어 텍스트')\n  end\n\n  it 'encoding_html_entities: Common HTML entities are decoded in output' do\n    result = HtmlToMarkdown.convert('<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('&')\n    expect(result.to_s).to include('<')\n    expect(result.to_s).to include('>')\n  end\n\n  it 'encoding_named_entities: Named HTML entities like &mdash; and &hellip; are decoded' do\n    result = HtmlToMarkdown.convert('<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('—')\n    expect(result.to_s).to include('…')\n  end\n\n  it 'encoding_numeric_entities: Numeric HTML entities (decimal and hex) are decoded' do\n    result = HtmlToMarkdown.convert('<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('©')\n    expect(result.to_s).to include('®')\n    expect(result.to_s).to include('€')\n  end\n\n  it 'encoding_unicode_emoji: Emoji and Unicode characters are preserved' do\n    result = HtmlToMarkdown.convert('<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('🌍')\n    expect(result.to_s).to include('🚀')\n    expect(result.to_s).to include('⭐')\n  end\n\n  it 'html_comments_only: Document containing only HTML comments produces empty output' do\n    result = HtmlToMarkdown.convert('<!-- This is a comment --><!-- Another comment -->')\n    expect(result.strip).to eq('')\n  end\n\n  it 'just_whitespace_input: Input that is only whitespace characters (spaces, tabs, newlines) produces empty output' do\n    result = HtmlToMarkdown.convert('   ')\n    expect(result.strip).to eq('')\n  end\n\n  it 'malformed_deeply_nested_elements: Deeply nested elements (100 levels) are handled without stack overflow' do\n    result = HtmlToMarkdown.convert('<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Deeply nested content')\n  end\n\n  it 'malformed_missing_block_closing_tags: Missing closing tags on block elements are auto-closed by parser' do\n    result = HtmlToMarkdown.convert('<div><h1>Title<p>First paragraph<p>Second paragraph</div>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Title')\n    expect(result.to_s).to include('First paragraph')\n    expect(result.to_s).to include('Second paragraph')\n  end\n\n  it 'malformed_overlapping_tags: Overlapping bold/italic tags are recovered by the HTML parser without panic' do\n    result = HtmlToMarkdown.convert('<p><b><i>bold and italic</b></i></p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('bold and italic')\n  end\n\n  it 'malformed_unclosed_paragraph: Unclosed <p> tag is recovered gracefully and content is preserved' do\n    result = HtmlToMarkdown.convert('<p>This paragraph is never closed')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('This paragraph is never closed')\n  end\n\n  it 'script_tags_only: Document with only script tags produces empty output (scripts are stripped)' do\n    result = HtmlToMarkdown.convert('<html><head><script>alert(\\'xss\\')</script></head><body><script>document.write(\\'hello\\')</script></body></html>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'style_tags_only: Document with only style tags produces empty output (styles are stripped)' do\n    result = HtmlToMarkdown.convert('<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'visitor_custom_element_with_nesting: Visitor handles custom elements with nested content' do\n    visitor = Class.new do\n      def visit_custom_element(ctx, tag_name, html)\n        { custom: '[CUSTOM WIDGET]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>', visitor)\n    expect(result.to_s).to include('[CUSTOM WIDGET]')\n    expect(result.to_s).not_to include('Widget content here')\n  end\n\n  it 'visitor_deeply_nested_skip: Visitor skips deeply nested elements' do\n    visitor = Class.new do\n      def visit_mark(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>', visitor)\n    expect(result.to_s).to include('Outer')\n    expect(result.to_s).to include('text')\n    expect(result.to_s).not_to include('highlight')\n  end\n\n  it 'visitor_element_end_modification: Visitor modifies element at end after children processed' do\n    visitor = Class.new do\n      def visit_element_end(ctx, output)\n        { custom: 'MODIFIED OUTPUT' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<blockquote><p>Original quote</p></blockquote>', visitor)\n    expect(result).not_to be_empty\n  end\n\n  it 'visitor_element_start_skip_entire_subtree: Visitor skips at element_start level removes entire subtree' do\n    visitor = Class.new do\n      def visit_element_start(ctx)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<div><h1>Title</h1><p>Content</p></div>', visitor)\n    expect(result).not_to be_empty\n  end\n\n  it 'visitor_unknown_tag_preservation: Visitor preserves unknown HTML tags as raw HTML' do\n    visitor = Class.new do\n      def visit_custom_element(ctx, tag_name, html)\n        'preserve_html'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>', visitor)\n    expect(result.to_s).to include('Article text')\n    expect(result.to_s).to include('More article text')\n    expect(result.to_s).to include('<x-custom>')\n  end\n\n  it 'whitespace_only: Whitespace-only content' do\n    result = HtmlToMarkdown.convert('<p>   </p>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'xss_onclick_handler_removed: onclick and other on* event handlers are removed from elements' do\n    result = HtmlToMarkdown.convert('<p><a href=\"https://example.com\" onclick=\"alert(\\'xss\\')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Click me')\n  end\n\n  it 'xss_script_tag_stripped: Script tag content is stripped and does not appear in output' do\n    result = HtmlToMarkdown.convert('<p>Safe content.</p><script>alert(\\'xss\\')</script><p>More safe content.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Safe content')\n    expect(result.to_s).to include('More safe content')\n  end\n\n  it 'xss_svg_nested_script_stripped: Script tags nested inside SVG are stripped' do\n    result = HtmlToMarkdown.convert('<p>Before SVG.</p><svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(\\'svg-xss\\')</script><text>SVG text</text></svg><p>After SVG.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Before SVG')\n    expect(result.to_s).to include('After SVG')\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/metadata_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:69b21f6edf35d358dc7bd075c57f956272d26c2161803fbcd122b82f8dc10399\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'metadata' do\n  it 'metadata_author_meta: Extract author from <meta name=\\'author\\'> tag' do\n    result = HtmlToMarkdown.convert('<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_canonical_url: Extract canonical URL from <link rel=\\'canonical\\'> tag' do\n    result = HtmlToMarkdown.convert('<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_description_meta: Extract description from <meta name=\\'description\\'> tag' do\n    result = HtmlToMarkdown.convert('<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_dublin_core: Extract Dublin Core metadata tags' do\n    result = HtmlToMarkdown.convert('<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('scholarly article')\n  end\n\n  it 'metadata_extract_all_images: Extract all images from a document into metadata' do\n    result = HtmlToMarkdown.convert('<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_extract_all_links: Extract all links from a document into metadata' do\n    result = HtmlToMarkdown.convert('<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_headers_hierarchy: Extract heading hierarchy from document into metadata' do\n    result = HtmlToMarkdown.convert('<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_keywords_meta: Extract keywords from <meta name=\\'keywords\\'> tag' do\n    result = HtmlToMarkdown.convert('<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'metadata_lang_attribute: Extract language from html lang attribute' do\n    result = HtmlToMarkdown.convert('<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Hola Mundo')\n  end\n\n  it 'metadata_microdata_schema_article: Extract schema.org microdata for Article' do\n    result = HtmlToMarkdown.convert('<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Breaking News Today')\n    expect(result.to_s).to include('Jane Reporter')\n    expect(result.to_s).to include('important information')\n  end\n\n  it 'metadata_microdata_schema_breadcrumb: Extract schema.org breadcrumb navigation microdata' do\n    result = HtmlToMarkdown.convert('<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Home')\n    expect(result.to_s).to include('Products')\n    expect(result.to_s).to include('Current Page')\n  end\n\n  it 'metadata_microdata_schema_organization: Extract schema.org microdata for Organization' do\n    result = HtmlToMarkdown.convert('<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Acme Corp')\n    expect(result.to_s).to include('2020')\n  end\n\n  it 'metadata_microdata_schema_person: Extract schema.org microdata for Person' do\n    result = HtmlToMarkdown.convert('<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('John Smith')\n    expect(result.to_s).to include('john@example.com')\n  end\n\n  it 'metadata_microdata_schema_product: Extract schema.org microdata for Product' do\n    result = HtmlToMarkdown.convert('<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Awesome Widget')\n    expect(result.to_s).to include('best widget')\n    expect(result.to_s).to include('29.99')\n  end\n\n  it 'metadata_text_direction_ltr: Extract text direction from lang attribute on html element' do\n    result = HtmlToMarkdown.convert('<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('left-to-right text')\n  end\n\n  it 'metadata_text_direction_rtl: Extract right-to-left text direction' do\n    result = HtmlToMarkdown.convert('<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('right-to-left text')\n  end\n\n  it 'metadata_title_tag: Extract title from <title> tag' do\n    result = HtmlToMarkdown.convert('<html><head><title>My Page</title></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'og_basic_tags: Extract og:title, og:description, and og:image from Open Graph meta tags' do\n    result = HtmlToMarkdown.convert('<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'og_multiple_tags: Extract multiple Open Graph tags including type, url, and site_name' do\n    result = HtmlToMarkdown.convert('<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>')\n    expect(result).not_to be_empty\n  end\n\n  it 'structured_data_json_ld: JSON-LD script tag is stripped from output (security) but metadata may be extracted' do\n    result = HtmlToMarkdown.convert('<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('My Article')\n  end\n\n  it 'structured_data_multiple_json_ld: Multiple JSON-LD blocks are all stripped from output' do\n    result = HtmlToMarkdown.convert('<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Widget')\n  end\n\n  it 'twitter_card_tags: Extract Twitter card meta tags' do\n    result = HtmlToMarkdown.convert('<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>')\n    expect(result).not_to be_empty\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/options_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6c008323d3fb4aab9b5becdd6b188b902c8d09e47dd1ff531723333241592406\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'options' do\n  it 'options_autolinks_false: Bare URL links rendered as regular markdown links when autolinks disabled' do\n    result = HtmlToMarkdown.convert('<p><a href=\\'https://example.com\\'>https://example.com</a></p>', {autolinks: false})\n    expect(result.to_s).to include('example.com')\n  end\n\n  it 'options_br_in_tables_false: BR elements in table cells are stripped when disabled' do\n    result = HtmlToMarkdown.convert('<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>', {br_in_tables: false})\n    expect(result.to_s).to include('Col')\n  end\n\n  it 'options_br_in_tables_true: BR elements in table cells render as line breaks' do\n    result = HtmlToMarkdown.convert('<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>', {br_in_tables: true})\n    expect(result.to_s).to include('Header')\n    expect(result.to_s).to include('Line 1')\n    expect(result.to_s).to include('Line 2')\n  end\n\n  it 'options_code_block_backticks: Backticks code block style uses triple backtick fences' do\n    result = HtmlToMarkdown.convert('<pre><code class=\"language-js\">console.log(\\'hi\\');</code></pre>', {code_block_style: 'backticks'})\n    expect(result.to_s).to include('```')\n    expect(result.to_s).to include('console.log(\\'hi\\');')\n  end\n\n  it 'options_code_block_indented: Code blocks use 4-space indentation' do\n    result = HtmlToMarkdown.convert('<pre><code>print(\\'hello\\')</code></pre>', {code_block_style: 'indented'})\n    expect(result.to_s).to include('print(\\'hello\\')')\n    expect(result.to_s).not_to include('```')\n  end\n\n  it 'options_code_block_tildes: Code blocks use tilde fences' do\n    result = HtmlToMarkdown.convert('<pre><code>let x = 1;</code></pre>', {code_block_style: 'tildes'})\n    expect(result.to_s).to include('~~~')\n    expect(result.to_s).to include('let x = 1;')\n  end\n\n  it 'options_code_block_tildes_style: Tildes code block style uses triple tilde fences' do\n    result = HtmlToMarkdown.convert('<pre><code>some code</code></pre>', {code_block_style: 'tildes'})\n    expect(result.to_s).to include('~~~')\n    expect(result.to_s).to include('some code')\n  end\n\n  it 'options_code_language_python: Default code language annotation on blocks without lang attribute' do\n    result = HtmlToMarkdown.convert('<pre><code>def hello(): pass</code></pre>', {code_language: 'python'})\n    expect(result.to_s).to include('```python')\n    expect(result.to_s).to include('def hello')\n  end\n\n  it 'options_convert_as_inline: Block elements treated as inline' do\n    result = HtmlToMarkdown.convert('<p>One</p><p>Two</p>', {convert_as_inline: true})\n    expect(result.to_s).to include('One')\n    expect(result.to_s).to include('Two')\n  end\n\n  it 'options_debug_true: Debug mode enabled does not crash and produces output' do\n    result = HtmlToMarkdown.convert('<p>Debug test</p>', {debug: true})\n    expect(result.to_s).to include('Debug test')\n  end\n\n  it 'options_default_title_true: Links without title get empty title attribute when defaultTitle is true' do\n    result = HtmlToMarkdown.convert('<p><a href=\\'https://example.com\\'>Link</a></p>', {default_title: true})\n    expect(result.to_s).to include('Link')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'options_encoding_utf8: UTF-8 encoding hint for special characters' do\n    result = HtmlToMarkdown.convert('<p>Café naïve résumé</p>', {encoding: 'utf-8'})\n    expect(result).not_to be_empty\n  end\n\n  it 'options_escape_ascii_enabled: ASCII Markdown characters are escaped when escapeAscii is true' do\n    result = HtmlToMarkdown.convert('<p>Text with # hash and [brackets] and * star</p>', {escape_ascii: true})\n    expect(result.to_s).to include('Text')\n    expect(result.to_s).to include('hash')\n    expect(result.to_s).to include('brackets')\n    expect(result.to_s).to include('star')\n  end\n\n  it 'options_escape_asterisks: escape_asterisks option escapes asterisks in plain text' do\n    result = HtmlToMarkdown.convert('<p>Use 2*3 = 6 in math.</p>', {escape_asterisks: true})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('2')\n    expect(result.to_s).to include('3')\n    expect(result.to_s).to include('6')\n  end\n\n  it 'options_escape_misc: escape_misc option escapes miscellaneous markdown characters' do\n    result = HtmlToMarkdown.convert('<p>Use # and | and ~ in text.</p>', {escape_misc: true})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Use')\n    expect(result.to_s).to include('and')\n    expect(result.to_s).to include('in text.')\n  end\n\n  it 'options_escape_underscores: escape_underscores option escapes underscores in plain text' do\n    result = HtmlToMarkdown.convert('<p>The variable_name is defined.</p>', {escape_underscores: true})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('variable')\n    expect(result.to_s).to include('name')\n    expect(result.to_s).to include('defined.')\n  end\n\n  it 'options_exclude_selectors_attribute: Elements matching CSS attribute selector are excluded entirely' do\n    result = HtmlToMarkdown.convert('<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>', {exclude_selectors: ['[role=\\'complementary\\']']})\n    expect(result.to_s).to include('Primary text')\n    expect(result.to_s).not_to include('Sidebar')\n  end\n\n  it 'options_exclude_selectors_class: Elements matching CSS class selector are excluded entirely' do\n    result = HtmlToMarkdown.convert('<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>', {exclude_selectors: ['.cookie-banner']})\n    expect(result.to_s).to include('Main content')\n    expect(result.to_s).not_to include('cookies')\n  end\n\n  it 'options_exclude_selectors_empty_noop: Empty exclude_selectors list does not affect output' do\n    result = HtmlToMarkdown.convert('<p>Hello world</p>', {exclude_selectors: []})\n    expect(result.to_s).to include('Hello world')\n  end\n\n  it 'options_exclude_selectors_id: Elements matching CSS id selector are excluded entirely' do\n    result = HtmlToMarkdown.convert('<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>', {exclude_selectors: ['#ad-container']})\n    expect(result.to_s).to include('Article text')\n    expect(result.to_s).not_to include('Buy stuff')\n  end\n\n  it 'options_exclude_selectors_multiple: Multiple CSS selectors each exclude their matched elements' do\n    result = HtmlToMarkdown.convert('<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>', {exclude_selectors: ['.nav', 'footer']})\n    expect(result.to_s).to include('Content')\n    expect(result.to_s).not_to include('Menu')\n    expect(result.to_s).not_to include('Footer')\n  end\n\n  it 'options_exclude_selectors_nested_content_dropped: All descendants of excluded elements are dropped' do\n    result = HtmlToMarkdown.convert('<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>', {exclude_selectors: ['.sidebar']})\n    expect(result.to_s).to include('Main text')\n    expect(result.to_s).not_to include('Related')\n    expect(result.to_s).not_to include('Sidebar text')\n  end\n\n  it 'options_exclude_selectors_plain_text_mode: Exclude selectors work in plain text output mode' do\n    result = HtmlToMarkdown.convert('<body><div class=\"nav\">Navigation</div><p>Article body</p></body>', {exclude_selectors: ['.nav'], output_format: 'plain'})\n    expect(result.to_s).to include('Article body')\n    expect(result.to_s).not_to include('Navigation')\n  end\n\n  it 'options_exclude_selectors_vs_strip_tags: exclude_selectors drops entire subtree unlike strip_tags which keeps children' do\n    result = HtmlToMarkdown.convert('<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>', {exclude_selectors: ['.wrapper']})\n    expect(result.to_s).to include('Outer text')\n    expect(result.to_s).not_to include('Inner paragraph')\n  end\n\n  it 'options_extract_metadata_true: Extract metadata returns document metadata when enabled' do\n    result = HtmlToMarkdown.convert('<html><head><title>Test Page</title><meta name=\\'description\\' content=\\'A test page\\'></head><body><p>Content</p></body></html>', {extract_metadata: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'options_heading_style_atx: ATX heading style produces hash-prefixed headings' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1><h2>Subtitle</h2>', {heading_style: 'atx'})\n    expect(result.to_s).to include('# Title')\n    expect(result.to_s).to include('## Subtitle')\n  end\n\n  it 'options_heading_style_atx_closed: ATX closed heading style adds closing hashes' do\n    result = HtmlToMarkdown.convert('<h1>Closed Heading</h1>', {heading_style: 'atx_closed'})\n    expect(result.to_s).to include('# Closed Heading #')\n  end\n\n  it 'options_heading_style_underlined: Underlined heading style produces setext-style headings for h1 and h2' do\n    result = HtmlToMarkdown.convert('<h1>Main Title</h1>', {heading_style: 'underlined'})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Main Title')\n  end\n\n  it 'options_highlight_bold: Mark tag rendered as bold' do\n    result = HtmlToMarkdown.convert('<p>Text with <mark>highlighted</mark> text.</p>', {highlight_style: 'bold'})\n    expect(result.to_s).to include('**highlighted**')\n  end\n\n  it 'options_highlight_double_equal: Mark tag with double equal highlight style' do\n    result = HtmlToMarkdown.convert('<p>Text with <mark>highlighted</mark> here.</p>', {highlight_style: 'double_equal'})\n    expect(result.to_s).to include('==highlighted==')\n  end\n\n  it 'options_highlight_none: Mark tag with no highlight style strips the mark' do\n    result = HtmlToMarkdown.convert('<p>Text with <mark>plain</mark> content.</p>', {highlight_style: 'none'})\n    expect(result.to_s).to include('plain')\n    expect(result.to_s).not_to include('==')\n  end\n\n  it 'options_keep_inline_images_in_paragraph: Images inside specified tags stay inline' do\n    result = HtmlToMarkdown.convert('<p>Text <img src=\\'icon.png\\' alt=\\'icon\\'> more text</p>', {keep_inline_images_in: ['p']})\n    expect(result.to_s).to include('Text')\n    expect(result.to_s).to include('more text')\n  end\n\n  it 'options_link_style_reference: Links use reference-style formatting' do\n    result = HtmlToMarkdown.convert('<p><a href=\\'https://example.com\\'>Example</a> and <a href=\\'https://other.com\\'>Other</a></p>', {link_style: 'reference'})\n    expect(result.to_s).to include('Example')\n    expect(result.to_s).to include('Other')\n    expect(result.to_s).to include('example.com')\n  end\n\n  it 'options_list_custom_bullets: Custom bullet character for unordered lists' do\n    result = HtmlToMarkdown.convert('<ul><li>Item A</li><li>Item B</li></ul>', {bullets: '*'})\n    expect(result.to_s).to include('* Item A')\n    expect(result.to_s).to include('* Item B')\n  end\n\n  it 'options_list_indent_tabs: Tab indentation type for nested list items' do\n    result = HtmlToMarkdown.convert('<ul><li>Parent<ul><li>Child</li></ul></li></ul>', {list_indent_type: 'tabs'})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Parent')\n    expect(result.to_s).to include('Child')\n  end\n\n  it 'options_list_indent_width_four: Nested lists indented with 4 spaces per level' do\n    result = HtmlToMarkdown.convert('<ul><li>Outer<ul><li>Inner</li></ul></li></ul>', {list_indent_width: 4})\n    expect(result.to_s).to include('Outer')\n    expect(result.to_s).to include('Inner')\n  end\n\n  it 'options_max_depth_default_unlimited: Default max_depth (null) converts deeply nested content fully' do\n    result = HtmlToMarkdown.convert('<div><div><div><div><p>Deep content</p></div></div></div></div>')\n    expect(result.to_s).to include('Deep content')\n  end\n\n  it 'options_max_depth_truncates: max_depth truncates content beyond the specified depth' do\n    result = HtmlToMarkdown.convert('<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>', {max_depth: 3})\n    expect(result.to_s).to include('Shallow')\n    expect(result.to_s).not_to include('Too deep')\n  end\n\n  it 'options_max_depth_zero_empty: max_depth of 0 produces empty output' do\n    result = HtmlToMarkdown.convert('<p>Hello</p>', {max_depth: 0})\n    expect(result.strip).to eq('')\n  end\n\n  it 'options_newline_backslash: Hard line breaks rendered with backslash' do\n    result = HtmlToMarkdown.convert('<p>Line one<br>Line two</p>', {newline_style: 'backslash'})\n    expect(result.to_s).to include('Line one')\n    expect(result.to_s).to include('Line two')\n  end\n\n  it 'options_newline_spaces: Hard line breaks rendered with trailing spaces' do\n    result = HtmlToMarkdown.convert('<p>First<br>Second</p>', {newline_style: 'spaces'})\n    expect(result.to_s).to include('First')\n    expect(result.to_s).to include('Second')\n  end\n\n  it 'options_output_format_djot: Djot output format produces djot-compatible markup' do\n    result = HtmlToMarkdown.convert('<p>Simple paragraph.</p>', {output_format: 'djot'})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Simple paragraph.')\n  end\n\n  it 'options_output_format_markdown: Default markdown output format produces standard markdown' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>Some text.</p>', {heading_style: 'atx', output_format: 'markdown'})\n    expect(result.to_s).to include('# Title')\n    expect(result.to_s).to include('Some text.')\n  end\n\n  it 'options_output_format_plain: Plain text output format strips markdown syntax' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>Some <strong>bold</strong> text.</p>', {output_format: 'plain'})\n    expect(result.to_s).to include('Title')\n    expect(result.to_s).to include('bold')\n    expect(result.to_s).to include('text.')\n  end\n\n  it 'options_preprocessing_aggressive: Aggressive preset removes nav, footer, aside unconditionally' do\n    result = HtmlToMarkdown.convert('<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>', {preprocessing: { 'preset' => 'Aggressive' }})\n    expect(result.to_s).to include('Title')\n    expect(result.to_s).to include('Content')\n    expect(result.to_s).not_to include('Menu')\n  end\n\n  it 'options_preprocessing_minimal: Minimal preset preserves nav, footer, aside' do\n    result = HtmlToMarkdown.convert('<nav>Navigation</nav><p>Content</p><footer>Footer</footer>', {preprocessing: { 'preset' => 'Minimal' }})\n    expect(result.to_s).to include('Navigation')\n    expect(result.to_s).to include('Content')\n    expect(result.to_s).to include('Footer')\n  end\n\n  it 'options_preprocessing_remove_forms: Forms are removed when remove_forms is true' do\n    result = HtmlToMarkdown.convert('<p>Before</p><form><input type=\\'text\\'/><button>Submit</button></form><p>After</p>', {preprocessing: { 'removeForms' => true }})\n    expect(result.to_s).to include('Before')\n    expect(result.to_s).to include('After')\n    expect(result.to_s).not_to include('Submit')\n  end\n\n  it 'options_preserve_tags_iframe: Iframe tags preserved as raw HTML in output' do\n    result = HtmlToMarkdown.convert('<p>Before</p><iframe src=\\'video.html\\' width=\\'560\\'></iframe><p>After</p>', {preserve_tags: ['iframe']})\n    expect(result.to_s).to include('Before')\n    expect(result.to_s).to include('After')\n    expect(result.to_s).to include('<iframe')\n  end\n\n  it 'options_skip_images_true: Images are omitted from output when skipImages is true' do\n    result = HtmlToMarkdown.convert('<p>Before <img src=\\'test.jpg\\' alt=\\'photo\\'> After</p>', {skip_images: true})\n    expect(result.to_s).to include('Before')\n    expect(result.to_s).to include('After')\n    expect(result.to_s).not_to include('photo')\n  end\n\n  it 'options_strip_newlines: Strip newlines produces single-line paragraphs' do\n    result = HtmlToMarkdown.convert('<p>First paragraph.</p><p>Second paragraph.</p>', {strip_newlines: true})\n    expect(result.to_s).to include('First paragraph.')\n    expect(result.to_s).to include('Second paragraph.')\n  end\n\n  it 'options_strip_tags_div_span: Div and span tags stripped but content preserved' do\n    result = HtmlToMarkdown.convert('<div class=\\'wrapper\\'><p>Inside div</p></div><p>Outside <span class=\\'hl\\'>span text</span></p>', {strip_tags: ['div', 'span']})\n    expect(result.to_s).to include('Inside div')\n    expect(result.to_s).to include('span text')\n  end\n\n  it 'options_strong_em_underscore: Strong and em tags use underscore symbol instead of asterisk' do\n    result = HtmlToMarkdown.convert('<p><strong>bold</strong> and <em>italic</em></p>', {strong_em_symbol: '_'})\n    expect(result.to_s).to include('__bold__')\n    expect(result.to_s).to include('_italic_')\n  end\n\n  it 'options_sub_symbol_tilde: Subscript rendered with tilde symbol' do\n    result = HtmlToMarkdown.convert('<p>H<sub>2</sub>O</p>', {sub_symbol: '~'})\n    expect(result.to_s).to include('~2~')\n  end\n\n  it 'options_sup_symbol_caret: Superscript rendered with caret symbol' do\n    result = HtmlToMarkdown.convert('<p>x<sup>2</sup></p>', {sup_symbol: '^'})\n    expect(result.to_s).to include('^2^')\n  end\n\n  it 'options_whitespace_normalized: Normalized whitespace mode collapses multiple spaces' do\n    result = HtmlToMarkdown.convert('<p>Text   with    extra   spaces.</p>', {whitespace_mode: 'normalized'})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Text')\n    expect(result.to_s).to include('with')\n    expect(result.to_s).to include('extra')\n    expect(result.to_s).to include('spaces.')\n  end\n\n  it 'options_whitespace_strict: Strict whitespace mode preserves whitespace as-is' do\n    result = HtmlToMarkdown.convert('<p>Preserved   spacing.</p>', {whitespace_mode: 'strict'})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Preserved')\n    expect(result.to_s).to include('spacing.')\n  end\n\n  it 'options_wrap_disabled: Wrap option disabled preserves long lines without breaking' do\n    result = HtmlToMarkdown.convert('<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>', {wrap: false})\n    expect(result.to_s).to include('This is a long paragraph that should not be wrapped at all because wrapping is disabled.')\n  end\n\n  it 'options_wrap_enabled: Wrap option enabled with custom width wraps long lines' do\n    result = HtmlToMarkdown.convert('<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>', {wrap: true, wrap_width: 40})\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('This is a long paragraph')\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/real_world_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:0ac7a58b3feabafe413963259db5718252c43f0b40f9534bb8df5b954544faf7\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'real-world' do\n  it 'real_world_blog_post: Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown' do\n    result = HtmlToMarkdown.convert(\"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\")\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('# Getting Started with Rust')\n    expect(result.to_s).to include('## Installation')\n    expect(result.to_s).to include('## Hello World')\n    expect(result.to_s).to include('## Key Concepts')\n    expect(result.to_s).to include('cargo run')\n    expect(result.to_s).to include('[Mozilla](https://www.mozilla.org)')\n    expect(result.to_s).to include('- Ownership and borrowing')\n  end\n\n  it 'real_world_documentation_page: Documentation page with nested lists, code examples, and blockquotes converts correctly' do\n    result = HtmlToMarkdown.convert(\"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"\\# Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration values.</p></blockquote></div>\")\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('# API Reference')\n    expect(result.to_s).to include('## convert_html')\n    expect(result.to_s).to include('### Parameters')\n    expect(result.to_s).to include('### Returns')\n    expect(result.to_s).to include('### Example')\n    expect(result.to_s).to include('## ConversionOptions')\n    expect(result.to_s).to include('> ')\n    expect(result.to_s).to include('thread-safe')\n    expect(result.to_s).to include('convert_html')\n    expect(result.to_s).to include('ConversionOptions')\n  end\n\n  it 'real_world_product_page: Product page with table, images, and lists converts correctly' do\n    result = HtmlToMarkdown.convert('<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What\\'s in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('# Wireless Keyboard Pro')\n    expect(result.to_s).to include('![Wireless Keyboard Pro](https://example.com/keyboard.jpg)')\n    expect(result.to_s).to include('## Specifications')\n    expect(result.to_s).to include('Battery Life')\n    expect(result.to_s).to include('12 months')\n    expect(result.to_s).to include('Bluetooth 5.0')\n    expect(result.to_s).to include('## What\\'s in the Box')\n    expect(result.to_s).to include('USB-C charging cable')\n    expect(result.to_s).to include('|')\n    expect(result.to_s).to include('---')\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/result_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6d9da64bad3a9323a655d38d6ca50a002fc23cd2ae7341d19a74fca3bb63a566\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'result' do\n  it 'result_tables_empty_when_no_tables: Result tables array is empty when input has no tables' do\n    result = HtmlToMarkdown.convert('<p>No tables here</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n    expect(result.length).to eq(0)\n  end\n\n  it 'result_tables_multiple: Multiple tables each appear in the tables array' do\n    result = HtmlToMarkdown.convert('<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>', {include_document_structure: true})\n    expect(result.length).to be >= 2\n  end\n\n  it 'result_tables_simple: Simple table populates the tables array in result' do\n    result = HtmlToMarkdown.convert('<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>', {include_document_structure: true})\n    expect(result).not_to be_empty\n    expect(result.length).to be >= 1\n  end\n\n  it 'result_tables_without_structure_flag: Tables array is empty when includeDocumentStructure is false' do\n    result = HtmlToMarkdown.convert('<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>')\n    expect(result).not_to be_empty\n    expect(result.length).to eq(0)\n  end\n\n  it 'result_warnings_empty_for_clean_input: Warnings array is empty for well-formed HTML without problematic content' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>Clean content with <a href=\\'https://example.com\\'>a link</a>.</p>')\n    expect(result).not_to be_empty\n    expect(result.length).to eq(0)\n  end\n\n  it 'result_warnings_empty_for_complex_input: Warnings array is empty for complex but valid HTML' do\n    result = HtmlToMarkdown.convert('<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>')\n    expect(result).not_to be_empty\n    expect(result.length).to eq(0)\n  end\n\n  it 'result_warnings_empty_for_malformed_html: Warnings array is empty even for malformed HTML (parser is lenient)' do\n    result = HtmlToMarkdown.convert('<p>Unclosed paragraph<div>Mixed nesting</p></div>')\n    expect(result).not_to be_empty\n    expect(result.length).to eq(0)\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/smoke_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:36fcde8236bdfe529126ee0890612361167b6d455a2d948aef6fbe43aa973d93\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'smoke' do\n  it 'smoke_empty_string: Empty string produces empty output' do\n    result = HtmlToMarkdown.convert('')\n    expect(result.strip).to eq('')\n  end\n\n  it 'smoke_simple_heading: H1 heading converts to ATX markdown' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1>')\n    expect(result.to_s).to include('# Title')\n  end\n\n  it 'smoke_simple_paragraph: Simple paragraph converts correctly' do\n    result = HtmlToMarkdown.convert('<p>Hello World</p>')\n    expect(result.strip).to eq('Hello World')\n    expect(result).not_to be_empty\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/structure_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:aa3cacddef2a11efc37a6fcff510ecd15e1832052adf5b21fe2134d3fc9e5092\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'structure' do\n  it 'structure_code_block: Fenced code block produces Code node' do\n    result = HtmlToMarkdown.convert('<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_deep_nesting_h1_h2_h3: H1 > H2 > H3 creates three levels of heading nesting' do\n    result = HtmlToMarkdown.convert('<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_h1_h2_nested_group: H1 followed by H2 creates a nested group under the H1' do\n    result = HtmlToMarkdown.convert('<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_heading_paragraph: Simple heading followed by paragraph produces Heading and Paragraph nodes' do\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>A paragraph of text.</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_list: Unordered list produces List and ListItem nodes' do\n    result = HtmlToMarkdown.convert('<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_multiple_headings: Multiple headings create multiple Heading nodes with correct levels' do\n    result = HtmlToMarkdown.convert('<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\n\n  it 'structure_sibling_h1_groups: H1, H2, then another H1 creates two sibling top-level groups' do\n    result = HtmlToMarkdown.convert('<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>', {include_document_structure: true})\n    expect(result).not_to be_empty\n  end\nend\n"
  },
  {
    "path": "e2e/ruby/spec/visitor_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:dbc25fe6abb5779d4b9852bcc760a116d9b64c9f4ab923eb86ac3b4f5285632a\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown'\nrequire 'json'\n\nRSpec.describe 'visitor' do\n  it 'visitor_audio_custom: Visitor replaces audio element with custom output' do\n    visitor = Class.new do\n      def visit_audio(ctx, src)\n        { custom: '[AUDIO: podcast.mp3]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>', visitor)\n    expect(result.to_s).to include('[AUDIO: podcast.mp3]')\n    expect(result.to_s).to include('Listen to this:')\n  end\n\n  it 'visitor_audio_skip: Visitor removes audio elements from output' do\n    visitor = Class.new do\n      def visit_audio(ctx, src)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>', visitor)\n    expect(result.to_s).to include('Background music:')\n    expect(result.to_s).to include('Enjoy!')\n    expect(result.to_s).not_to include('music.ogg')\n  end\n\n  it 'visitor_button_custom: Visitor replaces button with bracketed text' do\n    visitor = Class.new do\n      def visit_button(ctx, text)\n        { custom: '[BTN:{text}]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>', visitor)\n    expect(result.to_s).to include('[BTN:Click me]')\n    expect(result.to_s).to include('[BTN:Cancel]')\n    expect(result.to_s).to include('Confirm action:')\n  end\n\n  it 'visitor_button_skip: Visitor removes all buttons from output' do\n    visitor = Class.new do\n      def visit_button(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>', visitor)\n    expect(result.to_s).to include('Actions available:')\n    expect(result.to_s).not_to include('Save')\n    expect(result.to_s).not_to include('Delete')\n    expect(result.to_s).not_to include('Export')\n  end\n\n  it 'visitor_continue_default: Visitor continue action preserves default conversion' do\n    visitor = Class.new do\n      def visit_strong(ctx, text)\n        'continue'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Hello <strong>World</strong></p>', visitor)\n    expect(result.to_s).to include('**World**')\n  end\n\n  it 'visitor_custom_blockquote: Visitor replaces blockquote with custom format' do\n    visitor = Class.new do\n      def visit_blockquote(ctx, content, depth)\n        { custom: 'QUOTE: \"{content}\"' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<blockquote><p>A wise quote.</p></blockquote>', visitor)\n    expect(result.to_s).to include('QUOTE:')\n    expect(result.to_s).to include('A wise quote.')\n  end\n\n  it 'visitor_custom_emphasis: Visitor replaces emphasis with custom output' do\n    visitor = Class.new do\n      def visit_emphasis(ctx, text)\n        { custom: '>>>{text}<<<' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>This is <em>important</em> text.</p>', visitor)\n    expect(result.to_s).to include('>>>important<<<')\n    expect(result.to_s).not_to include('*important*')\n  end\n\n  it 'visitor_custom_heading: Visitor replaces heading with custom format' do\n    visitor = Class.new do\n      def visit_heading(ctx, level, text, id)\n        { custom: '--- {text} ---' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h2>Section Title</h2><p>Content below heading.</p>', visitor)\n    expect(result.to_s).to include('--- Section Title ---')\n    expect(result.to_s).not_to include('## Section Title')\n    expect(result.to_s).to include('Content below heading.')\n  end\n\n  it 'visitor_custom_image: Visitor replaces image with custom output using template' do\n    visitor = Class.new do\n      def visit_image(ctx, src, alt, title)\n        { custom: '[Image: {alt}]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<img src=\"banner.png\" alt=\"Banner\">', visitor)\n    expect(result.to_s).to include('[Image: Banner]')\n    expect(result.to_s).not_to include('banner.png')\n  end\n\n  it 'visitor_custom_link_format: Visitor reformats links using template interpolation' do\n    visitor = Class.new do\n      def visit_link(ctx, href, text, title)\n        { custom: '{text} ({href})' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>', visitor)\n    expect(result.to_s).to include('Example (https://example.com)')\n    expect(result.to_s).not_to include('[Example]')\n  end\n\n  it 'visitor_custom_link_static: Visitor replaces link with static custom output' do\n    visitor = Class.new do\n      def visit_link(ctx, href, text, title)\n        { custom: '[REDACTED LINK]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<a href=\"https://example.com\">Click here</a>', visitor)\n    expect(result.to_s).to include('[REDACTED LINK]')\n    expect(result.to_s).not_to include('example.com')\n  end\n\n  it 'visitor_custom_output: Visitor custom action replaces element output' do\n    visitor = Class.new do\n      def visit_heading(ctx, level, text, id)\n        { custom: '## REPLACED HEADING' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h1>Original Heading</h1>', visitor)\n    expect(result.to_s).to include('## REPLACED HEADING')\n    expect(result.to_s).not_to include('# Original Heading')\n  end\n\n  it 'visitor_definition_list_custom: Visitor customizes definition list items' do\n    visitor = Class.new do\n      def visit_definition_term(ctx, text)\n        { custom: '**{text}**' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>', visitor)\n    expect(result.to_s).to include('**HTML**')\n    expect(result.to_s).to include('**CSS**')\n    expect(result.to_s).to include('HyperText Markup Language')\n  end\n\n  it 'visitor_definition_list_custom_format: Visitor formats definition lists with custom templates' do\n    visitor = Class.new do\n      def visit_definition_term(ctx, text)\n        { custom: '### {text}' }\n      end\n      def visit_definition_description(ctx, text)\n        { custom: '> {text}' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>', visitor)\n    expect(result.to_s).to include('### Python')\n    expect(result.to_s).to include('### JavaScript')\n    expect(result.to_s).to include('> A high-level programming language')\n    expect(result.to_s).to include('> A scripting language for web browsers')\n  end\n\n  it 'visitor_definition_list_skip: Visitor skips definition list items from output' do\n    visitor = Class.new do\n      def visit_definition_description(ctx, text)\n        'skip'\n      end\n      def visit_definition_term(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>', visitor)\n    expect(result.to_s).to include('Glossary:')\n    expect(result.to_s).to include('End of glossary')\n    expect(result.to_s).not_to include('Term A')\n    expect(result.to_s).not_to include('Definition')\n  end\n\n  it 'visitor_details_summary_custom: Visitor customizes details/summary disclosure elements' do\n    visitor = Class.new do\n      def visit_summary(ctx, text)\n        { custom: '[EXPANDABLE] {text}' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>', visitor)\n    expect(result.to_s).to include('[EXPANDABLE] Click to expand')\n    expect(result.to_s).to include('This content is initially hidden.')\n  end\n\n  it 'visitor_details_summary_skip: Visitor removes details/summary elements entirely' do\n    visitor = Class.new do\n      def visit_details(ctx, is_open)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>', visitor)\n    expect(result.to_s).to include('Main content here.')\n    expect(result.to_s).to include('More main content.')\n    expect(result.to_s).not_to include('Hidden section')\n    expect(result.to_s).not_to include('Secret details')\n  end\n\n  it 'visitor_figure_custom: Visitor customizes figure and figcaption elements' do\n    visitor = Class.new do\n      def visit_figcaption(ctx, text)\n        { custom: '*{text}*' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>', visitor)\n    expect(result.to_s).to include('Article Title')\n    expect(result.to_s).to include('*Figure 1: System Architecture*')\n    expect(result.to_s).to include('Explanation of the figure.')\n  end\n\n  it 'visitor_figure_custom_wrap: Visitor wraps figure content with custom formatting' do\n    visitor = Class.new do\n      def visit_figure_start(ctx)\n        { custom: \"\\n[FIGURE]\\n\" }\n      end\n      def visit_figure_end(ctx, output)\n        { custom: \"{output}\\n[/FIGURE]\\n\" }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>', visitor)\n    expect(result.to_s).to include('[FIGURE]')\n    expect(result.to_s).to include('[/FIGURE]')\n    expect(result.to_s).to include('Gallery')\n  end\n\n  it 'visitor_figure_skip: Visitor removes figure elements with their captions' do\n    visitor = Class.new do\n      def visit_figure_start(ctx)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>', visitor)\n    expect(result.to_s).to include('See the chart below:')\n    expect(result.to_s).to include('As shown in the chart above.')\n    expect(result.to_s).not_to include('Revenue Trends')\n    expect(result.to_s).not_to include('chart.svg')\n  end\n\n  it 'visitor_form_custom: Visitor replaces form with custom output' do\n    visitor = Class.new do\n      def visit_form(ctx, action_url, method)\n        { custom: '[FORM PLACEHOLDER]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>', visitor)\n    expect(result.to_s).to include('[FORM PLACEHOLDER]')\n    expect(result.to_s).not_to include('submit')\n    expect(result.to_s).not_to include('input')\n  end\n\n  it 'visitor_form_skip: Visitor skips form elements entirely' do\n    visitor = Class.new do\n      def visit_form(ctx, action_url, method)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>', visitor)\n    expect(result.to_s).to include('Before form')\n    expect(result.to_s).to include('After form')\n    expect(result.to_s).not_to include('email')\n  end\n\n  it 'visitor_horizontal_rule_custom: Visitor replaces horizontal rule with custom output' do\n    visitor = Class.new do\n      def visit_horizontal_rule(ctx)\n        { custom: \"\\n[DIVIDER]\\n\" }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>', visitor)\n    expect(result.to_s).to include('[DIVIDER]')\n    expect(result.to_s).to include('Section A')\n    expect(result.to_s).to include('Section B')\n    expect(result.to_s).not_to include('---')\n  end\n\n  it 'visitor_horizontal_rule_skip: Visitor removes all horizontal rules' do\n    visitor = Class.new do\n      def visit_horizontal_rule(ctx)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>', visitor)\n    expect(result.to_s).to include('Part 1')\n    expect(result.to_s).to include('Part 2')\n    expect(result.to_s).to include('Part 3')\n    expect(result.to_s).not_to include('---')\n  end\n\n  it 'visitor_iframe_custom: Visitor replaces embedded iframe with custom text' do\n    visitor = Class.new do\n      def visit_iframe(ctx, src)\n        { custom: '[EMBEDDED: https://maps.example.com/embed]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>', visitor)\n    expect(result.to_s).to include('[EMBEDDED: https://maps.example.com/embed]')\n    expect(result.to_s).to include('Embedded map:')\n    expect(result.to_s).to include('End of map')\n  end\n\n  it 'visitor_iframe_skip: Visitor removes embedded iframes' do\n    visitor = Class.new do\n      def visit_iframe(ctx, src)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>', visitor)\n    expect(result.to_s).to include('Reviews')\n    expect(result.to_s).to include('See reviews from our partners.')\n    expect(result.to_s).not_to include('widget.example.com')\n  end\n\n  it 'visitor_input_custom: Visitor replaces input with labeled output' do\n    visitor = Class.new do\n      def visit_input(ctx, input_type, name, value)\n        { custom: '[INPUT:{input_type}]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>', visitor)\n    expect(result.to_s).to include('[INPUT:text]')\n    expect(result.to_s).to include('[INPUT:password]')\n  end\n\n  it 'visitor_input_skip: Visitor skips all input elements' do\n    visitor = Class.new do\n      def visit_input(ctx, input_type, name, value)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>', visitor)\n    expect(result.to_s).to include('Sign up:')\n    expect(result.to_s).to include('Continue')\n    expect(result.to_s).not_to include('email')\n  end\n\n  it 'visitor_line_break_custom: Visitor replaces line break with custom output' do\n    visitor = Class.new do\n      def visit_line_break(ctx)\n        { custom: ' | ' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>First line<br>Second line<br>Third line</p>', visitor)\n    expect(result.to_s).to include('First line | Second line | Third line')\n    expect(result.to_s).not_to include(\"\\n\\n\")\n  end\n\n  it 'visitor_line_break_skip: Visitor removes all line breaks' do\n    visitor = Class.new do\n      def visit_line_break(ctx)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>', visitor)\n    expect(result.to_s).to include('Address Line 1Address Line 2Address Line 3')\n  end\n\n  it 'visitor_mark_custom: Visitor replaces highlight/mark with custom template' do\n    visitor = Class.new do\n      def visit_mark(ctx, text)\n        { custom: '=={text}==' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>This is a <mark>highlighted passage</mark> in the text.</p>', visitor)\n    expect(result.to_s).to include('==highlighted passage==')\n    expect(result.to_s).to include('This is a')\n  end\n\n  it 'visitor_mark_skip: Visitor skips mark elements entirely' do\n    visitor = Class.new do\n      def visit_mark(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Key insight: <mark>always validate input</mark> for security.</p>', visitor)\n    expect(result.to_s).not_to include('always validate input')\n    expect(result.to_s).to include('Key insight:')\n    expect(result.to_s).to include('for security.')\n  end\n\n  it 'visitor_preserve_html: Visitor preserve_html action includes raw HTML in output' do\n    visitor = Class.new do\n      def visit_custom_element(ctx, tag_name, html)\n        'preserve_html'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<div><custom-tag>Custom content</custom-tag></div>', visitor)\n    expect(result.to_s).to include('<custom-tag>')\n  end\n\n  it 'visitor_skip_all_headings: Visitor skips all headings from document' do\n    visitor = Class.new do\n      def visit_heading(ctx, level, text, id)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>Body text remains.</p>', visitor)\n    expect(result.to_s).not_to include('Title')\n    expect(result.to_s).to include('Body text remains.')\n  end\n\n  it 'visitor_skip_code_blocks: Visitor skips code blocks from output' do\n    visitor = Class.new do\n      def visit_code_block(ctx, lang, code)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>', visitor)\n    expect(result.to_s).to include('Intro text')\n    expect(result.to_s).to include('Outro text')\n    expect(result.to_s).not_to include('let x = 42')\n  end\n\n  it 'visitor_skip_heading: Visitor skip action omits all headings from output' do\n    visitor = Class.new do\n      def visit_heading(ctx, level, text, id)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h1>Title</h1><p>Body text remains.</p>', visitor)\n    expect(result.to_s).not_to include('Title')\n    expect(result.to_s).to include('Body text remains.')\n  end\n\n  it 'visitor_skip_images: Visitor skips all images from output' do\n    visitor = Class.new do\n      def visit_image(ctx, src, alt, title)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>', visitor)\n    expect(result.to_s).to include('Before image')\n    expect(result.to_s).to include('After image')\n    expect(result.to_s).not_to include('photo.jpg')\n    expect(result.to_s).not_to include('A photo')\n  end\n\n  it 'visitor_skip_links: Visitor skips all links entirely' do\n    visitor = Class.new do\n      def visit_link(ctx, href, text, title)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Before <a href=\"https://example.com\">link text</a> after</p>', visitor)\n    expect(result.to_s).not_to include('link text')\n    expect(result.to_s).not_to include('example.com')\n  end\n\n  it 'visitor_skip_strong: Visitor skips bold/strong elements' do\n    visitor = Class.new do\n      def visit_strong(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Normal <strong>bold text</strong> normal</p>', visitor)\n    expect(result.to_s).not_to include('bold text')\n    expect(result.to_s).to include('Normal')\n  end\n\n  it 'visitor_subscript_custom: Visitor replaces subscript with custom template' do\n    visitor = Class.new do\n      def visit_subscript(ctx, text)\n        { custom: '~{text}~' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>H<sub>2</sub>O is water.</p>', visitor)\n    expect(result.to_s).to include('H~2~O')\n    expect(result.to_s).to include('is water')\n  end\n\n  it 'visitor_subscript_skip: Visitor skips subscript elements entirely' do\n    visitor = Class.new do\n      def visit_subscript(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>', visitor)\n    expect(result.to_s).to include('The formula CHO is sugar.')\n  end\n\n  it 'visitor_superscript_custom: Visitor replaces superscript with custom template' do\n    visitor = Class.new do\n      def visit_superscript(ctx, text)\n        { custom: '^{text}^' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Einstein\\'s E=mc<sup>2</sup> revolutionized physics.</p>', visitor)\n    expect(result.to_s).to include('E=mc^2^')\n    expect(result.to_s).to include('revolutionized physics')\n  end\n\n  it 'visitor_superscript_skip: Visitor skips superscript from output' do\n    visitor = Class.new do\n      def visit_superscript(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>', visitor)\n    expect(result.to_s).to include('The equation x + y = z has no solutions.')\n  end\n\n  it 'visitor_underline_custom: Visitor replaces underline with custom markup' do\n    visitor = Class.new do\n      def visit_underline(ctx, text)\n        { custom: '_{text}_' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>This is <u>very important</u> text.</p>', visitor)\n    expect(result.to_s).to include('_very important_')\n    expect(result.to_s).not_to include('**')\n  end\n\n  it 'visitor_underline_skip: Visitor skips underline elements from output' do\n    visitor = Class.new do\n      def visit_underline(ctx, text)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Normal text with <u>underlined part</u> and more text.</p>', visitor)\n    expect(result.to_s).to include('Normal text with')\n    expect(result.to_s).to include('and more text.')\n    expect(result.to_s).not_to include('underlined part')\n  end\n\n  it 'visitor_video_custom: Visitor replaces video with custom link' do\n    visitor = Class.new do\n      def visit_video(ctx, src)\n        { custom: '[VIDEO: {src}]' }\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>', visitor)\n    expect(result.to_s).to include('[VIDEO: tutorial.mp4]')\n    expect(result.to_s).to include('Watch our tutorial:')\n    expect(result.to_s).to include('Great content!')\n  end\n\n  it 'visitor_video_skip: Visitor removes video elements entirely' do\n    visitor = Class.new do\n      def visit_video(ctx, src)\n        'skip'\n      end\n    end.new\n    result = HtmlToMarkdown.convert('<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>', visitor)\n    expect(result.to_s).to include('Demo')\n    expect(result.to_s).to include('See the demo above.')\n    expect(result.to_s).not_to include('demo.webm')\n  end\nend\n"
  },
  {
    "path": "e2e/rust/Cargo.toml",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:b24e2cb7d3acaf9a4b79109ef557edb4d5179e89b434490464179c622db754de\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\n[workspace]\n\n[package]\nname = \"html_to_markdown_rs-e2e-rust\"\nversion = \"3.4.0-rc.25\"\nedition = \"2021\"\nlicense = \"MIT\"\npublish = false\n\n[dependencies]\nhtml_to_markdown_rs = { package = \"html-to-markdown-rs\", path = \"../../crates/html-to-markdown\", default-features = false, features = [\n  \"full\",\n  \"metadata\",\n  \"visitor\",\n  \"serde\",\n  \"inline-images\",\n] }\nserde_json = \"1\"\n\n[package.metadata.cargo-machete]\nignored = [\"serde_json\"]\n"
  },
  {
    "path": "e2e/rust/tests/conversion_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1a3e5af1ae836b26383f22033b7050fcfab4e461e1625a7950175341efcf5820\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: conversion\n\nuse html_to_markdown_rs::convert;\n\n#[test]\nfn test_blockquote_multiple_paragraphs() {\n    // Blockquote with multiple paragraphs has each paragraph prefixed\n    let html = r#\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> First paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"> First paragraph.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> Second paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"> Second paragraph.\"#\n    );\n}\n\n#[test]\nfn test_blockquote_nested() {\n    // Nested blockquote produces double-prefixed lines\n    let html = r#\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Outer quote.\"#),\n        \"expected to contain: {}\",\n        r#\"Outer quote.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Inner quote.\"#),\n        \"expected to contain: {}\",\n        r#\"Inner quote.\"#\n    );\n}\n\n#[test]\nfn test_blockquote_simple() {\n    // Simple blockquote\n    let html = r#\"<blockquote><p>Quote text</p></blockquote>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> Quote text\"#),\n        \"expected to contain: {}\",\n        r#\"> Quote text\"#\n    );\n}\n\n#[test]\nfn test_blockquote_with_list() {\n    // Blockquote containing a list preserves list items inside quote\n    let html = r#\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Quote intro:\"#),\n        \"expected to contain: {}\",\n        r#\"Quote intro:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Point one\"#),\n        \"expected to contain: {}\",\n        r#\"Point one\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Point two\"#),\n        \"expected to contain: {}\",\n        r#\"Point two\"#\n    );\n}\n\n#[test]\nfn test_bold_and_italic() {\n    // Nested bold and italic\n    let html = r#\"<p><strong><em>both</em></strong></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"***both***\"#),\n        \"expected to contain: {}\",\n        r#\"***both***\"#\n    );\n}\n\n#[test]\nfn test_bold_strong() {\n    // Strong tag converts to bold\n    let html = r#\"<p><strong>bold</strong></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**bold**\"#),\n        \"expected to contain: {}\",\n        r#\"**bold**\"#\n    );\n}\n\n#[test]\nfn test_code_block() {\n    // Code block with language preserves content\n    let html = r#\"<pre><code class=\"language-python\">print('hello')</code></pre>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"print('hello')\"#),\n        \"expected to contain: {}\",\n        r#\"print('hello')\"#\n    );\n}\n\n#[test]\nfn test_code_block_no_language() {\n    // Code block without a language class preserves content\n    let html = r#\"<pre><code>plain code here</code></pre>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"plain code here\"#),\n        \"expected to contain: {}\",\n        r#\"plain code here\"#\n    );\n}\n\n#[test]\nfn test_code_inline_in_paragraph() {\n    // Inline code element nested inside a paragraph\n    let html = r#\"<p>Call the <code>initialize()</code> method first.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"`initialize()`\"#),\n        \"expected to contain: {}\",\n        r#\"`initialize()`\"#\n    );\n}\n\n#[test]\nfn test_code_with_backticks_in_content() {\n    // Inline code containing backtick characters is properly escaped\n    let html = r#\"<p>Use <code>`backtick` here</code> carefully.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"backtick\"#),\n        \"expected to contain: {}\",\n        r#\"backtick\"#\n    );\n}\n\n#[test]\nfn test_emphasis_mark_highlight() {\n    // mark tag produces highlighted output\n    let html = r#\"<p><mark>highlighted</mark></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"highlighted\"#),\n        \"expected to contain: {}\",\n        r#\"highlighted\"#\n    );\n}\n\n#[test]\nfn test_emphasis_strikethrough_del() {\n    // del tag converts to GFM strikethrough\n    let html = r#\"<p><del>deleted text</del></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"~~deleted text~~\"#),\n        \"expected to contain: {}\",\n        r#\"~~deleted text~~\"#\n    );\n}\n\n#[test]\nfn test_emphasis_strikethrough_s() {\n    // s tag converts to GFM strikethrough\n    let html = r#\"<p><s>strikethrough</s></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"~~strikethrough~~\"#),\n        \"expected to contain: {}\",\n        r#\"~~strikethrough~~\"#\n    );\n}\n\n#[test]\nfn test_emphasis_subscript() {\n    // sub tag content is preserved\n    let html = r#\"<p>H<sub>2</sub>O</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"H\"#),\n        \"expected to contain: {}\",\n        r#\"H\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2\"#),\n        \"expected to contain: {}\",\n        r#\"2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"O\"#),\n        \"expected to contain: {}\",\n        r#\"O\"#\n    );\n}\n\n#[test]\nfn test_emphasis_superscript() {\n    // sup tag content is preserved\n    let html = r#\"<p>x<sup>2</sup></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"x\"#),\n        \"expected to contain: {}\",\n        r#\"x\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2\"#),\n        \"expected to contain: {}\",\n        r#\"2\"#\n    );\n}\n\n#[test]\nfn test_emphasis_underline_u() {\n    // u tag content is preserved in output\n    let html = r#\"<p><u>underlined</u></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"underlined\"#),\n        \"expected to contain: {}\",\n        r#\"underlined\"#\n    );\n}\n\n#[test]\nfn test_form_input_elements() {\n    // Form input elements produce readable output without form mechanics\n    let html = r#\"<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Name\"#),\n        \"expected to contain: {}\",\n        r#\"Name\"#\n    );\n}\n\n#[test]\nfn test_form_select_options() {\n    // Select element with options produces readable output\n    let html = r#\"<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Color\"#),\n        \"expected to contain: {}\",\n        r#\"Color\"#\n    );\n}\n\n#[test]\nfn test_form_textarea() {\n    // Textarea element produces readable output\n    let html = r#\"<form><label>Message:</label><textarea>Default text content</textarea></form>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Message\"#),\n        \"expected to contain: {}\",\n        r#\"Message\"#\n    );\n}\n\n#[test]\nfn test_heading_h1() {\n    // H1 heading\n    let html = r#\"<h1>Heading 1</h1>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"# Heading 1\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_heading_h2() {\n    // H2 heading\n    let html = r#\"<h2>Heading 2</h2>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"## Heading 2\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_heading_h3() {\n    // H3 heading\n    let html = r#\"<h3>Heading 3</h3>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"### Heading 3\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_heading_h4() {\n    // H4 heading\n    let html = r#\"<h4>Heading 4</h4>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"#### Heading 4\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_heading_h5() {\n    // H5 heading\n    let html = r#\"<h5>Heading 5</h5>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"##### Heading 5\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_heading_h6() {\n    // H6 heading\n    let html = r#\"<h6>Heading 6</h6>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"###### Heading 6\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_image_figure_figcaption() {\n    // Figure with figcaption preserves both image and caption\n    let html = r#\"<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![A sunset](sunset.jpg)\"#),\n        \"expected to contain: {}\",\n        r#\"![A sunset](sunset.jpg)\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Beautiful sunset over the ocean\"#),\n        \"expected to contain: {}\",\n        r#\"Beautiful sunset over the ocean\"#\n    );\n}\n\n#[test]\nfn test_image_linked() {\n    // Image inside an anchor produces a linked image\n    let html = r#\"<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![Icon](icon.png)\"#),\n        \"expected to contain: {}\",\n        r#\"![Icon](icon.png)\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"https://example.com\"#),\n        \"expected to contain: {}\",\n        r#\"https://example.com\"#\n    );\n}\n\n#[test]\nfn test_image_no_alt() {\n    // Image without alt text produces image markdown\n    let html = r#\"<img src=\"banner.jpg\">\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"banner.jpg\"#),\n        \"expected to contain: {}\",\n        r#\"banner.jpg\"#\n    );\n}\n\n#[test]\nfn test_image_simple() {\n    // Image with alt text\n    let html = r#\"<img src=\"photo.jpg\" alt=\"A photo\">\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![A photo](photo.jpg)\"#),\n        \"expected to contain: {}\",\n        r#\"![A photo](photo.jpg)\"#\n    );\n}\n\n#[test]\nfn test_image_with_title() {\n    // Image with title attribute includes title in output\n    let html = r#\"<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![Sales chart](chart.png\"#),\n        \"expected to contain: {}\",\n        r#\"![Sales chart](chart.png\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Q3 Sales\"#),\n        \"expected to contain: {}\",\n        r#\"Q3 Sales\"#\n    );\n}\n\n#[test]\nfn test_inline_code() {\n    // Inline code\n    let html = r#\"<p>Use <code>console.log()</code> to debug</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"`console.log()`\"#),\n        \"expected to contain: {}\",\n        r#\"`console.log()`\"#\n    );\n}\n\n#[test]\nfn test_italic_em() {\n    // Em tag converts to italic\n    let html = r#\"<p><em>italic</em></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"*italic*\"#),\n        \"expected to contain: {}\",\n        r#\"*italic*\"#\n    );\n}\n\n#[test]\nfn test_line_break_br_tag() {\n    // Single br tag produces a line break in output\n    let html = r#\"<p>First line.<br>Second line.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First line.\"#),\n        \"expected to contain: {}\",\n        r#\"First line.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second line.\"#),\n        \"expected to contain: {}\",\n        r#\"Second line.\"#\n    );\n}\n\n#[test]\nfn test_line_break_hr_tag() {\n    // hr tag produces a horizontal separator between content\n    let html = r#\"<p>Before rule.</p><hr><p>After rule.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before rule.\"#),\n        \"expected to contain: {}\",\n        r#\"Before rule.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After rule.\"#),\n        \"expected to contain: {}\",\n        r#\"After rule.\"#\n    );\n}\n\n#[test]\nfn test_line_break_multiple_br() {\n    // Multiple consecutive br tags in sequence\n    let html = r#\"<p>Start.<br><br>End.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Start.\"#),\n        \"expected to contain: {}\",\n        r#\"Start.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"End.\"#),\n        \"expected to contain: {}\",\n        r#\"End.\"#\n    );\n}\n\n#[test]\nfn test_link_anchor_fragment() {\n    // Fragment-only anchor link is preserved\n    let html = r##\"<a href=\"#section\">Jump to section</a>\"##;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[Jump to section](#section)\"#),\n        \"expected to contain: {}\",\n        r#\"[Jump to section](#section)\"#\n    );\n}\n\n#[test]\nfn test_link_empty_href() {\n    // Link with empty href produces output with the link text\n    let html = r#\"<a href=\"\">No destination</a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"No destination\"#),\n        \"expected to contain: {}\",\n        r#\"No destination\"#\n    );\n}\n\n#[test]\nfn test_link_image_inside() {\n    // Image inside a link produces a linked image\n    let html = r#\"<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![Logo](logo.png)\"#),\n        \"expected to contain: {}\",\n        r#\"![Logo](logo.png)\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"https://example.com\"#),\n        \"expected to contain: {}\",\n        r#\"https://example.com\"#\n    );\n}\n\n#[test]\nfn test_link_mailto() {\n    // Mailto link is preserved with mailto: scheme\n    let html = r#\"<a href=\"mailto:user@example.com\">Email us</a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"mailto:user@example.com\"#),\n        \"expected to contain: {}\",\n        r#\"mailto:user@example.com\"#\n    );\n}\n\n#[test]\nfn test_link_simple() {\n    // Simple link\n    let html = r#\"<a href=\"https://example.com\">Example</a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[Example](https://example.com)\"#),\n        \"expected to contain: {}\",\n        r#\"[Example](https://example.com)\"#\n    );\n}\n\n#[test]\nfn test_link_with_bold_text() {\n    // Link containing bold text preserves formatting\n    let html = r#\"<a href=\"https://example.com\"><strong>Bold link</strong></a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**Bold link**\"#),\n        \"expected to contain: {}\",\n        r#\"**Bold link**\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"https://example.com\"#),\n        \"expected to contain: {}\",\n        r#\"https://example.com\"#\n    );\n}\n\n#[test]\nfn test_link_with_title() {\n    // Link with title attribute\n    let html = r#\"<a href=\"https://example.com\" title=\"Example Site\">Example</a>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[Example](https://example.com\"#),\n        \"expected to contain: {}\",\n        r#\"[Example](https://example.com\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Example Site\"#),\n        \"expected to contain: {}\",\n        r#\"Example Site\"#\n    );\n}\n\n#[test]\nfn test_list_definition_dl() {\n    // Definition list with dt and dd elements\n    let html = r#\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Term One\"#),\n        \"expected to contain: {}\",\n        r#\"Term One\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Definition of term one.\"#),\n        \"expected to contain: {}\",\n        r#\"Definition of term one.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Term Two\"#),\n        \"expected to contain: {}\",\n        r#\"Term Two\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Definition of term two.\"#),\n        \"expected to contain: {}\",\n        r#\"Definition of term two.\"#\n    );\n}\n\n#[test]\nfn test_list_item_multiple_paragraphs() {\n    // List item containing multiple paragraphs\n    let html =\n        r#\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First paragraph in item.\"#),\n        \"expected to contain: {}\",\n        r#\"First paragraph in item.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second paragraph in item.\"#),\n        \"expected to contain: {}\",\n        r#\"Second paragraph in item.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Simple item\"#),\n        \"expected to contain: {}\",\n        r#\"Simple item\"#\n    );\n}\n\n#[test]\nfn test_list_mixed_nested() {\n    // Mixed list: ordered list nested inside unordered list\n    let html = r#\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Item A\"#),\n        \"expected to contain: {}\",\n        r#\"Item A\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Sub 1\"#),\n        \"expected to contain: {}\",\n        r#\"Sub 1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Sub 2\"#),\n        \"expected to contain: {}\",\n        r#\"Sub 2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Item B\"#),\n        \"expected to contain: {}\",\n        r#\"Item B\"#\n    );\n}\n\n#[test]\nfn test_list_nested_ordered() {\n    // Nested ordered list with two levels of depth\n    let html = r#\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Step 1\"#),\n        \"expected to contain: {}\",\n        r#\"Step 1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Step 1a\"#),\n        \"expected to contain: {}\",\n        r#\"Step 1a\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Step 1b\"#),\n        \"expected to contain: {}\",\n        r#\"Step 1b\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Step 2\"#),\n        \"expected to contain: {}\",\n        r#\"Step 2\"#\n    );\n}\n\n#[test]\nfn test_list_nested_unordered() {\n    // Nested unordered list with two levels of depth\n    let html = r#\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Parent A\"#),\n        \"expected to contain: {}\",\n        r#\"Parent A\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Child A1\"#),\n        \"expected to contain: {}\",\n        r#\"Child A1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Child A2\"#),\n        \"expected to contain: {}\",\n        r#\"Child A2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Parent B\"#),\n        \"expected to contain: {}\",\n        r#\"Parent B\"#\n    );\n}\n\n#[test]\nfn test_list_task_checkboxes() {\n    // Task list with checked and unchecked checkboxes\n    let html =\n        r#\"<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Done task\"#),\n        \"expected to contain: {}\",\n        r#\"Done task\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Pending task\"#),\n        \"expected to contain: {}\",\n        r#\"Pending task\"#\n    );\n}\n\n#[test]\nfn test_ordered_list() {\n    // Ordered list\n    let html = r#\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"1. First\"#),\n        \"expected to contain: {}\",\n        r#\"1. First\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2. Second\"#),\n        \"expected to contain: {}\",\n        r#\"2. Second\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"3. Third\"#),\n        \"expected to contain: {}\",\n        r#\"3. Third\"#\n    );\n}\n\n#[test]\nfn test_paragraph_multiple() {\n    // Multiple paragraphs are separated by a blank line\n    let html = r#\"<p>First paragraph.</p><p>Second paragraph.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"First paragraph.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"Second paragraph.\"#\n    );\n}\n\n#[test]\nfn test_paragraph_nested_divs() {\n    // Text nested inside divs is extracted correctly\n    let html = r#\"<div><div><p>Nested text</p></div></div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Nested text\"#),\n        \"expected to contain: {}\",\n        r#\"Nested text\"#\n    );\n}\n\n#[test]\nfn test_paragraph_simple() {\n    // Simple paragraph converts to plain text\n    let html = r#\"<p>Hello World</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"Hello World\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_paragraph_with_inline_formatting() {\n    // Paragraph with bold, italic, and a link\n    let html =\n        r#\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**bold**\"#),\n        \"expected to contain: {}\",\n        r#\"**bold**\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"*italic*\"#),\n        \"expected to contain: {}\",\n        r#\"*italic*\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[link](https://example.com)\"#),\n        \"expected to contain: {}\",\n        r#\"[link](https://example.com)\"#\n    );\n}\n\n#[test]\nfn test_paragraph_with_line_breaks() {\n    // Paragraph with br tags produces line breaks in output\n    let html = r#\"<p>Line one.<br>Line two.<br>Line three.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line one.\"#),\n        \"expected to contain: {}\",\n        r#\"Line one.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line two.\"#),\n        \"expected to contain: {}\",\n        r#\"Line two.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line three.\"#),\n        \"expected to contain: {}\",\n        r#\"Line three.\"#\n    );\n}\n\n#[test]\nfn test_semantic_abbr() {\n    // Abbreviation element text is preserved\n    let html = r#\"<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"WWW\"#),\n        \"expected to contain: {}\",\n        r#\"WWW\"#\n    );\n}\n\n#[test]\nfn test_semantic_article() {\n    // Article element wrapping content preserves inner content\n    let html = r#\"<article><h2>Article Title</h2><p>Article body.</p></article>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article Title\"#),\n        \"expected to contain: {}\",\n        r#\"Article Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article body.\"#),\n        \"expected to contain: {}\",\n        r#\"Article body.\"#\n    );\n}\n\n#[test]\nfn test_semantic_definition_list() {\n    // Definition list with term and description\n    let html = r#\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"HTML\"#),\n        \"expected to contain: {}\",\n        r#\"HTML\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"HyperText Markup Language\"#),\n        \"expected to contain: {}\",\n        r#\"HyperText Markup Language\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"CSS\"#),\n        \"expected to contain: {}\",\n        r#\"CSS\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Cascading Style Sheets\"#),\n        \"expected to contain: {}\",\n        r#\"Cascading Style Sheets\"#\n    );\n}\n\n#[test]\nfn test_semantic_details_summary() {\n    // Details and summary elements produce readable output\n    let html = r#\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Click to expand\"#),\n        \"expected to contain: {}\",\n        r#\"Click to expand\"#\n    );\n}\n\n#[test]\nfn test_semantic_hr() {\n    // Horizontal rule produces a separator in output\n    let html = r#\"<p>Above</p><hr><p>Below</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Above\"#),\n        \"expected to contain: {}\",\n        r#\"Above\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Below\"#),\n        \"expected to contain: {}\",\n        r#\"Below\"#\n    );\n}\n\n#[test]\nfn test_semantic_mark_highlight() {\n    // Mark tag produces highlighted output\n    let html = r#\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"highlighted text\"#),\n        \"expected to contain: {}\",\n        r#\"highlighted text\"#\n    );\n}\n\n#[test]\nfn test_semantic_section_with_heading() {\n    // Section element with heading preserves structure\n    let html = r#\"<section><h3>Section Heading</h3><p>Section content.</p></section>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Section Heading\"#),\n        \"expected to contain: {}\",\n        r#\"Section Heading\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Section content.\"#),\n        \"expected to contain: {}\",\n        r#\"Section content.\"#\n    );\n}\n\n#[test]\nfn test_semantic_sub_superscript() {\n    // Subscript and superscript elements are preserved in output\n    let html = r#\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"H\"#),\n        \"expected to contain: {}\",\n        r#\"H\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2\"#),\n        \"expected to contain: {}\",\n        r#\"2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"O\"#),\n        \"expected to contain: {}\",\n        r#\"O\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"E=mc\"#),\n        \"expected to contain: {}\",\n        r#\"E=mc\"#\n    );\n}\n\n#[test]\nfn test_simple_table() {\n    // Simple table with header\n    let html = r#\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Name\"#),\n        \"expected to contain: {}\",\n        r#\"Name\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Age\"#),\n        \"expected to contain: {}\",\n        r#\"Age\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Alice\"#),\n        \"expected to contain: {}\",\n        r#\"Alice\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"30\"#),\n        \"expected to contain: {}\",\n        r#\"30\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"|\"#),\n        \"expected to contain: {}\",\n        r#\"|\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"---\"#),\n        \"expected to contain: {}\",\n        r#\"---\"#\n    );\n}\n\n#[test]\nfn test_table_empty() {\n    // Empty table produces no output or minimal output\n    let html = r#\"<table></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_table_no_thead() {\n    // Table without thead uses first row as implied header\n    let html = r#\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Product\"#),\n        \"expected to contain: {}\",\n        r#\"Product\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Price\"#),\n        \"expected to contain: {}\",\n        r#\"Price\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Apple\"#),\n        \"expected to contain: {}\",\n        r#\"Apple\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"1.00\"#),\n        \"expected to contain: {}\",\n        r#\"1.00\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"|\"#),\n        \"expected to contain: {}\",\n        r#\"|\"#\n    );\n}\n\n#[test]\nfn test_table_pipe_chars_in_content() {\n    // Table cells containing pipe characters are escaped in output\n    let html = r#\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Expression\"#),\n        \"expected to contain: {}\",\n        r#\"Expression\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Result\"#),\n        \"expected to contain: {}\",\n        r#\"Result\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"true\"#),\n        \"expected to contain: {}\",\n        r#\"true\"#\n    );\n}\n\n#[test]\nfn test_table_with_alignment() {\n    // Table with column alignment attributes\n    let html = r#\"<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Left\"#),\n        \"expected to contain: {}\",\n        r#\"Left\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Center\"#),\n        \"expected to contain: {}\",\n        r#\"Center\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Right\"#),\n        \"expected to contain: {}\",\n        r#\"Right\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"L\"#),\n        \"expected to contain: {}\",\n        r#\"L\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"C\"#),\n        \"expected to contain: {}\",\n        r#\"C\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"R\"#),\n        \"expected to contain: {}\",\n        r#\"R\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"|\"#),\n        \"expected to contain: {}\",\n        r#\"|\"#\n    );\n}\n\n#[test]\nfn test_table_with_colspan() {\n    // Table with colspan attribute in a header cell\n    let html = r#\"<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Full Name\"#),\n        \"expected to contain: {}\",\n        r#\"Full Name\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"John\"#),\n        \"expected to contain: {}\",\n        r#\"John\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Doe\"#),\n        \"expected to contain: {}\",\n        r#\"Doe\"#\n    );\n}\n\n#[test]\nfn test_unordered_list() {\n    // Unordered list\n    let html = r#\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"- Item 1\"#),\n        \"expected to contain: {}\",\n        r#\"- Item 1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"- Item 2\"#),\n        \"expected to contain: {}\",\n        r#\"- Item 2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"- Item 3\"#),\n        \"expected to contain: {}\",\n        r#\"- Item 3\"#\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/edge_cases_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:95e543dcb96eccd6d7948f8ca7af684a8fc733e366f3e4da72577dd35e406d6c\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: edge-cases\n\nuse html_to_markdown_rs::convert;\nuse html_to_markdown_rs::visitor::HtmlVisitor;\nuse html_to_markdown_rs::{NodeContext, VisitResult};\n\n#[test]\nfn test_empty_html() {\n    // Empty HTML document\n    let html = r#\"<html><head></head><body></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_encoding_cjk_characters() {\n    // CJK (Chinese, Japanese, Korean) characters are preserved\n    let html = r#\"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"中文内容\"#),\n        \"expected to contain: {}\",\n        r#\"中文内容\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"日本語テキスト\"#),\n        \"expected to contain: {}\",\n        r#\"日本語テキスト\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"한국어 텍스트\"#),\n        \"expected to contain: {}\",\n        r#\"한국어 텍스트\"#\n    );\n}\n\n#[test]\nfn test_encoding_html_entities() {\n    // Common HTML entities are decoded in output\n    let html = r#\"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"&\"#),\n        \"expected to contain: {}\",\n        r#\"&\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"<\"#),\n        \"expected to contain: {}\",\n        r#\"<\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\">\"#),\n        \"expected to contain: {}\",\n        r#\">\"#\n    );\n}\n\n#[test]\nfn test_encoding_named_entities() {\n    // Named HTML entities like &mdash; and &hellip; are decoded\n    let html = r#\"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"—\"#),\n        \"expected to contain: {}\",\n        r#\"—\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"…\"#),\n        \"expected to contain: {}\",\n        r#\"…\"#\n    );\n}\n\n#[test]\nfn test_encoding_numeric_entities() {\n    // Numeric HTML entities (decimal and hex) are decoded\n    let html = r#\"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"©\"#),\n        \"expected to contain: {}\",\n        r#\"©\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"®\"#),\n        \"expected to contain: {}\",\n        r#\"®\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"€\"#),\n        \"expected to contain: {}\",\n        r#\"€\"#\n    );\n}\n\n#[test]\nfn test_encoding_unicode_emoji() {\n    // Emoji and Unicode characters are preserved\n    let html = r#\"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"🌍\"#),\n        \"expected to contain: {}\",\n        r#\"🌍\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"🚀\"#),\n        \"expected to contain: {}\",\n        r#\"🚀\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"⭐\"#),\n        \"expected to contain: {}\",\n        r#\"⭐\"#\n    );\n}\n\n#[test]\nfn test_html_comments_only() {\n    // Document containing only HTML comments produces empty output\n    let html = r#\"<!-- This is a comment --><!-- Another comment -->\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_just_whitespace_input() {\n    // Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\n    let html = r#\"   \"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_malformed_deeply_nested_elements() {\n    // Deeply nested elements (100 levels) are handled without stack overflow\n    let html = r#\"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Deeply nested content\"#),\n        \"expected to contain: {}\",\n        r#\"Deeply nested content\"#\n    );\n}\n\n#[test]\nfn test_malformed_missing_block_closing_tags() {\n    // Missing closing tags on block elements are auto-closed by parser\n    let html = r#\"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Title\"#),\n        \"expected to contain: {}\",\n        r#\"Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First paragraph\"#),\n        \"expected to contain: {}\",\n        r#\"First paragraph\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second paragraph\"#),\n        \"expected to contain: {}\",\n        r#\"Second paragraph\"#\n    );\n}\n\n#[test]\nfn test_malformed_overlapping_tags() {\n    // Overlapping bold/italic tags are recovered by the HTML parser without panic\n    let html = r#\"<p><b><i>bold and italic</b></i></p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"bold and italic\"#),\n        \"expected to contain: {}\",\n        r#\"bold and italic\"#\n    );\n}\n\n#[test]\nfn test_malformed_unclosed_paragraph() {\n    // Unclosed <p> tag is recovered gracefully and content is preserved\n    let html = r#\"<p>This paragraph is never closed\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"This paragraph is never closed\"#),\n        \"expected to contain: {}\",\n        r#\"This paragraph is never closed\"#\n    );\n}\n\n#[test]\nfn test_script_tags_only() {\n    // Document with only script tags produces empty output (scripts are stripped)\n    let html = r#\"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_style_tags_only() {\n    // Document with only style tags produces empty output (styles are stripped)\n    let html = r#\"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_visitor_custom_element_with_nesting() {\n    // Visitor handles custom elements with nested content\n    let html = r#\"<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_custom_element(&mut self, _: &NodeContext, _tag_name: &str, _html: &str) -> VisitResult {\n            VisitResult::Custom(\"[CUSTOM WIDGET]\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[CUSTOM WIDGET]\"#),\n        \"expected to contain: {}\",\n        r#\"[CUSTOM WIDGET]\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Widget content here\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Widget content here\"#\n    );\n}\n\n#[test]\nfn test_visitor_deeply_nested_skip() {\n    // Visitor skips deeply nested elements\n    let html =\n        r#\"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_mark(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Outer\"#),\n        \"expected to contain: {}\",\n        r#\"Outer\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"text\"#),\n        \"expected to contain: {}\",\n        r#\"text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"highlight\"#),\n        \"expected NOT to contain: {}\",\n        r#\"highlight\"#\n    );\n}\n\n#[test]\nfn test_visitor_element_end_modification() {\n    // Visitor modifies element at end after children processed\n    let html = r#\"<blockquote><p>Original quote</p></blockquote>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_element_end(&mut self, _: &NodeContext, _output: &str) -> VisitResult {\n            VisitResult::Custom(\"MODIFIED OUTPUT\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n}\n\n#[test]\nfn test_visitor_element_start_skip_entire_subtree() {\n    // Visitor skips at element_start level removes entire subtree\n    let html = r#\"<div><h1>Title</h1><p>Content</p></div>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_element_start(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n}\n\n#[test]\nfn test_visitor_unknown_tag_preservation() {\n    // Visitor preserves unknown HTML tags as raw HTML\n    let html = r#\"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_custom_element(&mut self, _: &NodeContext, _tag_name: &str, _html: &str) -> VisitResult {\n            VisitResult::PreserveHtml\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article text\"#),\n        \"expected to contain: {}\",\n        r#\"Article text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"More article text\"#),\n        \"expected to contain: {}\",\n        r#\"More article text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"<x-custom>\"#),\n        \"expected to contain: {}\",\n        r#\"<x-custom>\"#\n    );\n}\n\n#[test]\nfn test_whitespace_only() {\n    // Whitespace-only content\n    let html = r#\"<p>   </p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_xss_onclick_handler_removed() {\n    // onclick and other on* event handlers are removed from elements\n    let html = r#\"<p><a href=\"https://example.com\" onclick=\"alert('xss')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Click me\"#),\n        \"expected to contain: {}\",\n        r#\"Click me\"#\n    );\n}\n\n#[test]\nfn test_xss_script_tag_stripped() {\n    // Script tag content is stripped and does not appear in output\n    let html = r#\"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Safe content\"#),\n        \"expected to contain: {}\",\n        r#\"Safe content\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"More safe content\"#),\n        \"expected to contain: {}\",\n        r#\"More safe content\"#\n    );\n}\n\n#[test]\nfn test_xss_svg_nested_script_stripped() {\n    // Script tags nested inside SVG are stripped\n    let html = r#\"<p>Before SVG.</p><svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before SVG\"#),\n        \"expected to contain: {}\",\n        r#\"Before SVG\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After SVG\"#),\n        \"expected to contain: {}\",\n        r#\"After SVG\"#\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/metadata_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:f4797bfdecb2c73eb41a8bcdccaa4662703b32dc09672ffded4022e216b0ec4a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: metadata\n\nuse html_to_markdown_rs::convert;\n\n#[test]\nfn test_metadata_author_meta() {\n    // Extract author from <meta name='author'> tag\n    let html = r#\"<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_author = result.metadata.document.author.as_deref().unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_author.trim(),\n        r#\"Jane Doe\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_metadata_canonical_url() {\n    // Extract canonical URL from <link rel='canonical'> tag\n    let html = r#\"<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_canonical_url = result.metadata.document.canonical_url.as_deref().unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_canonical_url.trim(),\n        r#\"https://example.com/canonical-page\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_metadata_description_meta() {\n    // Extract description from <meta name='description'> tag\n    let html = r#\"<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_description = result.metadata.document.description.as_deref().unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_description.trim(),\n        r#\"This is the page description.\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_metadata_dublin_core() {\n    // Extract Dublin Core metadata tags\n    let html = r#\"<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"scholarly article\"#),\n        \"expected to contain: {}\",\n        r#\"scholarly article\"#\n    );\n}\n\n#[test]\nfn test_metadata_extract_all_images() {\n    // Extract all images from a document into metadata\n    let html = r#\"<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        result.metadata.images.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.metadata.images.len()\n    );\n}\n\n#[test]\nfn test_metadata_extract_all_links() {\n    // Extract all links from a document into metadata\n    let html = r#\"<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        result.metadata.links.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.metadata.links.len()\n    );\n}\n\n#[test]\nfn test_metadata_headers_hierarchy() {\n    // Extract heading hierarchy from document into metadata\n    let html = r#\"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        result.metadata.headers.len() >= 5,\n        \"expected at least 5 elements, got {}\",\n        result.metadata.headers.len()\n    );\n}\n\n#[test]\nfn test_metadata_keywords_meta() {\n    // Extract keywords from <meta name='keywords'> tag\n    let html = r#\"<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(!result.metadata.document.keywords.is_empty(), \"expected >= 1\");\n}\n\n#[test]\nfn test_metadata_lang_attribute() {\n    // Extract language from html lang attribute\n    let html = r#\"<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Hola Mundo\"#),\n        \"expected to contain: {}\",\n        r#\"Hola Mundo\"#\n    );\n}\n\n#[test]\nfn test_metadata_microdata_schema_article() {\n    // Extract schema.org microdata for Article\n    let html = r#\"<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Breaking News Today\"#),\n        \"expected to contain: {}\",\n        r#\"Breaking News Today\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Jane Reporter\"#),\n        \"expected to contain: {}\",\n        r#\"Jane Reporter\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"important information\"#),\n        \"expected to contain: {}\",\n        r#\"important information\"#\n    );\n}\n\n#[test]\nfn test_metadata_microdata_schema_breadcrumb() {\n    // Extract schema.org breadcrumb navigation microdata\n    let html = r#\"<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Home\"#),\n        \"expected to contain: {}\",\n        r#\"Home\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Products\"#),\n        \"expected to contain: {}\",\n        r#\"Products\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Current Page\"#),\n        \"expected to contain: {}\",\n        r#\"Current Page\"#\n    );\n}\n\n#[test]\nfn test_metadata_microdata_schema_organization() {\n    // Extract schema.org microdata for Organization\n    let html = r#\"<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Acme Corp\"#),\n        \"expected to contain: {}\",\n        r#\"Acme Corp\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2020\"#),\n        \"expected to contain: {}\",\n        r#\"2020\"#\n    );\n}\n\n#[test]\nfn test_metadata_microdata_schema_person() {\n    // Extract schema.org microdata for Person\n    let html = r#\"<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"John Smith\"#),\n        \"expected to contain: {}\",\n        r#\"John Smith\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"john@example.com\"#),\n        \"expected to contain: {}\",\n        r#\"john@example.com\"#\n    );\n}\n\n#[test]\nfn test_metadata_microdata_schema_product() {\n    // Extract schema.org microdata for Product\n    let html = r#\"<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Awesome Widget\"#),\n        \"expected to contain: {}\",\n        r#\"Awesome Widget\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"best widget\"#),\n        \"expected to contain: {}\",\n        r#\"best widget\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"29.99\"#),\n        \"expected to contain: {}\",\n        r#\"29.99\"#\n    );\n}\n\n#[test]\nfn test_metadata_text_direction_ltr() {\n    // Extract text direction from lang attribute on html element\n    let html = r#\"<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"left-to-right text\"#),\n        \"expected to contain: {}\",\n        r#\"left-to-right text\"#\n    );\n}\n\n#[test]\nfn test_metadata_text_direction_rtl() {\n    // Extract right-to-left text direction\n    let html = r#\"<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"right-to-left text\"#),\n        \"expected to contain: {}\",\n        r#\"right-to-left text\"#\n    );\n}\n\n#[test]\nfn test_metadata_title_tag() {\n    // Extract title from <title> tag\n    let html = r#\"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_title = result.metadata.document.title.as_deref().unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(metadata_document_title.trim(), r#\"My Page\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_og_basic_tags() {\n    // Extract og:title, og:description, and og:image from Open Graph meta tags\n    let html = r#\"<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_open_graph_title = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"title\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_open_graph_description = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"description\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_open_graph_image = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"image\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_open_graph_title.trim(),\n        r#\"OG Title\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_open_graph_description.trim(),\n        r#\"OG description text.\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_open_graph_image.trim(),\n        r#\"https://example.com/image.jpg\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_og_multiple_tags() {\n    // Extract multiple Open Graph tags including type, url, and site_name\n    let html = r#\"<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_open_graph_title = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"title\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_open_graph_type = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"type\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_open_graph_url = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"url\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_open_graph_site_name = result\n        .metadata\n        .document\n        .open_graph\n        .get(\"site_name\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_open_graph_title.trim(),\n        r#\"Article Title\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_open_graph_type.trim(),\n        r#\"article\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_open_graph_url.trim(),\n        r#\"https://example.com/article\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_open_graph_site_name.trim(),\n        r#\"Example Site\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_structured_data_json_ld() {\n    // JSON-LD script tag is stripped from output (security) but metadata may be extracted\n    let html = r#\"<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"My Article\"#),\n        \"expected to contain: {}\",\n        r#\"My Article\"#\n    );\n}\n\n#[test]\nfn test_structured_data_multiple_json_ld() {\n    // Multiple JSON-LD blocks are all stripped from output\n    let html = r#\"<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Widget\"#),\n        \"expected to contain: {}\",\n        r#\"Widget\"#\n    );\n}\n\n#[test]\nfn test_twitter_card_tags() {\n    // Extract Twitter card meta tags\n    let html = r#\"<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let metadata_document_twitter_card_card = result\n        .metadata\n        .document\n        .twitter_card\n        .get(\"card\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_twitter_card_title = result\n        .metadata\n        .document\n        .twitter_card\n        .get(\"title\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    let metadata_document_twitter_card_description = result\n        .metadata\n        .document\n        .twitter_card\n        .get(\"description\")\n        .map(|s| s.as_str())\n        .unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_twitter_card_card.trim(),\n        r#\"summary_large_image\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_twitter_card_title.trim(),\n        r#\"Twitter Card Title\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_twitter_card_description.trim(),\n        r#\"Twitter card description.\"#,\n        \"equals assertion failed\"\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/options_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:5550ecb8188a1d8a49fe40dec216c93f6eb7013a8a6673876448dc491e0a31bc\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: options\n\nuse html_to_markdown_rs::convert;\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_options_autolinks_false() {\n    // Bare URL links rendered as regular markdown links when autolinks disabled\n    let html = r#\"<p><a href='https://example.com'>https://example.com</a></p>\"#;\n    let options_json = serde_json::json!({\"autolinks\": false});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"example.com\"#),\n        \"expected to contain: {}\",\n        r#\"example.com\"#\n    );\n}\n\n#[test]\nfn test_options_br_in_tables_false() {\n    // BR elements in table cells are stripped when disabled\n    let html = r#\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\"#;\n    let options_json = serde_json::json!({\"br_in_tables\": false});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Col\"#),\n        \"expected to contain: {}\",\n        r#\"Col\"#\n    );\n}\n\n#[test]\nfn test_options_br_in_tables_true() {\n    // BR elements in table cells render as line breaks\n    let html = r#\"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\"#;\n    let options_json = serde_json::json!({\"br_in_tables\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Header\"#),\n        \"expected to contain: {}\",\n        r#\"Header\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line 1\"#),\n        \"expected to contain: {}\",\n        r#\"Line 1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line 2\"#),\n        \"expected to contain: {}\",\n        r#\"Line 2\"#\n    );\n}\n\n#[test]\nfn test_options_code_block_backticks() {\n    // Backticks code block style uses triple backtick fences\n    let html = r#\"<pre><code class=\"language-js\">console.log('hi');</code></pre>\"#;\n    let options_json = serde_json::json!({\"code_block_style\": \"Backticks\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"```\"#),\n        \"expected to contain: {}\",\n        r#\"```\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"console.log('hi');\"#),\n        \"expected to contain: {}\",\n        r#\"console.log('hi');\"#\n    );\n}\n\n#[test]\nfn test_options_code_block_indented() {\n    // Code blocks use 4-space indentation\n    let html = r#\"<pre><code>print('hello')</code></pre>\"#;\n    let options_json = serde_json::json!({\"code_block_style\": \"Indented\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"print('hello')\"#),\n        \"expected to contain: {}\",\n        r#\"print('hello')\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"```\"#),\n        \"expected NOT to contain: {}\",\n        r#\"```\"#\n    );\n}\n\n#[test]\nfn test_options_code_block_tildes() {\n    // Code blocks use tilde fences\n    let html = r#\"<pre><code>let x = 1;</code></pre>\"#;\n    let options_json = serde_json::json!({\"code_block_style\": \"Tildes\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"~~~\"#),\n        \"expected to contain: {}\",\n        r#\"~~~\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"let x = 1;\"#),\n        \"expected to contain: {}\",\n        r#\"let x = 1;\"#\n    );\n}\n\n#[test]\nfn test_options_code_block_tildes_style() {\n    // Tildes code block style uses triple tilde fences\n    let html = r#\"<pre><code>some code</code></pre>\"#;\n    let options_json = serde_json::json!({\"code_block_style\": \"Tildes\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"~~~\"#),\n        \"expected to contain: {}\",\n        r#\"~~~\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"some code\"#),\n        \"expected to contain: {}\",\n        r#\"some code\"#\n    );\n}\n\n#[test]\nfn test_options_code_language_python() {\n    // Default code language annotation on blocks without lang attribute\n    let html = r#\"<pre><code>def hello(): pass</code></pre>\"#;\n    let options_json = serde_json::json!({\"code_language\": \"python\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"```python\"#),\n        \"expected to contain: {}\",\n        r#\"```python\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"def hello\"#),\n        \"expected to contain: {}\",\n        r#\"def hello\"#\n    );\n}\n\n#[test]\nfn test_options_convert_as_inline() {\n    // Block elements treated as inline\n    let html = r#\"<p>One</p><p>Two</p>\"#;\n    let options_json = serde_json::json!({\"convert_as_inline\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"One\"#),\n        \"expected to contain: {}\",\n        r#\"One\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Two\"#),\n        \"expected to contain: {}\",\n        r#\"Two\"#\n    );\n}\n\n#[test]\nfn test_options_debug_true() {\n    // Debug mode enabled does not crash and produces output\n    let html = r#\"<p>Debug test</p>\"#;\n    let options_json = serde_json::json!({\"debug\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Debug test\"#),\n        \"expected to contain: {}\",\n        r#\"Debug test\"#\n    );\n}\n\n#[test]\nfn test_options_default_title_true() {\n    // Links without title get empty title attribute when defaultTitle is true\n    let html = r#\"<p><a href='https://example.com'>Link</a></p>\"#;\n    let options_json = serde_json::json!({\"default_title\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Link\"#),\n        \"expected to contain: {}\",\n        r#\"Link\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"https://example.com\"#),\n        \"expected to contain: {}\",\n        r#\"https://example.com\"#\n    );\n}\n\n#[test]\nfn test_options_encoding_utf8() {\n    // UTF-8 encoding hint for special characters\n    let html = r#\"<p>Café naïve résumé</p>\"#;\n    let options_json = serde_json::json!({\"encoding\": \"utf-8\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n}\n\n#[test]\nfn test_options_escape_ascii_enabled() {\n    // ASCII Markdown characters are escaped when escapeAscii is true\n    let html = r#\"<p>Text with # hash and [brackets] and * star</p>\"#;\n    let options_json = serde_json::json!({\"escape_ascii\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Text\"#),\n        \"expected to contain: {}\",\n        r#\"Text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"hash\"#),\n        \"expected to contain: {}\",\n        r#\"hash\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"brackets\"#),\n        \"expected to contain: {}\",\n        r#\"brackets\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"star\"#),\n        \"expected to contain: {}\",\n        r#\"star\"#\n    );\n}\n\n#[test]\nfn test_options_escape_asterisks() {\n    // escape_asterisks option escapes asterisks in plain text\n    let html = r#\"<p>Use 2*3 = 6 in math.</p>\"#;\n    let options_json = serde_json::json!({\"escape_asterisks\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"2\"#),\n        \"expected to contain: {}\",\n        r#\"2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"3\"#),\n        \"expected to contain: {}\",\n        r#\"3\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"6\"#),\n        \"expected to contain: {}\",\n        r#\"6\"#\n    );\n}\n\n#[test]\nfn test_options_escape_misc() {\n    // escape_misc option escapes miscellaneous markdown characters\n    let html = r#\"<p>Use # and | and ~ in text.</p>\"#;\n    let options_json = serde_json::json!({\"escape_misc\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Use\"#),\n        \"expected to contain: {}\",\n        r#\"Use\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"and\"#),\n        \"expected to contain: {}\",\n        r#\"and\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"in text.\"#),\n        \"expected to contain: {}\",\n        r#\"in text.\"#\n    );\n}\n\n#[test]\nfn test_options_escape_underscores() {\n    // escape_underscores option escapes underscores in plain text\n    let html = r#\"<p>The variable_name is defined.</p>\"#;\n    let options_json = serde_json::json!({\"escape_underscores\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"variable\"#),\n        \"expected to contain: {}\",\n        r#\"variable\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"name\"#),\n        \"expected to contain: {}\",\n        r#\"name\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"defined.\"#),\n        \"expected to contain: {}\",\n        r#\"defined.\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_attribute() {\n    // Elements matching CSS attribute selector are excluded entirely\n    let html = r#\"<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\"[role='complementary']\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Primary text\"#),\n        \"expected to contain: {}\",\n        r#\"Primary text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Sidebar\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Sidebar\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_class() {\n    // Elements matching CSS class selector are excluded entirely\n    let html = r#\"<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\".cookie-banner\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Main content\"#),\n        \"expected to contain: {}\",\n        r#\"Main content\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"cookies\"#),\n        \"expected NOT to contain: {}\",\n        r#\"cookies\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_empty_noop() {\n    // Empty exclude_selectors list does not affect output\n    let html = r#\"<p>Hello world</p>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": []});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Hello world\"#),\n        \"expected to contain: {}\",\n        r#\"Hello world\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_id() {\n    // Elements matching CSS id selector are excluded entirely\n    let html = r#\"<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\"#ad-container\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article text\"#),\n        \"expected to contain: {}\",\n        r#\"Article text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Buy stuff\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Buy stuff\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_multiple() {\n    // Multiple CSS selectors each exclude their matched elements\n    let html = r#\"<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\".nav\", \"footer\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Content\"#),\n        \"expected to contain: {}\",\n        r#\"Content\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Menu\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Menu\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Footer\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Footer\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_nested_content_dropped() {\n    // All descendants of excluded elements are dropped\n    let html = r#\"<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\".sidebar\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Main text\"#),\n        \"expected to contain: {}\",\n        r#\"Main text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Related\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Related\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Sidebar text\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Sidebar text\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_plain_text_mode() {\n    // Exclude selectors work in plain text output mode\n    let html = r#\"<body><div class=\"nav\">Navigation</div><p>Article body</p></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\".nav\"], \"output_format\": \"Plain\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article body\"#),\n        \"expected to contain: {}\",\n        r#\"Article body\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Navigation\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Navigation\"#\n    );\n}\n\n#[test]\nfn test_options_exclude_selectors_vs_strip_tags() {\n    // exclude_selectors drops entire subtree unlike strip_tags which keeps children\n    let html = r#\"<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>\"#;\n    let options_json = serde_json::json!({\"exclude_selectors\": [\".wrapper\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Outer text\"#),\n        \"expected to contain: {}\",\n        r#\"Outer text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Inner paragraph\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Inner paragraph\"#\n    );\n}\n\n#[test]\nfn test_options_extract_metadata_true() {\n    // Extract metadata returns document metadata when enabled\n    let html = r#\"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\"#;\n    let options_json = serde_json::json!({\"extract_metadata\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let metadata_document_title = result.metadata.document.title.as_deref().unwrap_or(\"\");\n    let metadata_document_description = result.metadata.document.description.as_deref().unwrap_or(\"\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        metadata_document_title.trim(),\n        r#\"Test Page\"#,\n        \"equals assertion failed\"\n    );\n    assert_eq!(\n        metadata_document_description.trim(),\n        r#\"A test page\"#,\n        \"equals assertion failed\"\n    );\n}\n\n#[test]\nfn test_options_heading_style_atx() {\n    // ATX heading style produces hash-prefixed headings\n    let html = r#\"<h1>Title</h1><h2>Subtitle</h2>\"#;\n    let options_json = serde_json::json!({\"heading_style\": \"Atx\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Title\"#),\n        \"expected to contain: {}\",\n        r#\"# Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## Subtitle\"#),\n        \"expected to contain: {}\",\n        r#\"## Subtitle\"#\n    );\n}\n\n#[test]\nfn test_options_heading_style_atx_closed() {\n    // ATX closed heading style adds closing hashes\n    let html = r#\"<h1>Closed Heading</h1>\"#;\n    let options_json = serde_json::json!({\"heading_style\": \"AtxClosed\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Closed Heading #\"#),\n        \"expected to contain: {}\",\n        r#\"# Closed Heading #\"#\n    );\n}\n\n#[test]\nfn test_options_heading_style_underlined() {\n    // Underlined heading style produces setext-style headings for h1 and h2\n    let html = r#\"<h1>Main Title</h1>\"#;\n    let options_json = serde_json::json!({\"heading_style\": \"Underlined\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Main Title\"#),\n        \"expected to contain: {}\",\n        r#\"Main Title\"#\n    );\n}\n\n#[test]\nfn test_options_highlight_bold() {\n    // Mark tag rendered as bold\n    let html = r#\"<p>Text with <mark>highlighted</mark> text.</p>\"#;\n    let options_json = serde_json::json!({\"highlight_style\": \"Bold\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**highlighted**\"#),\n        \"expected to contain: {}\",\n        r#\"**highlighted**\"#\n    );\n}\n\n#[test]\nfn test_options_highlight_double_equal() {\n    // Mark tag with double equal highlight style\n    let html = r#\"<p>Text with <mark>highlighted</mark> here.</p>\"#;\n    let options_json = serde_json::json!({\"highlight_style\": \"DoubleEqual\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"==highlighted==\"#),\n        \"expected to contain: {}\",\n        r#\"==highlighted==\"#\n    );\n}\n\n#[test]\nfn test_options_highlight_none() {\n    // Mark tag with no highlight style strips the mark\n    let html = r#\"<p>Text with <mark>plain</mark> content.</p>\"#;\n    let options_json = serde_json::json!({\"highlight_style\": \"None\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"plain\"#),\n        \"expected to contain: {}\",\n        r#\"plain\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"==\"#),\n        \"expected NOT to contain: {}\",\n        r#\"==\"#\n    );\n}\n\n#[test]\nfn test_options_keep_inline_images_in_paragraph() {\n    // Images inside specified tags stay inline\n    let html = r#\"<p>Text <img src='icon.png' alt='icon'> more text</p>\"#;\n    let options_json = serde_json::json!({\"keep_inline_images_in\": [\"p\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Text\"#),\n        \"expected to contain: {}\",\n        r#\"Text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"more text\"#),\n        \"expected to contain: {}\",\n        r#\"more text\"#\n    );\n}\n\n#[test]\nfn test_options_link_style_reference() {\n    // Links use reference-style formatting\n    let html = r#\"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\"#;\n    let options_json = serde_json::json!({\"link_style\": \"Reference\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Example\"#),\n        \"expected to contain: {}\",\n        r#\"Example\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Other\"#),\n        \"expected to contain: {}\",\n        r#\"Other\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"example.com\"#),\n        \"expected to contain: {}\",\n        r#\"example.com\"#\n    );\n}\n\n#[test]\nfn test_options_list_custom_bullets() {\n    // Custom bullet character for unordered lists\n    let html = r#\"<ul><li>Item A</li><li>Item B</li></ul>\"#;\n    let options_json = serde_json::json!({\"bullets\": \"*\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"* Item A\"#),\n        \"expected to contain: {}\",\n        r#\"* Item A\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"* Item B\"#),\n        \"expected to contain: {}\",\n        r#\"* Item B\"#\n    );\n}\n\n#[test]\nfn test_options_list_indent_tabs() {\n    // Tab indentation type for nested list items\n    let html = r#\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\"#;\n    let options_json = serde_json::json!({\"list_indent_type\": \"Tabs\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Parent\"#),\n        \"expected to contain: {}\",\n        r#\"Parent\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Child\"#),\n        \"expected to contain: {}\",\n        r#\"Child\"#\n    );\n}\n\n#[test]\nfn test_options_list_indent_width_four() {\n    // Nested lists indented with 4 spaces per level\n    let html = r#\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\"#;\n    let options_json = serde_json::json!({\"list_indent_width\": 4});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Outer\"#),\n        \"expected to contain: {}\",\n        r#\"Outer\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Inner\"#),\n        \"expected to contain: {}\",\n        r#\"Inner\"#\n    );\n}\n\n#[test]\nfn test_options_max_depth_default_unlimited() {\n    // Default max_depth (null) converts deeply nested content fully\n    let html = r#\"<div><div><div><div><p>Deep content</p></div></div></div></div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Deep content\"#),\n        \"expected to contain: {}\",\n        r#\"Deep content\"#\n    );\n}\n\n#[test]\nfn test_options_max_depth_truncates() {\n    // max_depth truncates content beyond the specified depth\n    let html = r#\"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\"#;\n    let options_json = serde_json::json!({\"max_depth\": 3});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Shallow\"#),\n        \"expected to contain: {}\",\n        r#\"Shallow\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Too deep\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Too deep\"#\n    );\n}\n\n#[test]\nfn test_options_max_depth_zero_empty() {\n    // max_depth of 0 produces empty output\n    let html = r#\"<p>Hello</p>\"#;\n    let options_json = serde_json::json!({\"max_depth\": 0});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_options_newline_backslash() {\n    // Hard line breaks rendered with backslash\n    let html = r#\"<p>Line one<br>Line two</p>\"#;\n    let options_json = serde_json::json!({\"newline_style\": \"Backslash\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line one\"#),\n        \"expected to contain: {}\",\n        r#\"Line one\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Line two\"#),\n        \"expected to contain: {}\",\n        r#\"Line two\"#\n    );\n}\n\n#[test]\nfn test_options_newline_spaces() {\n    // Hard line breaks rendered with trailing spaces\n    let html = r#\"<p>First<br>Second</p>\"#;\n    let options_json = serde_json::json!({\"newline_style\": \"Spaces\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First\"#),\n        \"expected to contain: {}\",\n        r#\"First\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second\"#),\n        \"expected to contain: {}\",\n        r#\"Second\"#\n    );\n}\n\n#[test]\nfn test_options_output_format_djot() {\n    // Djot output format produces djot-compatible markup\n    let html = r#\"<p>Simple paragraph.</p>\"#;\n    let options_json = serde_json::json!({\"output_format\": \"Djot\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Simple paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"Simple paragraph.\"#\n    );\n}\n\n#[test]\nfn test_options_output_format_markdown() {\n    // Default markdown output format produces standard markdown\n    let html = r#\"<h1>Title</h1><p>Some text.</p>\"#;\n    let options_json = serde_json::json!({\"heading_style\": \"Atx\", \"output_format\": \"Markdown\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Title\"#),\n        \"expected to contain: {}\",\n        r#\"# Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Some text.\"#),\n        \"expected to contain: {}\",\n        r#\"Some text.\"#\n    );\n}\n\n#[test]\nfn test_options_output_format_plain() {\n    // Plain text output format strips markdown syntax\n    let html = r#\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\"#;\n    let options_json = serde_json::json!({\"output_format\": \"Plain\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Title\"#),\n        \"expected to contain: {}\",\n        r#\"Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"bold\"#),\n        \"expected to contain: {}\",\n        r#\"bold\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"text.\"#),\n        \"expected to contain: {}\",\n        r#\"text.\"#\n    );\n}\n\n#[test]\nfn test_options_preprocessing_aggressive() {\n    // Aggressive preset removes nav, footer, aside unconditionally\n    let html = r#\"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\"#;\n    let options_json = serde_json::json!({\"preprocessing\": {\"preset\": \"Aggressive\"}});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Title\"#),\n        \"expected to contain: {}\",\n        r#\"Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Content\"#),\n        \"expected to contain: {}\",\n        r#\"Content\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Menu\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Menu\"#\n    );\n}\n\n#[test]\nfn test_options_preprocessing_minimal() {\n    // Minimal preset preserves nav, footer, aside\n    let html = r#\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\"#;\n    let options_json = serde_json::json!({\"preprocessing\": {\"preset\": \"Minimal\"}});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Navigation\"#),\n        \"expected to contain: {}\",\n        r#\"Navigation\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Content\"#),\n        \"expected to contain: {}\",\n        r#\"Content\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Footer\"#),\n        \"expected to contain: {}\",\n        r#\"Footer\"#\n    );\n}\n\n#[test]\nfn test_options_preprocessing_remove_forms() {\n    // Forms are removed when remove_forms is true\n    let html = r#\"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\"#;\n    let options_json = serde_json::json!({\"preprocessing\": {\"remove_forms\": true}});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before\"#),\n        \"expected to contain: {}\",\n        r#\"Before\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After\"#),\n        \"expected to contain: {}\",\n        r#\"After\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Submit\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Submit\"#\n    );\n}\n\n#[test]\nfn test_options_preserve_tags_iframe() {\n    // Iframe tags preserved as raw HTML in output\n    let html = r#\"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\"#;\n    let options_json = serde_json::json!({\"preserve_tags\": [\"iframe\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before\"#),\n        \"expected to contain: {}\",\n        r#\"Before\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After\"#),\n        \"expected to contain: {}\",\n        r#\"After\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"<iframe\"#),\n        \"expected to contain: {}\",\n        r#\"<iframe\"#\n    );\n}\n\n#[test]\nfn test_options_skip_images_true() {\n    // Images are omitted from output when skipImages is true\n    let html = r#\"<p>Before <img src='test.jpg' alt='photo'> After</p>\"#;\n    let options_json = serde_json::json!({\"skip_images\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before\"#),\n        \"expected to contain: {}\",\n        r#\"Before\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After\"#),\n        \"expected to contain: {}\",\n        r#\"After\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"photo\"#),\n        \"expected NOT to contain: {}\",\n        r#\"photo\"#\n    );\n}\n\n#[test]\nfn test_options_strip_newlines() {\n    // Strip newlines produces single-line paragraphs\n    let html = r#\"<p>First paragraph.</p><p>Second paragraph.</p>\"#;\n    let options_json = serde_json::json!({\"strip_newlines\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"First paragraph.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Second paragraph.\"#),\n        \"expected to contain: {}\",\n        r#\"Second paragraph.\"#\n    );\n}\n\n#[test]\nfn test_options_strip_tags_div_span() {\n    // Div and span tags stripped but content preserved\n    let html = r#\"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\"#;\n    let options_json = serde_json::json!({\"strip_tags\": [\"div\", \"span\"]});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Inside div\"#),\n        \"expected to contain: {}\",\n        r#\"Inside div\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"span text\"#),\n        \"expected to contain: {}\",\n        r#\"span text\"#\n    );\n}\n\n#[test]\nfn test_options_strong_em_underscore() {\n    // Strong and em tags use underscore symbol instead of asterisk\n    let html = r#\"<p><strong>bold</strong> and <em>italic</em></p>\"#;\n    let options_json = serde_json::json!({\"strong_em_symbol\": \"_\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"__bold__\"#),\n        \"expected to contain: {}\",\n        r#\"__bold__\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"_italic_\"#),\n        \"expected to contain: {}\",\n        r#\"_italic_\"#\n    );\n}\n\n#[test]\nfn test_options_sub_symbol_tilde() {\n    // Subscript rendered with tilde symbol\n    let html = r#\"<p>H<sub>2</sub>O</p>\"#;\n    let options_json = serde_json::json!({\"sub_symbol\": \"~\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"~2~\"#),\n        \"expected to contain: {}\",\n        r#\"~2~\"#\n    );\n}\n\n#[test]\nfn test_options_sup_symbol_caret() {\n    // Superscript rendered with caret symbol\n    let html = r#\"<p>x<sup>2</sup></p>\"#;\n    let options_json = serde_json::json!({\"sup_symbol\": \"^\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"^2^\"#),\n        \"expected to contain: {}\",\n        r#\"^2^\"#\n    );\n}\n\n#[test]\nfn test_options_whitespace_normalized() {\n    // Normalized whitespace mode collapses multiple spaces\n    let html = r#\"<p>Text   with    extra   spaces.</p>\"#;\n    let options_json = serde_json::json!({\"whitespace_mode\": \"Normalized\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Text\"#),\n        \"expected to contain: {}\",\n        r#\"Text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"with\"#),\n        \"expected to contain: {}\",\n        r#\"with\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"extra\"#),\n        \"expected to contain: {}\",\n        r#\"extra\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"spaces.\"#),\n        \"expected to contain: {}\",\n        r#\"spaces.\"#\n    );\n}\n\n#[test]\nfn test_options_whitespace_strict() {\n    // Strict whitespace mode preserves whitespace as-is\n    let html = r#\"<p>Preserved   spacing.</p>\"#;\n    let options_json = serde_json::json!({\"whitespace_mode\": \"Strict\"});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Preserved\"#),\n        \"expected to contain: {}\",\n        r#\"Preserved\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"spacing.\"#),\n        \"expected to contain: {}\",\n        r#\"spacing.\"#\n    );\n}\n\n#[test]\nfn test_options_wrap_disabled() {\n    // Wrap option disabled preserves long lines without breaking\n    let html = r#\"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\"#;\n    let options_json = serde_json::json!({\"wrap\": false});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content)\n            .contains(r#\"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\"#),\n        \"expected to contain: {}\",\n        r#\"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\"#\n    );\n}\n\n#[test]\nfn test_options_wrap_enabled() {\n    // Wrap option enabled with custom width wraps long lines\n    let html = r#\"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\"#;\n    let options_json = serde_json::json!({\"wrap\": true, \"wrap_width\": 40});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"This is a long paragraph\"#),\n        \"expected to contain: {}\",\n        r#\"This is a long paragraph\"#\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/real_world_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:21e0764b62de073140f60e4e3e169371b9443dc2bd91aafee902c8d5582c2df3\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: real-world\n\nuse html_to_markdown_rs::convert;\n\n#[test]\nfn test_real_world_blog_post() {\n    // Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\n    let html = r#\"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\"https://www.mozilla.org\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\"language-bash\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\"language-rust\">fn main() {\n    println!(\"Hello, world!\");\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\"https://doc.rust-lang.org/book/\">Rust Book</a>.</p></article>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Getting Started with Rust\"#),\n        \"expected to contain: {}\",\n        r#\"# Getting Started with Rust\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## Installation\"#),\n        \"expected to contain: {}\",\n        r#\"## Installation\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## Hello World\"#),\n        \"expected to contain: {}\",\n        r#\"## Hello World\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## Key Concepts\"#),\n        \"expected to contain: {}\",\n        r#\"## Key Concepts\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"cargo run\"#),\n        \"expected to contain: {}\",\n        r#\"cargo run\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[Mozilla](https://www.mozilla.org)\"#),\n        \"expected to contain: {}\",\n        r#\"[Mozilla](https://www.mozilla.org)\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"- Ownership and borrowing\"#),\n        \"expected to contain: {}\",\n        r#\"- Ownership and borrowing\"#\n    );\n}\n\n#[test]\nfn test_real_world_documentation_page() {\n    // Documentation page with nested lists, code examples, and blockquotes converts correctly\n    let html = r##\"<div class=\"docs\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\"language-rust\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\"language-rust\">let markdown = convert_html(\"&lt;h1&gt;Hello&lt;/h1&gt;\").unwrap();\nassert_eq!(markdown, \"# Hello\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\"language-rust\">let options = ConversionOptions::builder()\n    .heading_style(HeadingStyle::ATX)\n    .code_block_style(CodeBlockStyle::Fenced)\n    .build();</code></pre><blockquote><p>See the <a href=\"/docs/options\">options reference</a> for a full list of configuration values.</p></blockquote></div>\"##;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# API Reference\"#),\n        \"expected to contain: {}\",\n        r#\"# API Reference\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## convert_html\"#),\n        \"expected to contain: {}\",\n        r#\"## convert_html\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"### Parameters\"#),\n        \"expected to contain: {}\",\n        r#\"### Parameters\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"### Returns\"#),\n        \"expected to contain: {}\",\n        r#\"### Returns\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"### Example\"#),\n        \"expected to contain: {}\",\n        r#\"### Example\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## ConversionOptions\"#),\n        \"expected to contain: {}\",\n        r#\"## ConversionOptions\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> \"#),\n        \"expected to contain: {}\",\n        r#\"> \"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"thread-safe\"#),\n        \"expected to contain: {}\",\n        r#\"thread-safe\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"convert_html\"#),\n        \"expected to contain: {}\",\n        r#\"convert_html\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"ConversionOptions\"#),\n        \"expected to contain: {}\",\n        r#\"ConversionOptions\"#\n    );\n}\n\n#[test]\nfn test_real_world_product_page() {\n    // Product page with table, images, and lists converts correctly\n    let html = r#\"<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Wireless Keyboard Pro\"#),\n        \"expected to contain: {}\",\n        r#\"# Wireless Keyboard Pro\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\"#),\n        \"expected to contain: {}\",\n        r#\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## Specifications\"#),\n        \"expected to contain: {}\",\n        r#\"## Specifications\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Battery Life\"#),\n        \"expected to contain: {}\",\n        r#\"Battery Life\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"12 months\"#),\n        \"expected to contain: {}\",\n        r#\"12 months\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Bluetooth 5.0\"#),\n        \"expected to contain: {}\",\n        r#\"Bluetooth 5.0\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## What's in the Box\"#),\n        \"expected to contain: {}\",\n        r#\"## What's in the Box\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"USB-C charging cable\"#),\n        \"expected to contain: {}\",\n        r#\"USB-C charging cable\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"|\"#),\n        \"expected to contain: {}\",\n        r#\"|\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"---\"#),\n        \"expected to contain: {}\",\n        r#\"---\"#\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/result_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d257d0e298e7a409ffceb2f7a3bcf5a6f54a629c79f0b9406b9eb1da1f3fa3b0\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: result\n\nuse html_to_markdown_rs::convert;\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_result_tables_empty_when_no_tables() {\n    // Result tables array is empty when input has no tables\n    let html = r#\"<p>No tables here</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        result.tables.len(),\n        0,\n        \"expected exactly 0 elements, got {}\",\n        result.tables.len()\n    );\n}\n\n#[test]\nfn test_result_tables_multiple() {\n    // Multiple tables each appear in the tables array\n    let html = r#\"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(\n        result.tables.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.tables.len()\n    );\n}\n\n#[test]\nfn test_result_tables_simple() {\n    // Simple table populates the tables array in result\n    let html = r#\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(!result.tables.is_empty(), \"expected >= 1\");\n}\n\n#[test]\nfn test_result_tables_without_structure_flag() {\n    // Tables array is empty when includeDocumentStructure is false\n    let html = r#\"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        result.tables.len(),\n        0,\n        \"expected exactly 0 elements, got {}\",\n        result.tables.len()\n    );\n}\n\n#[test]\nfn test_result_warnings_empty_for_clean_input() {\n    // Warnings array is empty for well-formed HTML without problematic content\n    let html = r#\"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        result.warnings.len(),\n        0,\n        \"expected exactly 0 elements, got {}\",\n        result.warnings.len()\n    );\n}\n\n#[test]\nfn test_result_warnings_empty_for_complex_input() {\n    // Warnings array is empty for complex but valid HTML\n    let html = r#\"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        result.warnings.len(),\n        0,\n        \"expected exactly 0 elements, got {}\",\n        result.warnings.len()\n    );\n}\n\n#[test]\nfn test_result_warnings_empty_for_malformed_html() {\n    // Warnings array is empty even for malformed HTML (parser is lenient)\n    let html = r#\"<p>Unclosed paragraph<div>Mixed nesting</p></div>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert_eq!(\n        result.warnings.len(),\n        0,\n        \"expected exactly 0 elements, got {}\",\n        result.warnings.len()\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/smoke_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d99bf526e1bd8bbd56a5e5309bf0d9b3b2d29d29d8b16720c555c3783521e81c\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: smoke\n\nuse html_to_markdown_rs::convert;\n\n#[test]\nfn test_smoke_empty_string() {\n    // Empty string produces empty output\n    let html = r#\"\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"\"#, \"equals assertion failed\");\n}\n\n#[test]\nfn test_smoke_simple_heading() {\n    // H1 heading converts to ATX markdown\n    let html = r#\"<h1>Title</h1>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"# Title\"#),\n        \"expected to contain: {}\",\n        r#\"# Title\"#\n    );\n}\n\n#[test]\nfn test_smoke_simple_paragraph() {\n    // Simple paragraph converts correctly\n    let html = r#\"<p>Hello World</p>\"#;\n    let result = convert(html, None, None).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert_eq!(content.trim(), r#\"Hello World\"#, \"equals assertion failed\");\n    assert!(!content.is_empty(), \"expected non-empty value\");\n}\n"
  },
  {
    "path": "e2e/rust/tests/structure_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:67e99bcb1f65ed48775bb6d477efb06e94373fd43d1bf12b510d46823d227079\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: structure\n\nuse html_to_markdown_rs::convert;\nuse html_to_markdown_rs::ConversionOptions;\n\n#[test]\nfn test_structure_code_block() {\n    // Fenced code block produces Code node\n    let html = r#\"<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_deep_nesting_h1_h2_h3() {\n    // H1 > H2 > H3 creates three levels of heading nesting\n    let html = r#\"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 5,\n        \"expected at least 5 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_h1_h2_nested_group() {\n    // H1 followed by H2 creates a nested group under the H1\n    let html = r#\"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 3,\n        \"expected at least 3 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_heading_paragraph() {\n    // Simple heading followed by paragraph produces Heading and Paragraph nodes\n    let html = r#\"<h1>Title</h1><p>A paragraph of text.</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_list() {\n    // Unordered list produces List and ListItem nodes\n    let html = r#\"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 2,\n        \"expected at least 2 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_multiple_headings() {\n    // Multiple headings create multiple Heading nodes with correct levels\n    let html = r#\"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 4,\n        \"expected at least 4 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n\n#[test]\nfn test_structure_sibling_h1_groups() {\n    // H1, H2, then another H1 creates two sibling top-level groups\n    let html = r#\"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\"#;\n    let options_json = serde_json::json!({\"include_document_structure\": true});\n    let options = serde_json::from_value::<ConversionOptions>(options_json).unwrap();\n    let result = convert(html, Some(options.clone()), None).expect(\"should succeed\");\n    assert!(result.content.is_some(), \"expected content to be present\");\n    assert!(\n        !result.document.as_ref().unwrap().nodes.is_empty(),\n        \"expected non-empty value\"\n    );\n    assert!(\n        result.document.as_ref().unwrap().nodes.len() >= 4,\n        \"expected at least 4 elements, got {}\",\n        result.document.as_ref().unwrap().nodes.len()\n    );\n}\n"
  },
  {
    "path": "e2e/rust/tests/visitor_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:09fc3db1e3693a9a9b624db1b59f2ff395e851cdbfe0c531d1e2fd96b95da487\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: visitor\n\nuse html_to_markdown_rs::convert;\nuse html_to_markdown_rs::visitor::HtmlVisitor;\nuse html_to_markdown_rs::{NodeContext, VisitResult};\n\n#[test]\nfn test_visitor_audio_custom() {\n    // Visitor replaces audio element with custom output\n    let html = r#\"<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_audio(&mut self, _: &NodeContext, _src: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"[AUDIO: podcast.mp3]\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[AUDIO: podcast.mp3]\"#),\n        \"expected to contain: {}\",\n        r#\"[AUDIO: podcast.mp3]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Listen to this:\"#),\n        \"expected to contain: {}\",\n        r#\"Listen to this:\"#\n    );\n}\n\n#[test]\nfn test_visitor_audio_skip() {\n    // Visitor removes audio elements from output\n    let html = r#\"<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_audio(&mut self, _: &NodeContext, _src: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Background music:\"#),\n        \"expected to contain: {}\",\n        r#\"Background music:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Enjoy!\"#),\n        \"expected to contain: {}\",\n        r#\"Enjoy!\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"music.ogg\"#),\n        \"expected NOT to contain: {}\",\n        r#\"music.ogg\"#\n    );\n}\n\n#[test]\nfn test_visitor_button_custom() {\n    // Visitor replaces button with bracketed text\n    let html =\n        r#\"<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_button(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[BTN:{text}]\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[BTN:Click me]\"#),\n        \"expected to contain: {}\",\n        r#\"[BTN:Click me]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[BTN:Cancel]\"#),\n        \"expected to contain: {}\",\n        r#\"[BTN:Cancel]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Confirm action:\"#),\n        \"expected to contain: {}\",\n        r#\"Confirm action:\"#\n    );\n}\n\n#[test]\nfn test_visitor_button_skip() {\n    // Visitor removes all buttons from output\n    let html = r#\"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_button(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Actions available:\"#),\n        \"expected to contain: {}\",\n        r#\"Actions available:\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Save\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Save\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Delete\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Delete\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Export\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Export\"#\n    );\n}\n\n#[test]\nfn test_visitor_continue_default() {\n    // Visitor continue action preserves default conversion\n    let html = r#\"<p>Hello <strong>World</strong></p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_strong(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Continue\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**World**\"#),\n        \"expected to contain: {}\",\n        r#\"**World**\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_blockquote() {\n    // Visitor replaces blockquote with custom format\n    let html = r#\"<blockquote><p>A wise quote.</p></blockquote>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_blockquote(&mut self, _: &NodeContext, content: &str, _depth: usize) -> VisitResult {\n            VisitResult::Custom(format!(\"QUOTE: \\\"{content}\\\"\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"QUOTE:\"#),\n        \"expected to contain: {}\",\n        r#\"QUOTE:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"A wise quote.\"#),\n        \"expected to contain: {}\",\n        r#\"A wise quote.\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_emphasis() {\n    // Visitor replaces emphasis with custom output\n    let html = r#\"<p>This is <em>important</em> text.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_emphasis(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\">>>{text}<<<\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\">>>important<<<\"#),\n        \"expected to contain: {}\",\n        r#\">>>important<<<\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"*important*\"#),\n        \"expected NOT to contain: {}\",\n        r#\"*important*\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_heading() {\n    // Visitor replaces heading with custom format\n    let html = r#\"<h2>Section Title</h2><p>Content below heading.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_heading(&mut self, _: &NodeContext, _level: u32, text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Custom(format!(\"--- {text} ---\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"--- Section Title ---\"#),\n        \"expected to contain: {}\",\n        r#\"--- Section Title ---\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"## Section Title\"#),\n        \"expected NOT to contain: {}\",\n        r#\"## Section Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Content below heading.\"#),\n        \"expected to contain: {}\",\n        r#\"Content below heading.\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_image() {\n    // Visitor replaces image with custom output using template\n    let html = r#\"<img src=\"banner.png\" alt=\"Banner\">\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_image(&mut self, _: &NodeContext, _src: &str, alt: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Custom(format!(\"[Image: {alt}]\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[Image: Banner]\"#),\n        \"expected to contain: {}\",\n        r#\"[Image: Banner]\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"banner.png\"#),\n        \"expected NOT to contain: {}\",\n        r#\"banner.png\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_link_format() {\n    // Visitor reformats links using template interpolation\n    let html = r#\"<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_link(&mut self, _: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Custom(format!(\"{text} ({href})\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Example (https://example.com)\"#),\n        \"expected to contain: {}\",\n        r#\"Example (https://example.com)\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"[Example]\"#),\n        \"expected NOT to contain: {}\",\n        r#\"[Example]\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_link_static() {\n    // Visitor replaces link with static custom output\n    let html = r#\"<a href=\"https://example.com\">Click here</a>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_link(&mut self, _: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"[REDACTED LINK]\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[REDACTED LINK]\"#),\n        \"expected to contain: {}\",\n        r#\"[REDACTED LINK]\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"example.com\"#),\n        \"expected NOT to contain: {}\",\n        r#\"example.com\"#\n    );\n}\n\n#[test]\nfn test_visitor_custom_output() {\n    // Visitor custom action replaces element output\n    let html = r#\"<h1>Original Heading</h1>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_heading(&mut self, _: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"## REPLACED HEADING\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"## REPLACED HEADING\"#),\n        \"expected to contain: {}\",\n        r#\"## REPLACED HEADING\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"# Original Heading\"#),\n        \"expected NOT to contain: {}\",\n        r#\"# Original Heading\"#\n    );\n}\n\n#[test]\nfn test_visitor_definition_list_custom() {\n    // Visitor customizes definition list items\n    let html = r#\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_definition_term(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"**{text}**\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**HTML**\"#),\n        \"expected to contain: {}\",\n        r#\"**HTML**\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"**CSS**\"#),\n        \"expected to contain: {}\",\n        r#\"**CSS**\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"HyperText Markup Language\"#),\n        \"expected to contain: {}\",\n        r#\"HyperText Markup Language\"#\n    );\n}\n\n#[test]\nfn test_visitor_definition_list_custom_format() {\n    // Visitor formats definition lists with custom templates\n    let html = r#\"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_definition_term(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"### {text}\"))\n        }\n        fn visit_definition_description(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"> {text}\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"### Python\"#),\n        \"expected to contain: {}\",\n        r#\"### Python\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"### JavaScript\"#),\n        \"expected to contain: {}\",\n        r#\"### JavaScript\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> A high-level programming language\"#),\n        \"expected to contain: {}\",\n        r#\"> A high-level programming language\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"> A scripting language for web browsers\"#),\n        \"expected to contain: {}\",\n        r#\"> A scripting language for web browsers\"#\n    );\n}\n\n#[test]\nfn test_visitor_definition_list_skip() {\n    // Visitor skips definition list items from output\n    let html = r#\"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_definition_description(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n        fn visit_definition_term(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Glossary:\"#),\n        \"expected to contain: {}\",\n        r#\"Glossary:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"End of glossary\"#),\n        \"expected to contain: {}\",\n        r#\"End of glossary\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Term A\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Term A\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Definition\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Definition\"#\n    );\n}\n\n#[test]\nfn test_visitor_details_summary_custom() {\n    // Visitor customizes details/summary disclosure elements\n    let html = r#\"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_summary(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"[EXPANDABLE] {text}\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[EXPANDABLE] Click to expand\"#),\n        \"expected to contain: {}\",\n        r#\"[EXPANDABLE] Click to expand\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"This content is initially hidden.\"#),\n        \"expected to contain: {}\",\n        r#\"This content is initially hidden.\"#\n    );\n}\n\n#[test]\nfn test_visitor_details_summary_skip() {\n    // Visitor removes details/summary elements entirely\n    let html = r#\"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_details(&mut self, _: &NodeContext, _open: bool) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Main content here.\"#),\n        \"expected to contain: {}\",\n        r#\"Main content here.\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"More main content.\"#),\n        \"expected to contain: {}\",\n        r#\"More main content.\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Hidden section\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Hidden section\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Secret details\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Secret details\"#\n    );\n}\n\n#[test]\nfn test_visitor_figure_custom() {\n    // Visitor customizes figure and figcaption elements\n    let html = r#\"<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_figcaption(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"*{text}*\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Article Title\"#),\n        \"expected to contain: {}\",\n        r#\"Article Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"*Figure 1: System Architecture*\"#),\n        \"expected to contain: {}\",\n        r#\"*Figure 1: System Architecture*\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Explanation of the figure.\"#),\n        \"expected to contain: {}\",\n        r#\"Explanation of the figure.\"#\n    );\n}\n\n#[test]\nfn test_visitor_figure_custom_wrap() {\n    // Visitor wraps figure content with custom formatting\n    let html = r#\"<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_figure_start(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Custom(\"\\n[FIGURE]\\n\".to_string())\n        }\n        fn visit_figure_end(&mut self, _: &NodeContext, output: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"{output}\\n[/FIGURE]\\n\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[FIGURE]\"#),\n        \"expected to contain: {}\",\n        r#\"[FIGURE]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[/FIGURE]\"#),\n        \"expected to contain: {}\",\n        r#\"[/FIGURE]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Gallery\"#),\n        \"expected to contain: {}\",\n        r#\"Gallery\"#\n    );\n}\n\n#[test]\nfn test_visitor_figure_skip() {\n    // Visitor removes figure elements with their captions\n    let html = r#\"<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_figure_start(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"See the chart below:\"#),\n        \"expected to contain: {}\",\n        r#\"See the chart below:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"As shown in the chart above.\"#),\n        \"expected to contain: {}\",\n        r#\"As shown in the chart above.\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Revenue Trends\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Revenue Trends\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"chart.svg\"#),\n        \"expected NOT to contain: {}\",\n        r#\"chart.svg\"#\n    );\n}\n\n#[test]\nfn test_visitor_form_custom() {\n    // Visitor replaces form with custom output\n    let html = r#\"<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_form(&mut self, _: &NodeContext, _action: Option<&str>, _method: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"[FORM PLACEHOLDER]\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[FORM PLACEHOLDER]\"#),\n        \"expected to contain: {}\",\n        r#\"[FORM PLACEHOLDER]\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"submit\"#),\n        \"expected NOT to contain: {}\",\n        r#\"submit\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"input\"#),\n        \"expected NOT to contain: {}\",\n        r#\"input\"#\n    );\n}\n\n#[test]\nfn test_visitor_form_skip() {\n    // Visitor skips form elements entirely\n    let html = r#\"<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_form(&mut self, _: &NodeContext, _action: Option<&str>, _method: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before form\"#),\n        \"expected to contain: {}\",\n        r#\"Before form\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After form\"#),\n        \"expected to contain: {}\",\n        r#\"After form\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"email\"#),\n        \"expected NOT to contain: {}\",\n        r#\"email\"#\n    );\n}\n\n#[test]\nfn test_visitor_horizontal_rule_custom() {\n    // Visitor replaces horizontal rule with custom output\n    let html = r#\"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_horizontal_rule(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Custom(\"\\n[DIVIDER]\\n\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[DIVIDER]\"#),\n        \"expected to contain: {}\",\n        r#\"[DIVIDER]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Section A\"#),\n        \"expected to contain: {}\",\n        r#\"Section A\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Section B\"#),\n        \"expected to contain: {}\",\n        r#\"Section B\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"---\"#),\n        \"expected NOT to contain: {}\",\n        r#\"---\"#\n    );\n}\n\n#[test]\nfn test_visitor_horizontal_rule_skip() {\n    // Visitor removes all horizontal rules\n    let html = r#\"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_horizontal_rule(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Part 1\"#),\n        \"expected to contain: {}\",\n        r#\"Part 1\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Part 2\"#),\n        \"expected to contain: {}\",\n        r#\"Part 2\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Part 3\"#),\n        \"expected to contain: {}\",\n        r#\"Part 3\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"---\"#),\n        \"expected NOT to contain: {}\",\n        r#\"---\"#\n    );\n}\n\n#[test]\nfn test_visitor_iframe_custom() {\n    // Visitor replaces embedded iframe with custom text\n    let html = r#\"<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_iframe(&mut self, _: &NodeContext, _src: Option<&str>) -> VisitResult {\n            VisitResult::Custom(\"[EMBEDDED: https://maps.example.com/embed]\".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[EMBEDDED: https://maps.example.com/embed]\"#),\n        \"expected to contain: {}\",\n        r#\"[EMBEDDED: https://maps.example.com/embed]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Embedded map:\"#),\n        \"expected to contain: {}\",\n        r#\"Embedded map:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"End of map\"#),\n        \"expected to contain: {}\",\n        r#\"End of map\"#\n    );\n}\n\n#[test]\nfn test_visitor_iframe_skip() {\n    // Visitor removes embedded iframes\n    let html = r#\"<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_iframe(&mut self, _: &NodeContext, _src: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Reviews\"#),\n        \"expected to contain: {}\",\n        r#\"Reviews\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"See reviews from our partners.\"#),\n        \"expected to contain: {}\",\n        r#\"See reviews from our partners.\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"widget.example.com\"#),\n        \"expected NOT to contain: {}\",\n        r#\"widget.example.com\"#\n    );\n}\n\n#[test]\nfn test_visitor_input_custom() {\n    // Visitor replaces input with labeled output\n    let html = r#\"<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_input(\n            &mut self,\n            _: &NodeContext,\n            input_type: &str,\n            _name: Option<&str>,\n            _value: Option<&str>,\n        ) -> VisitResult {\n            VisitResult::Custom(format!(\"[INPUT:{input_type}]\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[INPUT:text]\"#),\n        \"expected to contain: {}\",\n        r#\"[INPUT:text]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[INPUT:password]\"#),\n        \"expected to contain: {}\",\n        r#\"[INPUT:password]\"#\n    );\n}\n\n#[test]\nfn test_visitor_input_skip() {\n    // Visitor skips all input elements\n    let html = r#\"<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_input(\n            &mut self,\n            _: &NodeContext,\n            _input_type: &str,\n            _name: Option<&str>,\n            _value: Option<&str>,\n        ) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Sign up:\"#),\n        \"expected to contain: {}\",\n        r#\"Sign up:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Continue\"#),\n        \"expected to contain: {}\",\n        r#\"Continue\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"email\"#),\n        \"expected NOT to contain: {}\",\n        r#\"email\"#\n    );\n}\n\n#[test]\nfn test_visitor_line_break_custom() {\n    // Visitor replaces line break with custom output\n    let html = r#\"<p>First line<br>Second line<br>Third line</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_line_break(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Custom(\" | \".to_string())\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"First line | Second line | Third line\"#),\n        \"expected to contain: {}\",\n        r#\"First line | Second line | Third line\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(\n            r#\"\n\n\"#\n        ),\n        \"expected NOT to contain: {}\",\n        r#\"\n\n\"#\n    );\n}\n\n#[test]\nfn test_visitor_line_break_skip() {\n    // Visitor removes all line breaks\n    let html = r#\"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_line_break(&mut self, _: &NodeContext) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Address Line 1Address Line 2Address Line 3\"#),\n        \"expected to contain: {}\",\n        r#\"Address Line 1Address Line 2Address Line 3\"#\n    );\n}\n\n#[test]\nfn test_visitor_mark_custom() {\n    // Visitor replaces highlight/mark with custom template\n    let html = r#\"<p>This is a <mark>highlighted passage</mark> in the text.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_mark(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"=={text}==\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"==highlighted passage==\"#),\n        \"expected to contain: {}\",\n        r#\"==highlighted passage==\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"This is a\"#),\n        \"expected to contain: {}\",\n        r#\"This is a\"#\n    );\n}\n\n#[test]\nfn test_visitor_mark_skip() {\n    // Visitor skips mark elements entirely\n    let html = r#\"<p>Key insight: <mark>always validate input</mark> for security.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_mark(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"always validate input\"#),\n        \"expected NOT to contain: {}\",\n        r#\"always validate input\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Key insight:\"#),\n        \"expected to contain: {}\",\n        r#\"Key insight:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"for security.\"#),\n        \"expected to contain: {}\",\n        r#\"for security.\"#\n    );\n}\n\n#[test]\nfn test_visitor_preserve_html() {\n    // Visitor preserve_html action includes raw HTML in output\n    let html = r#\"<div><custom-tag>Custom content</custom-tag></div>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_custom_element(&mut self, _: &NodeContext, _tag_name: &str, _html: &str) -> VisitResult {\n            VisitResult::PreserveHtml\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"<custom-tag>\"#),\n        \"expected to contain: {}\",\n        r#\"<custom-tag>\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_all_headings() {\n    // Visitor skips all headings from document\n    let html = r#\"<h1>Title</h1><p>Body text remains.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_heading(&mut self, _: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Title\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Body text remains.\"#),\n        \"expected to contain: {}\",\n        r#\"Body text remains.\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_code_blocks() {\n    // Visitor skips code blocks from output\n    let html = r#\"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_code_block(&mut self, _: &NodeContext, _lang: Option<&str>, _code: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Intro text\"#),\n        \"expected to contain: {}\",\n        r#\"Intro text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Outro text\"#),\n        \"expected to contain: {}\",\n        r#\"Outro text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"let x = 42\"#),\n        \"expected NOT to contain: {}\",\n        r#\"let x = 42\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_heading() {\n    // Visitor skip action omits all headings from output\n    let html = r#\"<h1>Title</h1><p>Body text remains.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_heading(&mut self, _: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"Title\"#),\n        \"expected NOT to contain: {}\",\n        r#\"Title\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Body text remains.\"#),\n        \"expected to contain: {}\",\n        r#\"Body text remains.\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_images() {\n    // Visitor skips all images from output\n    let html = r#\"<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_image(&mut self, _: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Before image\"#),\n        \"expected to contain: {}\",\n        r#\"Before image\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"After image\"#),\n        \"expected to contain: {}\",\n        r#\"After image\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"photo.jpg\"#),\n        \"expected NOT to contain: {}\",\n        r#\"photo.jpg\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"A photo\"#),\n        \"expected NOT to contain: {}\",\n        r#\"A photo\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_links() {\n    // Visitor skips all links entirely\n    let html = r#\"<p>Before <a href=\"https://example.com\">link text</a> after</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_link(&mut self, _: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"link text\"#),\n        \"expected NOT to contain: {}\",\n        r#\"link text\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"example.com\"#),\n        \"expected NOT to contain: {}\",\n        r#\"example.com\"#\n    );\n}\n\n#[test]\nfn test_visitor_skip_strong() {\n    // Visitor skips bold/strong elements\n    let html = r#\"<p>Normal <strong>bold text</strong> normal</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_strong(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"bold text\"#),\n        \"expected NOT to contain: {}\",\n        r#\"bold text\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Normal\"#),\n        \"expected to contain: {}\",\n        r#\"Normal\"#\n    );\n}\n\n#[test]\nfn test_visitor_subscript_custom() {\n    // Visitor replaces subscript with custom template\n    let html = r#\"<p>H<sub>2</sub>O is water.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_subscript(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"~{text}~\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"H~2~O\"#),\n        \"expected to contain: {}\",\n        r#\"H~2~O\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"is water\"#),\n        \"expected to contain: {}\",\n        r#\"is water\"#\n    );\n}\n\n#[test]\nfn test_visitor_subscript_skip() {\n    // Visitor skips subscript elements entirely\n    let html = r#\"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_subscript(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"The formula CHO is sugar.\"#),\n        \"expected to contain: {}\",\n        r#\"The formula CHO is sugar.\"#\n    );\n}\n\n#[test]\nfn test_visitor_superscript_custom() {\n    // Visitor replaces superscript with custom template\n    let html = r#\"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_superscript(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"^{text}^\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"E=mc^2^\"#),\n        \"expected to contain: {}\",\n        r#\"E=mc^2^\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"revolutionized physics\"#),\n        \"expected to contain: {}\",\n        r#\"revolutionized physics\"#\n    );\n}\n\n#[test]\nfn test_visitor_superscript_skip() {\n    // Visitor skips superscript from output\n    let html = r#\"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_superscript(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"The equation x + y = z has no solutions.\"#),\n        \"expected to contain: {}\",\n        r#\"The equation x + y = z has no solutions.\"#\n    );\n}\n\n#[test]\nfn test_visitor_underline_custom() {\n    // Visitor replaces underline with custom markup\n    let html = r#\"<p>This is <u>very important</u> text.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_underline(&mut self, _: &NodeContext, text: &str) -> VisitResult {\n            VisitResult::Custom(format!(\"_{text}_\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"_very important_\"#),\n        \"expected to contain: {}\",\n        r#\"_very important_\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"**\"#),\n        \"expected NOT to contain: {}\",\n        r#\"**\"#\n    );\n}\n\n#[test]\nfn test_visitor_underline_skip() {\n    // Visitor skips underline elements from output\n    let html = r#\"<p>Normal text with <u>underlined part</u> and more text.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_underline(&mut self, _: &NodeContext, _text: &str) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Normal text with\"#),\n        \"expected to contain: {}\",\n        r#\"Normal text with\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"and more text.\"#),\n        \"expected to contain: {}\",\n        r#\"and more text.\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"underlined part\"#),\n        \"expected NOT to contain: {}\",\n        r#\"underlined part\"#\n    );\n}\n\n#[test]\nfn test_visitor_video_custom() {\n    // Visitor replaces video with custom link\n    let html = r#\"<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_video(&mut self, _: &NodeContext, src: Option<&str>) -> VisitResult {\n            let src = src.unwrap_or_default();\n            VisitResult::Custom(format!(\"[VIDEO: {src}]\"))\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"[VIDEO: tutorial.mp4]\"#),\n        \"expected to contain: {}\",\n        r#\"[VIDEO: tutorial.mp4]\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Watch our tutorial:\"#),\n        \"expected to contain: {}\",\n        r#\"Watch our tutorial:\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Great content!\"#),\n        \"expected to contain: {}\",\n        r#\"Great content!\"#\n    );\n}\n\n#[test]\nfn test_visitor_video_skip() {\n    // Visitor removes video elements entirely\n    let html = r#\"<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>\"#;\n    #[derive(Debug)]\n    struct _TestVisitor;\n    impl HtmlVisitor for _TestVisitor {\n        fn visit_video(&mut self, _: &NodeContext, _src: Option<&str>) -> VisitResult {\n            VisitResult::Skip\n        }\n    }\n    let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));\n    let result = convert(html, None, Some(visitor)).expect(\"should succeed\");\n    let content = result.content.as_deref().unwrap_or(\"\");\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"Demo\"#),\n        \"expected to contain: {}\",\n        r#\"Demo\"#\n    );\n    assert!(\n        format!(\"{:?}\", content).contains(r#\"See the demo above.\"#),\n        \"expected to contain: {}\",\n        r#\"See the demo above.\"#\n    );\n    assert!(\n        !format!(\"{:?}\", content).contains(r#\"demo.webm\"#),\n        \"expected NOT to contain: {}\",\n        r#\"demo.webm\"#\n    );\n}\n"
  },
  {
    "path": "e2e/swift/Package.swift",
    "content": "// swift-tools-version: 5.9\nimport PackageDescription\n\nlet package = Package(\n    name: \"E2eSwift\",\n    platforms: [\n        .macOS(.v13),\n    ],\n    dependencies: [\n        .package(path: \"../../packages/swift\"),\n    ],\n    targets: [\n        .testTarget(\n            name: \"HtmlToMarkdownRsTests\",\n            dependencies: [\"HtmlToMarkdownRs\"]\n        ),\n    ]\n)\n"
  },
  {
    "path": "e2e/wasm/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-wasm-e2e-wasm\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"@kreuzberg/html-to-markdown-wasm\": \"file:../../crates/html-to-markdown-wasm\",\n    \"vite-plugin-top-level-await\": \"^1.4.0\",\n    \"vite-plugin-wasm\": \"^3.4.0\",\n    \"vitest\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "e2e/wasm/tests/conversion.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ddad3778c3d9f92cbf47159270ee19b1f5cf422a8dd15f2175caa14d0208e1a4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"conversion\", () => {\n  it(\"blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed\", () => {\n    const result = convert(\n      \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"> First paragraph.\");\n    expect(result.content).toContain(\"> Second paragraph.\");\n  });\n\n  it(\"blockquote_nested: Nested blockquote produces double-prefixed lines\", () => {\n    const result = convert(\n      \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Outer quote.\");\n    expect(result.content).toContain(\"Inner quote.\");\n  });\n\n  it(\"blockquote_simple: Simple blockquote\", () => {\n    const result = convert(\n      \"<blockquote><p>Quote text</p></blockquote>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"> Quote text\");\n  });\n\n  it(\"blockquote_with_list: Blockquote containing a list preserves list items inside quote\", () => {\n    const result = convert(\n      \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Quote intro:\");\n    expect(result.content).toContain(\"Point one\");\n    expect(result.content).toContain(\"Point two\");\n  });\n\n  it(\"bold_and_italic: Nested bold and italic\", () => {\n    const result = convert(\n      \"<p><strong><em>both</em></strong></p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"***both***\");\n  });\n\n  it(\"bold_strong: Strong tag converts to bold\", () => {\n    const result = convert(\"<p><strong>bold</strong></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"**bold**\");\n  });\n\n  it(\"code_block: Code block with language preserves content\", () => {\n    const result = convert(\n      \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"print('hello')\");\n  });\n\n  it(\"code_block_no_language: Code block without a language class preserves content\", () => {\n    const result = convert(\n      \"<pre><code>plain code here</code></pre>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"plain code here\");\n  });\n\n  it(\"code_inline_in_paragraph: Inline code element nested inside a paragraph\", () => {\n    const result = convert(\n      \"<p>Call the <code>initialize()</code> method first.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"`initialize()`\");\n  });\n\n  it(\"code_with_backticks_in_content: Inline code containing backtick characters is properly escaped\", () => {\n    const result = convert(\n      \"<p>Use <code>`backtick` here</code> carefully.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"backtick\");\n  });\n\n  it(\"emphasis_mark_highlight: mark tag produces highlighted output\", () => {\n    const result = convert(\"<p><mark>highlighted</mark></p>\", {} as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted\");\n  });\n\n  it(\"emphasis_strikethrough_del: del tag converts to GFM strikethrough\", () => {\n    const result = convert(\"<p><del>deleted text</del></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"~~deleted text~~\");\n  });\n\n  it(\"emphasis_strikethrough_s: s tag converts to GFM strikethrough\", () => {\n    const result = convert(\"<p><s>strikethrough</s></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"~~strikethrough~~\");\n  });\n\n  it(\"emphasis_subscript: sub tag content is preserved\", () => {\n    const result = convert(\"<p>H<sub>2</sub>O</p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n  });\n\n  it(\"emphasis_superscript: sup tag content is preserved\", () => {\n    const result = convert(\"<p>x<sup>2</sup></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"x\");\n    expect(result.content).toContain(\"2\");\n  });\n\n  it(\"emphasis_underline_u: u tag content is preserved in output\", () => {\n    const result = convert(\"<p><u>underlined</u></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"underlined\");\n  });\n\n  it(\"form_input_elements: Form input elements produce readable output without form mechanics\", () => {\n    const result = convert(\n      '<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Name\");\n  });\n\n  it(\"form_select_options: Select element with options produces readable output\", () => {\n    const result = convert(\n      '<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Color\");\n  });\n\n  it(\"form_textarea: Textarea element produces readable output\", () => {\n    const result = convert(\n      \"<form><label>Message:</label><textarea>Default text content</textarea></form>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Message\");\n  });\n\n  it(\"heading_h1: H1 heading\", () => {\n    const result = convert(\"<h1>Heading 1</h1>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"# Heading 1\");\n  });\n\n  it(\"heading_h2: H2 heading\", () => {\n    const result = convert(\"<h2>Heading 2</h2>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"## Heading 2\");\n  });\n\n  it(\"heading_h3: H3 heading\", () => {\n    const result = convert(\"<h3>Heading 3</h3>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"### Heading 3\");\n  });\n\n  it(\"heading_h4: H4 heading\", () => {\n    const result = convert(\"<h4>Heading 4</h4>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"#### Heading 4\");\n  });\n\n  it(\"heading_h5: H5 heading\", () => {\n    const result = convert(\"<h5>Heading 5</h5>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"##### Heading 5\");\n  });\n\n  it(\"heading_h6: H6 heading\", () => {\n    const result = convert(\"<h6>Heading 6</h6>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"###### Heading 6\");\n  });\n\n  it(\"image_figure_figcaption: Figure with figcaption preserves both image and caption\", () => {\n    const result = convert(\n      '<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"![A sunset](sunset.jpg)\");\n    expect(result.content).toContain(\"Beautiful sunset over the ocean\");\n  });\n\n  it(\"image_linked: Image inside an anchor produces a linked image\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"![Icon](icon.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"image_no_alt: Image without alt text produces image markdown\", () => {\n    const result = convert('<img src=\"banner.jpg\">', {} as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"banner.jpg\");\n  });\n\n  it(\"image_simple: Image with alt text\", () => {\n    const result = convert(\n      '<img src=\"photo.jpg\" alt=\"A photo\">',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"![A photo](photo.jpg)\");\n  });\n\n  it(\"image_with_title: Image with title attribute includes title in output\", () => {\n    const result = convert(\n      '<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"![Sales chart](chart.png\");\n    expect(result.content).toContain(\"Q3 Sales\");\n  });\n\n  it(\"inline_code: Inline code\", () => {\n    const result = convert(\n      \"<p>Use <code>console.log()</code> to debug</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"`console.log()`\");\n  });\n\n  it(\"italic_em: Em tag converts to italic\", () => {\n    const result = convert(\"<p><em>italic</em></p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"*italic*\");\n  });\n\n  it(\"line_break_br_tag: Single br tag produces a line break in output\", () => {\n    const result = convert(\n      \"<p>First line.<br>Second line.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"First line.\");\n    expect(result.content).toContain(\"Second line.\");\n  });\n\n  it(\"line_break_hr_tag: hr tag produces a horizontal separator between content\", () => {\n    const result = convert(\n      \"<p>Before rule.</p><hr><p>After rule.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Before rule.\");\n    expect(result.content).toContain(\"After rule.\");\n  });\n\n  it(\"line_break_multiple_br: Multiple consecutive br tags in sequence\", () => {\n    const result = convert(\"<p>Start.<br><br>End.</p>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Start.\");\n    expect(result.content).toContain(\"End.\");\n  });\n\n  it(\"link_anchor_fragment: Fragment-only anchor link is preserved\", () => {\n    const result = convert(\n      '<a href=\"#section\">Jump to section</a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"[Jump to section](#section)\");\n  });\n\n  it(\"link_empty_href: Link with empty href produces output with the link text\", () => {\n    const result = convert('<a href=\"\">No destination</a>', {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"No destination\");\n  });\n\n  it(\"link_image_inside: Image inside a link produces a linked image\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"![Logo](logo.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"link_mailto: Mailto link is preserved with mailto: scheme\", () => {\n    const result = convert(\n      '<a href=\"mailto:user@example.com\">Email us</a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"mailto:user@example.com\");\n  });\n\n  it(\"link_simple: Simple link\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\">Example</a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"[Example](https://example.com)\");\n  });\n\n  it(\"link_with_bold_text: Link containing bold text preserves formatting\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\"><strong>Bold link</strong></a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"**Bold link**\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"link_with_title: Link with title attribute\", () => {\n    const result = convert(\n      '<a href=\"https://example.com\" title=\"Example Site\">Example</a>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"[Example](https://example.com\");\n    expect(result.content).toContain(\"Example Site\");\n  });\n\n  it(\"list_definition_dl: Definition list with dt and dd elements\", () => {\n    const result = convert(\n      \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Term One\");\n    expect(result.content).toContain(\"Definition of term one.\");\n    expect(result.content).toContain(\"Term Two\");\n    expect(result.content).toContain(\"Definition of term two.\");\n  });\n\n  it(\"list_item_multiple_paragraphs: List item containing multiple paragraphs\", () => {\n    const result = convert(\n      \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"First paragraph in item.\");\n    expect(result.content).toContain(\"Second paragraph in item.\");\n    expect(result.content).toContain(\"Simple item\");\n  });\n\n  it(\"list_mixed_nested: Mixed list: ordered list nested inside unordered list\", () => {\n    const result = convert(\n      \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Item A\");\n    expect(result.content).toContain(\"Sub 1\");\n    expect(result.content).toContain(\"Sub 2\");\n    expect(result.content).toContain(\"Item B\");\n  });\n\n  it(\"list_nested_ordered: Nested ordered list with two levels of depth\", () => {\n    const result = convert(\n      \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Step 1\");\n    expect(result.content).toContain(\"Step 1a\");\n    expect(result.content).toContain(\"Step 1b\");\n    expect(result.content).toContain(\"Step 2\");\n  });\n\n  it(\"list_nested_unordered: Nested unordered list with two levels of depth\", () => {\n    const result = convert(\n      \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Parent A\");\n    expect(result.content).toContain(\"Child A1\");\n    expect(result.content).toContain(\"Child A2\");\n    expect(result.content).toContain(\"Parent B\");\n  });\n\n  it(\"list_task_checkboxes: Task list with checked and unchecked checkboxes\", () => {\n    const result = convert(\n      '<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Done task\");\n    expect(result.content).toContain(\"Pending task\");\n  });\n\n  it(\"ordered_list: Ordered list\", () => {\n    const result = convert(\n      \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"1. First\");\n    expect(result.content).toContain(\"2. Second\");\n    expect(result.content).toContain(\"3. Third\");\n  });\n\n  it(\"paragraph_multiple: Multiple paragraphs are separated by a blank line\", () => {\n    const result = convert(\n      \"<p>First paragraph.</p><p>Second paragraph.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"First paragraph.\");\n    expect(result.content).toContain(\"Second paragraph.\");\n  });\n\n  it(\"paragraph_nested_divs: Text nested inside divs is extracted correctly\", () => {\n    const result = convert(\n      \"<div><div><p>Nested text</p></div></div>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Nested text\");\n  });\n\n  it(\"paragraph_simple: Simple paragraph converts to plain text\", () => {\n    const result = convert(\"<p>Hello World</p>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n  });\n\n  it(\"paragraph_with_inline_formatting: Paragraph with bold, italic, and a link\", () => {\n    const result = convert(\n      '<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"**bold**\");\n    expect(result.content).toContain(\"*italic*\");\n    expect(result.content).toContain(\"[link](https://example.com)\");\n  });\n\n  it(\"paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output\", () => {\n    const result = convert(\n      \"<p>Line one.<br>Line two.<br>Line three.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Line one.\");\n    expect(result.content).toContain(\"Line two.\");\n    expect(result.content).toContain(\"Line three.\");\n  });\n\n  it(\"semantic_abbr: Abbreviation element text is preserved\", () => {\n    const result = convert(\n      '<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"WWW\");\n  });\n\n  it(\"semantic_article: Article element wrapping content preserves inner content\", () => {\n    const result = convert(\n      \"<article><h2>Article Title</h2><p>Article body.</p></article>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Article Title\");\n    expect(result.content).toContain(\"Article body.\");\n  });\n\n  it(\"semantic_definition_list: Definition list with term and description\", () => {\n    const result = convert(\n      \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"HTML\");\n    expect(result.content).toContain(\"HyperText Markup Language\");\n    expect(result.content).toContain(\"CSS\");\n    expect(result.content).toContain(\"Cascading Style Sheets\");\n  });\n\n  it(\"semantic_details_summary: Details and summary elements produce readable output\", () => {\n    const result = convert(\n      \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Click to expand\");\n  });\n\n  it(\"semantic_hr: Horizontal rule produces a separator in output\", () => {\n    const result = convert(\"<p>Above</p><hr><p>Below</p>\", {} as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Above\");\n    expect(result.content).toContain(\"Below\");\n  });\n\n  it(\"semantic_mark_highlight: Mark tag produces highlighted output\", () => {\n    const result = convert(\n      \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted text\");\n  });\n\n  it(\"semantic_section_with_heading: Section element with heading preserves structure\", () => {\n    const result = convert(\n      \"<section><h3>Section Heading</h3><p>Section content.</p></section>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Section Heading\");\n    expect(result.content).toContain(\"Section content.\");\n  });\n\n  it(\"semantic_sub_superscript: Subscript and superscript elements are preserved in output\", () => {\n    const result = convert(\n      \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n    expect(result.content).toContain(\"E=mc\");\n  });\n\n  it(\"simple_table: Simple table with header\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Name\");\n    expect(result.content).toContain(\"Age\");\n    expect(result.content).toContain(\"Alice\");\n    expect(result.content).toContain(\"30\");\n    expect(result.content).toContain(\"|\");\n    expect(result.content).toContain(\"---\");\n  });\n\n  it(\"table_empty: Empty table produces no output or minimal output\", () => {\n    const result = convert(\"<table></table>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"table_no_thead: Table without thead uses first row as implied header\", () => {\n    const result = convert(\n      \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Product\");\n    expect(result.content).toContain(\"Price\");\n    expect(result.content).toContain(\"Apple\");\n    expect(result.content).toContain(\"1.00\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it(\"table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Expression\");\n    expect(result.content).toContain(\"Result\");\n    expect(result.content).toContain(\"true\");\n  });\n\n  it(\"table_with_alignment: Table with column alignment attributes\", () => {\n    const result = convert(\n      '<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Left\");\n    expect(result.content).toContain(\"Center\");\n    expect(result.content).toContain(\"Right\");\n    expect(result.content).toContain(\"L\");\n    expect(result.content).toContain(\"C\");\n    expect(result.content).toContain(\"R\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it(\"table_with_colspan: Table with colspan attribute in a header cell\", () => {\n    const result = convert(\n      '<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Full Name\");\n    expect(result.content).toContain(\"John\");\n    expect(result.content).toContain(\"Doe\");\n  });\n\n  it(\"unordered_list: Unordered list\", () => {\n    const result = convert(\n      \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"- Item 1\");\n    expect(result.content).toContain(\"- Item 2\");\n    expect(result.content).toContain(\"- Item 3\");\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/edge_cases.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:eda02b97e989ab09bec8ead636ec4025bc7c207b40f8b7a64cba51da99fe1a76\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"edge-cases\", () => {\n  it(\"empty_html: Empty HTML document\", () => {\n    const result = convert(\n      \"<html><head></head><body></body></html>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"encoding_cjk_characters: CJK (Chinese, Japanese, Korean) characters are preserved\", () => {\n    const result = convert(\n      \"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"中文内容\");\n    expect(result.content).toContain(\"日本語テキスト\");\n    expect(result.content).toContain(\"한국어 텍스트\");\n  });\n\n  it(\"encoding_html_entities: Common HTML entities are decoded in output\", () => {\n    const result = convert(\n      \"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"&\");\n    expect(result.content).toContain(\"<\");\n    expect(result.content).toContain(\">\");\n  });\n\n  it(\"encoding_named_entities: Named HTML entities like &mdash; and &hellip; are decoded\", () => {\n    const result = convert(\n      \"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"—\");\n    expect(result.content).toContain(\"…\");\n  });\n\n  it(\"encoding_numeric_entities: Numeric HTML entities (decimal and hex) are decoded\", () => {\n    const result = convert(\n      \"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"©\");\n    expect(result.content).toContain(\"®\");\n    expect(result.content).toContain(\"€\");\n  });\n\n  it(\"encoding_unicode_emoji: Emoji and Unicode characters are preserved\", () => {\n    const result = convert(\n      \"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"🌍\");\n    expect(result.content).toContain(\"🚀\");\n    expect(result.content).toContain(\"⭐\");\n  });\n\n  it(\"html_comments_only: Document containing only HTML comments produces empty output\", () => {\n    const result = convert(\n      \"<!-- This is a comment --><!-- Another comment -->\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"just_whitespace_input: Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\", () => {\n    const result = convert(\"   \", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"malformed_deeply_nested_elements: Deeply nested elements (100 levels) are handled without stack overflow\", () => {\n    const result = convert(\n      \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Deeply nested content\");\n  });\n\n  it(\"malformed_missing_block_closing_tags: Missing closing tags on block elements are auto-closed by parser\", () => {\n    const result = convert(\n      \"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"First paragraph\");\n    expect(result.content).toContain(\"Second paragraph\");\n  });\n\n  it(\"malformed_overlapping_tags: Overlapping bold/italic tags are recovered by the HTML parser without panic\", () => {\n    const result = convert(\n      \"<p><b><i>bold and italic</b></i></p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"bold and italic\");\n  });\n\n  it(\"malformed_unclosed_paragraph: Unclosed <p> tag is recovered gracefully and content is preserved\", () => {\n    const result = convert(\n      \"<p>This paragraph is never closed\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"This paragraph is never closed\");\n  });\n\n  it(\"script_tags_only: Document with only script tags produces empty output (scripts are stripped)\", () => {\n    const result = convert(\n      \"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"style_tags_only: Document with only style tags produces empty output (styles are stripped)\", () => {\n    const result = convert(\n      \"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"visitor_custom_element_with_nesting: Visitor handles custom elements with nested content\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return { custom: \"[CUSTOM WIDGET]\" };\n      },\n    };\n\n    const result = convert(\n      '<div><custom-widget data-value=\"123\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[CUSTOM WIDGET]\");\n    expect(result.content).not.toContain(\"Widget content here\");\n  });\n\n  it(\"visitor_deeply_nested_skip: Visitor skips deeply nested elements\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Outer\");\n    expect(result.content ?? \"\").toContain(\"text\");\n    expect(result.content).not.toContain(\"highlight\");\n  });\n\n  it(\"visitor_element_end_modification: Visitor modifies element at end after children processed\", () => {\n    const _testVisitor = {\n      visitElementEnd(ctx, output): string | { custom: string } {\n        return { custom: \"MODIFIED OUTPUT\" };\n      },\n    };\n\n    const result = convert(\n      \"<blockquote><p>Original quote</p></blockquote>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"visitor_element_start_skip_entire_subtree: Visitor skips at element_start level removes entire subtree\", () => {\n    const _testVisitor = {\n      visitElementStart(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<div><h1>Title</h1><p>Content</p></div>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"visitor_unknown_tag_preservation: Visitor preserves unknown HTML tags as raw HTML\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return \"preserve_html\";\n      },\n    };\n\n    const result = convert(\n      \"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Article text\");\n    expect(result.content ?? \"\").toContain(\"More article text\");\n    expect(result.content ?? \"\").toContain(\"<x-custom>\");\n  });\n\n  it(\"whitespace_only: Whitespace-only content\", () => {\n    const result = convert(\"<p>   </p>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"xss_onclick_handler_removed: onclick and other on* event handlers are removed from elements\", () => {\n    const result = convert(\n      '<p><a href=\"https://example.com\" onclick=\"alert(\\'xss\\')\">Click me</a></p><button onmouseover=\"steal_data()\">Hover me</button>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Click me\");\n  });\n\n  it(\"xss_script_tag_stripped: Script tag content is stripped and does not appear in output\", () => {\n    const result = convert(\n      \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Safe content\");\n    expect(result.content).toContain(\"More safe content\");\n  });\n\n  it(\"xss_svg_nested_script_stripped: Script tags nested inside SVG are stripped\", () => {\n    const result = convert(\n      \"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Before SVG\");\n    expect(result.content).toContain(\"After SVG\");\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/metadata.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1bfe7d4e37ee96f39e5c696eec3b7722aeb5cdd8f1eb957752a534af748956d3\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"metadata\", () => {\n  it(\"metadata_author_meta: Extract author from <meta name='author'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"author\" content=\"Jane Doe\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.author ?? \"\").trim()).toBe(\"Jane Doe\");\n  });\n\n  it(\"metadata_canonical_url: Extract canonical URL from <link rel='canonical'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><link rel=\"canonical\" href=\"https://example.com/canonical-page\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.canonicalUrl ?? \"\").trim()).toBe(\n      \"https://example.com/canonical-page\",\n    );\n  });\n\n  it(\"metadata_description_meta: Extract description from <meta name='description'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"description\" content=\"This is the page description.\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.description ?? \"\").trim()).toBe(\n      \"This is the page description.\",\n    );\n  });\n\n  it(\"metadata_dublin_core: Extract Dublin Core metadata tags\", () => {\n    const result = convert(\n      '<html><head><title>Scholarly Work</title><meta name=\"DC.title\" content=\"Principles of Knowledge Management\"><meta name=\"DC.creator\" content=\"Dr. Alice Johnson\"><meta name=\"DC.date\" content=\"2023-06-15\"><meta name=\"DC.subject\" content=\"Knowledge Management\"><meta name=\"DC.publisher\" content=\"Academic Press\"></head><body><p>This is a scholarly article.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"scholarly article\");\n  });\n\n  it(\"metadata_extract_all_images: Extract all images from a document into metadata\", () => {\n    const result = convert(\n      '<html><head><title>Gallery</title></head><body><img src=\"https://example.com/photo1.jpg\" alt=\"Photo 1\"><img src=\"https://example.com/photo2.png\" alt=\"Photo 2\"><img src=\"/local/image.webp\" alt=\"Local image\"></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.images.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"metadata_extract_all_links: Extract all links from a document into metadata\", () => {\n    const result = convert(\n      '<html><head><title>Links Page</title></head><body><p>Visit <a href=\"https://example.com\">Example</a> or <a href=\"https://docs.example.com\">Docs</a>.</p><p>Also see <a href=\"/relative/path\">relative link</a> and <a href=\"mailto:hello@example.com\">email us</a>.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.links.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"metadata_headers_hierarchy: Extract heading hierarchy from document into metadata\", () => {\n    const result = convert(\n      \"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.headers.length).toBeGreaterThanOrEqual(5);\n  });\n\n  it(\"metadata_keywords_meta: Extract keywords from <meta name='keywords'> tag\", () => {\n    const result = convert(\n      '<html><head><title>Page</title><meta name=\"keywords\" content=\"rust, markdown, html, converter\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.metadata.document.keywords.length).toBeGreaterThanOrEqual(1);\n  });\n\n  it(\"metadata_lang_attribute: Extract language from html lang attribute\", () => {\n    const result = convert(\n      '<html lang=\"es\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Hola Mundo\");\n  });\n\n  it(\"metadata_microdata_schema_article: Extract schema.org microdata for Article\", () => {\n    const result = convert(\n      '<html><head><title>Article</title></head><body><article itemscope itemtype=\"https://schema.org/Article\"><h1 itemprop=\"headline\">Breaking News Today</h1><span itemprop=\"author\">Jane Reporter</span><span itemprop=\"datePublished\">2024-04-22</span><div itemprop=\"articleBody\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Breaking News Today\");\n    expect(result.content ?? \"\").toContain(\"Jane Reporter\");\n    expect(result.content ?? \"\").toContain(\"important information\");\n  });\n\n  it(\"metadata_microdata_schema_breadcrumb: Extract schema.org breadcrumb navigation microdata\", () => {\n    const result = convert(\n      '<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\"https://schema.org/BreadcrumbList\"><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com\"><span itemprop=\"name\">Home</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><a itemprop=\"item\" href=\"https://example.com/products\"><span itemprop=\"name\">Products</span></a></span><span itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\"><span itemprop=\"name\">Current Page</span></span></nav></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Home\");\n    expect(result.content ?? \"\").toContain(\"Products\");\n    expect(result.content ?? \"\").toContain(\"Current Page\");\n  });\n\n  it(\"metadata_microdata_schema_organization: Extract schema.org microdata for Organization\", () => {\n    const result = convert(\n      '<html><head><title>Company</title></head><body><div itemscope itemtype=\"https://schema.org/Organization\"><span itemprop=\"name\">Acme Corp</span><span itemprop=\"foundingDate\">2020</span><span itemprop=\"url\">https://acmecorp.example.com</span><span itemprop=\"logo\">https://acmecorp.example.com/logo.png</span></div></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Acme Corp\");\n    expect(result.content ?? \"\").toContain(\"2020\");\n  });\n\n  it(\"metadata_microdata_schema_person: Extract schema.org microdata for Person\", () => {\n    const result = convert(\n      '<html><head><title>Contact</title></head><body><div itemscope itemtype=\"https://schema.org/Person\"><span itemprop=\"name\">John Smith</span><span itemprop=\"email\">john@example.com</span><span itemprop=\"telephone\">+1-555-0100</span></div></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"John Smith\");\n    expect(result.content ?? \"\").toContain(\"john@example.com\");\n  });\n\n  it(\"metadata_microdata_schema_product: Extract schema.org microdata for Product\", () => {\n    const result = convert(\n      '<html><head><title>Product</title></head><body><div itemscope itemtype=\"https://schema.org/Product\"><h1 itemprop=\"name\">Awesome Widget</h1><span itemprop=\"description\">The best widget on the market</span><span itemprop=\"price\">29.99</span><span itemprop=\"priceCurrency\">USD</span><img itemprop=\"image\" src=\"widget.jpg\" alt=\"Widget\"><span itemprop=\"ratingValue\">4.5</span></div></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"Awesome Widget\");\n    expect(result.content ?? \"\").toContain(\"best widget\");\n    expect(result.content ?? \"\").toContain(\"29.99\");\n  });\n\n  it(\"metadata_text_direction_ltr: Extract text direction from lang attribute on html element\", () => {\n    const result = convert(\n      '<html lang=\"en\" dir=\"ltr\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"left-to-right text\");\n  });\n\n  it(\"metadata_text_direction_rtl: Extract right-to-left text direction\", () => {\n    const result = convert(\n      '<html lang=\"ar\" dir=\"rtl\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content ?? \"\").toContain(\"right-to-left text\");\n  });\n\n  it(\"metadata_title_tag: Extract title from <title> tag\", () => {\n    const result = convert(\n      \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.title ?? \"\").trim()).toBe(\"My Page\");\n  });\n\n  it(\"og_basic_tags: Extract og:title, og:description, and og:image from Open Graph meta tags\", () => {\n    const result = convert(\n      '<html><head><title>Fallback Title</title><meta property=\"og:title\" content=\"OG Title\"><meta property=\"og:description\" content=\"OG description text.\"><meta property=\"og:image\" content=\"https://example.com/image.jpg\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.openGraph[\"title\"] ?? \"\").trim()).toBe(\"OG Title\");\n    expect((result.metadata.document.openGraph[\"description\"] ?? \"\").trim()).toBe(\n      \"OG description text.\",\n    );\n    expect((result.metadata.document.openGraph[\"image\"] ?? \"\").trim()).toBe(\n      \"https://example.com/image.jpg\",\n    );\n  });\n\n  it(\"og_multiple_tags: Extract multiple Open Graph tags including type, url, and site_name\", () => {\n    const result = convert(\n      '<html><head><meta property=\"og:title\" content=\"Article Title\"><meta property=\"og:type\" content=\"article\"><meta property=\"og:url\" content=\"https://example.com/article\"><meta property=\"og:site_name\" content=\"Example Site\"><meta property=\"og:description\" content=\"An interesting article.\"><meta property=\"og:image\" content=\"https://example.com/article.jpg\"></head><body><article><p>Article content here.</p></article></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.openGraph[\"title\"] ?? \"\").trim()).toBe(\"Article Title\");\n    expect((result.metadata.document.openGraph[\"type\"] ?? \"\").trim()).toBe(\"article\");\n    expect((result.metadata.document.openGraph[\"url\"] ?? \"\").trim()).toBe(\n      \"https://example.com/article\",\n    );\n    expect((result.metadata.document.openGraph[\"site_name\"] ?? \"\").trim()).toBe(\"Example Site\");\n  });\n\n  it(\"structured_data_json_ld: JSON-LD script tag is stripped from output (security) but metadata may be extracted\", () => {\n    const result = convert(\n      '<html><head><title>Article</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"My Article\",\"author\":{\"@type\":\"Person\",\"name\":\"Jane Doe\"},\"datePublished\":\"2024-01-15\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"My Article\");\n  });\n\n  it(\"structured_data_multiple_json_ld: Multiple JSON-LD blocks are all stripped from output\", () => {\n    const result = convert(\n      '<html><head><title>Shop Page</title><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Widget\",\"price\":\"9.99\"}</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Widget\");\n  });\n\n  it(\"twitter_card_tags: Extract Twitter card meta tags\", () => {\n    const result = convert(\n      '<html><head><meta name=\"twitter:card\" content=\"summary_large_image\"><meta name=\"twitter:site\" content=\"@examplesite\"><meta name=\"twitter:title\" content=\"Twitter Card Title\"><meta name=\"twitter:description\" content=\"Twitter card description.\"><meta name=\"twitter:image\" content=\"https://example.com/twitter-image.jpg\"></head><body><p>Content</p></body></html>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.twitterCard[\"card\"] ?? \"\").trim()).toBe(\"summary_large_image\");\n    expect((result.metadata.document.twitterCard[\"title\"] ?? \"\").trim()).toBe(\"Twitter Card Title\");\n    expect((result.metadata.document.twitterCard[\"description\"] ?? \"\").trim()).toBe(\n      \"Twitter card description.\",\n    );\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/options.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:16832b506ddc91e3efb00199b20d2aa7aaac4e9923cdb597f3d918216b0b9fc1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"options\", () => {\n  it(\"options_autolinks_false: Bare URL links rendered as regular markdown links when autolinks disabled\", () => {\n    const result = convert(\"<p><a href='https://example.com'>https://example.com</a></p>\", {\n      autolinks: false,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"example.com\");\n  });\n\n  it(\"options_br_in_tables_false: BR elements in table cells are stripped when disabled\", () => {\n    const result = convert(\"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\", {\n      brInTables: false,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Col\");\n  });\n\n  it(\"options_br_in_tables_true: BR elements in table cells render as line breaks\", () => {\n    const result = convert(\n      \"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\",\n      { brInTables: true } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Header\");\n    expect(result.content).toContain(\"Line 1\");\n    expect(result.content).toContain(\"Line 2\");\n  });\n\n  it(\"options_code_block_backticks: Backticks code block style uses triple backtick fences\", () => {\n    const result = convert(\"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\", {\n      codeBlockStyle: \"Backticks\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"```\");\n    expect(result.content).toContain(\"console.log('hi');\");\n  });\n\n  it(\"options_code_block_indented: Code blocks use 4-space indentation\", () => {\n    const result = convert(\"<pre><code>print('hello')</code></pre>\", {\n      codeBlockStyle: \"Indented\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"print('hello')\");\n    expect(result.content).not.toContain(\"```\");\n  });\n\n  it(\"options_code_block_tildes: Code blocks use tilde fences\", () => {\n    const result = convert(\"<pre><code>let x = 1;</code></pre>\", {\n      codeBlockStyle: \"Tildes\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"~~~\");\n    expect(result.content).toContain(\"let x = 1;\");\n  });\n\n  it(\"options_code_block_tildes_style: Tildes code block style uses triple tilde fences\", () => {\n    const result = convert(\"<pre><code>some code</code></pre>\", {\n      codeBlockStyle: \"Tildes\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"~~~\");\n    expect(result.content).toContain(\"some code\");\n  });\n\n  it(\"options_code_language_python: Default code language annotation on blocks without lang attribute\", () => {\n    const result = convert(\"<pre><code>def hello(): pass</code></pre>\", {\n      codeLanguage: \"python\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"```python\");\n    expect(result.content).toContain(\"def hello\");\n  });\n\n  it(\"options_convert_as_inline: Block elements treated as inline\", () => {\n    const result = convert(\"<p>One</p><p>Two</p>\", {\n      convertAsInline: true,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"One\");\n    expect(result.content).toContain(\"Two\");\n  });\n\n  it(\"options_debug_true: Debug mode enabled does not crash and produces output\", () => {\n    const result = convert(\"<p>Debug test</p>\", { debug: true } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Debug test\");\n  });\n\n  it(\"options_default_title_true: Links without title get empty title attribute when defaultTitle is true\", () => {\n    const result = convert(\"<p><a href='https://example.com'>Link</a></p>\", {\n      defaultTitle: true,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Link\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it(\"options_encoding_utf8: UTF-8 encoding hint for special characters\", () => {\n    const result = convert(\"<p>Café naïve résumé</p>\", {\n      encoding: \"utf-8\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n\n  it(\"options_escape_ascii_enabled: ASCII Markdown characters are escaped when escapeAscii is true\", () => {\n    const result = convert(\"<p>Text with # hash and [brackets] and * star</p>\", {\n      escapeAscii: true,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"hash\");\n    expect(result.content).toContain(\"brackets\");\n    expect(result.content).toContain(\"star\");\n  });\n\n  it(\"options_escape_asterisks: escape_asterisks option escapes asterisks in plain text\", () => {\n    const result = convert(\"<p>Use 2*3 = 6 in math.</p>\", {\n      escapeAsterisks: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"3\");\n    expect(result.content).toContain(\"6\");\n  });\n\n  it(\"options_escape_misc: escape_misc option escapes miscellaneous markdown characters\", () => {\n    const result = convert(\"<p>Use # and | and ~ in text.</p>\", {\n      escapeMisc: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Use\");\n    expect(result.content).toContain(\"and\");\n    expect(result.content).toContain(\"in text.\");\n  });\n\n  it(\"options_escape_underscores: escape_underscores option escapes underscores in plain text\", () => {\n    const result = convert(\"<p>The variable_name is defined.</p>\", {\n      escapeUnderscores: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"variable\");\n    expect(result.content).toContain(\"name\");\n    expect(result.content).toContain(\"defined.\");\n  });\n\n  it(\"options_exclude_selectors_attribute: Elements matching CSS attribute selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div role=\"complementary\">Sidebar</div><p>Primary text</p></body>',\n      { excludeSelectors: [\"[role='complementary']\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Primary text\");\n    expect(result.content).not.toContain(\"Sidebar\");\n  });\n\n  it(\"options_exclude_selectors_class: Elements matching CSS class selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div class=\"cookie-banner\">Accept cookies</div><p>Main content</p></body>',\n      { excludeSelectors: [\".cookie-banner\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Main content\");\n    expect(result.content).not.toContain(\"cookies\");\n  });\n\n  it(\"options_exclude_selectors_empty_noop: Empty exclude_selectors list does not affect output\", () => {\n    const result = convert(\"<p>Hello world</p>\", {\n      excludeSelectors: [],\n    } as unknown as JsConversionOptions);\n    expect(result.content ?? \"\").toContain(\"Hello world\");\n  });\n\n  it(\"options_exclude_selectors_id: Elements matching CSS id selector are excluded entirely\", () => {\n    const result = convert(\n      '<body><div id=\"ad-container\">Buy stuff</div><p>Article text</p></body>',\n      { excludeSelectors: [\"#ad-container\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Article text\");\n    expect(result.content).not.toContain(\"Buy stuff\");\n  });\n\n  it(\"options_exclude_selectors_multiple: Multiple CSS selectors each exclude their matched elements\", () => {\n    const result = convert(\n      '<body><nav class=\"nav\">Menu</nav><p>Content</p><footer>Footer</footer></body>',\n      { excludeSelectors: [\".nav\", \"footer\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Content\");\n    expect(result.content).not.toContain(\"Menu\");\n    expect(result.content).not.toContain(\"Footer\");\n  });\n\n  it(\"options_exclude_selectors_nested_content_dropped: All descendants of excluded elements are dropped\", () => {\n    const result = convert(\n      '<body><aside class=\"sidebar\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>',\n      { excludeSelectors: [\".sidebar\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Main text\");\n    expect(result.content).not.toContain(\"Related\");\n    expect(result.content).not.toContain(\"Sidebar text\");\n  });\n\n  it(\"options_exclude_selectors_plain_text_mode: Exclude selectors work in plain text output mode\", () => {\n    const result = convert('<body><div class=\"nav\">Navigation</div><p>Article body</p></body>', {\n      excludeSelectors: [\".nav\"],\n      outputFormat: \"Plain\",\n    } as unknown as JsConversionOptions);\n    expect(result.content ?? \"\").toContain(\"Article body\");\n    expect(result.content).not.toContain(\"Navigation\");\n  });\n\n  it(\"options_exclude_selectors_vs_strip_tags: exclude_selectors drops entire subtree unlike strip_tags which keeps children\", () => {\n    const result = convert(\n      '<body><div class=\"wrapper\"><p>Inner paragraph</p></div><p>Outer text</p></body>',\n      { excludeSelectors: [\".wrapper\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Outer text\");\n    expect(result.content).not.toContain(\"Inner paragraph\");\n  });\n\n  it(\"options_extract_metadata_true: Extract metadata returns document metadata when enabled\", () => {\n    const result = convert(\n      \"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\",\n      { extractMetadata: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect((result.metadata.document.title ?? \"\").trim()).toBe(\"Test Page\");\n    expect((result.metadata.document.description ?? \"\").trim()).toBe(\"A test page\");\n  });\n\n  it(\"options_heading_style_atx: ATX heading style produces hash-prefixed headings\", () => {\n    const result = convert(\"<h1>Title</h1><h2>Subtitle</h2>\", {\n      headingStyle: \"Atx\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n    expect(result.content).toContain(\"## Subtitle\");\n  });\n\n  it(\"options_heading_style_atx_closed: ATX closed heading style adds closing hashes\", () => {\n    const result = convert(\"<h1>Closed Heading</h1>\", {\n      headingStyle: \"AtxClosed\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"# Closed Heading #\");\n  });\n\n  it(\"options_heading_style_underlined: Underlined heading style produces setext-style headings for h1 and h2\", () => {\n    const result = convert(\"<h1>Main Title</h1>\", {\n      headingStyle: \"Underlined\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Main Title\");\n  });\n\n  it(\"options_highlight_bold: Mark tag rendered as bold\", () => {\n    const result = convert(\"<p>Text with <mark>highlighted</mark> text.</p>\", {\n      highlightStyle: \"Bold\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"**highlighted**\");\n  });\n\n  it(\"options_highlight_double_equal: Mark tag with double equal highlight style\", () => {\n    const result = convert(\"<p>Text with <mark>highlighted</mark> here.</p>\", {\n      highlightStyle: \"DoubleEqual\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"==highlighted==\");\n  });\n\n  it(\"options_highlight_none: Mark tag with no highlight style strips the mark\", () => {\n    const result = convert(\"<p>Text with <mark>plain</mark> content.</p>\", {\n      highlightStyle: \"None\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"plain\");\n    expect(result.content).not.toContain(\"==\");\n  });\n\n  it(\"options_keep_inline_images_in_paragraph: Images inside specified tags stay inline\", () => {\n    const result = convert(\"<p>Text <img src='icon.png' alt='icon'> more text</p>\", {\n      keepInlineImagesIn: [\"p\"],\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"more text\");\n  });\n\n  it(\"options_link_style_reference: Links use reference-style formatting\", () => {\n    const result = convert(\n      \"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\",\n      { linkStyle: \"Reference\" } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Example\");\n    expect(result.content).toContain(\"Other\");\n    expect(result.content).toContain(\"example.com\");\n  });\n\n  it(\"options_list_custom_bullets: Custom bullet character for unordered lists\", () => {\n    const result = convert(\"<ul><li>Item A</li><li>Item B</li></ul>\", {\n      bullets: \"*\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"* Item A\");\n    expect(result.content).toContain(\"* Item B\");\n  });\n\n  it(\"options_list_indent_tabs: Tab indentation type for nested list items\", () => {\n    const result = convert(\"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\", {\n      listIndentType: \"Tabs\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Parent\");\n    expect(result.content).toContain(\"Child\");\n  });\n\n  it(\"options_list_indent_width_four: Nested lists indented with 4 spaces per level\", () => {\n    const result = convert(\"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\", {\n      listIndentWidth: 4,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Outer\");\n    expect(result.content).toContain(\"Inner\");\n  });\n\n  it(\"options_max_depth_default_unlimited: Default max_depth (null) converts deeply nested content fully\", () => {\n    const result = convert(\n      \"<div><div><div><div><p>Deep content</p></div></div></div></div>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Deep content\");\n  });\n\n  it(\"options_max_depth_truncates: max_depth truncates content beyond the specified depth\", () => {\n    const result = convert(\n      \"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\",\n      { maxDepth: 3 } as unknown as JsConversionOptions,\n    );\n    expect(result.content ?? \"\").toContain(\"Shallow\");\n    expect(result.content).not.toContain(\"Too deep\");\n  });\n\n  it(\"options_max_depth_zero_empty: max_depth of 0 produces empty output\", () => {\n    const result = convert(\"<p>Hello</p>\", { maxDepth: 0 } as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"options_newline_backslash: Hard line breaks rendered with backslash\", () => {\n    const result = convert(\"<p>Line one<br>Line two</p>\", {\n      newlineStyle: \"Backslash\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Line one\");\n    expect(result.content).toContain(\"Line two\");\n  });\n\n  it(\"options_newline_spaces: Hard line breaks rendered with trailing spaces\", () => {\n    const result = convert(\"<p>First<br>Second</p>\", {\n      newlineStyle: \"Spaces\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"First\");\n    expect(result.content).toContain(\"Second\");\n  });\n\n  it(\"options_output_format_djot: Djot output format produces djot-compatible markup\", () => {\n    const result = convert(\"<p>Simple paragraph.</p>\", {\n      outputFormat: \"Djot\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Simple paragraph.\");\n  });\n\n  it(\"options_output_format_markdown: Default markdown output format produces standard markdown\", () => {\n    const result = convert(\"<h1>Title</h1><p>Some text.</p>\", {\n      headingStyle: \"Atx\",\n      outputFormat: \"Markdown\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n    expect(result.content).toContain(\"Some text.\");\n  });\n\n  it(\"options_output_format_plain: Plain text output format strips markdown syntax\", () => {\n    const result = convert(\"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\", {\n      outputFormat: \"Plain\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"bold\");\n    expect(result.content).toContain(\"text.\");\n  });\n\n  it(\"options_preprocessing_aggressive: Aggressive preset removes nav, footer, aside unconditionally\", () => {\n    const result = convert(\n      \"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\",\n      { preprocessing: { preset: \"Aggressive\" } } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Title\");\n    expect(result.content).toContain(\"Content\");\n    expect(result.content).not.toContain(\"Menu\");\n  });\n\n  it(\"options_preprocessing_minimal: Minimal preset preserves nav, footer, aside\", () => {\n    const result = convert(\"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\", {\n      preprocessing: { preset: \"Minimal\" },\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Navigation\");\n    expect(result.content).toContain(\"Content\");\n    expect(result.content).toContain(\"Footer\");\n  });\n\n  it(\"options_preprocessing_remove_forms: Forms are removed when remove_forms is true\", () => {\n    const result = convert(\n      \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\",\n      { preprocessing: { removeForms: true } } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).not.toContain(\"Submit\");\n  });\n\n  it(\"options_preserve_tags_iframe: Iframe tags preserved as raw HTML in output\", () => {\n    const result = convert(\n      \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\",\n      { preserveTags: [\"iframe\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).toContain(\"<iframe\");\n  });\n\n  it(\"options_skip_images_true: Images are omitted from output when skipImages is true\", () => {\n    const result = convert(\"<p>Before <img src='test.jpg' alt='photo'> After</p>\", {\n      skipImages: true,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"Before\");\n    expect(result.content).toContain(\"After\");\n    expect(result.content).not.toContain(\"photo\");\n  });\n\n  it(\"options_strip_newlines: Strip newlines produces single-line paragraphs\", () => {\n    const result = convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", {\n      stripNewlines: true,\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"First paragraph.\");\n    expect(result.content).toContain(\"Second paragraph.\");\n  });\n\n  it(\"options_strip_tags_div_span: Div and span tags stripped but content preserved\", () => {\n    const result = convert(\n      \"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\",\n      { stripTags: [\"div\", \"span\"] } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\"Inside div\");\n    expect(result.content).toContain(\"span text\");\n  });\n\n  it(\"options_strong_em_underscore: Strong and em tags use underscore symbol instead of asterisk\", () => {\n    const result = convert(\"<p><strong>bold</strong> and <em>italic</em></p>\", {\n      strongEmSymbol: \"_\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"__bold__\");\n    expect(result.content).toContain(\"_italic_\");\n  });\n\n  it(\"options_sub_symbol_tilde: Subscript rendered with tilde symbol\", () => {\n    const result = convert(\"<p>H<sub>2</sub>O</p>\", {\n      subSymbol: \"~\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"~2~\");\n  });\n\n  it(\"options_sup_symbol_caret: Superscript rendered with caret symbol\", () => {\n    const result = convert(\"<p>x<sup>2</sup></p>\", {\n      supSymbol: \"^\",\n    } as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"^2^\");\n  });\n\n  it(\"options_whitespace_normalized: Normalized whitespace mode collapses multiple spaces\", () => {\n    const result = convert(\"<p>Text   with    extra   spaces.</p>\", {\n      whitespaceMode: \"Normalized\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Text\");\n    expect(result.content).toContain(\"with\");\n    expect(result.content).toContain(\"extra\");\n    expect(result.content).toContain(\"spaces.\");\n  });\n\n  it(\"options_whitespace_strict: Strict whitespace mode preserves whitespace as-is\", () => {\n    const result = convert(\"<p>Preserved   spacing.</p>\", {\n      whitespaceMode: \"Strict\",\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Preserved\");\n    expect(result.content).toContain(\"spacing.\");\n  });\n\n  it(\"options_wrap_disabled: Wrap option disabled preserves long lines without breaking\", () => {\n    const result = convert(\n      \"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\",\n      { wrap: false } as unknown as JsConversionOptions,\n    );\n    expect(result.content).toContain(\n      \"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\",\n    );\n  });\n\n  it(\"options_wrap_enabled: Wrap option enabled with custom width wraps long lines\", () => {\n    const result = convert(\n      \"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\",\n      { wrap: true, wrapWidth: 40 } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"This is a long paragraph\");\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/real_world.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1ea32192aa5a4cb860b146d5c42002d2368cacf43984433e7072669381b13b34\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"real-world\", () => {\n  it(\"real_world_blog_post: Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\", () => {\n    const result = convert(\n      '<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\"https://www.mozilla.org\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\"language-bash\">curl --proto \\'=https\\' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\"language-rust\">fn main() {\\n    println!(\"Hello, world!\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\"https://doc.rust-lang.org/book/\">Rust Book</a>.</p></article>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# Getting Started with Rust\");\n    expect(result.content).toContain(\"## Installation\");\n    expect(result.content).toContain(\"## Hello World\");\n    expect(result.content).toContain(\"## Key Concepts\");\n    expect(result.content).toContain(\"cargo run\");\n    expect(result.content).toContain(\"[Mozilla](https://www.mozilla.org)\");\n    expect(result.content).toContain(\"- Ownership and borrowing\");\n  });\n\n  it(\"real_world_documentation_page: Documentation page with nested lists, code examples, and blockquotes converts correctly\", () => {\n    const result = convert(\n      '<div class=\"docs\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\"language-rust\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\"language-rust\">let markdown = convert_html(\"&lt;h1&gt;Hello&lt;/h1&gt;\").unwrap();\\nassert_eq!(markdown, \"# Hello\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\"language-rust\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\"/docs/options\">options reference</a> for a full list of configuration values.</p></blockquote></div>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# API Reference\");\n    expect(result.content).toContain(\"## convert_html\");\n    expect(result.content).toContain(\"### Parameters\");\n    expect(result.content).toContain(\"### Returns\");\n    expect(result.content).toContain(\"### Example\");\n    expect(result.content).toContain(\"## ConversionOptions\");\n    expect(result.content).toContain(\"> \");\n    expect(result.content).toContain(\"thread-safe\");\n    expect(result.content).toContain(\"convert_html\");\n    expect(result.content).toContain(\"ConversionOptions\");\n  });\n\n  it(\"real_world_product_page: Product page with table, images, and lists converts correctly\", () => {\n    const result = convert(\n      '<div class=\"product\"><h1>Wireless Keyboard Pro</h1><img src=\"https://example.com/keyboard.jpg\" alt=\"Wireless Keyboard Pro\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What\\'s in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>',\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.content).toContain(\"# Wireless Keyboard Pro\");\n    expect(result.content).toContain(\"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\");\n    expect(result.content).toContain(\"## Specifications\");\n    expect(result.content).toContain(\"Battery Life\");\n    expect(result.content).toContain(\"12 months\");\n    expect(result.content).toContain(\"Bluetooth 5.0\");\n    expect(result.content).toContain(\"## What's in the Box\");\n    expect(result.content).toContain(\"USB-C charging cable\");\n    expect(result.content).toContain(\"|\");\n    expect(result.content).toContain(\"---\");\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/result.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e8d7c2f11c54159869122e995bb79a0d240113dc2cb3117de0f8a00722319f3a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"result\", () => {\n  it(\"result_tables_empty_when_no_tables: Result tables array is empty when input has no tables\", () => {\n    const result = convert(\"<p>No tables here</p>\", {\n      includeDocumentStructure: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBe(0);\n  });\n\n  it(\"result_tables_multiple: Multiple tables each appear in the tables array\", () => {\n    const result = convert(\n      \"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(result.tables.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"result_tables_simple: Simple table populates the tables array in result\", () => {\n    const result = convert(\n      \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBeGreaterThanOrEqual(1);\n  });\n\n  it(\"result_tables_without_structure_flag: Tables array is empty when includeDocumentStructure is false\", () => {\n    const result = convert(\n      \"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.tables.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_clean_input: Warnings array is empty for well-formed HTML without problematic content\", () => {\n    const result = convert(\n      \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_complex_input: Warnings array is empty for complex but valid HTML\", () => {\n    const result = convert(\n      \"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n\n  it(\"result_warnings_empty_for_malformed_html: Warnings array is empty even for malformed HTML (parser is lenient)\", () => {\n    const result = convert(\n      \"<p>Unclosed paragraph<div>Mixed nesting</p></div>\",\n      {} as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.warnings.length).toBe(0);\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/smoke.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:16d13b417071e2adae87066006e6f2c74e85bf1d6c0defe0f16a1a1484c4cbce\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"smoke\", () => {\n  it(\"smoke_empty_string: Empty string produces empty output\", () => {\n    const result = convert(\"\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it(\"smoke_simple_heading: H1 heading converts to ATX markdown\", () => {\n    const result = convert(\"<h1>Title</h1>\", {} as unknown as JsConversionOptions);\n    expect(result.content).toContain(\"# Title\");\n  });\n\n  it(\"smoke_simple_paragraph: Simple paragraph converts correctly\", () => {\n    const result = convert(\"<p>Hello World</p>\", {} as unknown as JsConversionOptions);\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/structure.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e43658d7b2a52852a869376b328a283f86ed110902b5a251da5497d381ceb130\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"structure\", () => {\n  it(\"structure_code_block: Fenced code block produces Code node\", () => {\n    const result = convert(\n      '<p>Example code:</p><pre><code class=\"language-rust\">fn main() { println!(\"Hello\"); }</code></pre>',\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_deep_nesting_h1_h2_h3: H1 > H2 > H3 creates three levels of heading nesting\", () => {\n    const result = convert(\n      \"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(5);\n  });\n\n  it(\"structure_h1_h2_nested_group: H1 followed by H2 creates a nested group under the H1\", () => {\n    const result = convert(\n      \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(3);\n  });\n\n  it(\"structure_heading_paragraph: Simple heading followed by paragraph produces Heading and Paragraph nodes\", () => {\n    const result = convert(\"<h1>Title</h1><p>A paragraph of text.</p>\", {\n      includeDocumentStructure: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_list: Unordered list produces List and ListItem nodes\", () => {\n    const result = convert(\"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\", {\n      includeDocumentStructure: true,\n    } as unknown as JsConversionOptions);\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"structure_multiple_headings: Multiple headings create multiple Heading nodes with correct levels\", () => {\n    const result = convert(\n      \"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(4);\n  });\n\n  it(\"structure_sibling_h1_groups: H1, H2, then another H1 creates two sibling top-level groups\", () => {\n    const result = convert(\n      \"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\",\n      { includeDocumentStructure: true } as unknown as JsConversionOptions,\n    );\n    expect(\n      (() => {\n        const v = (result.content as unknown) ?? \"\";\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(\n      (() => {\n        const v = result.document.nodes as unknown;\n        return typeof v === \"string\" || Array.isArray(v)\n          ? (v as { length: number }).length\n          : Object.keys(v as object).length;\n      })(),\n    ).toBeGreaterThan(0);\n    expect(result.document.nodes.length).toBeGreaterThanOrEqual(4);\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tests/visitor.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:dcf6ffc76053cfa87a64f1fecf88fd907fe7ae27c2a9e578dd499003971c8e03\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from \"vitest\";\nimport { convert, type JsConversionOptions } from \"@kreuzberg/html-to-markdown-wasm\";\n\ndescribe(\"visitor\", () => {\n  it(\"visitor_audio_custom: Visitor replaces audio element with custom output\", () => {\n    const _testVisitor = {\n      visitAudio(ctx, src): string | { custom: string } {\n        return { custom: \"[AUDIO: podcast.mp3]\" };\n      },\n    };\n\n    const result = convert(\n      '<p>Listen to this: <audio src=\"podcast.mp3\" controls></audio></p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[AUDIO: podcast.mp3]\");\n    expect(result.content ?? \"\").toContain(\"Listen to this:\");\n  });\n\n  it(\"visitor_audio_skip: Visitor removes audio elements from output\", () => {\n    const _testVisitor = {\n      visitAudio(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Background music:</p><audio src=\"music.ogg\" autoplay></audio><p>Enjoy!</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Background music:\");\n    expect(result.content ?? \"\").toContain(\"Enjoy!\");\n    expect(result.content).not.toContain(\"music.ogg\");\n  });\n\n  it(\"visitor_button_custom: Visitor replaces button with bracketed text\", () => {\n    const _testVisitor = {\n      visitButton(ctx, text): string | { custom: string } {\n        return { custom: `[BTN:{text}]` };\n      },\n    };\n\n    const result = convert(\n      '<p>Confirm action: <button type=\"submit\">Click me</button> or <button type=\"reset\">Cancel</button></p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[BTN:Click me]\");\n    expect(result.content ?? \"\").toContain(\"[BTN:Cancel]\");\n    expect(result.content ?? \"\").toContain(\"Confirm action:\");\n  });\n\n  it(\"visitor_button_skip: Visitor removes all buttons from output\", () => {\n    const _testVisitor = {\n      visitButton(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Actions available:\");\n    expect(result.content).not.toContain(\"Save\");\n    expect(result.content).not.toContain(\"Delete\");\n    expect(result.content).not.toContain(\"Export\");\n  });\n\n  it(\"visitor_continue_default: Visitor continue action preserves default conversion\", () => {\n    const _testVisitor = {\n      visitStrong(ctx, text): string | { custom: string } {\n        return \"continue\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Hello <strong>World</strong></p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"**World**\");\n  });\n\n  it(\"visitor_custom_blockquote: Visitor replaces blockquote with custom format\", () => {\n    const _testVisitor = {\n      visitBlockquote(ctx, content, depth): string | { custom: string } {\n        return { custom: `QUOTE: \"{content}\"` };\n      },\n    };\n\n    const result = convert(\n      \"<blockquote><p>A wise quote.</p></blockquote>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"QUOTE:\");\n    expect(result.content ?? \"\").toContain(\"A wise quote.\");\n  });\n\n  it(\"visitor_custom_emphasis: Visitor replaces emphasis with custom output\", () => {\n    const _testVisitor = {\n      visitEmphasis(ctx, text): string | { custom: string } {\n        return { custom: `>>>{text}<<<` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is <em>important</em> text.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\">>>important<<<\");\n    expect(result.content).not.toContain(\"*important*\");\n  });\n\n  it(\"visitor_custom_heading: Visitor replaces heading with custom format\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return { custom: `--- {text} ---` };\n      },\n    };\n\n    const result = convert(\n      \"<h2>Section Title</h2><p>Content below heading.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"--- Section Title ---\");\n    expect(result.content).not.toContain(\"## Section Title\");\n    expect(result.content ?? \"\").toContain(\"Content below heading.\");\n  });\n\n  it(\"visitor_custom_image: Visitor replaces image with custom output using template\", () => {\n    const _testVisitor = {\n      visitImage(ctx, src, alt, title): string | { custom: string } {\n        return { custom: `[Image: {alt}]` };\n      },\n    };\n\n    const result = convert(\n      '<img src=\"banner.png\" alt=\"Banner\">',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[Image: Banner]\");\n    expect(result.content).not.toContain(\"banner.png\");\n  });\n\n  it(\"visitor_custom_link_format: Visitor reformats links using template interpolation\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return { custom: `{text} ({href})` };\n      },\n    };\n\n    const result = convert(\n      '<p>Visit <a href=\"https://example.com\">Example</a> for more info.</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Example (https://example.com)\");\n    expect(result.content).not.toContain(\"[Example]\");\n  });\n\n  it(\"visitor_custom_link_static: Visitor replaces link with static custom output\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return { custom: \"[REDACTED LINK]\" };\n      },\n    };\n\n    const result = convert(\n      '<a href=\"https://example.com\">Click here</a>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[REDACTED LINK]\");\n    expect(result.content).not.toContain(\"example.com\");\n  });\n\n  it(\"visitor_custom_output: Visitor custom action replaces element output\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return { custom: \"## REPLACED HEADING\" };\n      },\n    };\n\n    const result = convert(\"<h1>Original Heading</h1>\", {} as unknown as JsConversionOptions, {\n      visitor: _testVisitor,\n    });\n    expect(result.content ?? \"\").toContain(\"## REPLACED HEADING\");\n    expect(result.content).not.toContain(\"# Original Heading\");\n  });\n\n  it(\"visitor_definition_list_custom: Visitor customizes definition list items\", () => {\n    const _testVisitor = {\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return { custom: `**{text}**` };\n      },\n    };\n\n    const result = convert(\n      \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"**HTML**\");\n    expect(result.content ?? \"\").toContain(\"**CSS**\");\n    expect(result.content ?? \"\").toContain(\"HyperText Markup Language\");\n  });\n\n  it(\"visitor_definition_list_custom_format: Visitor formats definition lists with custom templates\", () => {\n    const _testVisitor = {\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return { custom: `### {text}` };\n      },\n      visitDefinitionDescription(ctx, text): string | { custom: string } {\n        return { custom: `> {text}` };\n      },\n    };\n\n    const result = convert(\n      \"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"### Python\");\n    expect(result.content ?? \"\").toContain(\"### JavaScript\");\n    expect(result.content ?? \"\").toContain(\"> A high-level programming language\");\n    expect(result.content ?? \"\").toContain(\"> A scripting language for web browsers\");\n  });\n\n  it(\"visitor_definition_list_skip: Visitor skips definition list items from output\", () => {\n    const _testVisitor = {\n      visitDefinitionDescription(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n      visitDefinitionTerm(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Glossary:\");\n    expect(result.content ?? \"\").toContain(\"End of glossary\");\n    expect(result.content).not.toContain(\"Term A\");\n    expect(result.content).not.toContain(\"Definition\");\n  });\n\n  it(\"visitor_details_summary_custom: Visitor customizes details/summary disclosure elements\", () => {\n    const _testVisitor = {\n      visitSummary(ctx, text): string | { custom: string } {\n        return { custom: `[EXPANDABLE] {text}` };\n      },\n    };\n\n    const result = convert(\n      \"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[EXPANDABLE] Click to expand\");\n    expect(result.content ?? \"\").toContain(\"This content is initially hidden.\");\n  });\n\n  it(\"visitor_details_summary_skip: Visitor removes details/summary elements entirely\", () => {\n    const _testVisitor = {\n      visitDetails(ctx, isOpen): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Main content here.\");\n    expect(result.content ?? \"\").toContain(\"More main content.\");\n    expect(result.content).not.toContain(\"Hidden section\");\n    expect(result.content).not.toContain(\"Secret details\");\n  });\n\n  it(\"visitor_figure_custom: Visitor customizes figure and figcaption elements\", () => {\n    const _testVisitor = {\n      visitFigcaption(ctx, text): string | { custom: string } {\n        return { custom: `*{text}*` };\n      },\n    };\n\n    const result = convert(\n      '<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\"diagram.png\" alt=\"System architecture diagram\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Article Title\");\n    expect(result.content ?? \"\").toContain(\"*Figure 1: System Architecture*\");\n    expect(result.content ?? \"\").toContain(\"Explanation of the figure.\");\n  });\n\n  it(\"visitor_figure_custom_wrap: Visitor wraps figure content with custom formatting\", () => {\n    const _testVisitor = {\n      visitFigureStart(ctx): string | { custom: string } {\n        return { custom: \"\\n[FIGURE]\\n\" };\n      },\n      visitFigureEnd(ctx, output): string | { custom: string } {\n        return {\n          custom: `{output}\n[/FIGURE]\n`,\n        };\n      },\n    };\n\n    const result = convert(\n      '<section><h2>Gallery</h2><figure><img src=\"photo1.jpg\" alt=\"Photo\"><figcaption>Beautiful sunset</figcaption></figure></section>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[FIGURE]\");\n    expect(result.content ?? \"\").toContain(\"[/FIGURE]\");\n    expect(result.content ?? \"\").toContain(\"Gallery\");\n  });\n\n  it(\"visitor_figure_skip: Visitor removes figure elements with their captions\", () => {\n    const _testVisitor = {\n      visitFigureStart(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>See the chart below:</p><figure><img src=\"chart.svg\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"See the chart below:\");\n    expect(result.content ?? \"\").toContain(\"As shown in the chart above.\");\n    expect(result.content).not.toContain(\"Revenue Trends\");\n    expect(result.content).not.toContain(\"chart.svg\");\n  });\n\n  it(\"visitor_form_custom: Visitor replaces form with custom output\", () => {\n    const _testVisitor = {\n      visitForm(ctx, actionUrl, method): string | { custom: string } {\n        return { custom: \"[FORM PLACEHOLDER]\" };\n      },\n    };\n\n    const result = convert(\n      '<div><form action=\"/submit\" method=\"POST\"><label>Name: <input type=\"text\" name=\"name\"></label><button type=\"submit\">Submit</button></form></div>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[FORM PLACEHOLDER]\");\n    expect(result.content).not.toContain(\"submit\");\n    expect(result.content).not.toContain(\"input\");\n  });\n\n  it(\"visitor_form_skip: Visitor skips form elements entirely\", () => {\n    const _testVisitor = {\n      visitForm(ctx, actionUrl, method): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before form</p><form><input type=\"email\" name=\"email\"></form><p>After form</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Before form\");\n    expect(result.content ?? \"\").toContain(\"After form\");\n    expect(result.content).not.toContain(\"email\");\n  });\n\n  it(\"visitor_horizontal_rule_custom: Visitor replaces horizontal rule with custom output\", () => {\n    const _testVisitor = {\n      visitHorizontalRule(ctx): string | { custom: string } {\n        return { custom: \"\\n[DIVIDER]\\n\" };\n      },\n    };\n\n    const result = convert(\n      \"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[DIVIDER]\");\n    expect(result.content ?? \"\").toContain(\"Section A\");\n    expect(result.content ?? \"\").toContain(\"Section B\");\n    expect(result.content).not.toContain(\"---\");\n  });\n\n  it(\"visitor_horizontal_rule_skip: Visitor removes all horizontal rules\", () => {\n    const _testVisitor = {\n      visitHorizontalRule(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Part 1\");\n    expect(result.content ?? \"\").toContain(\"Part 2\");\n    expect(result.content ?? \"\").toContain(\"Part 3\");\n    expect(result.content).not.toContain(\"---\");\n  });\n\n  it(\"visitor_iframe_custom: Visitor replaces embedded iframe with custom text\", () => {\n    const _testVisitor = {\n      visitIframe(ctx, src): string | { custom: string } {\n        return { custom: \"[EMBEDDED: https://maps.example.com/embed]\" };\n      },\n    };\n\n    const result = convert(\n      '<p>Embedded map:</p><iframe src=\"https://maps.example.com/embed\" width=\"400\" height=\"300\"></iframe><p>End of map</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[EMBEDDED: https://maps.example.com/embed]\");\n    expect(result.content ?? \"\").toContain(\"Embedded map:\");\n    expect(result.content ?? \"\").toContain(\"End of map\");\n  });\n\n  it(\"visitor_iframe_skip: Visitor removes embedded iframes\", () => {\n    const _testVisitor = {\n      visitIframe(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<h3>Reviews</h3><iframe src=\"https://widget.example.com/reviews\"></iframe><p>See reviews from our partners.</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Reviews\");\n    expect(result.content ?? \"\").toContain(\"See reviews from our partners.\");\n    expect(result.content).not.toContain(\"widget.example.com\");\n  });\n\n  it(\"visitor_input_custom: Visitor replaces input with labeled output\", () => {\n    const _testVisitor = {\n      visitInput(ctx, inputType, name, value): string | { custom: string } {\n        return { custom: `[INPUT:{input_type}]` };\n      },\n    };\n\n    const result = convert(\n      '<form><label>Username: <input type=\"text\" name=\"username\" value=\"\"></label><label>Password: <input type=\"password\" name=\"password\"></label></form>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[INPUT:text]\");\n    expect(result.content ?? \"\").toContain(\"[INPUT:password]\");\n  });\n\n  it(\"visitor_input_skip: Visitor skips all input elements\", () => {\n    const _testVisitor = {\n      visitInput(ctx, inputType, name, value): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Sign up:</p><input type=\"text\" name=\"email\" placeholder=\"your@email.com\"><input type=\"checkbox\" name=\"agree\"><p>Continue</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Sign up:\");\n    expect(result.content ?? \"\").toContain(\"Continue\");\n    expect(result.content).not.toContain(\"email\");\n  });\n\n  it(\"visitor_line_break_custom: Visitor replaces line break with custom output\", () => {\n    const _testVisitor = {\n      visitLineBreak(ctx): string | { custom: string } {\n        return { custom: \" | \" };\n      },\n    };\n\n    const result = convert(\n      \"<p>First line<br>Second line<br>Third line</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"First line | Second line | Third line\");\n    expect(result.content).not.toContain(\"\\n\\n\");\n  });\n\n  it(\"visitor_line_break_skip: Visitor removes all line breaks\", () => {\n    const _testVisitor = {\n      visitLineBreak(ctx): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Address Line 1Address Line 2Address Line 3\");\n  });\n\n  it(\"visitor_mark_custom: Visitor replaces highlight/mark with custom template\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return { custom: `=={text}==` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is a <mark>highlighted passage</mark> in the text.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"==highlighted passage==\");\n    expect(result.content ?? \"\").toContain(\"This is a\");\n  });\n\n  it(\"visitor_mark_skip: Visitor skips mark elements entirely\", () => {\n    const _testVisitor = {\n      visitMark(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Key insight: <mark>always validate input</mark> for security.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"always validate input\");\n    expect(result.content ?? \"\").toContain(\"Key insight:\");\n    expect(result.content ?? \"\").toContain(\"for security.\");\n  });\n\n  it(\"visitor_preserve_html: Visitor preserve_html action includes raw HTML in output\", () => {\n    const _testVisitor = {\n      visitCustomElement(ctx, tagName, html): string | { custom: string } {\n        return \"preserve_html\";\n      },\n    };\n\n    const result = convert(\n      \"<div><custom-tag>Custom content</custom-tag></div>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"<custom-tag>\");\n  });\n\n  it(\"visitor_skip_all_headings: Visitor skips all headings from document\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<h1>Title</h1><p>Body text remains.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"Title\");\n    expect(result.content ?? \"\").toContain(\"Body text remains.\");\n  });\n\n  it(\"visitor_skip_code_blocks: Visitor skips code blocks from output\", () => {\n    const _testVisitor = {\n      visitCodeBlock(ctx, lang, code): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Intro text\");\n    expect(result.content ?? \"\").toContain(\"Outro text\");\n    expect(result.content).not.toContain(\"let x = 42\");\n  });\n\n  it(\"visitor_skip_heading: Visitor skip action omits all headings from output\", () => {\n    const _testVisitor = {\n      visitHeading(ctx, level, text, id): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<h1>Title</h1><p>Body text remains.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"Title\");\n    expect(result.content ?? \"\").toContain(\"Body text remains.\");\n  });\n\n  it(\"visitor_skip_images: Visitor skips all images from output\", () => {\n    const _testVisitor = {\n      visitImage(ctx, src, alt, title): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before image</p><img src=\"photo.jpg\" alt=\"A photo\"><p>After image</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Before image\");\n    expect(result.content ?? \"\").toContain(\"After image\");\n    expect(result.content).not.toContain(\"photo.jpg\");\n    expect(result.content).not.toContain(\"A photo\");\n  });\n\n  it(\"visitor_skip_links: Visitor skips all links entirely\", () => {\n    const _testVisitor = {\n      visitLink(ctx, href, text, title): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<p>Before <a href=\"https://example.com\">link text</a> after</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"link text\");\n    expect(result.content).not.toContain(\"example.com\");\n  });\n\n  it(\"visitor_skip_strong: Visitor skips bold/strong elements\", () => {\n    const _testVisitor = {\n      visitStrong(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Normal <strong>bold text</strong> normal</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content).not.toContain(\"bold text\");\n    expect(result.content ?? \"\").toContain(\"Normal\");\n  });\n\n  it(\"visitor_subscript_custom: Visitor replaces subscript with custom template\", () => {\n    const _testVisitor = {\n      visitSubscript(ctx, text): string | { custom: string } {\n        return { custom: `~{text}~` };\n      },\n    };\n\n    const result = convert(\n      \"<p>H<sub>2</sub>O is water.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"H~2~O\");\n    expect(result.content ?? \"\").toContain(\"is water\");\n  });\n\n  it(\"visitor_subscript_skip: Visitor skips subscript elements entirely\", () => {\n    const _testVisitor = {\n      visitSubscript(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"The formula CHO is sugar.\");\n  });\n\n  it(\"visitor_superscript_custom: Visitor replaces superscript with custom template\", () => {\n    const _testVisitor = {\n      visitSuperscript(ctx, text): string | { custom: string } {\n        return { custom: `^{text}^` };\n      },\n    };\n\n    const result = convert(\n      \"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"E=mc^2^\");\n    expect(result.content ?? \"\").toContain(\"revolutionized physics\");\n  });\n\n  it(\"visitor_superscript_skip: Visitor skips superscript from output\", () => {\n    const _testVisitor = {\n      visitSuperscript(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"The equation x + y = z has no solutions.\");\n  });\n\n  it(\"visitor_underline_custom: Visitor replaces underline with custom markup\", () => {\n    const _testVisitor = {\n      visitUnderline(ctx, text): string | { custom: string } {\n        return { custom: `_{text}_` };\n      },\n    };\n\n    const result = convert(\n      \"<p>This is <u>very important</u> text.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"_very important_\");\n    expect(result.content).not.toContain(\"**\");\n  });\n\n  it(\"visitor_underline_skip: Visitor skips underline elements from output\", () => {\n    const _testVisitor = {\n      visitUnderline(ctx, text): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      \"<p>Normal text with <u>underlined part</u> and more text.</p>\",\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Normal text with\");\n    expect(result.content ?? \"\").toContain(\"and more text.\");\n    expect(result.content).not.toContain(\"underlined part\");\n  });\n\n  it(\"visitor_video_custom: Visitor replaces video with custom link\", () => {\n    const _testVisitor = {\n      visitVideo(ctx, src): string | { custom: string } {\n        return { custom: `[VIDEO: {src}]` };\n      },\n    };\n\n    const result = convert(\n      '<p>Watch our tutorial:</p><video src=\"tutorial.mp4\" width=\"320\" height=\"240\" controls></video><p>Great content!</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"[VIDEO: tutorial.mp4]\");\n    expect(result.content ?? \"\").toContain(\"Watch our tutorial:\");\n    expect(result.content ?? \"\").toContain(\"Great content!\");\n  });\n\n  it(\"visitor_video_skip: Visitor removes video elements entirely\", () => {\n    const _testVisitor = {\n      visitVideo(ctx, src): string | { custom: string } {\n        return \"skip\";\n      },\n    };\n\n    const result = convert(\n      '<h2>Demo</h2><video src=\"demo.webm\"></video><p>See the demo above.</p>',\n      {} as unknown as JsConversionOptions,\n      { visitor: _testVisitor },\n    );\n    expect(result.content ?? \"\").toContain(\"Demo\");\n    expect(result.content ?? \"\").toContain(\"See the demo above.\");\n    expect(result.content).not.toContain(\"demo.webm\");\n  });\n});\n"
  },
  {
    "path": "e2e/wasm/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"strictNullChecks\": false,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"tests/**/*.ts\", \"vitest.config.ts\"]\n}\n"
  },
  {
    "path": "e2e/wasm/vitest.config.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:f9f423329d0158d54880082e671af8ca9bc0d02442da09f90d8ae79e3233ae65\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { defineConfig } from \"vitest/config\";\nimport wasm from \"vite-plugin-wasm\";\nimport topLevelAwait from \"vite-plugin-top-level-await\";\n\nexport default defineConfig({\n  plugins: [wasm(), topLevelAwait()],\n  test: {\n    include: [\"tests/**/*.test.ts\"],\n  },\n});\n"
  },
  {
    "path": "e2e/zig/build.zig",
    "content": "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    const target = b.standardTargetOptions(.{});\n    const optimize = b.standardOptimizeOption(.{});\n    const test_step = b.step(\"test\", \"Run tests\");\n\n    const conversion_module = b.createModule(.{\n        .root_source_file = b.path(\"src/conversion_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const conversion_tests = b.addTest(.{\n        .root_module = conversion_module,\n    });\n    const conversion_run = b.addRunArtifact(conversion_tests);\n    test_step.dependOn(&conversion_run.step);\n\n    const edge_cases_module = b.createModule(.{\n        .root_source_file = b.path(\"src/edge_cases_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const edge_cases_tests = b.addTest(.{\n        .root_module = edge_cases_module,\n    });\n    const edge_cases_run = b.addRunArtifact(edge_cases_tests);\n    test_step.dependOn(&edge_cases_run.step);\n\n    const metadata_module = b.createModule(.{\n        .root_source_file = b.path(\"src/metadata_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const metadata_tests = b.addTest(.{\n        .root_module = metadata_module,\n    });\n    const metadata_run = b.addRunArtifact(metadata_tests);\n    test_step.dependOn(&metadata_run.step);\n\n    const options_module = b.createModule(.{\n        .root_source_file = b.path(\"src/options_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const options_tests = b.addTest(.{\n        .root_module = options_module,\n    });\n    const options_run = b.addRunArtifact(options_tests);\n    test_step.dependOn(&options_run.step);\n\n    const real_world_module = b.createModule(.{\n        .root_source_file = b.path(\"src/real_world_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const real_world_tests = b.addTest(.{\n        .root_module = real_world_module,\n    });\n    const real_world_run = b.addRunArtifact(real_world_tests);\n    test_step.dependOn(&real_world_run.step);\n\n    const result_module = b.createModule(.{\n        .root_source_file = b.path(\"src/result_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const result_tests = b.addTest(.{\n        .root_module = result_module,\n    });\n    const result_run = b.addRunArtifact(result_tests);\n    test_step.dependOn(&result_run.step);\n\n    const smoke_module = b.createModule(.{\n        .root_source_file = b.path(\"src/smoke_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const smoke_tests = b.addTest(.{\n        .root_module = smoke_module,\n    });\n    const smoke_run = b.addRunArtifact(smoke_tests);\n    test_step.dependOn(&smoke_run.step);\n\n    const structure_module = b.createModule(.{\n        .root_source_file = b.path(\"src/structure_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const structure_tests = b.addTest(.{\n        .root_module = structure_module,\n    });\n    const structure_run = b.addRunArtifact(structure_tests);\n    test_step.dependOn(&structure_run.step);\n\n    const visitor_module = b.createModule(.{\n        .root_source_file = b.path(\"src/visitor_test.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    const visitor_tests = b.addTest(.{\n        .root_module = visitor_module,\n    });\n    const visitor_run = b.addRunArtifact(visitor_tests);\n    test_step.dependOn(&visitor_run.step);\n\n}\n"
  },
  {
    "path": "e2e/zig/build.zig.zon",
    "content": ".{\n    .name = .e2e_zig,\n    .version = \"0.1.0\",\n    .fingerprint = 0xf16334c0592376fc,\n    .minimum_zig_version = \"0.16.0\",\n    .dependencies = .{\n        .html_to_markdown_rs = .{ .path = \"../../packages/zig\" },\n    },\n    .paths = .{\n        \"build.zig\",\n        \"build.zig.zon\",\n        \"src\",\n    },\n}\n"
  },
  {
    "path": "fixtures/conversion/blockquotes.json",
    "content": "[\n  {\n    \"id\": \"blockquote_simple\",\n    \"description\": \"Simple blockquote\",\n    \"input\": {\n      \"html\": \"<blockquote><p>Quote text</p></blockquote>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"> Quote text\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"blockquote_nested\",\n    \"description\": \"Nested blockquote produces double-prefixed lines\",\n    \"input\": {\n      \"html\": \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Outer quote.\",\n          \"Inner quote.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"blockquote_multiple_paragraphs\",\n    \"description\": \"Blockquote with multiple paragraphs has each paragraph prefixed\",\n    \"input\": {\n      \"html\": \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"> First paragraph.\",\n          \"> Second paragraph.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"blockquote_with_list\",\n    \"description\": \"Blockquote containing a list preserves list items inside quote\",\n    \"input\": {\n      \"html\": \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Quote intro:\",\n          \"Point one\",\n          \"Point two\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/code.json",
    "content": "[\n  {\n    \"id\": \"inline_code\",\n    \"description\": \"Inline code\",\n    \"input\": {\n      \"html\": \"<p>Use <code>console.log()</code> to debug</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"`console.log()`\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"code_block\",\n    \"description\": \"Code block with language preserves content\",\n    \"input\": {\n      \"html\": \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"print('hello')\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"code_block_no_language\",\n    \"description\": \"Code block without a language class preserves content\",\n    \"input\": {\n      \"html\": \"<pre><code>plain code here</code></pre>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"plain code here\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"code_with_backticks_in_content\",\n    \"description\": \"Inline code containing backtick characters is properly escaped\",\n    \"input\": {\n      \"html\": \"<p>Use <code>`backtick` here</code> carefully.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"backtick\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"code_inline_in_paragraph\",\n    \"description\": \"Inline code element nested inside a paragraph\",\n    \"input\": {\n      \"html\": \"<p>Call the <code>initialize()</code> method first.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"`initialize()`\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/emphasis.json",
    "content": "[\n  {\n    \"id\": \"bold_strong\",\n    \"description\": \"Strong tag converts to bold\",\n    \"input\": {\n      \"html\": \"<p><strong>bold</strong></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"**bold**\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"italic_em\",\n    \"description\": \"Em tag converts to italic\",\n    \"input\": {\n      \"html\": \"<p><em>italic</em></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"*italic*\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"bold_and_italic\",\n    \"description\": \"Nested bold and italic\",\n    \"input\": {\n      \"html\": \"<p><strong><em>both</em></strong></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"***both***\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_strikethrough_s\",\n    \"description\": \"s tag converts to GFM strikethrough\",\n    \"input\": {\n      \"html\": \"<p><s>strikethrough</s></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"~~strikethrough~~\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_strikethrough_del\",\n    \"description\": \"del tag converts to GFM strikethrough\",\n    \"input\": {\n      \"html\": \"<p><del>deleted text</del></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"~~deleted text~~\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_underline_u\",\n    \"description\": \"u tag content is preserved in output\",\n    \"input\": {\n      \"html\": \"<p><u>underlined</u></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"underlined\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_mark_highlight\",\n    \"description\": \"mark tag produces highlighted output\",\n    \"input\": {\n      \"html\": \"<p><mark>highlighted</mark></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"highlighted\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_subscript\",\n    \"description\": \"sub tag content is preserved\",\n    \"input\": {\n      \"html\": \"<p>H<sub>2</sub>O</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"H\",\n          \"2\",\n          \"O\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"emphasis_superscript\",\n    \"description\": \"sup tag content is preserved\",\n    \"input\": {\n      \"html\": \"<p>x<sup>2</sup></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"x\",\n          \"2\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/forms.json",
    "content": "[\n  {\n    \"id\": \"form_input_elements\",\n    \"description\": \"Form input elements produce readable output without form mechanics\",\n    \"input\": {\n      \"html\": \"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Name\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"form_textarea\",\n    \"description\": \"Textarea element produces readable output\",\n    \"input\": {\n      \"html\": \"<form><label>Message:</label><textarea>Default text content</textarea></form>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Message\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"form_select_options\",\n    \"description\": \"Select element with options produces readable output\",\n    \"input\": {\n      \"html\": \"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Color\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/headings.json",
    "content": "[\n  {\n    \"id\": \"heading_h1\",\n    \"description\": \"H1 heading\",\n    \"input\": {\n      \"html\": \"<h1>Heading 1</h1>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"# Heading 1\"\n      }\n    ]\n  },\n  {\n    \"id\": \"heading_h2\",\n    \"description\": \"H2 heading\",\n    \"input\": {\n      \"html\": \"<h2>Heading 2</h2>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"## Heading 2\"\n      }\n    ]\n  },\n  {\n    \"id\": \"heading_h3\",\n    \"description\": \"H3 heading\",\n    \"input\": {\n      \"html\": \"<h3>Heading 3</h3>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"### Heading 3\"\n      }\n    ]\n  },\n  {\n    \"id\": \"heading_h4\",\n    \"description\": \"H4 heading\",\n    \"input\": {\n      \"html\": \"<h4>Heading 4</h4>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"#### Heading 4\"\n      }\n    ]\n  },\n  {\n    \"id\": \"heading_h5\",\n    \"description\": \"H5 heading\",\n    \"input\": {\n      \"html\": \"<h5>Heading 5</h5>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"##### Heading 5\"\n      }\n    ]\n  },\n  {\n    \"id\": \"heading_h6\",\n    \"description\": \"H6 heading\",\n    \"input\": {\n      \"html\": \"<h6>Heading 6</h6>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"###### Heading 6\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/images.json",
    "content": "[\n  {\n    \"id\": \"image_simple\",\n    \"description\": \"Image with alt text\",\n    \"input\": {\n      \"html\": \"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"![A photo](photo.jpg)\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"image_no_alt\",\n    \"description\": \"Image without alt text produces image markdown\",\n    \"input\": {\n      \"html\": \"<img src=\\\"banner.jpg\\\">\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"banner.jpg\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"image_with_title\",\n    \"description\": \"Image with title attribute includes title in output\",\n    \"input\": {\n      \"html\": \"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"![Sales chart](chart.png\",\n          \"Q3 Sales\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"image_figure_figcaption\",\n    \"description\": \"Figure with figcaption preserves both image and caption\",\n    \"input\": {\n      \"html\": \"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"![A sunset](sunset.jpg)\",\n          \"Beautiful sunset over the ocean\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"image_linked\",\n    \"description\": \"Image inside an anchor produces a linked image\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"![Icon](icon.png)\",\n          \"https://example.com\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/line_breaks.json",
    "content": "[\n  {\n    \"id\": \"line_break_br_tag\",\n    \"description\": \"Single br tag produces a line break in output\",\n    \"input\": {\n      \"html\": \"<p>First line.<br>Second line.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"First line.\",\n          \"Second line.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"line_break_hr_tag\",\n    \"description\": \"hr tag produces a horizontal separator between content\",\n    \"input\": {\n      \"html\": \"<p>Before rule.</p><hr><p>After rule.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Before rule.\",\n          \"After rule.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"line_break_multiple_br\",\n    \"description\": \"Multiple consecutive br tags in sequence\",\n    \"input\": {\n      \"html\": \"<p>Start.<br><br>End.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Start.\",\n          \"End.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/links.json",
    "content": "[\n  {\n    \"id\": \"link_simple\",\n    \"description\": \"Simple link\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\">Example</a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"[Example](https://example.com)\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_with_title\",\n    \"description\": \"Link with title attribute\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"[Example](https://example.com\",\n          \"Example Site\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_mailto\",\n    \"description\": \"Mailto link is preserved with mailto: scheme\",\n    \"input\": {\n      \"html\": \"<a href=\\\"mailto:user@example.com\\\">Email us</a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"mailto:user@example.com\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_anchor_fragment\",\n    \"description\": \"Fragment-only anchor link is preserved\",\n    \"input\": {\n      \"html\": \"<a href=\\\"#section\\\">Jump to section</a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"[Jump to section](#section)\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_with_bold_text\",\n    \"description\": \"Link containing bold text preserves formatting\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"**Bold link**\",\n          \"https://example.com\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_empty_href\",\n    \"description\": \"Link with empty href produces output with the link text\",\n    \"input\": {\n      \"html\": \"<a href=\\\"\\\">No destination</a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"No destination\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"link_image_inside\",\n    \"description\": \"Image inside a link produces a linked image\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"![Logo](logo.png)\",\n          \"https://example.com\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/lists.json",
    "content": "[\n  {\n    \"id\": \"unordered_list\",\n    \"description\": \"Unordered list\",\n    \"input\": {\n      \"html\": \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"- Item 1\",\n          \"- Item 2\",\n          \"- Item 3\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"ordered_list\",\n    \"description\": \"Ordered list\",\n    \"input\": {\n      \"html\": \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"1. First\",\n          \"2. Second\",\n          \"3. Third\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_nested_unordered\",\n    \"description\": \"Nested unordered list with two levels of depth\",\n    \"input\": {\n      \"html\": \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Parent A\",\n          \"Child A1\",\n          \"Child A2\",\n          \"Parent B\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_nested_ordered\",\n    \"description\": \"Nested ordered list with two levels of depth\",\n    \"input\": {\n      \"html\": \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Step 1\",\n          \"Step 1a\",\n          \"Step 1b\",\n          \"Step 2\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_mixed_nested\",\n    \"description\": \"Mixed list: ordered list nested inside unordered list\",\n    \"input\": {\n      \"html\": \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Item A\",\n          \"Sub 1\",\n          \"Sub 2\",\n          \"Item B\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_task_checkboxes\",\n    \"description\": \"Task list with checked and unchecked checkboxes\",\n    \"input\": {\n      \"html\": \"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Done task\",\n          \"Pending task\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_item_multiple_paragraphs\",\n    \"description\": \"List item containing multiple paragraphs\",\n    \"input\": {\n      \"html\": \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"First paragraph in item.\",\n          \"Second paragraph in item.\",\n          \"Simple item\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"list_definition_dl\",\n    \"description\": \"Definition list with dt and dd elements\",\n    \"input\": {\n      \"html\": \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Term One\",\n          \"Definition of term one.\",\n          \"Term Two\",\n          \"Definition of term two.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/paragraphs.json",
    "content": "[\n  {\n    \"id\": \"paragraph_simple\",\n    \"description\": \"Simple paragraph converts to plain text\",\n    \"input\": {\n      \"html\": \"<p>Hello World</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"Hello World\"\n      }\n    ]\n  },\n  {\n    \"id\": \"paragraph_multiple\",\n    \"description\": \"Multiple paragraphs are separated by a blank line\",\n    \"input\": {\n      \"html\": \"<p>First paragraph.</p><p>Second paragraph.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"First paragraph.\",\n          \"Second paragraph.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"paragraph_with_inline_formatting\",\n    \"description\": \"Paragraph with bold, italic, and a link\",\n    \"input\": {\n      \"html\": \"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"**bold**\",\n          \"*italic*\",\n          \"[link](https://example.com)\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"paragraph_with_line_breaks\",\n    \"description\": \"Paragraph with br tags produces line breaks in output\",\n    \"input\": {\n      \"html\": \"<p>Line one.<br>Line two.<br>Line three.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Line one.\",\n          \"Line two.\",\n          \"Line three.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"paragraph_nested_divs\",\n    \"description\": \"Text nested inside divs is extracted correctly\",\n    \"input\": {\n      \"html\": \"<div><div><p>Nested text</p></div></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Nested text\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/semantic.json",
    "content": "[\n  {\n    \"id\": \"semantic_article\",\n    \"description\": \"Article element wrapping content preserves inner content\",\n    \"input\": {\n      \"html\": \"<article><h2>Article Title</h2><p>Article body.</p></article>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Article Title\",\n          \"Article body.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_section_with_heading\",\n    \"description\": \"Section element with heading preserves structure\",\n    \"input\": {\n      \"html\": \"<section><h3>Section Heading</h3><p>Section content.</p></section>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Section Heading\",\n          \"Section content.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_details_summary\",\n    \"description\": \"Details and summary elements produce readable output\",\n    \"input\": {\n      \"html\": \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Click to expand\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_definition_list\",\n    \"description\": \"Definition list with term and description\",\n    \"input\": {\n      \"html\": \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"HTML\",\n          \"HyperText Markup Language\",\n          \"CSS\",\n          \"Cascading Style Sheets\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_mark_highlight\",\n    \"description\": \"Mark tag produces highlighted output\",\n    \"input\": {\n      \"html\": \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"highlighted text\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_sub_superscript\",\n    \"description\": \"Subscript and superscript elements are preserved in output\",\n    \"input\": {\n      \"html\": \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"H\",\n          \"2\",\n          \"O\",\n          \"E=mc\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_abbr\",\n    \"description\": \"Abbreviation element text is preserved\",\n    \"input\": {\n      \"html\": \"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"WWW\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"semantic_hr\",\n    \"description\": \"Horizontal rule produces a separator in output\",\n    \"input\": {\n      \"html\": \"<p>Above</p><hr><p>Below</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Above\",\n          \"Below\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/conversion/tables.json",
    "content": "[\n  {\n    \"id\": \"simple_table\",\n    \"description\": \"Simple table with header\",\n    \"input\": {\n      \"html\": \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Name\",\n          \"Age\",\n          \"Alice\",\n          \"30\",\n          \"|\",\n          \"---\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"table_no_thead\",\n    \"description\": \"Table without thead uses first row as implied header\",\n    \"input\": {\n      \"html\": \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Product\",\n          \"Price\",\n          \"Apple\",\n          \"1.00\",\n          \"|\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"table_with_colspan\",\n    \"description\": \"Table with colspan attribute in a header cell\",\n    \"input\": {\n      \"html\": \"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Full Name\",\n          \"John\",\n          \"Doe\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"table_with_alignment\",\n    \"description\": \"Table with column alignment attributes\",\n    \"input\": {\n      \"html\": \"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Left\",\n          \"Center\",\n          \"Right\",\n          \"L\",\n          \"C\",\n          \"R\",\n          \"|\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"table_pipe_chars_in_content\",\n    \"description\": \"Table cells containing pipe characters are escaped in output\",\n    \"input\": {\n      \"html\": \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Expression\",\n          \"Result\",\n          \"true\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"table_empty\",\n    \"description\": \"Empty table produces no output or minimal output\",\n    \"input\": {\n      \"html\": \"<table></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/edge-cases/empty.json",
    "content": "[\n  {\n    \"id\": \"empty_html\",\n    \"description\": \"Empty HTML document\",\n    \"input\": {\n      \"html\": \"<html><head></head><body></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"whitespace_only\",\n    \"description\": \"Whitespace-only content\",\n    \"input\": {\n      \"html\": \"<p>   </p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"just_whitespace_input\",\n    \"description\": \"Input that is only whitespace characters (spaces, tabs, newlines) produces empty output\",\n    \"input\": {\n      \"html\": \"   \"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"html_comments_only\",\n    \"description\": \"Document containing only HTML comments produces empty output\",\n    \"input\": {\n      \"html\": \"<!-- This is a comment --><!-- Another comment -->\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"script_tags_only\",\n    \"description\": \"Document with only script tags produces empty output (scripts are stripped)\",\n    \"input\": {\n      \"html\": \"<html><head><script>alert('xss')</script></head><body><script>document.write('hello')</script></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"style_tags_only\",\n    \"description\": \"Document with only style tags produces empty output (styles are stripped)\",\n    \"input\": {\n      \"html\": \"<html><head><style>body { color: red; }</style></head><body><style>.foo { margin: 0; }</style></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/edge-cases/encoding.json",
    "content": "[\n  {\n    \"id\": \"encoding_html_entities\",\n    \"description\": \"Common HTML entities are decoded in output\",\n    \"input\": {\n      \"html\": \"<p>&amp; &lt; &gt; &nbsp; &quot; &apos;</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"&\",\n          \"<\",\n          \">\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"encoding_unicode_emoji\",\n    \"description\": \"Emoji and Unicode characters are preserved\",\n    \"input\": {\n      \"html\": \"<p>Hello 🌍 World 🚀</p><p>Stars: ⭐ ✨</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"🌍\",\n          \"🚀\",\n          \"⭐\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"encoding_cjk_characters\",\n    \"description\": \"CJK (Chinese, Japanese, Korean) characters are preserved\",\n    \"input\": {\n      \"html\": \"<p>中文内容</p><p>日本語テキスト</p><p>한국어 텍스트</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"中文内容\",\n          \"日本語テキスト\",\n          \"한국어 텍스트\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"encoding_numeric_entities\",\n    \"description\": \"Numeric HTML entities (decimal and hex) are decoded\",\n    \"input\": {\n      \"html\": \"<p>Copyright: &#169; Trade: &#174; Euro: &#8364; Hex: &#x00A9;</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"©\",\n          \"®\",\n          \"€\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"encoding_named_entities\",\n    \"description\": \"Named HTML entities like &mdash; and &hellip; are decoded\",\n    \"input\": {\n      \"html\": \"<p>Em dash&mdash;used for parenthetical remarks&mdash;is common. Ellipsis&hellip; indicates omission. Non-breaking&nbsp;space.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"—\",\n          \"…\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/edge-cases/malformed.json",
    "content": "[\n  {\n    \"id\": \"malformed_unclosed_paragraph\",\n    \"description\": \"Unclosed <p> tag is recovered gracefully and content is preserved\",\n    \"input\": {\n      \"html\": \"<p>This paragraph is never closed\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"This paragraph is never closed\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"malformed_overlapping_tags\",\n    \"description\": \"Overlapping bold/italic tags are recovered by the HTML parser without panic\",\n    \"input\": {\n      \"html\": \"<p><b><i>bold and italic</b></i></p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"bold and italic\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"malformed_missing_block_closing_tags\",\n    \"description\": \"Missing closing tags on block elements are auto-closed by parser\",\n    \"input\": {\n      \"html\": \"<div><h1>Title<p>First paragraph<p>Second paragraph</div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Title\",\n          \"First paragraph\",\n          \"Second paragraph\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"malformed_deeply_nested_elements\",\n    \"description\": \"Deeply nested elements (100 levels) are handled without stack overflow\",\n    \"input\": {\n      \"html\": \"<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><p>Deeply nested content</p></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Deeply nested content\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/edge-cases/visitor_errors.json",
    "content": "[\n  {\n    \"id\": \"visitor_custom_element_with_nesting\",\n    \"description\": \"Visitor handles custom elements with nested content\",\n    \"input\": {\n      \"html\": \"<div><custom-widget data-value=\\\"123\\\"><p>Widget content here</p><span>With nested elements</span></custom-widget></div>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_custom_element\": {\n          \"action\": \"custom\",\n          \"output\": \"[CUSTOM WIDGET]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[CUSTOM WIDGET]\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Widget content here\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_unknown_tag_preservation\",\n    \"description\": \"Visitor preserves unknown HTML tags as raw HTML\",\n    \"input\": {\n      \"html\": \"<article><p>Article text</p><x-custom>Custom element with content</x-custom><p>More article text</p></article>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_custom_element\": {\n          \"action\": \"preserve_html\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Article text\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"More article text\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"<x-custom>\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_deeply_nested_skip\",\n    \"description\": \"Visitor skips deeply nested elements\",\n    \"input\": {\n      \"html\": \"<div><p>Outer <em>emphasis <strong>with bold <mark>and highlight</mark></strong></em> text</p></div>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_mark\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Outer\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"highlight\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_element_start_skip_entire_subtree\",\n    \"description\": \"Visitor skips at element_start level removes entire subtree\",\n    \"input\": {\n      \"html\": \"<div><h1>Title</h1><p>Content</p></div>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_element_start\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_element_end_modification\",\n    \"description\": \"Visitor modifies element at end after children processed\",\n    \"input\": {\n      \"html\": \"<blockquote><p>Original quote</p></blockquote>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_element_end\": {\n          \"action\": \"custom\",\n          \"output\": \"MODIFIED OUTPUT\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/edge-cases/xss.json",
    "content": "[\n  {\n    \"id\": \"xss_script_tag_stripped\",\n    \"description\": \"Script tag content is stripped and does not appear in output\",\n    \"input\": {\n      \"html\": \"<p>Safe content.</p><script>alert('xss')</script><p>More safe content.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Safe content\",\n          \"More safe content\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"<script>\",\n          \"alert(\",\n          \"xss\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"xss_onclick_handler_removed\",\n    \"description\": \"onclick and other on* event handlers are removed from elements\",\n    \"input\": {\n      \"html\": \"<p><a href=\\\"https://example.com\\\" onclick=\\\"alert('xss')\\\">Click me</a></p><button onmouseover=\\\"steal_data()\\\">Hover me</button>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Click me\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"onclick\",\n          \"onmouseover\",\n          \"alert(\",\n          \"steal_data\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"xss_svg_nested_script_stripped\",\n    \"description\": \"Script tags nested inside SVG are stripped\",\n    \"input\": {\n      \"html\": \"<p>Before SVG.</p><svg xmlns=\\\"http://www.w3.org/2000/svg\\\"><script>alert('svg-xss')</script><text>SVG text</text></svg><p>After SVG.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Before SVG\",\n          \"After SVG\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"<script>\",\n          \"alert(\",\n          \"svg-xss\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/metadata/basic.json",
    "content": "[\n  {\n    \"id\": \"metadata_title_tag\",\n    \"description\": \"Extract title from <title> tag\",\n    \"input\": {\n      \"html\": \"<html><head><title>My Page</title></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.title\",\n        \"value\": \"My Page\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_description_meta\",\n    \"description\": \"Extract description from <meta name='description'> tag\",\n    \"input\": {\n      \"html\": \"<html><head><title>Page</title><meta name=\\\"description\\\" content=\\\"This is the page description.\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.description\",\n        \"value\": \"This is the page description.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_author_meta\",\n    \"description\": \"Extract author from <meta name='author'> tag\",\n    \"input\": {\n      \"html\": \"<html><head><title>Page</title><meta name=\\\"author\\\" content=\\\"Jane Doe\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.author\",\n        \"value\": \"Jane Doe\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_keywords_meta\",\n    \"description\": \"Extract keywords from <meta name='keywords'> tag\",\n    \"input\": {\n      \"html\": \"<html><head><title>Page</title><meta name=\\\"keywords\\\" content=\\\"rust, markdown, html, converter\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"metadata.document.keywords\",\n        \"value\": 1\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_canonical_url\",\n    \"description\": \"Extract canonical URL from <link rel='canonical'> tag\",\n    \"input\": {\n      \"html\": \"<html><head><title>Page</title><link rel=\\\"canonical\\\" href=\\\"https://example.com/canonical-page\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.canonical_url\",\n        \"value\": \"https://example.com/canonical-page\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/metadata/document_properties.json",
    "content": "[\n  {\n    \"id\": \"metadata_text_direction_ltr\",\n    \"description\": \"Extract text direction from lang attribute on html element\",\n    \"input\": {\n      \"html\": \"<html lang=\\\"en\\\" dir=\\\"ltr\\\"><head><title>LTR Document</title></head><body><p>This is left-to-right text.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"left-to-right text\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_text_direction_rtl\",\n    \"description\": \"Extract right-to-left text direction\",\n    \"input\": {\n      \"html\": \"<html lang=\\\"ar\\\" dir=\\\"rtl\\\"><head><title>RTL Document</title></head><body><p>This is right-to-left text.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"right-to-left text\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_lang_attribute\",\n    \"description\": \"Extract language from html lang attribute\",\n    \"input\": {\n      \"html\": \"<html lang=\\\"es\\\"><head><title>Spanish Page</title></head><body><h1>Hola Mundo</h1><p>Este es un documento en español.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Hola Mundo\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_microdata_schema_person\",\n    \"description\": \"Extract schema.org microdata for Person\",\n    \"input\": {\n      \"html\": \"<html><head><title>Contact</title></head><body><div itemscope itemtype=\\\"https://schema.org/Person\\\"><span itemprop=\\\"name\\\">John Smith</span><span itemprop=\\\"email\\\">john@example.com</span><span itemprop=\\\"telephone\\\">+1-555-0100</span></div></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"John Smith\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"john@example.com\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_microdata_schema_organization\",\n    \"description\": \"Extract schema.org microdata for Organization\",\n    \"input\": {\n      \"html\": \"<html><head><title>Company</title></head><body><div itemscope itemtype=\\\"https://schema.org/Organization\\\"><span itemprop=\\\"name\\\">Acme Corp</span><span itemprop=\\\"foundingDate\\\">2020</span><span itemprop=\\\"url\\\">https://acmecorp.example.com</span><span itemprop=\\\"logo\\\">https://acmecorp.example.com/logo.png</span></div></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Acme Corp\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"2020\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_microdata_schema_article\",\n    \"description\": \"Extract schema.org microdata for Article\",\n    \"input\": {\n      \"html\": \"<html><head><title>Article</title></head><body><article itemscope itemtype=\\\"https://schema.org/Article\\\"><h1 itemprop=\\\"headline\\\">Breaking News Today</h1><span itemprop=\\\"author\\\">Jane Reporter</span><span itemprop=\\\"datePublished\\\">2024-04-22</span><div itemprop=\\\"articleBody\\\"><p>The article content goes here with important information about the breaking news story.</p></div></article></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Breaking News Today\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Jane Reporter\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"important information\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_microdata_schema_breadcrumb\",\n    \"description\": \"Extract schema.org breadcrumb navigation microdata\",\n    \"input\": {\n      \"html\": \"<html><head><title>Navigation</title></head><body><nav itemscope itemtype=\\\"https://schema.org/BreadcrumbList\\\"><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com\\\"><span itemprop=\\\"name\\\">Home</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><a itemprop=\\\"item\\\" href=\\\"https://example.com/products\\\"><span itemprop=\\\"name\\\">Products</span></a></span><span itemprop=\\\"itemListElement\\\" itemscope itemtype=\\\"https://schema.org/ListItem\\\"><span itemprop=\\\"name\\\">Current Page</span></span></nav></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Home\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Products\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Current Page\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_microdata_schema_product\",\n    \"description\": \"Extract schema.org microdata for Product\",\n    \"input\": {\n      \"html\": \"<html><head><title>Product</title></head><body><div itemscope itemtype=\\\"https://schema.org/Product\\\"><h1 itemprop=\\\"name\\\">Awesome Widget</h1><span itemprop=\\\"description\\\">The best widget on the market</span><span itemprop=\\\"price\\\">29.99</span><span itemprop=\\\"priceCurrency\\\">USD</span><img itemprop=\\\"image\\\" src=\\\"widget.jpg\\\" alt=\\\"Widget\\\"><span itemprop=\\\"ratingValue\\\">4.5</span></div></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Awesome Widget\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"best widget\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"29.99\"\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_dublin_core\",\n    \"description\": \"Extract Dublin Core metadata tags\",\n    \"input\": {\n      \"html\": \"<html><head><title>Scholarly Work</title><meta name=\\\"DC.title\\\" content=\\\"Principles of Knowledge Management\\\"><meta name=\\\"DC.creator\\\" content=\\\"Dr. Alice Johnson\\\"><meta name=\\\"DC.date\\\" content=\\\"2023-06-15\\\"><meta name=\\\"DC.subject\\\" content=\\\"Knowledge Management\\\"><meta name=\\\"DC.publisher\\\" content=\\\"Academic Press\\\"></head><body><p>This is a scholarly article.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"scholarly article\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/metadata/links_and_images.json",
    "content": "[\n  {\n    \"id\": \"metadata_extract_all_links\",\n    \"description\": \"Extract all links from a document into metadata\",\n    \"input\": {\n      \"html\": \"<html><head><title>Links Page</title></head><body><p>Visit <a href=\\\"https://example.com\\\">Example</a> or <a href=\\\"https://docs.example.com\\\">Docs</a>.</p><p>Also see <a href=\\\"/relative/path\\\">relative link</a> and <a href=\\\"mailto:hello@example.com\\\">email us</a>.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"metadata.links\",\n        \"value\": 2\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_extract_all_images\",\n    \"description\": \"Extract all images from a document into metadata\",\n    \"input\": {\n      \"html\": \"<html><head><title>Gallery</title></head><body><img src=\\\"https://example.com/photo1.jpg\\\" alt=\\\"Photo 1\\\"><img src=\\\"https://example.com/photo2.png\\\" alt=\\\"Photo 2\\\"><img src=\\\"/local/image.webp\\\" alt=\\\"Local image\\\"></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"metadata.images\",\n        \"value\": 2\n      }\n    ]\n  },\n  {\n    \"id\": \"metadata_headers_hierarchy\",\n    \"description\": \"Extract heading hierarchy from document into metadata\",\n    \"input\": {\n      \"html\": \"<html><head><title>Docs</title></head><body><h1>Introduction</h1><h2>Getting Started</h2><h3>Installation</h3><h3>Configuration</h3><h2>Advanced Usage</h2><h3>Custom Options</h3></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"metadata.headings\",\n        \"value\": 5\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/metadata/open_graph.json",
    "content": "[\n  {\n    \"id\": \"og_basic_tags\",\n    \"description\": \"Extract og:title, og:description, and og:image from Open Graph meta tags\",\n    \"input\": {\n      \"html\": \"<html><head><title>Fallback Title</title><meta property=\\\"og:title\\\" content=\\\"OG Title\\\"><meta property=\\\"og:description\\\" content=\\\"OG description text.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/image.jpg\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.title\",\n        \"value\": \"OG Title\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.description\",\n        \"value\": \"OG description text.\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.image\",\n        \"value\": \"https://example.com/image.jpg\"\n      }\n    ]\n  },\n  {\n    \"id\": \"og_multiple_tags\",\n    \"description\": \"Extract multiple Open Graph tags including type, url, and site_name\",\n    \"input\": {\n      \"html\": \"<html><head><meta property=\\\"og:title\\\" content=\\\"Article Title\\\"><meta property=\\\"og:type\\\" content=\\\"article\\\"><meta property=\\\"og:url\\\" content=\\\"https://example.com/article\\\"><meta property=\\\"og:site_name\\\" content=\\\"Example Site\\\"><meta property=\\\"og:description\\\" content=\\\"An interesting article.\\\"><meta property=\\\"og:image\\\" content=\\\"https://example.com/article.jpg\\\"></head><body><article><p>Article content here.</p></article></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.title\",\n        \"value\": \"Article Title\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.type\",\n        \"value\": \"article\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.url\",\n        \"value\": \"https://example.com/article\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.open_graph.site_name\",\n        \"value\": \"Example Site\"\n      }\n    ]\n  },\n  {\n    \"id\": \"twitter_card_tags\",\n    \"description\": \"Extract Twitter card meta tags\",\n    \"input\": {\n      \"html\": \"<html><head><meta name=\\\"twitter:card\\\" content=\\\"summary_large_image\\\"><meta name=\\\"twitter:site\\\" content=\\\"@examplesite\\\"><meta name=\\\"twitter:title\\\" content=\\\"Twitter Card Title\\\"><meta name=\\\"twitter:description\\\" content=\\\"Twitter card description.\\\"><meta name=\\\"twitter:image\\\" content=\\\"https://example.com/twitter-image.jpg\\\"></head><body><p>Content</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.twitter.card\",\n        \"value\": \"summary_large_image\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.twitter.title\",\n        \"value\": \"Twitter Card Title\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.twitter.description\",\n        \"value\": \"Twitter card description.\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/metadata/structured_data.json",
    "content": "[\n  {\n    \"id\": \"structured_data_json_ld\",\n    \"description\": \"JSON-LD script tag is stripped from output (security) but metadata may be extracted\",\n    \"input\": {\n      \"html\": \"<html><head><title>Article</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Article\\\",\\\"headline\\\":\\\"My Article\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Jane Doe\\\"},\\\"datePublished\\\":\\\"2024-01-15\\\"}</script></head><body><h1>My Article</h1><p>Article body text.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"My Article\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"application/ld+json\",\n          \"@context\",\n          \"schema.org\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"structured_data_multiple_json_ld\",\n    \"description\": \"Multiple JSON-LD blocks are all stripped from output\",\n    \"input\": {\n      \"html\": \"<html><head><title>Shop Page</title><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"Product\\\",\\\"name\\\":\\\"Widget\\\",\\\"price\\\":\\\"9.99\\\"}</script><script type=\\\"application/ld+json\\\">{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"BreadcrumbList\\\",\\\"itemListElement\\\":[{\\\"@type\\\":\\\"ListItem\\\",\\\"position\\\":1,\\\"name\\\":\\\"Home\\\"}]}</script></head><body><h1>Widget</h1><p>A great widget for all purposes.</p></body></html>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Widget\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"@context\",\n          \"BreadcrumbList\",\n          \"ListItem\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/br_in_tables.json",
    "content": "[\n  {\n    \"id\": \"options_br_in_tables_true\",\n    \"description\": \"BR elements in table cells render as line breaks\",\n    \"input\": {\n      \"html\": \"<table><tr><th>Header</th></tr><tr><td>Line 1<br>Line 2</td></tr></table>\",\n      \"options\": {\n        \"brInTables\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Header\", \"Line 1\", \"Line 2\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_br_in_tables_false\",\n    \"description\": \"BR elements in table cells are stripped when disabled\",\n    \"input\": {\n      \"html\": \"<table><tr><th>Col</th></tr><tr><td>A<br>B</td></tr></table>\",\n      \"options\": {\n        \"brInTables\": false\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Col\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/code_block_style.json",
    "content": "[\n  {\n    \"id\": \"options_code_block_tildes\",\n    \"description\": \"Code blocks use tilde fences\",\n    \"input\": {\n      \"html\": \"<pre><code>let x = 1;</code></pre>\",\n      \"options\": {\n        \"codeBlockStyle\": \"Tildes\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"~~~\", \"let x = 1;\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_code_block_indented\",\n    \"description\": \"Code blocks use 4-space indentation\",\n    \"input\": {\n      \"html\": \"<pre><code>print('hello')</code></pre>\",\n      \"options\": {\n        \"codeBlockStyle\": \"Indented\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"print('hello')\"]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"```\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_code_language_python\",\n    \"description\": \"Default code language annotation on blocks without lang attribute\",\n    \"input\": {\n      \"html\": \"<pre><code>def hello(): pass</code></pre>\",\n      \"options\": {\n        \"codeLanguage\": \"python\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"```python\", \"def hello\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/code_options.json",
    "content": "[\n  {\n    \"id\": \"options_code_block_backticks\",\n    \"description\": \"Backticks code block style uses triple backtick fences\",\n    \"input\": {\n      \"html\": \"<pre><code class=\\\"language-js\\\">console.log('hi');</code></pre>\",\n      \"options\": {\n        \"codeBlockStyle\": \"Backticks\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"```\",\n          \"console.log('hi');\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_code_block_tildes_style\",\n    \"description\": \"Tildes code block style uses triple tilde fences\",\n    \"input\": {\n      \"html\": \"<pre><code>some code</code></pre>\",\n      \"options\": {\n        \"codeBlockStyle\": \"Tildes\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"~~~\",\n          \"some code\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/escape_ascii.json",
    "content": "[\n  {\n    \"id\": \"options_escape_ascii_enabled\",\n    \"description\": \"ASCII Markdown characters are escaped when escapeAscii is true\",\n    \"input\": {\n      \"html\": \"<p>Text with # hash and [brackets] and * star</p>\",\n      \"options\": {\n        \"escapeAscii\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Text\", \"hash\", \"brackets\", \"star\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/escaping.json",
    "content": "[\n  {\n    \"id\": \"options_escape_asterisks\",\n    \"description\": \"escape_asterisks option escapes asterisks in plain text\",\n    \"input\": {\n      \"html\": \"<p>Use 2*3 = 6 in math.</p>\",\n      \"options\": {\n        \"escapeAsterisks\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"2\",\n          \"3\",\n          \"6\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_escape_underscores\",\n    \"description\": \"escape_underscores option escapes underscores in plain text\",\n    \"input\": {\n      \"html\": \"<p>The variable_name is defined.</p>\",\n      \"options\": {\n        \"escapeUnderscores\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"variable\",\n          \"name\",\n          \"defined.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_escape_misc\",\n    \"description\": \"escape_misc option escapes miscellaneous markdown characters\",\n    \"input\": {\n      \"html\": \"<p>Use # and | and ~ in text.</p>\",\n      \"options\": {\n        \"escapeMisc\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Use\",\n          \"and\",\n          \"in text.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/exclude_selectors.json",
    "content": "[\n  {\n    \"id\": \"options_exclude_selectors_class\",\n    \"description\": \"Elements matching CSS class selector are excluded entirely\",\n    \"input\": {\n      \"html\": \"<body><div class=\\\"cookie-banner\\\">Accept cookies</div><p>Main content</p></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\".cookie-banner\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Main content\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"cookies\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_id\",\n    \"description\": \"Elements matching CSS id selector are excluded entirely\",\n    \"input\": {\n      \"html\": \"<body><div id=\\\"ad-container\\\">Buy stuff</div><p>Article text</p></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\"#ad-container\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Article text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Buy stuff\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_attribute\",\n    \"description\": \"Elements matching CSS attribute selector are excluded entirely\",\n    \"input\": {\n      \"html\": \"<body><div role=\\\"complementary\\\">Sidebar</div><p>Primary text</p></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\"[role='complementary']\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Primary text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Sidebar\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_multiple\",\n    \"description\": \"Multiple CSS selectors each exclude their matched elements\",\n    \"input\": {\n      \"html\": \"<body><nav class=\\\"nav\\\">Menu</nav><p>Content</p><footer>Footer</footer></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\".nav\", \"footer\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Content\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Menu\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Footer\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_nested_content_dropped\",\n    \"description\": \"All descendants of excluded elements are dropped\",\n    \"input\": {\n      \"html\": \"<body><aside class=\\\"sidebar\\\"><h2>Related</h2><p>Sidebar text</p></aside><main><p>Main text</p></main></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\".sidebar\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Main text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Related\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Sidebar text\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_empty_noop\",\n    \"description\": \"Empty exclude_selectors list does not affect output\",\n    \"input\": {\n      \"html\": \"<p>Hello world</p>\",\n      \"options\": {\n        \"excludeSelectors\": []\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Hello world\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_plain_text_mode\",\n    \"description\": \"Exclude selectors work in plain text output mode\",\n    \"input\": {\n      \"html\": \"<body><div class=\\\"nav\\\">Navigation</div><p>Article body</p></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\".nav\"],\n        \"outputFormat\": \"Plain\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Article body\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Navigation\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_exclude_selectors_vs_strip_tags\",\n    \"description\": \"exclude_selectors drops entire subtree unlike strip_tags which keeps children\",\n    \"input\": {\n      \"html\": \"<body><div class=\\\"wrapper\\\"><p>Inner paragraph</p></div><p>Outer text</p></body>\",\n      \"options\": {\n        \"excludeSelectors\": [\".wrapper\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Outer text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Inner paragraph\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/heading_style.json",
    "content": "[\n  {\n    \"id\": \"options_heading_style_atx\",\n    \"description\": \"ATX heading style produces hash-prefixed headings\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><h2>Subtitle</h2>\",\n      \"options\": {\n        \"headingStyle\": \"Atx\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Title\",\n          \"## Subtitle\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_heading_style_underlined\",\n    \"description\": \"Underlined heading style produces setext-style headings for h1 and h2\",\n    \"input\": {\n      \"html\": \"<h1>Main Title</h1>\",\n      \"options\": {\n        \"headingStyle\": \"Underlined\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Main Title\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_heading_style_atx_closed\",\n    \"description\": \"ATX closed heading style adds closing hashes\",\n    \"input\": {\n      \"html\": \"<h1>Closed Heading</h1>\",\n      \"options\": {\n        \"headingStyle\": \"AtxClosed\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Closed Heading #\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/highlight_style.json",
    "content": "[\n  {\n    \"id\": \"options_highlight_double_equal\",\n    \"description\": \"Mark tag with double equal highlight style\",\n    \"input\": {\n      \"html\": \"<p>Text with <mark>highlighted</mark> here.</p>\",\n      \"options\": {\n        \"highlightStyle\": \"DoubleEqual\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"==highlighted==\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_highlight_bold\",\n    \"description\": \"Mark tag rendered as bold\",\n    \"input\": {\n      \"html\": \"<p>Text with <mark>highlighted</mark> text.</p>\",\n      \"options\": {\n        \"highlightStyle\": \"Bold\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"**highlighted**\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_highlight_none\",\n    \"description\": \"Mark tag with no highlight style strips the mark\",\n    \"input\": {\n      \"html\": \"<p>Text with <mark>plain</mark> content.</p>\",\n      \"options\": {\n        \"highlightStyle\": \"None\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"plain\"]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"==\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/inline_and_newlines.json",
    "content": "[\n  {\n    \"id\": \"options_strip_newlines\",\n    \"description\": \"Strip newlines produces single-line paragraphs\",\n    \"input\": {\n      \"html\": \"<p>First paragraph.</p><p>Second paragraph.</p>\",\n      \"options\": {\n        \"stripNewlines\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"First paragraph.\", \"Second paragraph.\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_convert_as_inline\",\n    \"description\": \"Block elements treated as inline\",\n    \"input\": {\n      \"html\": \"<p>One</p><p>Two</p>\",\n      \"options\": {\n        \"convertAsInline\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"One\", \"Two\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/list_options.json",
    "content": "[\n  {\n    \"id\": \"options_list_indent_tabs\",\n    \"description\": \"Tab indentation type for nested list items\",\n    \"input\": {\n      \"html\": \"<ul><li>Parent<ul><li>Child</li></ul></li></ul>\",\n      \"options\": {\n        \"listIndentType\": \"Tabs\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Parent\",\n          \"Child\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_list_custom_bullets\",\n    \"description\": \"Custom bullet character for unordered lists\",\n    \"input\": {\n      \"html\": \"<ul><li>Item A</li><li>Item B</li></ul>\",\n      \"options\": {\n        \"bullets\": \"*\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"* Item A\",\n          \"* Item B\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/max_depth.json",
    "content": "[\n  {\n    \"id\": \"options_max_depth_default_unlimited\",\n    \"description\": \"Default max_depth (null) converts deeply nested content fully\",\n    \"input\": {\n      \"html\": \"<div><div><div><div><p>Deep content</p></div></div></div></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Deep content\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_max_depth_truncates\",\n    \"description\": \"max_depth truncates content beyond the specified depth\",\n    \"input\": {\n      \"html\": \"<div><p>Shallow</p><div><div><div><p>Too deep</p></div></div></div></div>\",\n      \"options\": {\n        \"maxDepth\": 3\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Shallow\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Too deep\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_max_depth_zero_empty\",\n    \"description\": \"max_depth of 0 produces empty output\",\n    \"input\": {\n      \"html\": \"<p>Hello</p>\",\n      \"options\": {\n        \"maxDepth\": 0\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/newline_style.json",
    "content": "[\n  {\n    \"id\": \"options_newline_backslash\",\n    \"description\": \"Hard line breaks rendered with backslash\",\n    \"input\": {\n      \"html\": \"<p>Line one<br>Line two</p>\",\n      \"options\": {\n        \"newlineStyle\": \"Backslash\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Line one\", \"Line two\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_newline_spaces\",\n    \"description\": \"Hard line breaks rendered with trailing spaces\",\n    \"input\": {\n      \"html\": \"<p>First<br>Second</p>\",\n      \"options\": {\n        \"newlineStyle\": \"Spaces\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"First\", \"Second\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/output_format.json",
    "content": "[\n  {\n    \"id\": \"options_output_format_markdown\",\n    \"description\": \"Default markdown output format produces standard markdown\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>Some text.</p>\",\n      \"options\": {\n        \"outputFormat\": \"Markdown\",\n        \"headingStyle\": \"Atx\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Title\",\n          \"Some text.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_output_format_plain\",\n    \"description\": \"Plain text output format strips markdown syntax\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>Some <strong>bold</strong> text.</p>\",\n      \"options\": {\n        \"outputFormat\": \"Plain\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Title\",\n          \"bold\",\n          \"text.\"\n        ]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"values\": [\n          \"**bold**\",\n          \"# Title\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_output_format_djot\",\n    \"description\": \"Djot output format produces djot-compatible markup\",\n    \"input\": {\n      \"html\": \"<p>Simple paragraph.</p>\",\n      \"options\": {\n        \"outputFormat\": \"Djot\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Simple paragraph.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/preprocessing.json",
    "content": "[\n  {\n    \"id\": \"options_preprocessing_minimal\",\n    \"description\": \"Minimal preset preserves nav, footer, aside\",\n    \"input\": {\n      \"html\": \"<nav>Navigation</nav><p>Content</p><footer>Footer</footer>\",\n      \"options\": {\n        \"preprocessing\": {\n          \"preset\": \"Minimal\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Navigation\", \"Content\", \"Footer\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_preprocessing_aggressive\",\n    \"description\": \"Aggressive preset removes nav, footer, aside unconditionally\",\n    \"input\": {\n      \"html\": \"<nav>Menu</nav><article><h1>Title</h1><p>Content</p></article><aside>Sidebar</aside><footer>Footer</footer>\",\n      \"options\": {\n        \"preprocessing\": {\n          \"preset\": \"Aggressive\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Title\", \"Content\"]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Menu\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_preprocessing_remove_forms\",\n    \"description\": \"Forms are removed when remove_forms is true\",\n    \"input\": {\n      \"html\": \"<p>Before</p><form><input type='text'/><button>Submit</button></form><p>After</p>\",\n      \"options\": {\n        \"preprocessing\": {\n          \"removeForms\": true\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Before\", \"After\"]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Submit\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/remaining_options.json",
    "content": "[\n  {\n    \"id\": \"options_extract_metadata_true\",\n    \"description\": \"Extract metadata returns document metadata when enabled\",\n    \"input\": {\n      \"html\": \"<html><head><title>Test Page</title><meta name='description' content='A test page'></head><body><p>Content</p></body></html>\",\n      \"options\": {\n        \"extractMetadata\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.title\",\n        \"value\": \"Test Page\"\n      },\n      {\n        \"type\": \"equals\",\n        \"field\": \"metadata.description\",\n        \"value\": \"A test page\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_skip_images_true\",\n    \"description\": \"Images are omitted from output when skipImages is true\",\n    \"input\": {\n      \"html\": \"<p>Before <img src='test.jpg' alt='photo'> After</p>\",\n      \"options\": {\n        \"skipImages\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Before\", \"After\"]\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"photo\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_keep_inline_images_in_paragraph\",\n    \"description\": \"Images inside specified tags stay inline\",\n    \"input\": {\n      \"html\": \"<p>Text <img src='icon.png' alt='icon'> more text</p>\",\n      \"options\": {\n        \"keepInlineImagesIn\": [\"p\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Text\", \"more text\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_list_indent_width_four\",\n    \"description\": \"Nested lists indented with 4 spaces per level\",\n    \"input\": {\n      \"html\": \"<ul><li>Outer<ul><li>Inner</li></ul></li></ul>\",\n      \"options\": {\n        \"listIndentWidth\": 4\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Outer\", \"Inner\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_encoding_utf8\",\n    \"description\": \"UTF-8 encoding hint for special characters\",\n    \"input\": {\n      \"html\": \"<p>Caf\\u00e9 na\\u00efve r\\u00e9sum\\u00e9</p>\",\n      \"options\": {\n        \"encoding\": \"utf-8\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      }\n    ]\n  },\n  {\n    \"id\": \"options_debug_true\",\n    \"description\": \"Debug mode enabled does not crash and produces output\",\n    \"input\": {\n      \"html\": \"<p>Debug test</p>\",\n      \"options\": {\n        \"debug\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Debug test\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/strong_em_symbol.json",
    "content": "[\n  {\n    \"id\": \"options_strong_em_underscore\",\n    \"description\": \"Strong and em tags use underscore symbol instead of asterisk\",\n    \"input\": {\n      \"html\": \"<p><strong>bold</strong> and <em>italic</em></p>\",\n      \"options\": {\n        \"strongEmSymbol\": \"_\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"__bold__\", \"_italic_\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/sub_sup_symbols.json",
    "content": "[\n  {\n    \"id\": \"options_sub_symbol_tilde\",\n    \"description\": \"Subscript rendered with tilde symbol\",\n    \"input\": {\n      \"html\": \"<p>H<sub>2</sub>O</p>\",\n      \"options\": {\n        \"subSymbol\": \"~\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"~2~\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_sup_symbol_caret\",\n    \"description\": \"Superscript rendered with caret symbol\",\n    \"input\": {\n      \"html\": \"<p>x<sup>2</sup></p>\",\n      \"options\": {\n        \"supSymbol\": \"^\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"^2^\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/tag_control.json",
    "content": "[\n  {\n    \"id\": \"options_preserve_tags_iframe\",\n    \"description\": \"Iframe tags preserved as raw HTML in output\",\n    \"input\": {\n      \"html\": \"<p>Before</p><iframe src='video.html' width='560'></iframe><p>After</p>\",\n      \"options\": {\n        \"preserveTags\": [\"iframe\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Before\", \"After\", \"<iframe\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_strip_tags_div_span\",\n    \"description\": \"Div and span tags stripped but content preserved\",\n    \"input\": {\n      \"html\": \"<div class='wrapper'><p>Inside div</p></div><p>Outside <span class='hl'>span text</span></p>\",\n      \"options\": {\n        \"stripTags\": [\"div\", \"span\"]\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Inside div\", \"span text\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_default_title_true\",\n    \"description\": \"Links without title get empty title attribute when defaultTitle is true\",\n    \"input\": {\n      \"html\": \"<p><a href='https://example.com'>Link</a></p>\",\n      \"options\": {\n        \"defaultTitle\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Link\", \"https://example.com\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_autolinks_false\",\n    \"description\": \"Bare URL links rendered as regular markdown links when autolinks disabled\",\n    \"input\": {\n      \"html\": \"<p><a href='https://example.com'>https://example.com</a></p>\",\n      \"options\": {\n        \"autolinks\": false\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"example.com\"]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_link_style_reference\",\n    \"description\": \"Links use reference-style formatting\",\n    \"input\": {\n      \"html\": \"<p><a href='https://example.com'>Example</a> and <a href='https://other.com'>Other</a></p>\",\n      \"options\": {\n        \"linkStyle\": \"Reference\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\"Example\", \"Other\", \"example.com\"]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/whitespace_mode.json",
    "content": "[\n  {\n    \"id\": \"options_whitespace_normalized\",\n    \"description\": \"Normalized whitespace mode collapses multiple spaces\",\n    \"input\": {\n      \"html\": \"<p>Text   with    extra   spaces.</p>\",\n      \"options\": {\n        \"whitespaceMode\": \"Normalized\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Text\",\n          \"with\",\n          \"extra\",\n          \"spaces.\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_whitespace_strict\",\n    \"description\": \"Strict whitespace mode preserves whitespace as-is\",\n    \"input\": {\n      \"html\": \"<p>Preserved   spacing.</p>\",\n      \"options\": {\n        \"whitespaceMode\": \"Strict\"\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"Preserved\",\n          \"spacing.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/options/wrapping.json",
    "content": "[\n  {\n    \"id\": \"options_wrap_enabled\",\n    \"description\": \"Wrap option enabled with custom width wraps long lines\",\n    \"input\": {\n      \"html\": \"<p>This is a long paragraph that should be wrapped at the specified column width when the wrap option is enabled.</p>\",\n      \"options\": {\n        \"wrap\": true,\n        \"wrapWidth\": 40\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"This is a long paragraph\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"options_wrap_disabled\",\n    \"description\": \"Wrap option disabled preserves long lines without breaking\",\n    \"input\": {\n      \"html\": \"<p>This is a long paragraph that should not be wrapped at all because wrapping is disabled.</p>\",\n      \"options\": {\n        \"wrap\": false\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"This is a long paragraph that should not be wrapped at all because wrapping is disabled.\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/real-world/articles.json",
    "content": "[\n  {\n    \"id\": \"real_world_blog_post\",\n    \"description\": \"Blog post with headings, paragraphs, code blocks, and links converts to readable Markdown\",\n    \"input\": {\n      \"html\": \"<article><h1>Getting Started with Rust</h1><p>Rust is a systems programming language focused on <strong>safety</strong>, <em>performance</em>, and concurrency. It was created by <a href=\\\"https://www.mozilla.org\\\">Mozilla</a> and has grown significantly in popularity.</p><h2>Installation</h2><p>Install Rust using the official installer:</p><pre><code class=\\\"language-bash\\\">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code></pre><h2>Hello World</h2><p>Create your first Rust program:</p><pre><code class=\\\"language-rust\\\">fn main() {\\n    println!(\\\"Hello, world!\\\");\\n}</code></pre><p>Run it with <code>cargo run</code> from your project directory.</p><h2>Key Concepts</h2><ul><li>Ownership and borrowing</li><li>Lifetimes</li><li>Traits and generics</li><li>Pattern matching</li></ul><p>For more information, visit the <a href=\\\"https://doc.rust-lang.org/book/\\\">Rust Book</a>.</p></article>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Getting Started with Rust\",\n          \"## Installation\",\n          \"## Hello World\",\n          \"## Key Concepts\",\n          \"cargo run\",\n          \"[Mozilla](https://www.mozilla.org)\",\n          \"- Ownership and borrowing\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"real_world_product_page\",\n    \"description\": \"Product page with table, images, and lists converts correctly\",\n    \"input\": {\n      \"html\": \"<div class=\\\"product\\\"><h1>Wireless Keyboard Pro</h1><img src=\\\"https://example.com/keyboard.jpg\\\" alt=\\\"Wireless Keyboard Pro\\\"><p>The ultimate wireless keyboard for professionals. Features a comfortable layout with <strong>backlit keys</strong> and <em>ultra-long battery life</em>.</p><h2>Specifications</h2><table><thead><tr><th>Feature</th><th>Details</th></tr></thead><tbody><tr><td>Battery Life</td><td>Up to 12 months</td></tr><tr><td>Connectivity</td><td>Bluetooth 5.0</td></tr><tr><td>Key Travel</td><td>2mm</td></tr><tr><td>Weight</td><td>750g</td></tr></tbody></table><h2>What's in the Box</h2><ul><li>Wireless Keyboard Pro</li><li>USB-C charging cable</li><li>USB receiver dongle</li><li>Quick start guide</li></ul><h2>Compatibility</h2><p>Compatible with <strong>Windows</strong>, <strong>macOS</strong>, <strong>Linux</strong>, <strong>iOS</strong>, and <strong>Android</strong>.</p></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Wireless Keyboard Pro\",\n          \"![Wireless Keyboard Pro](https://example.com/keyboard.jpg)\",\n          \"## Specifications\",\n          \"Battery Life\",\n          \"12 months\",\n          \"Bluetooth 5.0\",\n          \"## What's in the Box\",\n          \"USB-C charging cable\",\n          \"|\",\n          \"---\"\n        ]\n      }\n    ]\n  },\n  {\n    \"id\": \"real_world_documentation_page\",\n    \"description\": \"Documentation page with nested lists, code examples, and blockquotes converts correctly\",\n    \"input\": {\n      \"html\": \"<div class=\\\"docs\\\"><h1>API Reference</h1><p>This guide covers the core API for the <code>html-to-markdown</code> library.</p><blockquote><p><strong>Note:</strong> All functions are thread-safe and can be called from multiple threads concurrently.</p></blockquote><h2>convert_html</h2><p>Converts an HTML string to Markdown format.</p><pre><code class=\\\"language-rust\\\">pub fn convert_html(html: &amp;str) -&gt; Result&lt;String, ConversionError&gt;</code></pre><h3>Parameters</h3><ul><li><code>html</code> - The HTML input string<ul><li>Must be valid UTF-8</li><li>Maximum size: 50MB</li></ul></li></ul><h3>Returns</h3><ul><li><code>Ok(String)</code> - The converted Markdown</li><li><code>Err(ConversionError)</code> - If conversion fails</li></ul><h3>Example</h3><pre><code class=\\\"language-rust\\\">let markdown = convert_html(\\\"&lt;h1&gt;Hello&lt;/h1&gt;\\\").unwrap();\\nassert_eq!(markdown, \\\"# Hello\\\");</code></pre><h2>ConversionOptions</h2><p>Configure conversion behavior using the builder pattern:</p><pre><code class=\\\"language-rust\\\">let options = ConversionOptions::builder()\\n    .heading_style(HeadingStyle::ATX)\\n    .code_block_style(CodeBlockStyle::Fenced)\\n    .build();</code></pre><blockquote><p>See the <a href=\\\"/docs/options\\\">options reference</a> for a full list of configuration values.</p></blockquote></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# API Reference\",\n          \"## convert_html\",\n          \"### Parameters\",\n          \"### Returns\",\n          \"### Example\",\n          \"## ConversionOptions\",\n          \"> \",\n          \"thread-safe\",\n          \"convert_html\",\n          \"ConversionOptions\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/result/tables.json",
    "content": "[\n  {\n    \"id\": \"result_tables_simple\",\n    \"description\": \"Simple table populates the tables array in result\",\n    \"input\": {\n      \"html\": \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"tables\",\n        \"value\": 1\n      }\n    ]\n  },\n  {\n    \"id\": \"result_tables_multiple\",\n    \"description\": \"Multiple tables each appear in the tables array\",\n    \"input\": {\n      \"html\": \"<table><tr><th>A</th></tr><tr><td>1</td></tr></table><p>Between</p><table><tr><th>B</th></tr><tr><td>2</td></tr></table>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"count_min\",\n        \"field\": \"tables\",\n        \"value\": 2\n      }\n    ]\n  },\n  {\n    \"id\": \"result_tables_empty_when_no_tables\",\n    \"description\": \"Result tables array is empty when input has no tables\",\n    \"input\": {\n      \"html\": \"<p>No tables here</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_equals\",\n        \"field\": \"tables\",\n        \"value\": 0\n      }\n    ]\n  },\n  {\n    \"id\": \"result_tables_without_structure_flag\",\n    \"description\": \"Tables array is empty when includeDocumentStructure is false\",\n    \"input\": {\n      \"html\": \"<table><tr><th>X</th></tr><tr><td>Y</td></tr></table>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_equals\",\n        \"field\": \"tables\",\n        \"value\": 0\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/result/warnings.json",
    "content": "[\n  {\n    \"id\": \"result_warnings_empty_for_clean_input\",\n    \"description\": \"Warnings array is empty for well-formed HTML without problematic content\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>Clean content with <a href='https://example.com'>a link</a>.</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_equals\",\n        \"field\": \"warnings\",\n        \"value\": 0\n      }\n    ]\n  },\n  {\n    \"id\": \"result_warnings_empty_for_complex_input\",\n    \"description\": \"Warnings array is empty for complex but valid HTML\",\n    \"input\": {\n      \"html\": \"<article><h1>Article</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><ul><li>Item 1</li><li>Item 2</li></ul></article>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_equals\",\n        \"field\": \"warnings\",\n        \"value\": 0\n      }\n    ]\n  },\n  {\n    \"id\": \"result_warnings_empty_for_malformed_html\",\n    \"description\": \"Warnings array is empty even for malformed HTML (parser is lenient)\",\n    \"input\": {\n      \"html\": \"<p>Unclosed paragraph<div>Mixed nesting</p></div>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"count_equals\",\n        \"field\": \"warnings\",\n        \"value\": 0\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/smoke/basic.json",
    "content": "[\n  {\n    \"id\": \"smoke_empty_string\",\n    \"description\": \"Empty string produces empty output\",\n    \"input\": {\n      \"html\": \"\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"\"\n      }\n    ]\n  },\n  {\n    \"id\": \"smoke_simple_paragraph\",\n    \"description\": \"Simple paragraph converts correctly\",\n    \"input\": {\n      \"html\": \"<p>Hello World</p>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"equals\",\n        \"field\": \"content\",\n        \"value\": \"Hello World\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      }\n    ]\n  },\n  {\n    \"id\": \"smoke_simple_heading\",\n    \"description\": \"H1 heading converts to ATX markdown\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1>\"\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains_all\",\n        \"field\": \"content\",\n        \"values\": [\n          \"# Title\"\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/structure/basic.json",
    "content": "[\n  {\n    \"id\": \"structure_heading_paragraph\",\n    \"description\": \"Simple heading followed by paragraph produces Heading and Paragraph nodes\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>A paragraph of text.</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 2\n      }\n    ]\n  },\n  {\n    \"id\": \"structure_multiple_headings\",\n    \"description\": \"Multiple headings create multiple Heading nodes with correct levels\",\n    \"input\": {\n      \"html\": \"<h1>Main Title</h1><h2>Section One</h2><p>Section one content.</p><h2>Section Two</h2><p>Section two content.</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 4\n      }\n    ]\n  },\n  {\n    \"id\": \"structure_list\",\n    \"description\": \"Unordered list produces List and ListItem nodes\",\n    \"input\": {\n      \"html\": \"<p>Items:</p><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 2\n      }\n    ]\n  },\n  {\n    \"id\": \"structure_code_block\",\n    \"description\": \"Fenced code block produces Code node\",\n    \"input\": {\n      \"html\": \"<p>Example code:</p><pre><code class=\\\"language-rust\\\">fn main() { println!(\\\"Hello\\\"); }</code></pre>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 2\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/structure/nesting.json",
    "content": "[\n  {\n    \"id\": \"structure_h1_h2_nested_group\",\n    \"description\": \"H1 followed by H2 creates a nested group under the H1\",\n    \"input\": {\n      \"html\": \"<h1>Chapter One</h1><p>Chapter intro.</p><h2>Section One</h2><p>Section content.</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 3\n      }\n    ]\n  },\n  {\n    \"id\": \"structure_sibling_h1_groups\",\n    \"description\": \"H1, H2, then another H1 creates two sibling top-level groups\",\n    \"input\": {\n      \"html\": \"<h1>Chapter One</h1><h2>Section A</h2><p>Section A content.</p><h1>Chapter Two</h1><h2>Section B</h2><p>Section B content.</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 4\n      }\n    ]\n  },\n  {\n    \"id\": \"structure_deep_nesting_h1_h2_h3\",\n    \"description\": \"H1 > H2 > H3 creates three levels of heading nesting\",\n    \"input\": {\n      \"html\": \"<h1>Top Level</h1><p>Top intro.</p><h2>Mid Level</h2><p>Mid content.</p><h3>Deep Level</h3><p>Deep content.</p>\",\n      \"options\": {\n        \"includeDocumentStructure\": true\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"content\"\n      },\n      {\n        \"type\": \"not_empty\",\n        \"field\": \"document.nodes\"\n      },\n      {\n        \"type\": \"count_min\",\n        \"field\": \"document.nodes\",\n        \"value\": 5\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/advanced_elements.json",
    "content": "[\n  {\n    \"id\": \"visitor_definition_list_skip\",\n    \"description\": \"Visitor skips definition list items from output\",\n    \"input\": {\n      \"html\": \"<p>Glossary:</p><dl><dt>Term A</dt><dd>Definition of term A</dd><dt>Term B</dt><dd>Definition of term B</dd></dl><p>End of glossary</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_definition_term\": {\n          \"action\": \"skip\"\n        },\n        \"visit_definition_description\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Glossary:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"End of glossary\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Term A\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Definition\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_definition_list_custom_format\",\n    \"description\": \"Visitor formats definition lists with custom templates\",\n    \"input\": {\n      \"html\": \"<dl><dt>Python</dt><dd>A high-level programming language</dd><dt>JavaScript</dt><dd>A scripting language for web browsers</dd></dl>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_definition_term\": {\n          \"action\": \"custom_template\",\n          \"template\": \"### {text}\"\n        },\n        \"visit_definition_description\": {\n          \"action\": \"custom_template\",\n          \"template\": \"> {text}\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"### Python\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"### JavaScript\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"> A high-level programming language\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"> A scripting language for web browsers\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_details_summary_custom\",\n    \"description\": \"Visitor customizes details/summary disclosure elements\",\n    \"input\": {\n      \"html\": \"<details><summary>Click to expand</summary><p>This content is initially hidden.</p><p>But can be revealed by the user.</p></details>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_summary\": {\n          \"action\": \"custom_template\",\n          \"template\": \"[EXPANDABLE] {text}\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[EXPANDABLE] Click to expand\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"This content is initially hidden.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_details_summary_skip\",\n    \"description\": \"Visitor removes details/summary elements entirely\",\n    \"input\": {\n      \"html\": \"<p>Main content here.</p><details><summary>Hidden section</summary><p>Secret details</p></details><p>More main content.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_details\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Main content here.\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"More main content.\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Hidden section\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Secret details\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_figure_custom\",\n    \"description\": \"Visitor customizes figure and figcaption elements\",\n    \"input\": {\n      \"html\": \"<article><h1>Article Title</h1><p>Introduction paragraph.</p><figure><img src=\\\"diagram.png\\\" alt=\\\"System architecture diagram\\\"><figcaption>Figure 1: System Architecture</figcaption></figure><p>Explanation of the figure.</p></article>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_figcaption\": {\n          \"action\": \"custom_template\",\n          \"template\": \"*{text}*\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Article Title\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"*Figure 1: System Architecture*\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Explanation of the figure.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_figure_skip\",\n    \"description\": \"Visitor removes figure elements with their captions\",\n    \"input\": {\n      \"html\": \"<p>See the chart below:</p><figure><img src=\\\"chart.svg\\\"><figcaption>Revenue Trends 2020-2024</figcaption></figure><p>As shown in the chart above.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_figure_start\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"See the chart below:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"As shown in the chart above.\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Revenue Trends\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"chart.svg\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_figure_custom_wrap\",\n    \"description\": \"Visitor wraps figure content with custom formatting\",\n    \"input\": {\n      \"html\": \"<section><h2>Gallery</h2><figure><img src=\\\"photo1.jpg\\\" alt=\\\"Photo\\\"><figcaption>Beautiful sunset</figcaption></figure></section>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_figure_start\": {\n          \"action\": \"custom\",\n          \"output\": \"\\n[FIGURE]\\n\"\n        },\n        \"visit_figure_end\": {\n          \"action\": \"custom_template\",\n          \"template\": \"{output}\\n[/FIGURE]\\n\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[FIGURE]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[/FIGURE]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Gallery\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/basic.json",
    "content": "[\n  {\n    \"id\": \"visitor_skip_heading\",\n    \"description\": \"Visitor skip action omits all headings from output\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>Body text remains.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_heading\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Title\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Body text remains.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_custom_output\",\n    \"description\": \"Visitor custom action replaces element output\",\n    \"input\": {\n      \"html\": \"<h1>Original Heading</h1>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_heading\": {\n          \"action\": \"custom\",\n          \"output\": \"## REPLACED HEADING\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"## REPLACED HEADING\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"# Original Heading\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_preserve_html\",\n    \"description\": \"Visitor preserve_html action includes raw HTML in output\",\n    \"input\": {\n      \"html\": \"<div><custom-tag>Custom content</custom-tag></div>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_custom_element\": {\n          \"action\": \"preserve_html\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"<custom-tag>\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_continue_default\",\n    \"description\": \"Visitor continue action preserves default conversion\",\n    \"input\": {\n      \"html\": \"<p>Hello <strong>World</strong></p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_strong\": {\n          \"action\": \"continue\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"**World**\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/elements.json",
    "content": "[\n  {\n    \"id\": \"visitor_skip_code_blocks\",\n    \"description\": \"Visitor skips code blocks from output\",\n    \"input\": {\n      \"html\": \"<p>Intro text</p><pre><code>let x = 42;</code></pre><p>Outro text</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_code_block\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Intro text\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Outro text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"let x = 42\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_custom_emphasis\",\n    \"description\": \"Visitor replaces emphasis with custom output\",\n    \"input\": {\n      \"html\": \"<p>This is <em>important</em> text.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_emphasis\": {\n          \"action\": \"custom_template\",\n          \"template\": \">>>{text}<<<\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \">>>important<<<\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"*important*\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_custom_blockquote\",\n    \"description\": \"Visitor replaces blockquote with custom format\",\n    \"input\": {\n      \"html\": \"<blockquote><p>A wise quote.</p></blockquote>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_blockquote\": {\n          \"action\": \"custom_template\",\n          \"template\": \"QUOTE: \\\"{content}\\\"\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"QUOTE:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"A wise quote.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_skip_strong\",\n    \"description\": \"Visitor skips bold/strong elements\",\n    \"input\": {\n      \"html\": \"<p>Normal <strong>bold text</strong> normal</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_strong\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"bold text\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Normal\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/formatting.json",
    "content": "[\n  {\n    \"id\": \"visitor_underline_skip\",\n    \"description\": \"Visitor skips underline elements from output\",\n    \"input\": {\n      \"html\": \"<p>Normal text with <u>underlined part</u> and more text.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_underline\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Normal text with\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"and more text.\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"underlined part\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_underline_custom\",\n    \"description\": \"Visitor replaces underline with custom markup\",\n    \"input\": {\n      \"html\": \"<p>This is <u>very important</u> text.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_underline\": {\n          \"action\": \"custom_template\",\n          \"template\": \"_{text}_\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"_very important_\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"**\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_subscript_custom\",\n    \"description\": \"Visitor replaces subscript with custom template\",\n    \"input\": {\n      \"html\": \"<p>H<sub>2</sub>O is water.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_subscript\": {\n          \"action\": \"custom_template\",\n          \"template\": \"~{text}~\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"H~2~O\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"is water\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_subscript_skip\",\n    \"description\": \"Visitor skips subscript elements entirely\",\n    \"input\": {\n      \"html\": \"<p>The formula C<sub>12</sub>H<sub>22</sub>O<sub>11</sub> is sugar.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_subscript\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"The formula CHO is sugar.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_superscript_custom\",\n    \"description\": \"Visitor replaces superscript with custom template\",\n    \"input\": {\n      \"html\": \"<p>Einstein's E=mc<sup>2</sup> revolutionized physics.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_superscript\": {\n          \"action\": \"custom_template\",\n          \"template\": \"^{text}^\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"E=mc^2^\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"revolutionized physics\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_superscript_skip\",\n    \"description\": \"Visitor skips superscript from output\",\n    \"input\": {\n      \"html\": \"<p>The equation x<sup>3</sup> + y<sup>3</sup> = z<sup>3</sup> has no solutions.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_superscript\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"The equation x + y = z has no solutions.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_mark_custom\",\n    \"description\": \"Visitor replaces highlight/mark with custom template\",\n    \"input\": {\n      \"html\": \"<p>This is a <mark>highlighted passage</mark> in the text.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_mark\": {\n          \"action\": \"custom_template\",\n          \"template\": \"=={text}==\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"==highlighted passage==\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"This is a\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_mark_skip\",\n    \"description\": \"Visitor skips mark elements entirely\",\n    \"input\": {\n      \"html\": \"<p>Key insight: <mark>always validate input</mark> for security.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_mark\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"always validate input\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Key insight:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"for security.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_line_break_custom\",\n    \"description\": \"Visitor replaces line break with custom output\",\n    \"input\": {\n      \"html\": \"<p>First line<br>Second line<br>Third line</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_line_break\": {\n          \"action\": \"custom\",\n          \"output\": \" | \"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"First line | Second line | Third line\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"\\n\\n\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_line_break_skip\",\n    \"description\": \"Visitor removes all line breaks\",\n    \"input\": {\n      \"html\": \"<p>Address Line 1<br>Address Line 2<br>Address Line 3</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_line_break\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Address Line 1Address Line 2Address Line 3\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_horizontal_rule_custom\",\n    \"description\": \"Visitor replaces horizontal rule with custom output\",\n    \"input\": {\n      \"html\": \"<h1>Section A</h1><p>Content A</p><hr><h1>Section B</h1><p>Content B</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_horizontal_rule\": {\n          \"action\": \"custom\",\n          \"output\": \"\\n[DIVIDER]\\n\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[DIVIDER]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Section A\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Section B\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"---\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_horizontal_rule_skip\",\n    \"description\": \"Visitor removes all horizontal rules\",\n    \"input\": {\n      \"html\": \"<p>Part 1</p><hr><p>Part 2</p><hr><p>Part 3</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_horizontal_rule\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Part 1\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Part 2\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Part 3\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"---\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/forms_and_semantics.json",
    "content": "[\n  {\n    \"id\": \"visitor_form_custom\",\n    \"description\": \"Visitor replaces form with custom output\",\n    \"input\": {\n      \"html\": \"<div><form action=\\\"/submit\\\" method=\\\"POST\\\"><label>Name: <input type=\\\"text\\\" name=\\\"name\\\"></label><button type=\\\"submit\\\">Submit</button></form></div>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_form\": {\n          \"action\": \"custom\",\n          \"output\": \"[FORM PLACEHOLDER]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[FORM PLACEHOLDER]\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"submit\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"input\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_form_skip\",\n    \"description\": \"Visitor skips form elements entirely\",\n    \"input\": {\n      \"html\": \"<p>Before form</p><form><input type=\\\"email\\\" name=\\\"email\\\"></form><p>After form</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_form\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Before form\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"After form\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"email\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_input_custom\",\n    \"description\": \"Visitor replaces input with labeled output\",\n    \"input\": {\n      \"html\": \"<form><label>Username: <input type=\\\"text\\\" name=\\\"username\\\" value=\\\"\\\"></label><label>Password: <input type=\\\"password\\\" name=\\\"password\\\"></label></form>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_input\": {\n          \"action\": \"custom_template\",\n          \"template\": \"[INPUT:{input_type}]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[INPUT:text]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[INPUT:password]\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_input_skip\",\n    \"description\": \"Visitor skips all input elements\",\n    \"input\": {\n      \"html\": \"<p>Sign up:</p><input type=\\\"text\\\" name=\\\"email\\\" placeholder=\\\"your@email.com\\\"><input type=\\\"checkbox\\\" name=\\\"agree\\\"><p>Continue</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_input\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Sign up:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Continue\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"email\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_button_custom\",\n    \"description\": \"Visitor replaces button with bracketed text\",\n    \"input\": {\n      \"html\": \"<p>Confirm action: <button type=\\\"submit\\\">Click me</button> or <button type=\\\"reset\\\">Cancel</button></p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_button\": {\n          \"action\": \"custom_template\",\n          \"template\": \"[BTN:{text}]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[BTN:Click me]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[BTN:Cancel]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Confirm action:\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_button_skip\",\n    \"description\": \"Visitor removes all buttons from output\",\n    \"input\": {\n      \"html\": \"<p>Actions available: <button>Save</button> <button>Delete</button> <button>Export</button></p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_button\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Actions available:\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Save\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Delete\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Export\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_definition_list_custom\",\n    \"description\": \"Visitor customizes definition list items\",\n    \"input\": {\n      \"html\": \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_definition_term\": {\n          \"action\": \"custom_template\",\n          \"template\": \"**{text}**\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"**HTML**\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"**CSS**\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"HyperText Markup Language\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/headings.json",
    "content": "[\n  {\n    \"id\": \"visitor_custom_heading\",\n    \"description\": \"Visitor replaces heading with custom format\",\n    \"input\": {\n      \"html\": \"<h2>Section Title</h2><p>Content below heading.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_heading\": {\n          \"action\": \"custom_template\",\n          \"template\": \"--- {text} ---\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"--- Section Title ---\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"## Section Title\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Content below heading.\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_skip_all_headings\",\n    \"description\": \"Visitor skips all headings from document\",\n    \"input\": {\n      \"html\": \"<h1>Title</h1><p>Body text remains.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_heading\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"Title\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Body text remains.\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/images.json",
    "content": "[\n  {\n    \"id\": \"visitor_skip_images\",\n    \"description\": \"Visitor skips all images from output\",\n    \"input\": {\n      \"html\": \"<p>Before image</p><img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\"><p>After image</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_image\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Before image\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"After image\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"photo.jpg\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"A photo\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_custom_image\",\n    \"description\": \"Visitor replaces image with custom output using template\",\n    \"input\": {\n      \"html\": \"<img src=\\\"banner.png\\\" alt=\\\"Banner\\\">\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_image\": {\n          \"action\": \"custom_template\",\n          \"template\": \"[Image: {alt}]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[Image: Banner]\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"banner.png\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/links.json",
    "content": "[\n  {\n    \"id\": \"visitor_custom_link_format\",\n    \"description\": \"Visitor reformats links using template interpolation\",\n    \"input\": {\n      \"html\": \"<p>Visit <a href=\\\"https://example.com\\\">Example</a> for more info.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_link\": {\n          \"action\": \"custom_template\",\n          \"template\": \"{text} ({href})\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Example (https://example.com)\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"[Example]\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_skip_links\",\n    \"description\": \"Visitor skips all links entirely\",\n    \"input\": {\n      \"html\": \"<p>Before <a href=\\\"https://example.com\\\">link text</a> after</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_link\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"link text\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"example.com\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_custom_link_static\",\n    \"description\": \"Visitor replaces link with static custom output\",\n    \"input\": {\n      \"html\": \"<a href=\\\"https://example.com\\\">Click here</a>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_link\": {\n          \"action\": \"custom\",\n          \"output\": \"[REDACTED LINK]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[REDACTED LINK]\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"example.com\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "fixtures/visitor/media.json",
    "content": "[\n  {\n    \"id\": \"visitor_audio_custom\",\n    \"description\": \"Visitor replaces audio element with custom output\",\n    \"input\": {\n      \"html\": \"<p>Listen to this: <audio src=\\\"podcast.mp3\\\" controls></audio></p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_audio\": {\n          \"action\": \"custom\",\n          \"output\": \"[AUDIO: podcast.mp3]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[AUDIO: podcast.mp3]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Listen to this:\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_audio_skip\",\n    \"description\": \"Visitor removes audio elements from output\",\n    \"input\": {\n      \"html\": \"<p>Background music:</p><audio src=\\\"music.ogg\\\" autoplay></audio><p>Enjoy!</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_audio\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Background music:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Enjoy!\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"music.ogg\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_video_custom\",\n    \"description\": \"Visitor replaces video with custom link\",\n    \"input\": {\n      \"html\": \"<p>Watch our tutorial:</p><video src=\\\"tutorial.mp4\\\" width=\\\"320\\\" height=\\\"240\\\" controls></video><p>Great content!</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_video\": {\n          \"action\": \"custom_template\",\n          \"template\": \"[VIDEO: {src}]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[VIDEO: tutorial.mp4]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Watch our tutorial:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Great content!\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_video_skip\",\n    \"description\": \"Visitor removes video elements entirely\",\n    \"input\": {\n      \"html\": \"<h2>Demo</h2><video src=\\\"demo.webm\\\"></video><p>See the demo above.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_video\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Demo\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"See the demo above.\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"demo.webm\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_iframe_custom\",\n    \"description\": \"Visitor replaces embedded iframe with custom text\",\n    \"input\": {\n      \"html\": \"<p>Embedded map:</p><iframe src=\\\"https://maps.example.com/embed\\\" width=\\\"400\\\" height=\\\"300\\\"></iframe><p>End of map</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_iframe\": {\n          \"action\": \"custom\",\n          \"output\": \"[EMBEDDED: https://maps.example.com/embed]\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"[EMBEDDED: https://maps.example.com/embed]\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Embedded map:\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"End of map\"\n      }\n    ]\n  },\n  {\n    \"id\": \"visitor_iframe_skip\",\n    \"description\": \"Visitor removes embedded iframes\",\n    \"input\": {\n      \"html\": \"<h3>Reviews</h3><iframe src=\\\"https://widget.example.com/reviews\\\"></iframe><p>See reviews from our partners.</p>\"\n    },\n    \"visitor\": {\n      \"callbacks\": {\n        \"visit_iframe\": {\n          \"action\": \"skip\"\n        }\n      }\n    },\n    \"assertions\": [\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"Reviews\"\n      },\n      {\n        \"type\": \"contains\",\n        \"field\": \"content\",\n        \"value\": \"See reviews from our partners.\"\n      },\n      {\n        \"type\": \"not_contains\",\n        \"field\": \"content\",\n        \"value\": \"widget.example.com\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "just",
    "content": ""
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"html-to-markdown-monorepo\",\n  \"version\": \"3.1.0\",\n  \"private\": true,\n  \"description\": \"HTML to Markdown converter - monorepo root\",\n  \"scripts\": {\n    \"build\": \"pnpm run build:node && pnpm run build:wasm && pnpm run build:ts\",\n    \"build:node\": \"pnpm --filter @kreuzberg/html-to-markdown-node run build\",\n    \"build:wasm\": \"pnpm --filter @kreuzberg/html-to-markdown-wasm run build:all\",\n    \"build:ts\": \"pnpm --filter @kreuzberg/html-to-markdown run build\",\n    \"test\": \"pnpm -r test\",\n    \"test:node\": \"pnpm --filter @kreuzberg/html-to-markdown-node test\",\n    \"test:wasm\": \"pnpm --filter @kreuzberg/html-to-markdown-wasm test\",\n    \"test:ts\": \"pnpm --filter @kreuzberg/html-to-markdown test\",\n    \"clean\": \"pnpm -r run clean && rm -rf node_modules\",\n    \"lint\": \"pnpm --filter @kreuzberg/html-to-markdown-node exec biome check --write --no-errors-on-unmatched . && pnpm --filter @kreuzberg/html-to-markdown-wasm exec biome check --write --no-errors-on-unmatched . && pnpm --filter @kreuzberg/html-to-markdown exec biome check --write --no-errors-on-unmatched .\",\n    \"format\": \"biome format --write .\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^2.4.14\",\n    \"@types/node\": \"^25.6.0\",\n    \"typescript\": \"^6.0.3\",\n    \"vitest\": \"^4.1.5\"\n  },\n  \"engines\": {\n    \"node\": \">=18\",\n    \"pnpm\": \">=10\"\n  },\n  \"packageManager\": \"pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8\"\n}\n"
  },
  {
    "path": "packages/csharp/.editorconfig",
    "content": "root = true\n\n[*.cs]\nindent_style = space\nindent_size = 4\nmax_line_length = 120\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": "packages/csharp/Directory.Build.props",
    "content": "<!-- auto-generated by alef (generate_bindings) -->\n<!-- alef:hash:621e4ce001d01abbff130814d1f5392b5fc6048ddaa8a88587f7b64411941ad5 -->\n<Project>\n  <PropertyGroup>\n    <Nullable>enable</Nullable>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/AnnotationKind.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:5d9e46627570fa6b0b2601f62f7b7b19e7993b9004a097e674c7ccd5de4ff3ce\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// The type of an inline text annotation.\n///\n/// Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n/// </summary>\n[JsonConverter(typeof(AnnotationKindJsonConverter))]\npublic abstract record AnnotationKind\n{\n    /// <summary>\n    /// Bold / strong emphasis.\n    /// </summary>\n    public sealed record Bold() : AnnotationKind;\n\n    /// <summary>\n    /// Italic / emphasis.\n    /// </summary>\n    public sealed record Italic() : AnnotationKind;\n\n    /// <summary>\n    /// Underline.\n    /// </summary>\n    public sealed record Underline() : AnnotationKind;\n\n    /// <summary>\n    /// Strikethrough / deleted text.\n    /// </summary>\n    public sealed record Strikethrough() : AnnotationKind;\n\n    /// <summary>\n    /// Inline code.\n    /// </summary>\n    public sealed record Code() : AnnotationKind;\n\n    /// <summary>\n    /// Subscript text.\n    /// </summary>\n    public sealed record Subscript() : AnnotationKind;\n\n    /// <summary>\n    /// Superscript text.\n    /// </summary>\n    public sealed record Superscript() : AnnotationKind;\n\n    /// <summary>\n    /// Highlighted / marked text.\n    /// </summary>\n    public sealed record Highlight() : AnnotationKind;\n\n    /// <summary>\n    /// A hyperlink.\n    /// </summary>\n    public sealed record Link(\n        [property: JsonPropertyName(\"url\")] string Url,\n        [property: JsonPropertyName(\"title\")] string? Title\n    ) : AnnotationKind;\n\n}\n\n/// <summary>Custom JSON converter for <see cref=\"AnnotationKind\"/> that reads the \"annotation_type\" discriminator from any position.</summary>\ninternal sealed class AnnotationKindJsonConverter : JsonConverter<AnnotationKind>\n{\n    public override AnnotationKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        using var doc = JsonDocument.ParseValue(ref reader);\n        var root = doc.RootElement;\n        if (!root.TryGetProperty(\"annotation_type\", out var tagEl))\n            throw new JsonException(\"AnnotationKind: missing \\\"annotation_type\\\" discriminator\");\n        var tag = tagEl.GetString();\n        var json = root.GetRawText();\n        return tag switch\n        {\n            \"bold\" => JsonSerializer.Deserialize<AnnotationKind.Bold>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Bold\"),\n            \"italic\" => JsonSerializer.Deserialize<AnnotationKind.Italic>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Italic\"),\n            \"underline\" => JsonSerializer.Deserialize<AnnotationKind.Underline>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Underline\"),\n            \"strikethrough\" => JsonSerializer.Deserialize<AnnotationKind.Strikethrough>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Strikethrough\"),\n            \"code\" => JsonSerializer.Deserialize<AnnotationKind.Code>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Code\"),\n            \"subscript\" => JsonSerializer.Deserialize<AnnotationKind.Subscript>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Subscript\"),\n            \"superscript\" => JsonSerializer.Deserialize<AnnotationKind.Superscript>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Superscript\"),\n            \"highlight\" => JsonSerializer.Deserialize<AnnotationKind.Highlight>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Highlight\"),\n            \"link\" => JsonSerializer.Deserialize<AnnotationKind.Link>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize AnnotationKind.Link\"),\n            _ => throw new JsonException($\"Unknown AnnotationKind discriminator: {tag}\")\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, AnnotationKind value, JsonSerializerOptions options)\n    {\n        // Serialize the concrete type, then inject the discriminator\n        switch (value)\n        {\n            case AnnotationKind.Bold v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"bold\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Italic v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"italic\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Underline v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"underline\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Strikethrough v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"strikethrough\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Code v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"code\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Subscript v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"subscript\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Superscript v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"superscript\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Highlight v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"highlight\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case AnnotationKind.Link v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"annotation_type\", \"link\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"annotation_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            default: throw new JsonException($\"Unknown AnnotationKind subtype: {value.GetType().Name}\");\n        }\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/CodeBlockStyle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a655aee618757ea10c0a678fe5da82f45e3234e82737721ebce671e65ffe4573\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Code block fence style in Markdown output.\n///\n/// Determines how code blocks (`<pre><code>`) are rendered in Markdown.\n/// </summary>\npublic enum CodeBlockStyle\n{\n    /// <summary>\n    /// Indented code blocks (4 spaces). `CommonMark` standard.\n    /// </summary>\n    [JsonPropertyName(\"indented\")]\n    Indented,\n    /// <summary>\n    /// Fenced code blocks with backticks (```). Default (GFM). Supports language hints.\n    /// </summary>\n    [JsonPropertyName(\"backticks\")]\n    Backticks,\n    /// <summary>\n    /// Fenced code blocks with tildes (~~~). Supports language hints.\n    /// </summary>\n    [JsonPropertyName(\"tildes\")]\n    Tildes,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConfigErrorException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:25809b956085c1aa67ca36b7f4c4398097f471e96ca84190c4e60ec41714f81f\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Invalid configuration\n/// </summary>\npublic class ConfigErrorException : ConversionErrorException\n{\n    public ConfigErrorException(string message) : base(message) { }\n\n    public ConfigErrorException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConversionErrorException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:33e16141906ca3210e6f4fec18e3ad5ddc84dbfe968bf07e8c9132f4a389e7a3\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Errors that can occur during HTML to Markdown conversion.\n/// </summary>\npublic class ConversionErrorException : Exception\n{\n    public ConversionErrorException(string message) : base(message) { }\n\n    public ConversionErrorException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConversionOptions.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0024d24bbe348143fe86cb114e590c1fee82d27d1742cd20b4ab4743fdc82060\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Main conversion options for HTML to Markdown conversion.\n///\n/// Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::ConversionOptions;\n///\n/// let options = ConversionOptions::builder()\n///     .heading_style(HeadingStyle::Atx)\n///     .wrap(true)\n///     .wrap_width(100)\n///     .build();\n/// ```\n/// </summary>\npublic sealed class ConversionOptions\n{\n    /// <summary>\n    /// Heading style to use in Markdown output (ATX `#` or Setext underline).\n    /// </summary>\n    [JsonPropertyName(\"heading_style\")]\n    public HeadingStyle? HeadingStyle { get; set; } = null;\n\n    /// <summary>\n    /// How to indent nested list items (spaces or tab).\n    /// </summary>\n    [JsonPropertyName(\"list_indent_type\")]\n    public ListIndentType? ListIndentType { get; set; } = null;\n\n    /// <summary>\n    /// Number of spaces (or tabs) to use for each level of list indentation.\n    /// </summary>\n    [JsonPropertyName(\"list_indent_width\")]\n    public ulong ListIndentWidth { get; set; } = 2;\n\n    /// <summary>\n    /// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n    /// </summary>\n    [JsonPropertyName(\"bullets\")]\n    public string Bullets { get; set; } = \"-*+\";\n\n    /// <summary>\n    /// Character used for bold/italic emphasis markers (`*` or `_`).\n    /// </summary>\n    [JsonPropertyName(\"strong_em_symbol\")]\n    public string StrongEmSymbol { get; set; } = \"*\";\n\n    /// <summary>\n    /// Escape `*` characters in plain text to avoid unintended bold/italic.\n    /// </summary>\n    [JsonPropertyName(\"escape_asterisks\")]\n    public bool EscapeAsterisks { get; set; } = false;\n\n    /// <summary>\n    /// Escape `_` characters in plain text to avoid unintended bold/italic.\n    /// </summary>\n    [JsonPropertyName(\"escape_underscores\")]\n    public bool EscapeUnderscores { get; set; } = false;\n\n    /// <summary>\n    /// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n    /// </summary>\n    [JsonPropertyName(\"escape_misc\")]\n    public bool EscapeMisc { get; set; } = false;\n\n    /// <summary>\n    /// Escape ASCII characters that have special meaning in certain Markdown dialects.\n    /// </summary>\n    [JsonPropertyName(\"escape_ascii\")]\n    public bool EscapeAscii { get; set; } = false;\n\n    /// <summary>\n    /// Default language annotation for fenced code blocks that have no language hint.\n    /// </summary>\n    [JsonPropertyName(\"code_language\")]\n    public string CodeLanguage { get; set; } = \"\";\n\n    /// <summary>\n    /// Automatically convert bare URLs into Markdown autolinks.\n    /// </summary>\n    [JsonPropertyName(\"autolinks\")]\n    public bool Autolinks { get; set; } = true;\n\n    /// <summary>\n    /// Emit a default title when no `<title>` tag is present.\n    /// </summary>\n    [JsonPropertyName(\"default_title\")]\n    public bool DefaultTitle { get; set; } = false;\n\n    /// <summary>\n    /// Render `<br>` elements inside table cells as literal line breaks.\n    /// </summary>\n    [JsonPropertyName(\"br_in_tables\")]\n    public bool BrInTables { get; set; } = false;\n\n    /// <summary>\n    /// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n    /// </summary>\n    [JsonPropertyName(\"highlight_style\")]\n    public HighlightStyle? HighlightStyle { get; set; } = null;\n\n    /// <summary>\n    /// Extract `<meta>` and `<head>` information into the result metadata.\n    /// </summary>\n    [JsonPropertyName(\"extract_metadata\")]\n    public bool ExtractMetadata { get; set; } = true;\n\n    /// <summary>\n    /// Controls how whitespace is normalised during conversion.\n    /// </summary>\n    [JsonPropertyName(\"whitespace_mode\")]\n    public WhitespaceMode? WhitespaceMode { get; set; } = null;\n\n    /// <summary>\n    /// Strip all newlines from the output, producing a single-line result.\n    /// </summary>\n    [JsonPropertyName(\"strip_newlines\")]\n    public bool StripNewlines { get; set; } = false;\n\n    /// <summary>\n    /// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n    /// </summary>\n    [JsonPropertyName(\"wrap\")]\n    public bool Wrap { get; set; } = false;\n\n    /// <summary>\n    /// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n    /// </summary>\n    [JsonPropertyName(\"wrap_width\")]\n    public ulong WrapWidth { get; set; } = 80;\n\n    /// <summary>\n    /// Treat the entire document as inline content (no block-level wrappers).\n    /// </summary>\n    [JsonPropertyName(\"convert_as_inline\")]\n    public bool ConvertAsInline { get; set; } = false;\n\n    /// <summary>\n    /// Markdown notation for subscript text (e.g. `\"~\"`).\n    /// </summary>\n    [JsonPropertyName(\"sub_symbol\")]\n    public string SubSymbol { get; set; } = \"\";\n\n    /// <summary>\n    /// Markdown notation for superscript text (e.g. `\"^\"`).\n    /// </summary>\n    [JsonPropertyName(\"sup_symbol\")]\n    public string SupSymbol { get; set; } = \"\";\n\n    /// <summary>\n    /// How to encode hard line breaks (`<br>`) in Markdown.\n    /// </summary>\n    [JsonPropertyName(\"newline_style\")]\n    public NewlineStyle NewlineStyle { get; set; } = NewlineStyle.Spaces;\n\n    /// <summary>\n    /// Style used for fenced code blocks (backticks or tilde).\n    /// </summary>\n    [JsonPropertyName(\"code_block_style\")]\n    public CodeBlockStyle? CodeBlockStyle { get; set; } = null;\n\n    /// <summary>\n    /// HTML tag names whose `<img>` children are kept inline instead of block.\n    /// </summary>\n    [JsonPropertyName(\"keep_inline_images_in\")]\n    public List<string> KeepInlineImagesIn { get; set; } = [];\n\n    /// <summary>\n    /// Pre-processing options applied to the HTML before conversion.\n    /// </summary>\n    [JsonPropertyName(\"preprocessing\")]\n    public PreprocessingOptions Preprocessing { get; set; } = default!;\n\n    /// <summary>\n    /// Expected character encoding of the input HTML (default `\"utf-8\"`).\n    /// </summary>\n    [JsonPropertyName(\"encoding\")]\n    public string Encoding { get; set; } = \"utf-8\";\n\n    /// <summary>\n    /// Emit debug information during conversion.\n    /// </summary>\n    [JsonPropertyName(\"debug\")]\n    public bool Debug { get; set; } = false;\n\n    /// <summary>\n    /// HTML tag names whose content is stripped from the output entirely.\n    /// </summary>\n    [JsonPropertyName(\"strip_tags\")]\n    public List<string> StripTags { get; set; } = [];\n\n    /// <summary>\n    /// HTML tag names that are preserved verbatim in the output.\n    /// </summary>\n    [JsonPropertyName(\"preserve_tags\")]\n    public List<string> PreserveTags { get; set; } = [];\n\n    /// <summary>\n    /// Skip conversion of `<img>` elements (omit images from output).\n    /// </summary>\n    [JsonPropertyName(\"skip_images\")]\n    public bool SkipImages { get; set; } = false;\n\n    /// <summary>\n    /// Link rendering style (inline or reference).\n    /// </summary>\n    [JsonPropertyName(\"link_style\")]\n    public LinkStyle? LinkStyle { get; set; } = null;\n\n    /// <summary>\n    /// Target output format (Markdown, plain text, etc.).\n    /// </summary>\n    [JsonPropertyName(\"output_format\")]\n    public OutputFormat? OutputFormat { get; set; } = null;\n\n    /// <summary>\n    /// Include structured document tree in result.\n    /// </summary>\n    [JsonPropertyName(\"include_document_structure\")]\n    public bool IncludeDocumentStructure { get; set; } = false;\n\n    /// <summary>\n    /// Extract inline images from data URIs and SVGs.\n    /// </summary>\n    [JsonPropertyName(\"extract_images\")]\n    public bool ExtractImages { get; set; } = false;\n\n    /// <summary>\n    /// Maximum decoded image size in bytes (default 5MB).\n    /// </summary>\n    [JsonPropertyName(\"max_image_size\")]\n    public ulong MaxImageSize { get; set; } = 5242880;\n\n    /// <summary>\n    /// Capture SVG elements as images.\n    /// </summary>\n    [JsonPropertyName(\"capture_svg\")]\n    public bool CaptureSvg { get; set; } = false;\n\n    /// <summary>\n    /// Infer image dimensions from data.\n    /// </summary>\n    [JsonPropertyName(\"infer_dimensions\")]\n    public bool InferDimensions { get; set; } = true;\n\n    /// <summary>\n    /// Maximum DOM traversal depth. `None` means unlimited.\n    /// When set, subtrees beyond this depth are silently truncated.\n    /// </summary>\n    [JsonPropertyName(\"max_depth\")]\n    public ulong? MaxDepth { get; set; } = null;\n\n    /// <summary>\n    /// CSS selectors for elements to exclude entirely (element + all content).\n    ///\n    /// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n    /// excluded elements and all their descendants are dropped from the output.\n    /// Supports any CSS selector that `tl` supports: tag names, `.class`,\n    /// `#id`, `[attribute]`, etc.\n    ///\n    /// Invalid selectors are silently skipped at conversion time.\n    ///\n    /// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n    /// </summary>\n    [JsonPropertyName(\"exclude_selectors\")]\n    public List<string> ExcludeSelectors { get; set; } = [];\n\n    /// <summary>\n    /// Optional HtmlVisitor bridge. When set, the native converter will call back\n    /// into the managed implementation for each visited node.\n    /// Not serialized to JSON — attached via the FFI setter before conversion.\n    /// </summary>\n    [JsonIgnore]\n    public HtmlVisitorBridge? Visitor { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConversionOptionsBuilder.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:45d472f6f4930033c8486678bf963fdc52fcf26ce67c12b53041b271039702a8\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Builder for [`ConversionOptions`].\n///\n/// All fields start with default values. Call `.build()` to produce the final options.\n/// </summary>\npublic sealed class ConversionOptionsBuilder : IDisposable\n{\n    internal IntPtr Handle { get; }\n\n    internal ConversionOptionsBuilder(IntPtr handle)\n    {\n        Handle = handle;\n    }\n\n    public void Dispose()\n    {\n        // Native free will be called by the runtime\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConversionOptionsUpdate.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:4160ff7370cfc7303326df8997005f91999b67db948a676184b26f8555671dc7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Partial update for `ConversionOptions`.\n///\n/// Uses `Option<T>` fields for selective updates. Bindings use this to construct\n/// options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n/// </summary>\npublic sealed class ConversionOptionsUpdate\n{\n    /// <summary>\n    /// Optional override for [`ConversionOptions::heading_style`].\n    /// </summary>\n    [JsonPropertyName(\"heading_style\")]\n    public HeadingStyle? HeadingStyle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::list_indent_type`].\n    /// </summary>\n    [JsonPropertyName(\"list_indent_type\")]\n    public ListIndentType? ListIndentType { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::list_indent_width`].\n    /// </summary>\n    [JsonPropertyName(\"list_indent_width\")]\n    public ulong? ListIndentWidth { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::bullets`].\n    /// </summary>\n    [JsonPropertyName(\"bullets\")]\n    public string? Bullets { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::strong_em_symbol`].\n    /// </summary>\n    [JsonPropertyName(\"strong_em_symbol\")]\n    public string? StrongEmSymbol { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::escape_asterisks`].\n    /// </summary>\n    [JsonPropertyName(\"escape_asterisks\")]\n    public bool? EscapeAsterisks { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::escape_underscores`].\n    /// </summary>\n    [JsonPropertyName(\"escape_underscores\")]\n    public bool? EscapeUnderscores { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::escape_misc`].\n    /// </summary>\n    [JsonPropertyName(\"escape_misc\")]\n    public bool? EscapeMisc { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::escape_ascii`].\n    /// </summary>\n    [JsonPropertyName(\"escape_ascii\")]\n    public bool? EscapeAscii { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::code_language`].\n    /// </summary>\n    [JsonPropertyName(\"code_language\")]\n    public string? CodeLanguage { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::autolinks`].\n    /// </summary>\n    [JsonPropertyName(\"autolinks\")]\n    public bool? Autolinks { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::default_title`].\n    /// </summary>\n    [JsonPropertyName(\"default_title\")]\n    public bool? DefaultTitle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::br_in_tables`].\n    /// </summary>\n    [JsonPropertyName(\"br_in_tables\")]\n    public bool? BrInTables { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::highlight_style`].\n    /// </summary>\n    [JsonPropertyName(\"highlight_style\")]\n    public HighlightStyle? HighlightStyle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::extract_metadata`].\n    /// </summary>\n    [JsonPropertyName(\"extract_metadata\")]\n    public bool? ExtractMetadata { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::whitespace_mode`].\n    /// </summary>\n    [JsonPropertyName(\"whitespace_mode\")]\n    public WhitespaceMode? WhitespaceMode { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::strip_newlines`].\n    /// </summary>\n    [JsonPropertyName(\"strip_newlines\")]\n    public bool? StripNewlines { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::wrap`].\n    /// </summary>\n    [JsonPropertyName(\"wrap\")]\n    public bool? Wrap { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::wrap_width`].\n    /// </summary>\n    [JsonPropertyName(\"wrap_width\")]\n    public ulong? WrapWidth { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::convert_as_inline`].\n    /// </summary>\n    [JsonPropertyName(\"convert_as_inline\")]\n    public bool? ConvertAsInline { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::sub_symbol`].\n    /// </summary>\n    [JsonPropertyName(\"sub_symbol\")]\n    public string? SubSymbol { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::sup_symbol`].\n    /// </summary>\n    [JsonPropertyName(\"sup_symbol\")]\n    public string? SupSymbol { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::newline_style`].\n    /// </summary>\n    [JsonPropertyName(\"newline_style\")]\n    public NewlineStyle? NewlineStyle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::code_block_style`].\n    /// </summary>\n    [JsonPropertyName(\"code_block_style\")]\n    public CodeBlockStyle? CodeBlockStyle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::keep_inline_images_in`].\n    /// </summary>\n    [JsonPropertyName(\"keep_inline_images_in\")]\n    public List<string>? KeepInlineImagesIn { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::preprocessing`].\n    /// </summary>\n    [JsonPropertyName(\"preprocessing\")]\n    public PreprocessingOptionsUpdate? Preprocessing { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::encoding`].\n    /// </summary>\n    [JsonPropertyName(\"encoding\")]\n    public string? Encoding { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::debug`].\n    /// </summary>\n    [JsonPropertyName(\"debug\")]\n    public bool? Debug { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::strip_tags`].\n    /// </summary>\n    [JsonPropertyName(\"strip_tags\")]\n    public List<string>? StripTags { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::preserve_tags`].\n    /// </summary>\n    [JsonPropertyName(\"preserve_tags\")]\n    public List<string>? PreserveTags { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::skip_images`].\n    /// </summary>\n    [JsonPropertyName(\"skip_images\")]\n    public bool? SkipImages { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::link_style`].\n    /// </summary>\n    [JsonPropertyName(\"link_style\")]\n    public LinkStyle? LinkStyle { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::output_format`].\n    /// </summary>\n    [JsonPropertyName(\"output_format\")]\n    public OutputFormat? OutputFormat { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::include_document_structure`].\n    /// </summary>\n    [JsonPropertyName(\"include_document_structure\")]\n    public bool? IncludeDocumentStructure { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::extract_images`].\n    /// </summary>\n    [JsonPropertyName(\"extract_images\")]\n    public bool? ExtractImages { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::max_image_size`].\n    /// </summary>\n    [JsonPropertyName(\"max_image_size\")]\n    public ulong? MaxImageSize { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::capture_svg`].\n    /// </summary>\n    [JsonPropertyName(\"capture_svg\")]\n    public bool? CaptureSvg { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::infer_dimensions`].\n    /// </summary>\n    [JsonPropertyName(\"infer_dimensions\")]\n    public bool? InferDimensions { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::max_depth`].\n    /// </summary>\n    [JsonPropertyName(\"max_depth\")]\n    public ulong? MaxDepth { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::exclude_selectors`].\n    /// </summary>\n    [JsonPropertyName(\"exclude_selectors\")]\n    public List<string>? ExcludeSelectors { get; set; } = null;\n\n    /// <summary>\n    /// Optional override for [`ConversionOptions::visitor`].\n    /// </summary>\n    [JsonPropertyName(\"visitor\")]\n    public VisitorHandle? Visitor { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ConversionResult.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bd9b9625cdd69302d5fb9c1b1e66ffe00b84ec113e0526e8d9e2ed4fd996b77b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// The primary result of HTML conversion and extraction.\n///\n/// Contains the converted text output, optional structured document tree,\n/// metadata, extracted tables, images, and processing warnings.\n///\n/// # Example\n///\n/// ```text\n/// use html_to_markdown_rs::{convert, ConversionOptions};\n///\n/// let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n/// assert!(result.content.is_some());\n/// assert!(result.warnings.is_empty());\n/// ```\n/// </summary>\npublic sealed class ConversionResult\n{\n    /// <summary>\n    /// Converted text output (markdown, djot, or plain text).\n    ///\n    /// `None` when `output_format` is set to `OutputFormat::None`,\n    /// indicating extraction-only mode.\n    /// </summary>\n    [JsonPropertyName(\"content\")]\n    public string? Content { get; set; } = null;\n\n    /// <summary>\n    /// Structured document tree with semantic elements.\n    ///\n    /// Populated when `include_document_structure` is `true` in options.\n    /// </summary>\n    [JsonPropertyName(\"document\")]\n    public DocumentStructure? Document { get; set; } = null;\n\n    /// <summary>\n    /// Extracted HTML metadata (title, OG, links, images, structured data).\n    /// </summary>\n    [JsonPropertyName(\"metadata\")]\n    public HtmlMetadata Metadata { get; set; } = default!;\n\n    /// <summary>\n    /// Extracted tables with structured cell data and markdown representation.\n    /// </summary>\n    [JsonPropertyName(\"tables\")]\n    public List<TableData> Tables { get; set; } = [];\n\n    /// <summary>\n    /// Extracted inline images (data URIs and SVGs).\n    ///\n    /// Populated when `extract_images` is `true` in options.\n    /// </summary>\n    [JsonPropertyName(\"images\")]\n    public List<string> Images { get; set; } = [];\n\n    /// <summary>\n    /// Non-fatal processing warnings.\n    /// </summary>\n    [JsonPropertyName(\"warnings\")]\n    public List<ProcessingWarning> Warnings { get; set; } = [];\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/DocumentMetadata.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0ad1da93935c7569d71ca5d79c5b5df78ebcdf68213e32c92d2f30b3cb0e81e5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Document-level metadata extracted from `<head>` and top-level elements.\n///\n/// Contains all metadata typically used by search engines, social media platforms,\n/// and browsers for document indexing and presentation.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::DocumentMetadata;\n/// let doc = DocumentMetadata {\n///     title: Some(\"My Article\".to_string()),\n///     description: Some(\"A great article about Rust\".to_string()),\n///     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n///     ..Default::default()\n/// };\n///\n/// assert_eq!(doc.title, Some(\"My Article\".to_string()));\n/// ```\n/// </summary>\npublic sealed class DocumentMetadata\n{\n    /// <summary>\n    /// Document title from `<title>` tag\n    /// </summary>\n    [JsonPropertyName(\"title\")]\n    public string? Title { get; set; } = null;\n\n    /// <summary>\n    /// Document description from `<meta name=\"description\">` tag\n    /// </summary>\n    [JsonPropertyName(\"description\")]\n    public string? Description { get; set; } = null;\n\n    /// <summary>\n    /// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n    /// </summary>\n    [JsonPropertyName(\"keywords\")]\n    public List<string> Keywords { get; set; } = [];\n\n    /// <summary>\n    /// Document author from `<meta name=\"author\">` tag\n    /// </summary>\n    [JsonPropertyName(\"author\")]\n    public string? Author { get; set; } = null;\n\n    /// <summary>\n    /// Canonical URL from `<link rel=\"canonical\">` tag\n    /// </summary>\n    [JsonPropertyName(\"canonical_url\")]\n    public string? CanonicalUrl { get; set; } = null;\n\n    /// <summary>\n    /// Base URL from `<base href=\"\">` tag for resolving relative URLs\n    /// </summary>\n    [JsonPropertyName(\"base_href\")]\n    public string? BaseHref { get; set; } = null;\n\n    /// <summary>\n    /// Document language from `lang` attribute\n    /// </summary>\n    [JsonPropertyName(\"language\")]\n    public string? Language { get; set; } = null;\n\n    /// <summary>\n    /// Document text direction from `dir` attribute\n    /// </summary>\n    [JsonConverter(typeof(TextDirectionJsonConverter))]\n    [JsonPropertyName(\"text_direction\")]\n    public TextDirection? TextDirection { get; set; } = null;\n\n    /// <summary>\n    /// Open Graph metadata (og:* properties) for social media\n    /// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n    /// </summary>\n    [JsonPropertyName(\"open_graph\")]\n    public Dictionary<string, string> OpenGraph { get; set; } = new Dictionary<string, string>();\n\n    /// <summary>\n    /// Twitter Card metadata (twitter:* properties)\n    /// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n    /// </summary>\n    [JsonPropertyName(\"twitter_card\")]\n    public Dictionary<string, string> TwitterCard { get; set; } = new Dictionary<string, string>();\n\n    /// <summary>\n    /// Additional meta tags not covered by specific fields\n    /// Keys are meta name/property attributes, values are content\n    /// </summary>\n    [JsonPropertyName(\"meta_tags\")]\n    public Dictionary<string, string> MetaTags { get; set; } = new Dictionary<string, string>();\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/DocumentNode.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c82a4c989cde2bf70b53b34df61553c5c873fce99cb509b64798eef0db944b7d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A single node in the document tree.\n/// </summary>\npublic sealed class DocumentNode\n{\n    /// <summary>\n    /// Deterministic node identifier.\n    /// </summary>\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; } = \"\";\n\n    /// <summary>\n    /// The semantic content of this node.\n    /// </summary>\n    [JsonConverter(typeof(NodeContentJsonConverter))]\n    [JsonPropertyName(\"content\")]\n    public NodeContent Content { get; set; } = default!;\n\n    /// <summary>\n    /// Index of the parent node (None for root nodes).\n    /// </summary>\n    [JsonPropertyName(\"parent\")]\n    public uint? Parent { get; set; } = null;\n\n    /// <summary>\n    /// Indices of child nodes in reading order.\n    /// </summary>\n    [JsonPropertyName(\"children\")]\n    public List<uint> Children { get; set; } = [];\n\n    /// <summary>\n    /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n    /// </summary>\n    [JsonPropertyName(\"annotations\")]\n    public List<TextAnnotation> Annotations { get; set; } = [];\n\n    /// <summary>\n    /// Format-specific attributes (e.g. class, id, data-* attributes).\n    /// </summary>\n    [JsonPropertyName(\"attributes\")]\n    public Dictionary<string, string>? Attributes { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/DocumentStructure.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:eb348e1ed6394ab5b15b3754af415e1beaa6a037b6755715eb8d10b2c2a8c27b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A structured document tree representing the semantic content of an HTML document.\n///\n/// Uses a flat node array with index-based parent/child references for efficient traversal.\n/// </summary>\npublic sealed class DocumentStructure\n{\n    /// <summary>\n    /// All nodes in document reading order.\n    /// </summary>\n    [JsonPropertyName(\"nodes\")]\n    public List<DocumentNode> Nodes { get; set; } = [];\n\n    /// <summary>\n    /// The source format (always \"html\" for this crate).\n    /// </summary>\n    [JsonPropertyName(\"source_format\")]\n    public string? SourceFormat { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/GridCell.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:08d9e25dd663f9a503f50811c008a79d7477116d1ecdd49d495d848673ba2aec\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A single cell in a table grid.\n/// </summary>\npublic sealed class GridCell\n{\n    /// <summary>\n    /// The text content of the cell.\n    /// </summary>\n    [JsonPropertyName(\"content\")]\n    public string Content { get; set; } = \"\";\n\n    /// <summary>\n    /// 0-indexed row position.\n    /// </summary>\n    [JsonPropertyName(\"row\")]\n    public uint Row { get; set; } = 0;\n\n    /// <summary>\n    /// 0-indexed column position.\n    /// </summary>\n    [JsonPropertyName(\"col\")]\n    public uint Col { get; set; } = 0;\n\n    /// <summary>\n    /// Number of rows this cell spans (default 1).\n    /// </summary>\n    [JsonPropertyName(\"row_span\")]\n    public uint RowSpan { get; set; } = 0;\n\n    /// <summary>\n    /// Number of columns this cell spans (default 1).\n    /// </summary>\n    [JsonPropertyName(\"col_span\")]\n    public uint ColSpan { get; set; } = 0;\n\n    /// <summary>\n    /// Whether this is a header cell (`<th>`).\n    /// </summary>\n    [JsonPropertyName(\"is_header\")]\n    public bool IsHeader { get; set; } = false;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HeaderMetadata.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:95fc63d9c116972c3ff2b54e1706ef0be421eb15090bfb48e538502db951db24\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Header element metadata with hierarchy tracking.\n///\n/// Captures heading elements (h1-h6) with their text content, identifiers,\n/// and position in the document structure.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HeaderMetadata;\n/// let header = HeaderMetadata {\n///     level: 1,\n///     text: \"Main Title\".to_string(),\n///     id: Some(\"main-title\".to_string()),\n///     depth: 0,\n///     html_offset: 145,\n/// };\n///\n/// assert_eq!(header.level, 1);\n/// assert!(header.is_valid());\n/// ```\n/// </summary>\npublic sealed class HeaderMetadata\n{\n    /// <summary>\n    /// Header level: 1 (h1) through 6 (h6)\n    /// </summary>\n    [JsonPropertyName(\"level\")]\n    public byte Level { get; set; } = 0;\n\n    /// <summary>\n    /// Normalized text content of the header\n    /// </summary>\n    [JsonPropertyName(\"text\")]\n    public string Text { get; set; } = \"\";\n\n    /// <summary>\n    /// HTML id attribute if present\n    /// </summary>\n    [JsonPropertyName(\"id\")]\n    public string? Id { get; set; } = null;\n\n    /// <summary>\n    /// Document tree depth at the header element\n    /// </summary>\n    [JsonPropertyName(\"depth\")]\n    public ulong Depth { get; set; } = 0;\n\n    /// <summary>\n    /// Byte offset in original HTML document\n    /// </summary>\n    [JsonPropertyName(\"html_offset\")]\n    public ulong HtmlOffset { get; set; } = 0;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HeadingStyle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:8173aedccc8a54c461239dd04660fed11947e120bb8aaae77979adf7bf89db39\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Heading style options for Markdown output.\n///\n/// Controls how headings (h1-h6) are rendered in the output Markdown.\n/// </summary>\npublic enum HeadingStyle\n{\n    /// <summary>\n    /// Underlined style (=== for h1, --- for h2).\n    /// </summary>\n    [JsonPropertyName(\"underlined\")]\n    Underlined,\n    /// <summary>\n    /// ATX style (# for h1, ## for h2, etc.). Default.\n    /// </summary>\n    [JsonPropertyName(\"atx\")]\n    Atx,\n    /// <summary>\n    /// ATX closed style (# title #, with closing hashes).\n    /// </summary>\n    [JsonPropertyName(\"atxclosed\")]\n    AtxClosed,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HighlightStyle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:df9450e08ad85127f5666a489f9dc907d00d11a2061e69a5d08175061a2592da\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Highlight rendering style for `<mark>` elements.\n///\n/// Controls how highlighted text is rendered in Markdown output.\n/// </summary>\npublic enum HighlightStyle\n{\n    /// <summary>\n    /// Double equals syntax (==text==). Default. Pandoc-compatible.\n    /// </summary>\n    [JsonPropertyName(\"doubleequal\")]\n    DoubleEqual,\n    /// <summary>\n    /// Preserve as HTML (==text==). Original HTML tag.\n    /// </summary>\n    [JsonPropertyName(\"html\")]\n    Html,\n    /// <summary>\n    /// Render as bold (**text**). Uses strong emphasis.\n    /// </summary>\n    [JsonPropertyName(\"bold\")]\n    Bold,\n    /// <summary>\n    /// Strip formatting, render as plain text. No markup.\n    /// </summary>\n    [JsonPropertyName(\"none\")]\n    None,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HtmlMetadata.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2c19de58109c6ef14c58d698ccf7d849c5af2f07457053a3da8bab18e88887d7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Comprehensive metadata extraction result from HTML document.\n///\n/// Contains all extracted metadata types in a single structure,\n/// suitable for serialization and transmission across language boundaries.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HtmlMetadata;\n/// let metadata = HtmlMetadata {\n///     document: Default::default(),\n///     headers: Vec::new(),\n///     links: Vec::new(),\n///     images: Vec::new(),\n///     structured_data: Vec::new(),\n/// };\n///\n/// assert!(metadata.headers.is_empty());\n/// ```\n/// </summary>\npublic sealed class HtmlMetadata\n{\n    /// <summary>\n    /// Document-level metadata (title, description, canonical, etc.)\n    /// </summary>\n    [JsonPropertyName(\"document\")]\n    public DocumentMetadata Document { get; set; } = default!;\n\n    /// <summary>\n    /// Extracted header elements with hierarchy\n    /// </summary>\n    [JsonPropertyName(\"headers\")]\n    public List<HeaderMetadata> Headers { get; set; } = [];\n\n    /// <summary>\n    /// Extracted hyperlinks with type classification\n    /// </summary>\n    [JsonPropertyName(\"links\")]\n    public List<LinkMetadata> Links { get; set; } = [];\n\n    /// <summary>\n    /// Extracted images with source and dimensions\n    /// </summary>\n    [JsonPropertyName(\"images\")]\n    public List<ImageMetadata> Images { get; set; } = [];\n\n    /// <summary>\n    /// Extracted structured data blocks\n    /// </summary>\n    [JsonPropertyName(\"structured_data\")]\n    public List<StructuredData> StructuredData { get; set; } = [];\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>HtmlToMarkdown</RootNamespace>\n    <PackageId>HtmlToMarkdown</PackageId>\n    <Version>3.4.0-rc.25</Version>\n    <Description>High-performance HTML to Markdown converter</Description>\n    <PackageLicenseFile>LICENSE</PackageLicenseFile>\n    <RepositoryUrl>https://github.com/kreuzberg-dev/html-to-markdown</RepositoryUrl>\n    <Authors>Kreuzberg Team</Authors>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"../../LICENSE\" Pack=\"true\" PackagePath=\"/\" />\n    <None Include=\"runtimes/**\" Pack=\"true\" PackagePath=\"runtimes/\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HtmlToMarkdownRs.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:fae32c4818bd3a813db7c2f376ddc8dbe8cf9b8626c5cb2d71e67da2b63c8455\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\n\nnamespace HtmlToMarkdown;\n\npublic static class HtmlToMarkdownRs\n{\n    private static readonly JsonSerializerOptions JsonOptions = new()\n    {\n        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n    };\n\n    /// <summary>\n    /// Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n    /// and warnings.\n    ///\n    /// # Arguments\n    ///\n    /// * `html` — the HTML string to convert.\n    /// * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n    ///   When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n    ///   attached via the `visitor` field on `ConversionOptions`.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use html_to_markdown_rs::convert;\n    ///\n    /// let html = \"&lt;h1&gt;Hello World&lt;/h1&gt;\";\n    /// let result = convert(html, None).unwrap();\n    /// assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n    /// </summary>\n    /// <param name=\"html\"></param>\n    /// <param name=\"options\">Optional.</param>\n    public static ConversionResult Convert(string html, ConversionOptions? options)\n    {\n        ArgumentNullException.ThrowIfNull(html);\n        var optionsJson = options != null ? JsonSerializer.Serialize(options, JsonOptions) : \"null\";\n        var optionsHandle = NativeMethods.ConversionOptionsFromJson(optionsJson);\n        // options-field bridge: attach visitor when present\n        if (options != null && options.Visitor != null)\n        {\n            using var _HtmlVisitorBridge = new HtmlVisitorBridge(options.Visitor);\n            NativeMethods.ConversionOptionsSetVisitor(optionsHandle, _HtmlVisitorBridge._vtable);\n        }\n        var nativeResult = NativeMethods.Convert(\n            html,\n            optionsHandle\n        );\n        if (nativeResult == IntPtr.Zero)\n        {\n            var err = GetLastError();\n            if (err.Code != 0)\n            {\n                throw err;\n            }\n        }\n        var jsonPtr = NativeMethods.ConversionResultToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.ConversionResultFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<ConversionResult>(json ?? \"null\", JsonOptions)!;\n        NativeMethods.ConversionOptionsFree(optionsHandle);\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Validate that the header level is within valid range (1-6).\n    ///\n    /// # Returns\n    ///\n    /// `true` if level is 1-6, `false` otherwise.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::HeaderMetadata;\n    /// let valid = HeaderMetadata {\n    ///     level: 3,\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(valid.is_valid());\n    ///\n    /// let invalid = HeaderMetadata {\n    ///     level: 7,  // Invalid\n    ///     text: \"Title\".to_string(),\n    ///     id: None,\n    ///     depth: 2,\n    ///     html_offset: 100,\n    /// };\n    /// assert!(!invalid.is_valid());\n    /// ```\n    /// </summary>\n    public static bool HeaderMetadataIsValid(IntPtr handle)\n    {\n        var nativeResult = NativeMethods.HeaderMetadataIsValid(\n            handle\n        );\n        var returnValue = nativeResult != 0;\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Classify a link based on href value.\n    ///\n    /// # Arguments\n    ///\n    /// * `href` - The href attribute value\n    ///\n    /// # Returns\n    ///\n    /// Appropriate [`LinkType`] based on protocol and content.\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n    /// assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n    /// assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n    /// assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n    /// assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n    /// ```\n    /// </summary>\n    /// <param name=\"href\"></param>\n    public static LinkType LinkMetadataClassifyLink(string href)\n    {\n        ArgumentNullException.ThrowIfNull(href);\n        var nativeResult = NativeMethods.LinkMetadataClassifyLink(\n            href\n        );\n        var json = Marshal.PtrToStringUTF8(nativeResult);\n        NativeMethods.FreeString(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<LinkType>(json ?? \"null\", JsonOptions)!;\n        return returnValue;\n    }\n\n    public static ConversionOptions ConversionOptionsDefault()\n    {\n        var nativeResult = NativeMethods.ConversionOptionsDefault();\n        var jsonPtr = NativeMethods.ConversionOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.ConversionOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<ConversionOptions>(json ?? \"null\", JsonOptions)!;\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Create a new builder with default values.\n    /// </summary>\n    public static ConversionOptionsBuilder ConversionOptionsBuilder()\n    {\n        var nativeResult = NativeMethods.ConversionOptionsBuilder();\n        var returnValue = new ConversionOptionsBuilder(nativeResult);\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Apply a partial update to these conversion options.\n    /// </summary>\n    /// <param name=\"update\"></param>\n    public static void ConversionOptionsApplyUpdate(IntPtr handle, ConversionOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.ConversionOptionsUpdateFromJson(updateJson);\n        NativeMethods.ConversionOptionsApplyUpdate(\n            handle,\n            updateHandle\n        );\n        NativeMethods.ConversionOptionsUpdateFree(updateHandle);\n    }\n\n    /// <summary>\n    /// Create from a partial update, applying to defaults.\n    /// </summary>\n    /// <param name=\"update\"></param>\n    public static ConversionOptions ConversionOptionsFromUpdate(ConversionOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.ConversionOptionsUpdateFromJson(updateJson);\n        var nativeResult = NativeMethods.ConversionOptionsFromUpdate(\n            updateHandle\n        );\n        var jsonPtr = NativeMethods.ConversionOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.ConversionOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<ConversionOptions>(json ?? \"null\", JsonOptions)!;\n        NativeMethods.ConversionOptionsUpdateFree(updateHandle);\n        return returnValue;\n    }\n\n    public static ConversionOptions ConversionOptionsFrom(ConversionOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.ConversionOptionsUpdateFromJson(updateJson);\n        var nativeResult = NativeMethods.ConversionOptionsFrom(\n            updateHandle\n        );\n        var jsonPtr = NativeMethods.ConversionOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.ConversionOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<ConversionOptions>(json ?? \"null\", JsonOptions)!;\n        NativeMethods.ConversionOptionsUpdateFree(updateHandle);\n        return returnValue;\n    }\n\n    public static PreprocessingOptions PreprocessingOptionsDefault()\n    {\n        var nativeResult = NativeMethods.PreprocessingOptionsDefault();\n        var jsonPtr = NativeMethods.PreprocessingOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.PreprocessingOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<PreprocessingOptions>(json ?? \"null\", JsonOptions)!;\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Apply a partial update to these preprocessing options.\n    ///\n    /// Any specified fields in the update will override the current values.\n    /// Unspecified fields (None) are left unchanged.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    /// </summary>\n    /// <param name=\"update\"></param>\n    public static void PreprocessingOptionsApplyUpdate(IntPtr handle, PreprocessingOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.PreprocessingOptionsUpdateFromJson(updateJson);\n        NativeMethods.PreprocessingOptionsApplyUpdate(\n            handle,\n            updateHandle\n        );\n        NativeMethods.PreprocessingOptionsUpdateFree(updateHandle);\n    }\n\n    /// <summary>\n    /// Create new preprocessing options from a partial update.\n    ///\n    /// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n    /// Fields not specified in the update keep their default values.\n    ///\n    /// # Arguments\n    ///\n    /// * `update` - Partial preprocessing options update\n    ///\n    /// # Returns\n    ///\n    /// New `PreprocessingOptions` with specified updates applied to defaults\n    /// </summary>\n    /// <param name=\"update\"></param>\n    public static PreprocessingOptions PreprocessingOptionsFromUpdate(PreprocessingOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.PreprocessingOptionsUpdateFromJson(updateJson);\n        var nativeResult = NativeMethods.PreprocessingOptionsFromUpdate(\n            updateHandle\n        );\n        var jsonPtr = NativeMethods.PreprocessingOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.PreprocessingOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<PreprocessingOptions>(json ?? \"null\", JsonOptions)!;\n        NativeMethods.PreprocessingOptionsUpdateFree(updateHandle);\n        return returnValue;\n    }\n\n    public static PreprocessingOptions PreprocessingOptionsFrom(PreprocessingOptionsUpdate update)\n    {\n        ArgumentNullException.ThrowIfNull(update);\n        var updateJson = JsonSerializer.Serialize(update, JsonOptions);\n        var updateHandle = NativeMethods.PreprocessingOptionsUpdateFromJson(updateJson);\n        var nativeResult = NativeMethods.PreprocessingOptionsFrom(\n            updateHandle\n        );\n        var jsonPtr = NativeMethods.PreprocessingOptionsToJson(nativeResult);\n        var json = Marshal.PtrToStringUTF8(jsonPtr);\n        NativeMethods.FreeString(jsonPtr);\n        NativeMethods.PreprocessingOptionsFree(nativeResult);\n        var returnValue = JsonSerializer.Deserialize<PreprocessingOptions>(json ?? \"null\", JsonOptions)!;\n        NativeMethods.PreprocessingOptionsUpdateFree(updateHandle);\n        return returnValue;\n    }\n\n    /// <summary>\n    /// Convert HTML to Markdown, invoking visitor callbacks during processing.\n    /// </summary>\n    public static ConversionResult? ConvertWithVisitor(string html, ConversionOptions? options, IVisitor visitor)\n    {\n        ArgumentNullException.ThrowIfNull(html);\n        ArgumentNullException.ThrowIfNull(visitor);\n\n        using var callbacks = new VisitorCallbacks(visitor);\n\n        var optionsHandle = IntPtr.Zero;\n        if (options != null)\n        {\n            var optionsJson = JsonSerializer.Serialize(options, JsonOptions);\n            optionsHandle = NativeMethods.ConversionOptionsFromJson(optionsJson);\n        }\n\n        var visitorHandle = NativeMethods.VisitorCreate(callbacks.NativePtr);\n        if (visitorHandle == IntPtr.Zero)\n        {\n            if (optionsHandle != IntPtr.Zero) NativeMethods.ConversionOptionsFree(optionsHandle);\n            throw GetLastError();\n        }\n\n        try\n        {\n            var resultPtr = NativeMethods.ConvertWithVisitor(html, optionsHandle, visitorHandle);\n            if (optionsHandle != IntPtr.Zero) NativeMethods.ConversionOptionsFree(optionsHandle);\n            if (resultPtr == IntPtr.Zero)\n            {\n                var err = GetLastError();\n                if (err.Code != 0) throw err;\n                return null;\n            }\n            var json = Marshal.PtrToStringAnsi(resultPtr);\n            NativeMethods.FreeString(resultPtr);\n            return JsonSerializer.Deserialize<ConversionResult>(json!, JsonOptions);\n        }\n        finally\n        {\n            NativeMethods.VisitorFree(visitorHandle);\n        }\n    }\n    private static HtmlToMarkdownRsException GetLastError()\n    {\n        var code = NativeMethods.LastErrorCode();\n        var ctxPtr = NativeMethods.LastErrorContext();\n        var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n        return new HtmlToMarkdownRsException(code, message);\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/HtmlToMarkdownRsException.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e85a24e0870eb948fa6b94e98a473e95cf3de039c21d0a16696200db0aaf28eb\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\npublic class HtmlToMarkdownRsException : Exception\n{\n    public int Code { get; }\n\n    public HtmlToMarkdownRsException(int code, string message) : base(message)\n    {\n        Code = code;\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/IVisitor.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:315e041e4f01c5f0f18237447fc2db0701202db6e18ad290fd0005ce5c9dd322\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>Visitor interface for the HTML-to-Markdown conversion pipeline.</summary>\npublic interface IVisitor\n{\n    /// <summary>Called for text nodes.</summary>\n    VisitResult VisitText(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called before entering any element.</summary>\n    VisitResult VisitElementStart(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called after exiting any element; receives the default markdown output.</summary>\n    VisitResult VisitElementEnd(NodeContext context, string output) => new VisitResult.Continue();\n    /// <summary>Called for anchor links. title is null when the attribute is absent.</summary>\n    VisitResult VisitLink(NodeContext context, string href, string text, string? title) => new VisitResult.Continue();\n    /// <summary>Called for images. title is null when absent.</summary>\n    VisitResult VisitImage(NodeContext context, string src, string alt, string? title) => new VisitResult.Continue();\n    /// <summary>Called for heading elements h1-h6. id is null when absent.</summary>\n    VisitResult VisitHeading(NodeContext context, uint level, string text, string? id) => new VisitResult.Continue();\n    /// <summary>Called for code blocks. lang is null when absent.</summary>\n    VisitResult VisitCodeBlock(NodeContext context, string? lang, string code) => new VisitResult.Continue();\n    /// <summary>Called for inline code elements.</summary>\n    VisitResult VisitCodeInline(NodeContext context, string code) => new VisitResult.Continue();\n    /// <summary>Called for list items.</summary>\n    VisitResult VisitListItem(NodeContext context, bool ordered, string marker, string text) => new VisitResult.Continue();\n    /// <summary>Called before processing a list.</summary>\n    VisitResult VisitListStart(NodeContext context, bool ordered) => new VisitResult.Continue();\n    /// <summary>Called after processing a list.</summary>\n    VisitResult VisitListEnd(NodeContext context, bool ordered, string output) => new VisitResult.Continue();\n    /// <summary>Called before processing a table.</summary>\n    VisitResult VisitTableStart(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called for table rows. cells contains the cell text values.</summary>\n    VisitResult VisitTableRow(NodeContext context, string[] cells, bool isHeader) => new VisitResult.Continue();\n    /// <summary>Called after processing a table.</summary>\n    VisitResult VisitTableEnd(NodeContext context, string output) => new VisitResult.Continue();\n    /// <summary>Called for blockquote elements.</summary>\n    VisitResult VisitBlockquote(NodeContext context, string content, ulong depth) => new VisitResult.Continue();\n    /// <summary>Called for strong/bold elements.</summary>\n    VisitResult VisitStrong(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for emphasis/italic elements.</summary>\n    VisitResult VisitEmphasis(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for strikethrough elements.</summary>\n    VisitResult VisitStrikethrough(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for underline elements.</summary>\n    VisitResult VisitUnderline(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for subscript elements.</summary>\n    VisitResult VisitSubscript(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for superscript elements.</summary>\n    VisitResult VisitSuperscript(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for mark/highlight elements.</summary>\n    VisitResult VisitMark(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for line break elements.</summary>\n    VisitResult VisitLineBreak(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called for horizontal rule elements.</summary>\n    VisitResult VisitHorizontalRule(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called for custom or unknown elements.</summary>\n    VisitResult VisitCustomElement(NodeContext context, string tagName, string html) => new VisitResult.Continue();\n    /// <summary>Called before a definition list.</summary>\n    VisitResult VisitDefinitionListStart(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called for definition term elements.</summary>\n    VisitResult VisitDefinitionTerm(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for definition description elements.</summary>\n    VisitResult VisitDefinitionDescription(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called after a definition list.</summary>\n    VisitResult VisitDefinitionListEnd(NodeContext context, string output) => new VisitResult.Continue();\n    /// <summary>Called for form elements. action and method may be null.</summary>\n    VisitResult VisitForm(NodeContext context, string? action, string? method) => new VisitResult.Continue();\n    /// <summary>Called for input elements. name and value may be null.</summary>\n    VisitResult VisitInput(NodeContext context, string inputType, string? name, string? value) => new VisitResult.Continue();\n    /// <summary>Called for button elements.</summary>\n    VisitResult VisitButton(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called for audio elements. src may be null.</summary>\n    VisitResult VisitAudio(NodeContext context, string? src) => new VisitResult.Continue();\n    /// <summary>Called for video elements. src may be null.</summary>\n    VisitResult VisitVideo(NodeContext context, string? src) => new VisitResult.Continue();\n    /// <summary>Called for iframe elements. src may be null.</summary>\n    VisitResult VisitIframe(NodeContext context, string? src) => new VisitResult.Continue();\n    /// <summary>Called for details elements.</summary>\n    VisitResult VisitDetails(NodeContext context, bool open) => new VisitResult.Continue();\n    /// <summary>Called for summary elements.</summary>\n    VisitResult VisitSummary(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called before a figure element.</summary>\n    VisitResult VisitFigureStart(NodeContext context) => new VisitResult.Continue();\n    /// <summary>Called for figcaption elements.</summary>\n    VisitResult VisitFigcaption(NodeContext context, string text) => new VisitResult.Continue();\n    /// <summary>Called after a figure element.</summary>\n    VisitResult VisitFigureEnd(NodeContext context, string output) => new VisitResult.Continue();\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ImageMetadata.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e373481429c04effce0d8cf8aef775960585f51da783642b66ed85d71b716ac5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Image metadata with source and dimensions.\n///\n/// Captures `<img>` elements and inline `<svg>` elements with metadata\n/// for image analysis and optimization.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n/// let img = ImageMetadata {\n///     src: \"https://example.com/image.jpg\".to_string(),\n///     alt: Some(\"An example image\".to_string()),\n///     title: Some(\"Example\".to_string()),\n///     dimensions: Some((800, 600)),\n///     image_type: ImageType::External,\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(img.image_type, ImageType::External);\n/// ```\n/// </summary>\npublic sealed class ImageMetadata\n{\n    /// <summary>\n    /// Image source (URL, data URI, or SVG content identifier)\n    /// </summary>\n    [JsonPropertyName(\"src\")]\n    public string Src { get; set; } = \"\";\n\n    /// <summary>\n    /// Alternative text from alt attribute (for accessibility)\n    /// </summary>\n    [JsonPropertyName(\"alt\")]\n    public string? Alt { get; set; } = null;\n\n    /// <summary>\n    /// Title attribute (often shown as tooltip)\n    /// </summary>\n    [JsonPropertyName(\"title\")]\n    public string? Title { get; set; } = null;\n\n    /// <summary>\n    /// Image dimensions as (width, height) if available\n    /// </summary>\n    [JsonPropertyName(\"dimensions\")]\n    public List<uint>? Dimensions { get; set; } = null;\n\n    /// <summary>\n    /// Image type classification\n    /// </summary>\n    [JsonPropertyName(\"image_type\")]\n    public ImageType ImageType { get; set; } = default!;\n\n    /// <summary>\n    /// Additional HTML attributes\n    /// </summary>\n    [JsonPropertyName(\"attributes\")]\n    public Dictionary<string, string> Attributes { get; set; } = default!;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ImageType.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6f955ce5ffee0c967a882b621654d6640648b98fab301cfa31842365e8fb56cc\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Image source classification for proper handling and processing.\n///\n/// Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n/// </summary>\npublic enum ImageType\n{\n    /// <summary>\n    /// Data URI embedded image (base64 or other encoding)\n    /// </summary>\n    [JsonPropertyName(\"datauri\")]\n    DataUri,\n    /// <summary>\n    /// Inline SVG element\n    /// </summary>\n    [JsonPropertyName(\"inlinesvg\")]\n    InlineSvg,\n    /// <summary>\n    /// External image URL (http/https)\n    /// </summary>\n    [JsonPropertyName(\"external\")]\n    External,\n    /// <summary>\n    /// Relative image path\n    /// </summary>\n    [JsonPropertyName(\"relative\")]\n    Relative,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/InvalidInputException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:fa4fc57affa885c6cb410e76f259ea1251641bf8ce33df63a4144139491c15b6\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Invalid input data\n/// </summary>\npublic class InvalidInputException : ConversionErrorException\n{\n    public InvalidInputException(string message) : base(message) { }\n\n    public InvalidInputException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/IoErrorException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:28ac7bbf70f4d9ba424deebc98942f1b9801ac36bc33f0aa5804b05265689d3c\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// I/O error\n/// </summary>\npublic class IoErrorException : ConversionErrorException\n{\n    public IoErrorException(string message) : base(message) { }\n\n    public IoErrorException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/LinkMetadata.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:31e6774e54f9b8225e24243da81adf3a8edef0abfad5ff2e25545c02415eaa27\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Hyperlink metadata with categorization and attributes.\n///\n/// Represents `<a>` elements with parsed href values, text content, and link type classification.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n/// let link = LinkMetadata {\n///     href: \"https://example.com\".to_string(),\n///     text: \"Example\".to_string(),\n///     title: Some(\"Visit Example\".to_string()),\n///     link_type: LinkType::External,\n///     rel: vec![\"nofollow\".to_string()],\n///     attributes: Default::default(),\n/// };\n///\n/// assert_eq!(link.link_type, LinkType::External);\n/// assert_eq!(link.text, \"Example\");\n/// ```\n/// </summary>\npublic sealed class LinkMetadata\n{\n    /// <summary>\n    /// The href URL value\n    /// </summary>\n    [JsonPropertyName(\"href\")]\n    public string Href { get; set; } = \"\";\n\n    /// <summary>\n    /// Link text content (normalized, concatenated if mixed with elements)\n    /// </summary>\n    [JsonPropertyName(\"text\")]\n    public string Text { get; set; } = \"\";\n\n    /// <summary>\n    /// Optional title attribute (often shown as tooltip)\n    /// </summary>\n    [JsonPropertyName(\"title\")]\n    public string? Title { get; set; } = null;\n\n    /// <summary>\n    /// Link type classification\n    /// </summary>\n    [JsonPropertyName(\"link_type\")]\n    public LinkType LinkType { get; set; } = default!;\n\n    /// <summary>\n    /// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n    /// </summary>\n    [JsonPropertyName(\"rel\")]\n    public List<string> Rel { get; set; } = [];\n\n    /// <summary>\n    /// Additional HTML attributes\n    /// </summary>\n    [JsonPropertyName(\"attributes\")]\n    public Dictionary<string, string> Attributes { get; set; } = default!;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/LinkStyle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:88a7cbb3f7c4d1f478d76f77607340f41cc17afad05242f533aab147fae8c142\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Link rendering style in Markdown output.\n///\n/// Controls whether links and images use inline `[text](url)` syntax or\n/// reference-style `[text][1]` syntax with definitions collected at the end.\n/// </summary>\npublic enum LinkStyle\n{\n    /// <summary>\n    /// Inline links: `[text](url)`. Default.\n    /// </summary>\n    [JsonPropertyName(\"inline\")]\n    Inline,\n    /// <summary>\n    /// Reference-style links: `[text][1]` with `[1]: url` at end of document.\n    /// </summary>\n    [JsonPropertyName(\"reference\")]\n    Reference,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/LinkType.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:27a4b70f697e7f06f2b85fd69eaa1c2b5b8bcdf767151d6a519b434758f7e307\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Link classification based on href value and document context.\n///\n/// Used to categorize links during extraction for filtering and analysis.\n/// </summary>\npublic enum LinkType\n{\n    /// <summary>\n    /// Anchor link within same document (href starts with #)\n    /// </summary>\n    [JsonPropertyName(\"anchor\")]\n    Anchor,\n    /// <summary>\n    /// Internal link within same domain\n    /// </summary>\n    [JsonPropertyName(\"internal\")]\n    Internal,\n    /// <summary>\n    /// External link to different domain\n    /// </summary>\n    [JsonPropertyName(\"external\")]\n    External,\n    /// <summary>\n    /// Email link (mailto:)\n    /// </summary>\n    [JsonPropertyName(\"email\")]\n    Email,\n    /// <summary>\n    /// Phone link (tel:)\n    /// </summary>\n    [JsonPropertyName(\"phone\")]\n    Phone,\n    /// <summary>\n    /// Other protocol or unclassifiable\n    /// </summary>\n    [JsonPropertyName(\"other\")]\n    Other,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ListIndentType.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c13f602a1321efa8b1fd612e3b3d86ad7cd89c59cdb051eca8cd6d1fd0f4ebd9\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// List indentation character type.\n///\n/// Controls whether list items are indented with spaces or tabs.\n/// </summary>\npublic enum ListIndentType\n{\n    /// <summary>\n    /// Use spaces for indentation. Default. Width controlled by `list_indent_width`.\n    /// </summary>\n    [JsonPropertyName(\"spaces\")]\n    Spaces,\n    /// <summary>\n    /// Use tabs for indentation.\n    /// </summary>\n    [JsonPropertyName(\"tabs\")]\n    Tabs,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/NativeMethods.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:55535e8857e7a3ca0434a75775c901107493f318661e880c86a88312610b14db\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Runtime.InteropServices;\n\nnamespace HtmlToMarkdown;\n\ninternal static partial class NativeMethods\n{\n    private const string LibName = \"html_to_markdown_ffi\";\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_from_json\")]\n    internal static extern IntPtr ConversionOptionsFromJson([MarshalAs(UnmanagedType.LPStr)] string json);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_free\")]\n    internal static extern void ConversionOptionsFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_update_from_json\")]\n    internal static extern IntPtr ConversionOptionsUpdateFromJson([MarshalAs(UnmanagedType.LPStr)] string json);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_update_free\")]\n    internal static extern void ConversionOptionsUpdateFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_from_json\")]\n    internal static extern IntPtr PreprocessingOptionsFromJson([MarshalAs(UnmanagedType.LPStr)] string json);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_free\")]\n    internal static extern void PreprocessingOptionsFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_update_from_json\")]\n    internal static extern IntPtr PreprocessingOptionsUpdateFromJson([MarshalAs(UnmanagedType.LPStr)] string json);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_update_free\")]\n    internal static extern void PreprocessingOptionsUpdateFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_visitor_handle_free\")]\n    internal static extern void VisitorHandleFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_to_json\")]\n    internal static extern IntPtr ConversionOptionsToJson(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_free\")]\n    internal static extern void ConversionOptionsBuilderFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_result_to_json\")]\n    internal static extern IntPtr ConversionResultToJson(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_result_free\")]\n    internal static extern void ConversionResultFree(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_to_json\")]\n    internal static extern IntPtr PreprocessingOptionsToJson(IntPtr ptr);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_convert\")]\n    internal static extern IntPtr Convert(\n        [MarshalAs(UnmanagedType.LPStr)] string html,\n        IntPtr options\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_header_metadata_is_valid\")]\n    internal static extern int HeaderMetadataIsValid(\n        IntPtr handle\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_link_metadata_classify_link\")]\n    internal static extern IntPtr LinkMetadataClassifyLink(\n        [MarshalAs(UnmanagedType.LPStr)] string href\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_default\")]\n    internal static extern IntPtr ConversionOptionsDefault();\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder\")]\n    internal static extern IntPtr ConversionOptionsBuilder();\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_apply_update\")]\n    internal static extern void ConversionOptionsApplyUpdate(\n        IntPtr handle,\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_from_update\")]\n    internal static extern IntPtr ConversionOptionsFromUpdate(\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_from\")]\n    internal static extern IntPtr ConversionOptionsFrom(\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_strip_tags\")]\n    internal static extern IntPtr ConversionOptionsBuilderStripTags(\n        IntPtr handle,\n        IntPtr tags\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_preserve_tags\")]\n    internal static extern IntPtr ConversionOptionsBuilderPreserveTags(\n        IntPtr handle,\n        IntPtr tags\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_keep_inline_images_in\")]\n    internal static extern IntPtr ConversionOptionsBuilderKeepInlineImagesIn(\n        IntPtr handle,\n        IntPtr tags\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_exclude_selectors\")]\n    internal static extern IntPtr ConversionOptionsBuilderExcludeSelectors(\n        IntPtr handle,\n        IntPtr selectors\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_visitor\")]\n    internal static extern IntPtr ConversionOptionsBuilderVisitor(\n        IntPtr handle,\n        IntPtr visitor\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_preprocessing\")]\n    internal static extern IntPtr ConversionOptionsBuilderPreprocessing(\n        IntPtr handle,\n        IntPtr preprocessing\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_builder_build\")]\n    internal static extern IntPtr ConversionOptionsBuilderBuild(\n        IntPtr handle\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_default\")]\n    internal static extern IntPtr PreprocessingOptionsDefault();\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_apply_update\")]\n    internal static extern void PreprocessingOptionsApplyUpdate(\n        IntPtr handle,\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_from_update\")]\n    internal static extern IntPtr PreprocessingOptionsFromUpdate(\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_preprocessing_options_from\")]\n    internal static extern IntPtr PreprocessingOptionsFrom(\n        IntPtr update\n    );\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_last_error_code\")]\n    internal static extern int LastErrorCode();\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_last_error_context\")]\n    internal static extern IntPtr LastErrorContext();\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_free_string\")]\n    internal static extern void FreeString(IntPtr ptr);\n\n\n    // Visitor FFI\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_visitor_create\")]\n    internal static extern IntPtr VisitorCreate(IntPtr callbacks);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_visitor_free\")]\n    internal static extern void VisitorFree(IntPtr visitor);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_convert_with_visitor\")]\n    internal static extern IntPtr ConvertWithVisitor([MarshalAs(UnmanagedType.LPStr)] string html, IntPtr options, IntPtr visitor);\n\n\n    // Trait Bridge FFI\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_register_html_visitor\")]\n    internal static extern int RegisterHtmlVisitor([MarshalAs(UnmanagedType.LPUTF8Str)] string name, IntPtr vtable, IntPtr userData, out IntPtr outError);\n\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_unregister_html_visitor\")]\n    internal static extern int UnregisterHtmlVisitor([MarshalAs(UnmanagedType.LPUTF8Str)] string name, out IntPtr outError);\n\n    // Options-field bridge setter\n    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"htm_conversion_options_set_visitor\")]\n    internal static extern void ConversionOptionsSetVisitor(IntPtr options, IntPtr vtable);\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/NewlineStyle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d68e2e733f58a4bd19cde72fcd4b318120e804a1cc5ba8a73b322fb2744fc0e9\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Line break syntax in Markdown output.\n///\n/// Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n/// </summary>\npublic enum NewlineStyle\n{\n    /// <summary>\n    /// Two trailing spaces at end of line. Default. Standard Markdown syntax.\n    /// </summary>\n    [JsonPropertyName(\"spaces\")]\n    Spaces,\n    /// <summary>\n    /// Backslash at end of line. Alternative Markdown syntax.\n    /// </summary>\n    [JsonPropertyName(\"backslash\")]\n    Backslash,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/NodeContent.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:30fbd357e0f2bb22be8d25bb0519b7d339f25ee0835b58156aef73db42f51ce5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// The semantic content type of a document node.\n///\n/// Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n/// </summary>\n[JsonConverter(typeof(NodeContentJsonConverter))]\npublic abstract record NodeContent\n{\n    /// <summary>\n    /// A heading element (h1-h6).\n    /// </summary>\n    public sealed record Heading(\n        [property: JsonPropertyName(\"level\")] byte Level,\n        [property: JsonPropertyName(\"text\")] string Text\n    ) : NodeContent;\n\n    /// <summary>\n    /// A paragraph of text.\n    /// </summary>\n    public sealed record Paragraph(\n        [property: JsonPropertyName(\"text\")] string Text\n    ) : NodeContent;\n\n    /// <summary>\n    /// A list container (ordered or unordered). Children are `ListItem` nodes.\n    /// </summary>\n    public sealed record List(\n        [property: JsonPropertyName(\"ordered\")] bool Ordered\n    ) : NodeContent;\n\n    /// <summary>\n    /// A single list item.\n    /// </summary>\n    public sealed record ListItem(\n        [property: JsonPropertyName(\"text\")] string Text\n    ) : NodeContent;\n\n    /// <summary>\n    /// A table with structured cell data.\n    /// </summary>\n    public sealed record Table(\n        [property: JsonPropertyName(\"grid\")] TableGrid Grid\n    ) : NodeContent;\n\n    /// <summary>\n    /// An image element.\n    /// </summary>\n    public sealed record Image(\n        [property: JsonPropertyName(\"description\")] string? Description,\n        [property: JsonPropertyName(\"src\")] string? Src,\n        [property: JsonPropertyName(\"image_index\")] uint? ImageIndex\n    ) : NodeContent;\n\n    /// <summary>\n    /// A code block or inline code.\n    /// </summary>\n    public sealed record Code(\n        [property: JsonPropertyName(\"text\")] string Text,\n        [property: JsonPropertyName(\"language\")] string? Language\n    ) : NodeContent;\n\n    /// <summary>\n    /// A block quote container.\n    /// </summary>\n    public sealed record Quote() : NodeContent;\n\n    /// <summary>\n    /// A definition list container.\n    /// </summary>\n    public sealed record DefinitionList() : NodeContent;\n\n    /// <summary>\n    /// A definition list entry with term and description.\n    /// </summary>\n    public sealed record DefinitionItem(\n        [property: JsonPropertyName(\"term\")] string Term,\n        [property: JsonPropertyName(\"definition\")] string Definition\n    ) : NodeContent;\n\n    /// <summary>\n    /// A raw block preserved as-is (e.g. `<script>`, `<style>` content).\n    /// </summary>\n    public sealed record RawBlock(\n        [property: JsonPropertyName(\"format\")] string Format,\n        [property: JsonPropertyName(\"content\")] string Content\n    ) : NodeContent;\n\n    /// <summary>\n    /// A block of key-value metadata pairs (from `<head>` meta tags).\n    /// </summary>\n    public sealed record MetadataBlock(\n        [property: JsonPropertyName(\"entries\")] List<string> Entries\n    ) : NodeContent;\n\n    /// <summary>\n    /// A section grouping container (auto-generated from heading hierarchy).\n    /// </summary>\n    public sealed record Group(\n        [property: JsonPropertyName(\"label\")] string? Label,\n        [property: JsonPropertyName(\"heading_level\")] byte? HeadingLevel,\n        [property: JsonPropertyName(\"heading_text\")] string? HeadingText\n    ) : NodeContent;\n\n}\n\n/// <summary>Custom JSON converter for <see cref=\"NodeContent\"/> that reads the \"node_type\" discriminator from any position.</summary>\ninternal sealed class NodeContentJsonConverter : JsonConverter<NodeContent>\n{\n    public override NodeContent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        using var doc = JsonDocument.ParseValue(ref reader);\n        var root = doc.RootElement;\n        if (!root.TryGetProperty(\"node_type\", out var tagEl))\n            throw new JsonException(\"NodeContent: missing \\\"node_type\\\" discriminator\");\n        var tag = tagEl.GetString();\n        var json = root.GetRawText();\n        return tag switch\n        {\n            \"heading\" => JsonSerializer.Deserialize<NodeContent.Heading>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Heading\"),\n            \"paragraph\" => JsonSerializer.Deserialize<NodeContent.Paragraph>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Paragraph\"),\n            \"list\" => JsonSerializer.Deserialize<NodeContent.List>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.List\"),\n            \"listitem\" => JsonSerializer.Deserialize<NodeContent.ListItem>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.ListItem\"),\n            \"table\" => JsonSerializer.Deserialize<NodeContent.Table>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Table\"),\n            \"image\" => JsonSerializer.Deserialize<NodeContent.Image>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Image\"),\n            \"code\" => JsonSerializer.Deserialize<NodeContent.Code>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Code\"),\n            \"quote\" => JsonSerializer.Deserialize<NodeContent.Quote>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Quote\"),\n            \"definitionlist\" => JsonSerializer.Deserialize<NodeContent.DefinitionList>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.DefinitionList\"),\n            \"definitionitem\" => JsonSerializer.Deserialize<NodeContent.DefinitionItem>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.DefinitionItem\"),\n            \"rawblock\" => JsonSerializer.Deserialize<NodeContent.RawBlock>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.RawBlock\"),\n            \"metadatablock\" => JsonSerializer.Deserialize<NodeContent.MetadataBlock>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.MetadataBlock\"),\n            \"group\" => JsonSerializer.Deserialize<NodeContent.Group>(json, options)!\n                ?? throw new JsonException(\"Failed to deserialize NodeContent.Group\"),\n            _ => throw new JsonException($\"Unknown NodeContent discriminator: {tag}\")\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, NodeContent value, JsonSerializerOptions options)\n    {\n        // Serialize the concrete type, then inject the discriminator\n        switch (value)\n        {\n            case NodeContent.Heading v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"heading\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Paragraph v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"paragraph\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.List v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"list\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.ListItem v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"listitem\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Table v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"table\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Image v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"image\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Code v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"code\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Quote v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"quote\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.DefinitionList v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"definitionlist\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.DefinitionItem v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"definitionitem\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.RawBlock v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"rawblock\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.MetadataBlock v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"metadatablock\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            case NodeContent.Group v:\n                {\n                    var doc = JsonSerializer.SerializeToDocument(v, options);\n                    writer.WriteStartObject();\n                    writer.WriteString(\"node_type\", \"group\");\n                    foreach (var prop in doc.RootElement.EnumerateObject())\n                        if (prop.Name != \"node_type\") prop.WriteTo(writer);\n                    writer.WriteEndObject();\n                    break;\n                }\n            default: throw new JsonException($\"Unknown NodeContent subtype: {value.GetType().Name}\");\n        }\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/NodeContext.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c4178da3a800ac9325894f5b8e2ea02444f6b932c22d378a336c0a162e4c5984\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>Context passed to every visitor callback.</summary>\npublic record NodeContext(\n    /// <summary>Coarse-grained node type tag.</summary>\n    int NodeType,\n    /// <summary>HTML element tag name (e.g. \"div\").</summary>\n    string TagName,\n    /// <summary>DOM depth (0 = root).</summary>\n    ulong Depth,\n    /// <summary>0-based sibling index.</summary>\n    ulong IndexInParent,\n    /// <summary>Parent element tag name, or null at the root.</summary>\n    string? ParentTag,\n    /// <summary>True when this element is treated as inline.</summary>\n    bool IsInline\n);\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/NodeType.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a337875faacb32e52f65aac9eb19387331c4c2c67320b510d0fc225689d433d4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Node type enumeration covering all HTML element types.\n///\n/// This enum categorizes all HTML elements that the converter recognizes,\n/// providing a coarse-grained classification for visitor dispatch.\n/// </summary>\npublic enum NodeType\n{\n    /// <summary>\n    /// Text node (most frequent - 100+ per document)\n    /// </summary>\n    [JsonPropertyName(\"text\")]\n    Text,\n    /// <summary>\n    /// Generic element node\n    /// </summary>\n    [JsonPropertyName(\"element\")]\n    Element,\n    /// <summary>\n    /// Heading elements (h1-h6)\n    /// </summary>\n    [JsonPropertyName(\"heading\")]\n    Heading,\n    /// <summary>\n    /// Paragraph element\n    /// </summary>\n    [JsonPropertyName(\"paragraph\")]\n    Paragraph,\n    /// <summary>\n    /// Generic div container\n    /// </summary>\n    [JsonPropertyName(\"div\")]\n    Div,\n    /// <summary>\n    /// Blockquote element\n    /// </summary>\n    [JsonPropertyName(\"blockquote\")]\n    Blockquote,\n    /// <summary>\n    /// Preformatted text block\n    /// </summary>\n    [JsonPropertyName(\"pre\")]\n    Pre,\n    /// <summary>\n    /// Horizontal rule\n    /// </summary>\n    [JsonPropertyName(\"hr\")]\n    Hr,\n    /// <summary>\n    /// Ordered or unordered list (ul, ol)\n    /// </summary>\n    [JsonPropertyName(\"list\")]\n    List,\n    /// <summary>\n    /// List item (li)\n    /// </summary>\n    [JsonPropertyName(\"listitem\")]\n    ListItem,\n    /// <summary>\n    /// Definition list (dl)\n    /// </summary>\n    [JsonPropertyName(\"definitionlist\")]\n    DefinitionList,\n    /// <summary>\n    /// Definition term (dt)\n    /// </summary>\n    [JsonPropertyName(\"definitionterm\")]\n    DefinitionTerm,\n    /// <summary>\n    /// Definition description (dd)\n    /// </summary>\n    [JsonPropertyName(\"definitiondescription\")]\n    DefinitionDescription,\n    /// <summary>\n    /// Table element\n    /// </summary>\n    [JsonPropertyName(\"table\")]\n    Table,\n    /// <summary>\n    /// Table row (tr)\n    /// </summary>\n    [JsonPropertyName(\"tablerow\")]\n    TableRow,\n    /// <summary>\n    /// Table cell (td, th)\n    /// </summary>\n    [JsonPropertyName(\"tablecell\")]\n    TableCell,\n    /// <summary>\n    /// Table header cell (th)\n    /// </summary>\n    [JsonPropertyName(\"tableheader\")]\n    TableHeader,\n    /// <summary>\n    /// Table body (tbody)\n    /// </summary>\n    [JsonPropertyName(\"tablebody\")]\n    TableBody,\n    /// <summary>\n    /// Table head (thead)\n    /// </summary>\n    [JsonPropertyName(\"tablehead\")]\n    TableHead,\n    /// <summary>\n    /// Table foot (tfoot)\n    /// </summary>\n    [JsonPropertyName(\"tablefoot\")]\n    TableFoot,\n    /// <summary>\n    /// Anchor link (a)\n    /// </summary>\n    [JsonPropertyName(\"link\")]\n    Link,\n    /// <summary>\n    /// Image (img)\n    /// </summary>\n    [JsonPropertyName(\"image\")]\n    Image,\n    /// <summary>\n    /// Strong/bold (strong, b)\n    /// </summary>\n    [JsonPropertyName(\"strong\")]\n    Strong,\n    /// <summary>\n    /// Emphasis/italic (em, i)\n    /// </summary>\n    [JsonPropertyName(\"em\")]\n    Em,\n    /// <summary>\n    /// Inline code (code)\n    /// </summary>\n    [JsonPropertyName(\"code\")]\n    Code,\n    /// <summary>\n    /// Strikethrough (s, del, strike)\n    /// </summary>\n    [JsonPropertyName(\"strikethrough\")]\n    Strikethrough,\n    /// <summary>\n    /// Underline (u, ins)\n    /// </summary>\n    [JsonPropertyName(\"underline\")]\n    Underline,\n    /// <summary>\n    /// Subscript (sub)\n    /// </summary>\n    [JsonPropertyName(\"subscript\")]\n    Subscript,\n    /// <summary>\n    /// Superscript (sup)\n    /// </summary>\n    [JsonPropertyName(\"superscript\")]\n    Superscript,\n    /// <summary>\n    /// Mark/highlight (mark)\n    /// </summary>\n    [JsonPropertyName(\"mark\")]\n    Mark,\n    /// <summary>\n    /// Small text (small)\n    /// </summary>\n    [JsonPropertyName(\"small\")]\n    Small,\n    /// <summary>\n    /// Line break (br)\n    /// </summary>\n    [JsonPropertyName(\"br\")]\n    Br,\n    /// <summary>\n    /// Span element\n    /// </summary>\n    [JsonPropertyName(\"span\")]\n    Span,\n    /// <summary>\n    /// Article element\n    /// </summary>\n    [JsonPropertyName(\"article\")]\n    Article,\n    /// <summary>\n    /// Section element\n    /// </summary>\n    [JsonPropertyName(\"section\")]\n    Section,\n    /// <summary>\n    /// Navigation element\n    /// </summary>\n    [JsonPropertyName(\"nav\")]\n    Nav,\n    /// <summary>\n    /// Aside element\n    /// </summary>\n    [JsonPropertyName(\"aside\")]\n    Aside,\n    /// <summary>\n    /// Header element\n    /// </summary>\n    [JsonPropertyName(\"header\")]\n    Header,\n    /// <summary>\n    /// Footer element\n    /// </summary>\n    [JsonPropertyName(\"footer\")]\n    Footer,\n    /// <summary>\n    /// Main element\n    /// </summary>\n    [JsonPropertyName(\"main\")]\n    Main,\n    /// <summary>\n    /// Figure element\n    /// </summary>\n    [JsonPropertyName(\"figure\")]\n    Figure,\n    /// <summary>\n    /// Figure caption\n    /// </summary>\n    [JsonPropertyName(\"figcaption\")]\n    Figcaption,\n    /// <summary>\n    /// Time element\n    /// </summary>\n    [JsonPropertyName(\"time\")]\n    Time,\n    /// <summary>\n    /// Details element\n    /// </summary>\n    [JsonPropertyName(\"details\")]\n    Details,\n    /// <summary>\n    /// Summary element\n    /// </summary>\n    [JsonPropertyName(\"summary\")]\n    Summary,\n    /// <summary>\n    /// Form element\n    /// </summary>\n    [JsonPropertyName(\"form\")]\n    Form,\n    /// <summary>\n    /// Input element\n    /// </summary>\n    [JsonPropertyName(\"input\")]\n    Input,\n    /// <summary>\n    /// Select element\n    /// </summary>\n    [JsonPropertyName(\"select\")]\n    Select,\n    /// <summary>\n    /// Option element\n    /// </summary>\n    [JsonPropertyName(\"option\")]\n    Option,\n    /// <summary>\n    /// Button element\n    /// </summary>\n    [JsonPropertyName(\"button\")]\n    Button,\n    /// <summary>\n    /// Textarea element\n    /// </summary>\n    [JsonPropertyName(\"textarea\")]\n    Textarea,\n    /// <summary>\n    /// Label element\n    /// </summary>\n    [JsonPropertyName(\"label\")]\n    Label,\n    /// <summary>\n    /// Fieldset element\n    /// </summary>\n    [JsonPropertyName(\"fieldset\")]\n    Fieldset,\n    /// <summary>\n    /// Legend element\n    /// </summary>\n    [JsonPropertyName(\"legend\")]\n    Legend,\n    /// <summary>\n    /// Audio element\n    /// </summary>\n    [JsonPropertyName(\"audio\")]\n    Audio,\n    /// <summary>\n    /// Video element\n    /// </summary>\n    [JsonPropertyName(\"video\")]\n    Video,\n    /// <summary>\n    /// Picture element\n    /// </summary>\n    [JsonPropertyName(\"picture\")]\n    Picture,\n    /// <summary>\n    /// Source element\n    /// </summary>\n    [JsonPropertyName(\"source\")]\n    Source,\n    /// <summary>\n    /// Iframe element\n    /// </summary>\n    [JsonPropertyName(\"iframe\")]\n    Iframe,\n    /// <summary>\n    /// SVG element\n    /// </summary>\n    [JsonPropertyName(\"svg\")]\n    Svg,\n    /// <summary>\n    /// Canvas element\n    /// </summary>\n    [JsonPropertyName(\"canvas\")]\n    Canvas,\n    /// <summary>\n    /// Ruby annotation\n    /// </summary>\n    [JsonPropertyName(\"ruby\")]\n    Ruby,\n    /// <summary>\n    /// Ruby text\n    /// </summary>\n    [JsonPropertyName(\"rt\")]\n    Rt,\n    /// <summary>\n    /// Ruby parenthesis\n    /// </summary>\n    [JsonPropertyName(\"rp\")]\n    Rp,\n    /// <summary>\n    /// Abbreviation\n    /// </summary>\n    [JsonPropertyName(\"abbr\")]\n    Abbr,\n    /// <summary>\n    /// Keyboard input\n    /// </summary>\n    [JsonPropertyName(\"kbd\")]\n    Kbd,\n    /// <summary>\n    /// Sample output\n    /// </summary>\n    [JsonPropertyName(\"samp\")]\n    Samp,\n    /// <summary>\n    /// Variable\n    /// </summary>\n    [JsonPropertyName(\"var\")]\n    Var,\n    /// <summary>\n    /// Citation\n    /// </summary>\n    [JsonPropertyName(\"cite\")]\n    Cite,\n    /// <summary>\n    /// Quote\n    /// </summary>\n    [JsonPropertyName(\"q\")]\n    Q,\n    /// <summary>\n    /// Deleted text\n    /// </summary>\n    [JsonPropertyName(\"del\")]\n    Del,\n    /// <summary>\n    /// Inserted text\n    /// </summary>\n    [JsonPropertyName(\"ins\")]\n    Ins,\n    /// <summary>\n    /// Data element\n    /// </summary>\n    [JsonPropertyName(\"data\")]\n    Data,\n    /// <summary>\n    /// Meter element\n    /// </summary>\n    [JsonPropertyName(\"meter\")]\n    Meter,\n    /// <summary>\n    /// Progress element\n    /// </summary>\n    [JsonPropertyName(\"progress\")]\n    Progress,\n    /// <summary>\n    /// Output element\n    /// </summary>\n    [JsonPropertyName(\"output\")]\n    Output,\n    /// <summary>\n    /// Template element\n    /// </summary>\n    [JsonPropertyName(\"template\")]\n    Template,\n    /// <summary>\n    /// Slot element\n    /// </summary>\n    [JsonPropertyName(\"slot\")]\n    Slot,\n    /// <summary>\n    /// HTML root element\n    /// </summary>\n    [JsonPropertyName(\"html\")]\n    Html,\n    /// <summary>\n    /// Head element\n    /// </summary>\n    [JsonPropertyName(\"head\")]\n    Head,\n    /// <summary>\n    /// Body element\n    /// </summary>\n    [JsonPropertyName(\"body\")]\n    Body,\n    /// <summary>\n    /// Title element\n    /// </summary>\n    [JsonPropertyName(\"title\")]\n    Title,\n    /// <summary>\n    /// Meta element\n    /// </summary>\n    [JsonPropertyName(\"meta\")]\n    Meta,\n    /// <summary>\n    /// Link element (not anchor)\n    /// </summary>\n    [JsonPropertyName(\"linktag\")]\n    LinkTag,\n    /// <summary>\n    /// Style element\n    /// </summary>\n    [JsonPropertyName(\"style\")]\n    Style,\n    /// <summary>\n    /// Script element\n    /// </summary>\n    [JsonPropertyName(\"script\")]\n    Script,\n    /// <summary>\n    /// Base element\n    /// </summary>\n    [JsonPropertyName(\"base\")]\n    Base,\n    /// <summary>\n    /// Custom element (web components) or unknown tag\n    /// </summary>\n    [JsonPropertyName(\"custom\")]\n    Custom,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/OtherException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:78b1bdf0ebfe69b185c36221bac94b311059591c2b206eb6c860e2ce7afb47cf\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Generic conversion error\n/// </summary>\npublic class OtherException : ConversionErrorException\n{\n    public OtherException(string message) : base(message) { }\n\n    public OtherException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/OutputFormat.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e30780fb82682d69df2d3291a3bba1fe755b8e193163e71e7070590944366933\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Output format for conversion.\n///\n/// Specifies the target markup language format for the conversion output.\n/// </summary>\npublic enum OutputFormat\n{\n    /// <summary>\n    /// Standard Markdown (CommonMark compatible). Default.\n    /// </summary>\n    [JsonPropertyName(\"markdown\")]\n    Markdown,\n    /// <summary>\n    /// Djot lightweight markup language.\n    /// </summary>\n    [JsonPropertyName(\"djot\")]\n    Djot,\n    /// <summary>\n    /// Plain text output (no markup, visible text only).\n    /// </summary>\n    [JsonPropertyName(\"plain\")]\n    Plain,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/PanicException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:a0e8c368659cc455c37d44bdc02398b7bd02ca5e289fae30cb8c79715de21fcb\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Panic caught during conversion to prevent unwinding across FFI boundaries\n/// </summary>\npublic class PanicException : ConversionErrorException\n{\n    public PanicException(string message) : base(message) { }\n\n    public PanicException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ParseErrorException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:92ed8383046a7b217a37a9bfacff3339dd1cd64db3f1abba4d1345b74b00059d\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// HTML parsing error\n/// </summary>\npublic class ParseErrorException : ConversionErrorException\n{\n    public ParseErrorException(string message) : base(message) { }\n\n    public ParseErrorException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/PreprocessingOptions.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:3cd4f68a41ad9ee2a05d6635ff9359b9668630d43626502aaa2a814680aad990\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// HTML preprocessing options for document cleanup before conversion.\n/// </summary>\npublic sealed class PreprocessingOptions\n{\n    /// <summary>\n    /// Enable HTML preprocessing globally\n    /// </summary>\n    [JsonPropertyName(\"enabled\")]\n    public bool Enabled { get; set; } = true;\n\n    /// <summary>\n    /// Preprocessing preset level (Minimal, Standard, Aggressive)\n    /// </summary>\n    [JsonPropertyName(\"preset\")]\n    public PreprocessingPreset? Preset { get; set; } = null;\n\n    /// <summary>\n    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n    /// </summary>\n    [JsonPropertyName(\"remove_navigation\")]\n    public bool RemoveNavigation { get; set; } = true;\n\n    /// <summary>\n    /// Remove form elements (forms, inputs, buttons, etc.)\n    /// </summary>\n    [JsonPropertyName(\"remove_forms\")]\n    public bool RemoveForms { get; set; } = true;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/PreprocessingOptionsUpdate.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2d4fc51e84a921c821892bea2e13db959ea6bbf238dd3b03947797522d708776\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Partial update for `PreprocessingOptions`.\n///\n/// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n/// Only specified fields (Some values) will override existing options; None values leave the\n/// corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n/// </summary>\npublic sealed class PreprocessingOptionsUpdate\n{\n    /// <summary>\n    /// Optional global preprocessing enablement override\n    /// </summary>\n    [JsonPropertyName(\"enabled\")]\n    public bool? Enabled { get; set; } = null;\n\n    /// <summary>\n    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n    /// </summary>\n    [JsonPropertyName(\"preset\")]\n    public PreprocessingPreset? Preset { get; set; } = null;\n\n    /// <summary>\n    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n    /// </summary>\n    [JsonPropertyName(\"remove_navigation\")]\n    public bool? RemoveNavigation { get; set; } = null;\n\n    /// <summary>\n    /// Optional form element removal override (forms, inputs, buttons, etc.)\n    /// </summary>\n    [JsonPropertyName(\"remove_forms\")]\n    public bool? RemoveForms { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/PreprocessingPreset.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0bfcba03723b0bcd721fc2225632e8e9f09dfaa94ba4dff92c43f2f1f1e4cdfe\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// HTML preprocessing aggressiveness level.\n///\n/// Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n/// </summary>\npublic enum PreprocessingPreset\n{\n    /// <summary>\n    /// Minimal cleanup. Remove only essential noise (scripts, styles).\n    /// </summary>\n    [JsonPropertyName(\"minimal\")]\n    Minimal,\n    /// <summary>\n    /// Standard cleanup. Default. Removes navigation, forms, and other auxiliary content.\n    /// </summary>\n    [JsonPropertyName(\"standard\")]\n    Standard,\n    /// <summary>\n    /// Aggressive cleanup. Remove extensive non-content elements and structure.\n    /// </summary>\n    [JsonPropertyName(\"aggressive\")]\n    Aggressive,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/ProcessingWarning.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:67670539b6020d511265119606244e0cf006c4859e198232d4bc2b73d618b557\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A non-fatal warning generated during HTML processing.\n/// </summary>\npublic sealed class ProcessingWarning\n{\n    /// <summary>\n    /// Human-readable warning message.\n    /// </summary>\n    [JsonPropertyName(\"message\")]\n    public string Message { get; set; } = \"\";\n\n    /// <summary>\n    /// The category of warning.\n    /// </summary>\n    [JsonPropertyName(\"kind\")]\n    public WarningKind Kind { get; set; } = default!;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/SanitizationErrorException.cs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:ad51365c3944481194921af9183a71068bbc847bd25d7fbbf279f0ca079c0b78\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// HTML sanitization error\n/// </summary>\npublic class SanitizationErrorException : ConversionErrorException\n{\n    public SanitizationErrorException(string message) : base(message) { }\n\n    public SanitizationErrorException(string message, Exception innerException) : base(message, innerException) { }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/StructuredData.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a890700f3ddeb327ba1e532b476fada3b5dcc44d489a7eb76c7ec5d0e953871f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Structured data block (JSON-LD, Microdata, or RDFa).\n///\n/// Represents machine-readable structured data found in the document.\n/// JSON-LD blocks are collected as raw JSON strings for flexibility.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n/// let schema = StructuredData {\n///     data_type: StructuredDataType::JsonLd,\n///     raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n///     schema_type: Some(\"Article\".to_string()),\n/// };\n///\n/// assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n/// ```\n/// </summary>\npublic sealed class StructuredData\n{\n    /// <summary>\n    /// Type of structured data (JSON-LD, Microdata, RDFa)\n    /// </summary>\n    [JsonConverter(typeof(StructuredDataTypeJsonConverter))]\n    [JsonPropertyName(\"data_type\")]\n    public StructuredDataType DataType { get; set; } = default!;\n\n    /// <summary>\n    /// Raw JSON string (for JSON-LD) or serialized representation\n    /// </summary>\n    [JsonPropertyName(\"raw_json\")]\n    public string RawJson { get; set; } = \"\";\n\n    /// <summary>\n    /// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n    /// </summary>\n    [JsonPropertyName(\"schema_type\")]\n    public string? SchemaType { get; set; } = null;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/StructuredDataType.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2674740a93d38fe955dd4ab43c2e06908d9324a7539ebfc31dec806a1bb4f26a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Structured data format type.\n///\n/// Identifies the schema/format used for structured data markup.\n/// </summary>\n[JsonConverter(typeof(StructuredDataTypeJsonConverter))]\npublic enum StructuredDataType\n{\n    /// <summary>\n    /// JSON-LD (JSON for Linking Data) script blocks\n    /// </summary>\n    [JsonPropertyName(\"json_ld\")]\n    JsonLd,\n    /// <summary>\n    /// HTML5 Microdata attributes (itemscope, itemtype, itemprop)\n    /// </summary>\n    [JsonPropertyName(\"microdata\")]\n    Microdata,\n    /// <summary>\n    /// RDF in Attributes (RDFa) markup\n    /// </summary>\n    [JsonPropertyName(\"rdfa\")]\n    RdFa,\n}\n\n/// <summary>Custom JSON converter for <see cref=\"StructuredDataType\"/> that respects explicit variant names.</summary>\ninternal sealed class StructuredDataTypeJsonConverter : JsonConverter<StructuredDataType>\n{\n    public override StructuredDataType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var value = reader.GetString();\n        return value switch\n        {\n            \"json_ld\" => StructuredDataType.JsonLd,\n            \"microdata\" => StructuredDataType.Microdata,\n            \"rdfa\" => StructuredDataType.RdFa,\n            _ => throw new JsonException($\"Unknown StructuredDataType value: {value}\")\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, StructuredDataType value, JsonSerializerOptions options)\n    {\n        var str = value switch\n        {\n            StructuredDataType.JsonLd => \"json_ld\",\n            StructuredDataType.Microdata => \"microdata\",\n            StructuredDataType.RdFa => \"rdfa\",\n            _ => throw new JsonException($\"Unknown StructuredDataType value: {value}\")\n        };\n        writer.WriteStringValue(str);\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/TableData.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0cb360f92cfeed74cd97b768dd9ea9f3603a06b1c27ab352146f21fa0f88bf11\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A top-level extracted table with both structured data and markdown representation.\n/// </summary>\npublic sealed class TableData\n{\n    /// <summary>\n    /// The structured table grid.\n    /// </summary>\n    [JsonPropertyName(\"grid\")]\n    public TableGrid Grid { get; set; } = default!;\n\n    /// <summary>\n    /// The markdown rendering of this table.\n    /// </summary>\n    [JsonPropertyName(\"markdown\")]\n    public string Markdown { get; set; } = \"\";\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/TableGrid.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:8a438b7b16ce8847bad77d60c3913e1ae8a5deef4ef9b82b79380e97bf4e45cc\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// A structured table grid with cell-level data including spans.\n/// </summary>\npublic sealed class TableGrid\n{\n    /// <summary>\n    /// Number of rows.\n    /// </summary>\n    [JsonPropertyName(\"rows\")]\n    public uint Rows { get; set; } = 0;\n\n    /// <summary>\n    /// Number of columns.\n    /// </summary>\n    [JsonPropertyName(\"cols\")]\n    public uint Cols { get; set; } = 0;\n\n    /// <summary>\n    /// All cells in the table (may be fewer than rows*cols due to spans).\n    /// </summary>\n    [JsonPropertyName(\"cells\")]\n    public List<GridCell> Cells { get; set; } = [];\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/TextAnnotation.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ff0d5d6d12736187185f649783c29399453488faecab577285885a011c728cd8\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// An inline text annotation with byte-range offsets.\n///\n/// Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n/// </summary>\npublic sealed class TextAnnotation\n{\n    /// <summary>\n    /// Start byte offset (inclusive) into the parent node's text.\n    /// </summary>\n    [JsonPropertyName(\"start\")]\n    public uint Start { get; set; } = 0;\n\n    /// <summary>\n    /// End byte offset (exclusive) into the parent node's text.\n    /// </summary>\n    [JsonPropertyName(\"end\")]\n    public uint End { get; set; } = 0;\n\n    /// <summary>\n    /// The type of annotation.\n    /// </summary>\n    [JsonConverter(typeof(AnnotationKindJsonConverter))]\n    [JsonPropertyName(\"kind\")]\n    public AnnotationKind Kind { get; set; } = default!;\n\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/TextDirection.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:3c3bf7fac29d874add2ab55a873805d3e3bf3fde312ac429ebc89341e2a2568c\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Text directionality of document content.\n///\n/// Corresponds to the HTML `dir` attribute and `bdi` element directionality.\n/// </summary>\n[JsonConverter(typeof(TextDirectionJsonConverter))]\npublic enum TextDirection\n{\n    /// <summary>\n    /// Left-to-right text flow (default for Latin scripts)\n    /// </summary>\n    [JsonPropertyName(\"ltr\")]\n    LeftToRight,\n    /// <summary>\n    /// Right-to-left text flow (Hebrew, Arabic, Urdu, etc.)\n    /// </summary>\n    [JsonPropertyName(\"rtl\")]\n    RightToLeft,\n    /// <summary>\n    /// Automatic directionality detection\n    /// </summary>\n    [JsonPropertyName(\"auto\")]\n    Auto,\n}\n\n/// <summary>Custom JSON converter for <see cref=\"TextDirection\"/> that respects explicit variant names.</summary>\ninternal sealed class TextDirectionJsonConverter : JsonConverter<TextDirection>\n{\n    public override TextDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var value = reader.GetString();\n        return value switch\n        {\n            \"ltr\" => TextDirection.LeftToRight,\n            \"rtl\" => TextDirection.RightToLeft,\n            \"auto\" => TextDirection.Auto,\n            _ => throw new JsonException($\"Unknown TextDirection value: {value}\")\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, TextDirection value, JsonSerializerOptions options)\n    {\n        var str = value switch\n        {\n            TextDirection.LeftToRight => \"ltr\",\n            TextDirection.RightToLeft => \"rtl\",\n            TextDirection.Auto => \"auto\",\n            _ => throw new JsonException($\"Unknown TextDirection value: {value}\")\n        };\n        writer.WriteStringValue(str);\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/TraitBridges.cs",
    "content": "// DO NOT EDIT — generated by alef\n// This file bridges managed C# trait implementations to the native FFI layer\n#nullable enable\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Bridge interface for HtmlVisitor trait implementation via native FFI\n/// </summary>\npublic interface IHtmlVisitor\n{\n\n    /// <summary>visit_element_start</summary>\n    VisitResult VisitElementStart(NodeContext Ctx);\n\n    /// <summary>visit_element_end</summary>\n    VisitResult VisitElementEnd(NodeContext Ctx, string Output);\n\n    /// <summary>visit_text</summary>\n    VisitResult VisitText(NodeContext Ctx, string Text);\n\n    /// <summary>visit_link</summary>\n    VisitResult VisitLink(NodeContext Ctx, string Href, string Text, string Title);\n\n    /// <summary>visit_image</summary>\n    VisitResult VisitImage(NodeContext Ctx, string Src, string Alt, string Title);\n\n    /// <summary>visit_heading</summary>\n    VisitResult VisitHeading(NodeContext Ctx, uint Level, string Text, string Id);\n\n    /// <summary>visit_code_block</summary>\n    VisitResult VisitCodeBlock(NodeContext Ctx, string Lang, string Code);\n\n    /// <summary>visit_code_inline</summary>\n    VisitResult VisitCodeInline(NodeContext Ctx, string Code);\n\n    /// <summary>visit_list_item</summary>\n    VisitResult VisitListItem(NodeContext Ctx, bool Ordered, string Marker, string Text);\n\n    /// <summary>visit_list_start</summary>\n    VisitResult VisitListStart(NodeContext Ctx, bool Ordered);\n\n    /// <summary>visit_list_end</summary>\n    VisitResult VisitListEnd(NodeContext Ctx, bool Ordered, string Output);\n\n    /// <summary>visit_table_start</summary>\n    VisitResult VisitTableStart(NodeContext Ctx);\n\n    /// <summary>visit_table_row</summary>\n    VisitResult VisitTableRow(NodeContext Ctx, List<string> Cells, bool IsHeader);\n\n    /// <summary>visit_table_end</summary>\n    VisitResult VisitTableEnd(NodeContext Ctx, string Output);\n\n    /// <summary>visit_blockquote</summary>\n    VisitResult VisitBlockquote(NodeContext Ctx, string Content, ulong Depth);\n\n    /// <summary>visit_strong</summary>\n    VisitResult VisitStrong(NodeContext Ctx, string Text);\n\n    /// <summary>visit_emphasis</summary>\n    VisitResult VisitEmphasis(NodeContext Ctx, string Text);\n\n    /// <summary>visit_strikethrough</summary>\n    VisitResult VisitStrikethrough(NodeContext Ctx, string Text);\n\n    /// <summary>visit_underline</summary>\n    VisitResult VisitUnderline(NodeContext Ctx, string Text);\n\n    /// <summary>visit_subscript</summary>\n    VisitResult VisitSubscript(NodeContext Ctx, string Text);\n\n    /// <summary>visit_superscript</summary>\n    VisitResult VisitSuperscript(NodeContext Ctx, string Text);\n\n    /// <summary>visit_mark</summary>\n    VisitResult VisitMark(NodeContext Ctx, string Text);\n\n    /// <summary>visit_line_break</summary>\n    VisitResult VisitLineBreak(NodeContext Ctx);\n\n    /// <summary>visit_horizontal_rule</summary>\n    VisitResult VisitHorizontalRule(NodeContext Ctx);\n\n    /// <summary>visit_custom_element</summary>\n    VisitResult VisitCustomElement(NodeContext Ctx, string TagName, string Html);\n\n    /// <summary>visit_definition_list_start</summary>\n    VisitResult VisitDefinitionListStart(NodeContext Ctx);\n\n    /// <summary>visit_definition_term</summary>\n    VisitResult VisitDefinitionTerm(NodeContext Ctx, string Text);\n\n    /// <summary>visit_definition_description</summary>\n    VisitResult VisitDefinitionDescription(NodeContext Ctx, string Text);\n\n    /// <summary>visit_definition_list_end</summary>\n    VisitResult VisitDefinitionListEnd(NodeContext Ctx, string Output);\n\n    /// <summary>visit_form</summary>\n    VisitResult VisitForm(NodeContext Ctx, string Action, string Method);\n\n    /// <summary>visit_input</summary>\n    VisitResult VisitInput(NodeContext Ctx, string InputType, string Name, string Value);\n\n    /// <summary>visit_button</summary>\n    VisitResult VisitButton(NodeContext Ctx, string Text);\n\n    /// <summary>visit_audio</summary>\n    VisitResult VisitAudio(NodeContext Ctx, string Src);\n\n    /// <summary>visit_video</summary>\n    VisitResult VisitVideo(NodeContext Ctx, string Src);\n\n    /// <summary>visit_iframe</summary>\n    VisitResult VisitIframe(NodeContext Ctx, string Src);\n\n    /// <summary>visit_details</summary>\n    VisitResult VisitDetails(NodeContext Ctx, bool Open);\n\n    /// <summary>visit_summary</summary>\n    VisitResult VisitSummary(NodeContext Ctx, string Text);\n\n    /// <summary>visit_figure_start</summary>\n    VisitResult VisitFigureStart(NodeContext Ctx);\n\n    /// <summary>visit_figcaption</summary>\n    VisitResult VisitFigcaption(NodeContext Ctx, string Text);\n\n    /// <summary>visit_figure_end</summary>\n    VisitResult VisitFigureEnd(NodeContext Ctx, string Output);\n\n}\n\n/// <summary>\n/// Manages the FFI vtable and delegates for a HtmlVisitor implementation\n/// </summary>\npublic sealed class HtmlVisitorBridge : IDisposable\n{\n\n    private readonly IHtmlVisitor _impl;\n    private readonly GCHandle _implHandle;\n    internal IntPtr _vtable;\n    private bool _disposed;\n\n    // Vtable slot delegates (41)\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitElementStartFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitElementEndFn(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTextFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitLinkFn(IntPtr userData, IntPtr Ctx, IntPtr Href, IntPtr Text, IntPtr Title, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitImageFn(IntPtr userData, IntPtr Ctx, IntPtr Src, IntPtr Alt, IntPtr Title, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitHeadingFn(IntPtr userData, IntPtr Ctx, uint Level, IntPtr Text, IntPtr Id, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCodeBlockFn(IntPtr userData, IntPtr Ctx, IntPtr Lang, IntPtr Code, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCodeInlineFn(IntPtr userData, IntPtr Ctx, IntPtr Code, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListItemFn(IntPtr userData, IntPtr Ctx, bool Ordered, IntPtr Marker, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListStartFn(IntPtr userData, IntPtr Ctx, bool Ordered, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListEndFn(IntPtr userData, IntPtr Ctx, bool Ordered, IntPtr Output, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableStartFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableRowFn(IntPtr userData, IntPtr Ctx, IntPtr Cells, bool IsHeader, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableEndFn(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitBlockquoteFn(IntPtr userData, IntPtr Ctx, IntPtr Content, ulong Depth, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitStrongFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitEmphasisFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitStrikethroughFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitUnderlineFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSubscriptFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSuperscriptFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitMarkFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitLineBreakFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitHorizontalRuleFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCustomElementFn(IntPtr userData, IntPtr Ctx, IntPtr TagName, IntPtr Html, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionListStartFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionTermFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionDescriptionFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionListEndFn(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFormFn(IntPtr userData, IntPtr Ctx, IntPtr Action, IntPtr Method, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitInputFn(IntPtr userData, IntPtr Ctx, IntPtr InputType, IntPtr Name, IntPtr Value, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitButtonFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitAudioFn(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitVideoFn(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitIframeFn(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDetailsFn(IntPtr userData, IntPtr Ctx, bool Open, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSummaryFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigureStartFn(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigcaptionFn(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigureEndFn(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError);\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate void FreeUserDataFn(IntPtr userData);\n\n    public HtmlVisitorBridge(IHtmlVisitor impl)\n    {\n        _impl = impl ?? throw new ArgumentNullException(nameof(impl));\n        _implHandle = GCHandle.Alloc(impl, GCHandleType.Normal);\n        _vtable = IntPtr.Zero;\n        _disposed = false;\n        BuildVtable();\n    }\n\n    private void BuildVtable()\n    {\n        // Allocate unmanaged vtable struct (array of function pointers)\n        _vtable = Marshal.AllocHGlobal(IntPtr.Size * 41);\n\n        // Slot 0: visit_element_start_fn\n        var visitElementStartFn = new VisitElementStartFn(VisitElementStartFnCallback);\n        Marshal.WriteIntPtr(_vtable, 0, Marshal.GetFunctionPointerForDelegate(visitElementStartFn));\n\n        // Slot 1: visit_element_end_fn\n        var visitElementEndFn = new VisitElementEndFn(VisitElementEndFnCallback);\n        Marshal.WriteIntPtr(_vtable, 8, Marshal.GetFunctionPointerForDelegate(visitElementEndFn));\n\n        // Slot 2: visit_text_fn\n        var visitTextFn = new VisitTextFn(VisitTextFnCallback);\n        Marshal.WriteIntPtr(_vtable, 16, Marshal.GetFunctionPointerForDelegate(visitTextFn));\n\n        // Slot 3: visit_link_fn\n        var visitLinkFn = new VisitLinkFn(VisitLinkFnCallback);\n        Marshal.WriteIntPtr(_vtable, 24, Marshal.GetFunctionPointerForDelegate(visitLinkFn));\n\n        // Slot 4: visit_image_fn\n        var visitImageFn = new VisitImageFn(VisitImageFnCallback);\n        Marshal.WriteIntPtr(_vtable, 32, Marshal.GetFunctionPointerForDelegate(visitImageFn));\n\n        // Slot 5: visit_heading_fn\n        var visitHeadingFn = new VisitHeadingFn(VisitHeadingFnCallback);\n        Marshal.WriteIntPtr(_vtable, 40, Marshal.GetFunctionPointerForDelegate(visitHeadingFn));\n\n        // Slot 6: visit_code_block_fn\n        var visitCodeBlockFn = new VisitCodeBlockFn(VisitCodeBlockFnCallback);\n        Marshal.WriteIntPtr(_vtable, 48, Marshal.GetFunctionPointerForDelegate(visitCodeBlockFn));\n\n        // Slot 7: visit_code_inline_fn\n        var visitCodeInlineFn = new VisitCodeInlineFn(VisitCodeInlineFnCallback);\n        Marshal.WriteIntPtr(_vtable, 56, Marshal.GetFunctionPointerForDelegate(visitCodeInlineFn));\n\n        // Slot 8: visit_list_item_fn\n        var visitListItemFn = new VisitListItemFn(VisitListItemFnCallback);\n        Marshal.WriteIntPtr(_vtable, 64, Marshal.GetFunctionPointerForDelegate(visitListItemFn));\n\n        // Slot 9: visit_list_start_fn\n        var visitListStartFn = new VisitListStartFn(VisitListStartFnCallback);\n        Marshal.WriteIntPtr(_vtable, 72, Marshal.GetFunctionPointerForDelegate(visitListStartFn));\n\n        // Slot 10: visit_list_end_fn\n        var visitListEndFn = new VisitListEndFn(VisitListEndFnCallback);\n        Marshal.WriteIntPtr(_vtable, 80, Marshal.GetFunctionPointerForDelegate(visitListEndFn));\n\n        // Slot 11: visit_table_start_fn\n        var visitTableStartFn = new VisitTableStartFn(VisitTableStartFnCallback);\n        Marshal.WriteIntPtr(_vtable, 88, Marshal.GetFunctionPointerForDelegate(visitTableStartFn));\n\n        // Slot 12: visit_table_row_fn\n        var visitTableRowFn = new VisitTableRowFn(VisitTableRowFnCallback);\n        Marshal.WriteIntPtr(_vtable, 96, Marshal.GetFunctionPointerForDelegate(visitTableRowFn));\n\n        // Slot 13: visit_table_end_fn\n        var visitTableEndFn = new VisitTableEndFn(VisitTableEndFnCallback);\n        Marshal.WriteIntPtr(_vtable, 104, Marshal.GetFunctionPointerForDelegate(visitTableEndFn));\n\n        // Slot 14: visit_blockquote_fn\n        var visitBlockquoteFn = new VisitBlockquoteFn(VisitBlockquoteFnCallback);\n        Marshal.WriteIntPtr(_vtable, 112, Marshal.GetFunctionPointerForDelegate(visitBlockquoteFn));\n\n        // Slot 15: visit_strong_fn\n        var visitStrongFn = new VisitStrongFn(VisitStrongFnCallback);\n        Marshal.WriteIntPtr(_vtable, 120, Marshal.GetFunctionPointerForDelegate(visitStrongFn));\n\n        // Slot 16: visit_emphasis_fn\n        var visitEmphasisFn = new VisitEmphasisFn(VisitEmphasisFnCallback);\n        Marshal.WriteIntPtr(_vtable, 128, Marshal.GetFunctionPointerForDelegate(visitEmphasisFn));\n\n        // Slot 17: visit_strikethrough_fn\n        var visitStrikethroughFn = new VisitStrikethroughFn(VisitStrikethroughFnCallback);\n        Marshal.WriteIntPtr(_vtable, 136, Marshal.GetFunctionPointerForDelegate(visitStrikethroughFn));\n\n        // Slot 18: visit_underline_fn\n        var visitUnderlineFn = new VisitUnderlineFn(VisitUnderlineFnCallback);\n        Marshal.WriteIntPtr(_vtable, 144, Marshal.GetFunctionPointerForDelegate(visitUnderlineFn));\n\n        // Slot 19: visit_subscript_fn\n        var visitSubscriptFn = new VisitSubscriptFn(VisitSubscriptFnCallback);\n        Marshal.WriteIntPtr(_vtable, 152, Marshal.GetFunctionPointerForDelegate(visitSubscriptFn));\n\n        // Slot 20: visit_superscript_fn\n        var visitSuperscriptFn = new VisitSuperscriptFn(VisitSuperscriptFnCallback);\n        Marshal.WriteIntPtr(_vtable, 160, Marshal.GetFunctionPointerForDelegate(visitSuperscriptFn));\n\n        // Slot 21: visit_mark_fn\n        var visitMarkFn = new VisitMarkFn(VisitMarkFnCallback);\n        Marshal.WriteIntPtr(_vtable, 168, Marshal.GetFunctionPointerForDelegate(visitMarkFn));\n\n        // Slot 22: visit_line_break_fn\n        var visitLineBreakFn = new VisitLineBreakFn(VisitLineBreakFnCallback);\n        Marshal.WriteIntPtr(_vtable, 176, Marshal.GetFunctionPointerForDelegate(visitLineBreakFn));\n\n        // Slot 23: visit_horizontal_rule_fn\n        var visitHorizontalRuleFn = new VisitHorizontalRuleFn(VisitHorizontalRuleFnCallback);\n        Marshal.WriteIntPtr(_vtable, 184, Marshal.GetFunctionPointerForDelegate(visitHorizontalRuleFn));\n\n        // Slot 24: visit_custom_element_fn\n        var visitCustomElementFn = new VisitCustomElementFn(VisitCustomElementFnCallback);\n        Marshal.WriteIntPtr(_vtable, 192, Marshal.GetFunctionPointerForDelegate(visitCustomElementFn));\n\n        // Slot 25: visit_definition_list_start_fn\n        var visitDefinitionListStartFn = new VisitDefinitionListStartFn(VisitDefinitionListStartFnCallback);\n        Marshal.WriteIntPtr(_vtable, 200, Marshal.GetFunctionPointerForDelegate(visitDefinitionListStartFn));\n\n        // Slot 26: visit_definition_term_fn\n        var visitDefinitionTermFn = new VisitDefinitionTermFn(VisitDefinitionTermFnCallback);\n        Marshal.WriteIntPtr(_vtable, 208, Marshal.GetFunctionPointerForDelegate(visitDefinitionTermFn));\n\n        // Slot 27: visit_definition_description_fn\n        var visitDefinitionDescriptionFn = new VisitDefinitionDescriptionFn(VisitDefinitionDescriptionFnCallback);\n        Marshal.WriteIntPtr(_vtable, 216, Marshal.GetFunctionPointerForDelegate(visitDefinitionDescriptionFn));\n\n        // Slot 28: visit_definition_list_end_fn\n        var visitDefinitionListEndFn = new VisitDefinitionListEndFn(VisitDefinitionListEndFnCallback);\n        Marshal.WriteIntPtr(_vtable, 224, Marshal.GetFunctionPointerForDelegate(visitDefinitionListEndFn));\n\n        // Slot 29: visit_form_fn\n        var visitFormFn = new VisitFormFn(VisitFormFnCallback);\n        Marshal.WriteIntPtr(_vtable, 232, Marshal.GetFunctionPointerForDelegate(visitFormFn));\n\n        // Slot 30: visit_input_fn\n        var visitInputFn = new VisitInputFn(VisitInputFnCallback);\n        Marshal.WriteIntPtr(_vtable, 240, Marshal.GetFunctionPointerForDelegate(visitInputFn));\n\n        // Slot 31: visit_button_fn\n        var visitButtonFn = new VisitButtonFn(VisitButtonFnCallback);\n        Marshal.WriteIntPtr(_vtable, 248, Marshal.GetFunctionPointerForDelegate(visitButtonFn));\n\n        // Slot 32: visit_audio_fn\n        var visitAudioFn = new VisitAudioFn(VisitAudioFnCallback);\n        Marshal.WriteIntPtr(_vtable, 256, Marshal.GetFunctionPointerForDelegate(visitAudioFn));\n\n        // Slot 33: visit_video_fn\n        var visitVideoFn = new VisitVideoFn(VisitVideoFnCallback);\n        Marshal.WriteIntPtr(_vtable, 264, Marshal.GetFunctionPointerForDelegate(visitVideoFn));\n\n        // Slot 34: visit_iframe_fn\n        var visitIframeFn = new VisitIframeFn(VisitIframeFnCallback);\n        Marshal.WriteIntPtr(_vtable, 272, Marshal.GetFunctionPointerForDelegate(visitIframeFn));\n\n        // Slot 35: visit_details_fn\n        var visitDetailsFn = new VisitDetailsFn(VisitDetailsFnCallback);\n        Marshal.WriteIntPtr(_vtable, 280, Marshal.GetFunctionPointerForDelegate(visitDetailsFn));\n\n        // Slot 36: visit_summary_fn\n        var visitSummaryFn = new VisitSummaryFn(VisitSummaryFnCallback);\n        Marshal.WriteIntPtr(_vtable, 288, Marshal.GetFunctionPointerForDelegate(visitSummaryFn));\n\n        // Slot 37: visit_figure_start_fn\n        var visitFigureStartFn = new VisitFigureStartFn(VisitFigureStartFnCallback);\n        Marshal.WriteIntPtr(_vtable, 296, Marshal.GetFunctionPointerForDelegate(visitFigureStartFn));\n\n        // Slot 38: visit_figcaption_fn\n        var visitFigcaptionFn = new VisitFigcaptionFn(VisitFigcaptionFnCallback);\n        Marshal.WriteIntPtr(_vtable, 304, Marshal.GetFunctionPointerForDelegate(visitFigcaptionFn));\n\n        // Slot 39: visit_figure_end_fn\n        var visitFigureEndFn = new VisitFigureEndFn(VisitFigureEndFnCallback);\n        Marshal.WriteIntPtr(_vtable, 312, Marshal.GetFunctionPointerForDelegate(visitFigureEndFn));\n\n        // Slot 40: free_user_data\n        var freeFn = new FreeUserDataFn(FreeUserDataCallback);\n        Marshal.WriteIntPtr(_vtable, 320, Marshal.GetFunctionPointerForDelegate(freeFn));\n    }\n\n    private static string ToJsonString<T>(T value)\n    {\n        return JsonSerializer.Serialize(value);\n    }\n\n    private int VisitElementStartFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitElementStart(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitElementEndFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Output = Marshal.PtrToStringUTF8(Output) ?? string.Empty;\n            var result = _impl.VisitElementEnd(managed_Ctx, managed_Output);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitTextFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitText(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitLinkFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Href, IntPtr Text, IntPtr Title, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Href = Marshal.PtrToStringUTF8(Href) ?? string.Empty;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var managed_Title = Marshal.PtrToStringUTF8(Title) ?? string.Empty;\n            var result = _impl.VisitLink(managed_Ctx, managed_Href, managed_Text, managed_Title);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitImageFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Src, IntPtr Alt, IntPtr Title, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Src = Marshal.PtrToStringUTF8(Src) ?? string.Empty;\n            var managed_Alt = Marshal.PtrToStringUTF8(Alt) ?? string.Empty;\n            var managed_Title = Marshal.PtrToStringUTF8(Title) ?? string.Empty;\n            var result = _impl.VisitImage(managed_Ctx, managed_Src, managed_Alt, managed_Title);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitHeadingFnCallback(IntPtr userData, IntPtr Ctx, uint Level, IntPtr Text, IntPtr Id, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var managed_Id = Marshal.PtrToStringUTF8(Id) ?? string.Empty;\n            var result = _impl.VisitHeading(managed_Ctx, Level, managed_Text, managed_Id);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitCodeBlockFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Lang, IntPtr Code, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Lang = Marshal.PtrToStringUTF8(Lang) ?? string.Empty;\n            var managed_Code = Marshal.PtrToStringUTF8(Code) ?? string.Empty;\n            var result = _impl.VisitCodeBlock(managed_Ctx, managed_Lang, managed_Code);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitCodeInlineFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Code, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Code = Marshal.PtrToStringUTF8(Code) ?? string.Empty;\n            var result = _impl.VisitCodeInline(managed_Ctx, managed_Code);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitListItemFnCallback(IntPtr userData, IntPtr Ctx, bool Ordered, IntPtr Marker, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Marker = Marshal.PtrToStringUTF8(Marker) ?? string.Empty;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitListItem(managed_Ctx, Ordered, managed_Marker, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitListStartFnCallback(IntPtr userData, IntPtr Ctx, bool Ordered, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitListStart(managed_Ctx, Ordered);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitListEndFnCallback(IntPtr userData, IntPtr Ctx, bool Ordered, IntPtr Output, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Output = Marshal.PtrToStringUTF8(Output) ?? string.Empty;\n            var result = _impl.VisitListEnd(managed_Ctx, Ordered, managed_Output);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitTableStartFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitTableStart(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitTableRowFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Cells, bool IsHeader, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var json_Cells = Marshal.PtrToStringUTF8(Cells) ?? \"{}\";\n            var managed_Cells = JsonSerializer.Deserialize<List<string>>(json_Cells)!;\n            var result = _impl.VisitTableRow(managed_Ctx, managed_Cells, IsHeader);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitTableEndFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Output = Marshal.PtrToStringUTF8(Output) ?? string.Empty;\n            var result = _impl.VisitTableEnd(managed_Ctx, managed_Output);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitBlockquoteFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Content, ulong Depth, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Content = Marshal.PtrToStringUTF8(Content) ?? string.Empty;\n            var result = _impl.VisitBlockquote(managed_Ctx, managed_Content, Depth);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitStrongFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitStrong(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitEmphasisFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitEmphasis(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitStrikethroughFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitStrikethrough(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitUnderlineFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitUnderline(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitSubscriptFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitSubscript(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitSuperscriptFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitSuperscript(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitMarkFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitMark(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitLineBreakFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitLineBreak(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitHorizontalRuleFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitHorizontalRule(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitCustomElementFnCallback(IntPtr userData, IntPtr Ctx, IntPtr TagName, IntPtr Html, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_TagName = Marshal.PtrToStringUTF8(TagName) ?? string.Empty;\n            var managed_Html = Marshal.PtrToStringUTF8(Html) ?? string.Empty;\n            var result = _impl.VisitCustomElement(managed_Ctx, managed_TagName, managed_Html);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitDefinitionListStartFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitDefinitionListStart(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitDefinitionTermFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitDefinitionTerm(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitDefinitionDescriptionFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitDefinitionDescription(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitDefinitionListEndFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Output = Marshal.PtrToStringUTF8(Output) ?? string.Empty;\n            var result = _impl.VisitDefinitionListEnd(managed_Ctx, managed_Output);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitFormFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Action, IntPtr Method, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Action = Marshal.PtrToStringUTF8(Action) ?? string.Empty;\n            var managed_Method = Marshal.PtrToStringUTF8(Method) ?? string.Empty;\n            var result = _impl.VisitForm(managed_Ctx, managed_Action, managed_Method);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitInputFnCallback(IntPtr userData, IntPtr Ctx, IntPtr InputType, IntPtr Name, IntPtr Value, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_InputType = Marshal.PtrToStringUTF8(InputType) ?? string.Empty;\n            var managed_Name = Marshal.PtrToStringUTF8(Name) ?? string.Empty;\n            var managed_Value = Marshal.PtrToStringUTF8(Value) ?? string.Empty;\n            var result = _impl.VisitInput(managed_Ctx, managed_InputType, managed_Name, managed_Value);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitButtonFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitButton(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitAudioFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Src = Marshal.PtrToStringUTF8(Src) ?? string.Empty;\n            var result = _impl.VisitAudio(managed_Ctx, managed_Src);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitVideoFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Src = Marshal.PtrToStringUTF8(Src) ?? string.Empty;\n            var result = _impl.VisitVideo(managed_Ctx, managed_Src);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitIframeFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Src, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Src = Marshal.PtrToStringUTF8(Src) ?? string.Empty;\n            var result = _impl.VisitIframe(managed_Ctx, managed_Src);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitDetailsFnCallback(IntPtr userData, IntPtr Ctx, bool Open, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitDetails(managed_Ctx, Open);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitSummaryFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitSummary(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitFigureStartFnCallback(IntPtr userData, IntPtr Ctx, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var result = _impl.VisitFigureStart(managed_Ctx);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitFigcaptionFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Text, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Text = Marshal.PtrToStringUTF8(Text) ?? string.Empty;\n            var result = _impl.VisitFigcaption(managed_Ctx, managed_Text);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private int VisitFigureEndFnCallback(IntPtr userData, IntPtr Ctx, IntPtr Output, out IntPtr outResult, out IntPtr outError)\n    {\n        try\n        {\n            var json_Ctx = Marshal.PtrToStringUTF8(Ctx) ?? \"{}\";\n            var managed_Ctx = JsonSerializer.Deserialize<NodeContext>(json_Ctx)!;\n            var managed_Output = Marshal.PtrToStringUTF8(Output) ?? string.Empty;\n            var result = _impl.VisitFigureEnd(managed_Ctx, managed_Output);\n            outResult = Marshal.StringToCoTaskMemUTF8(ToJsonString(result));\n            outError = IntPtr.Zero;\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            outResult = IntPtr.Zero;\n            outError = Marshal.StringToCoTaskMemUTF8(ex.Message);\n            return 1;\n        }\n    }\n\n    private void FreeUserDataCallback(IntPtr userData)\n    {\n        if (userData != IntPtr.Zero)\n        {\n            try\n            {\n                var handle = GCHandle.FromIntPtr(userData);\n                handle.Free();\n            }\n            catch (ObjectDisposedException)\n            {\n                // Handle already freed; safe to ignore during finalization\n            }\n        }\n    }\n\n    public void Dispose()\n    {\n        if (_disposed) return;\n        _disposed = true;\n\n        if (_vtable != IntPtr.Zero)\n        {\n            Marshal.FreeHGlobal(_vtable);\n            _vtable = IntPtr.Zero;\n        }\n\n        if (_implHandle.IsAllocated)\n        {\n            _implHandle.Free();\n        }\n    }\n}\n\n/// <summary>Static helpers for registering trait implementations</summary>\npublic static class HtmlVisitorRegistry\n{\n\n    private static readonly ConcurrentDictionary<string, HtmlVisitorBridge> _bridges =\n        new ConcurrentDictionary<string, HtmlVisitorBridge>();\n\n    /// <summary>Register a HtmlVisitor implementation</summary>\n    public static void Register(IHtmlVisitor impl, string name)\n    {\n        if (impl == null)\n            throw new ArgumentNullException(nameof(impl));\n\n        var bridge = new HtmlVisitorBridge(impl);\n\n        try\n        {\n            var userDataHandle = GCHandle.Alloc(bridge, GCHandleType.Normal);\n            var userData = GCHandle.ToIntPtr(userDataHandle);\n            var vtablePtr = bridge._vtable;\n\n            var result = NativeMethods.RegisterHtmlVisitor(name, vtablePtr, userData, out var outError);\n            if (result != 0)\n            {\n                userDataHandle.Free();\n                bridge.Dispose();\n                var errorMsg = Marshal.PtrToStringUTF8(outError) ?? \"Unknown error\";\n                Marshal.FreeCoTaskMem(outError);\n                throw new InvalidOperationException($\"Failed to register {name}: {errorMsg}\");\n            }\n\n            _bridges.TryAdd(name, bridge);\n        }\n        catch\n        {\n            bridge.Dispose();\n            throw;\n        }\n    }\n\n    /// <summary>Unregister a HtmlVisitor implementation</summary>\n    public static void Unregister(string name)\n    {\n        if (string.IsNullOrEmpty(name))\n            throw new ArgumentException(\"Name cannot be empty\", nameof(name));\n\n        var result = NativeMethods.UnregisterHtmlVisitor(name, out var outError);\n        if (result != 0)\n        {\n            var errorMsg = Marshal.PtrToStringUTF8(outError) ?? \"Unknown error\";\n            Marshal.FreeCoTaskMem(outError);\n            throw new InvalidOperationException($\"Failed to unregister {name}: {errorMsg}\");\n        }\n\n        if (_bridges.TryRemove(name, out var bridge))\n        {\n            bridge.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/VisitResult.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:262b996c51c4e238240ad61b49ec898048b964030cc9947c24b3c8836713d006\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>Controls how the visitor affects the conversion pipeline.</summary>\npublic abstract record VisitResult\n{\n    private VisitResult() { }\n\n    /// <summary>Proceed with default conversion.</summary>\n    public sealed record Continue : VisitResult;\n\n    /// <summary>Omit this element from output entirely.</summary>\n    public sealed record Skip : VisitResult;\n\n    /// <summary>Keep original HTML verbatim.</summary>\n    public sealed record PreserveHtml : VisitResult;\n\n    /// <summary>Replace with custom Markdown.</summary>\n    public sealed record Custom(string Markdown) : VisitResult;\n\n    /// <summary>Abort conversion with an error message.</summary>\n    public sealed record Error(string Message) : VisitResult;\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/VisitorCallbacks.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:68053c11b67be4fe3186da6975ea6505ea5f0c3c85652c8b818476e63f6ca3cf\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\nusing System.Runtime.InteropServices;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Allocates P/Invoke delegates for a IVisitor and assembles\n/// the C HTMHtmVisitorCallbacks struct in unmanaged memory.\n/// </summary>\ninternal sealed class VisitorCallbacks : IDisposable\n{\n    private readonly IVisitor _visitor;\n    private readonly IntPtr _nativeStruct; // HTMHtmVisitorCallbacks\n    private bool _disposed;\n\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTextDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitTextDelegate _delVisitText;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitElementStartDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitElementStartDelegate _delVisitElementStart;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitElementEndDelegate(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitElementEndDelegate _delVisitElementEnd;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitLinkDelegate(IntPtr ctx, IntPtr userData, IntPtr rawHref0, IntPtr rawText0, IntPtr rawTitle0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitLinkDelegate _delVisitLink;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitImageDelegate(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr rawAlt0, IntPtr rawTitle0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitImageDelegate _delVisitImage;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitHeadingDelegate(IntPtr ctx, IntPtr userData, uint rawLevel0, IntPtr rawText0, IntPtr rawId0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitHeadingDelegate _delVisitHeading;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCodeBlockDelegate(IntPtr ctx, IntPtr userData, IntPtr rawLang0, IntPtr rawCode0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitCodeBlockDelegate _delVisitCodeBlock;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCodeInlineDelegate(IntPtr ctx, IntPtr userData, IntPtr rawCode0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitCodeInlineDelegate _delVisitCodeInline;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListItemDelegate(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr rawMarker0, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitListItemDelegate _delVisitListItem;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListStartDelegate(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitListStartDelegate _delVisitListStart;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitListEndDelegate(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitListEndDelegate _delVisitListEnd;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableStartDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitTableStartDelegate _delVisitTableStart;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableRowDelegate(IntPtr ctx, IntPtr userData, IntPtr rawCells0, UIntPtr rawCells1, int isHeader, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitTableRowDelegate _delVisitTableRow;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitTableEndDelegate(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitTableEndDelegate _delVisitTableEnd;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitBlockquoteDelegate(IntPtr ctx, IntPtr userData, IntPtr rawContent0, UIntPtr rawDepth0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitBlockquoteDelegate _delVisitBlockquote;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitStrongDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitStrongDelegate _delVisitStrong;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitEmphasisDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitEmphasisDelegate _delVisitEmphasis;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitStrikethroughDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitStrikethroughDelegate _delVisitStrikethrough;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitUnderlineDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitUnderlineDelegate _delVisitUnderline;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSubscriptDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitSubscriptDelegate _delVisitSubscript;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSuperscriptDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitSuperscriptDelegate _delVisitSuperscript;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitMarkDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitMarkDelegate _delVisitMark;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitLineBreakDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitLineBreakDelegate _delVisitLineBreak;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitHorizontalRuleDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitHorizontalRuleDelegate _delVisitHorizontalRule;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitCustomElementDelegate(IntPtr ctx, IntPtr userData, IntPtr rawTagName0, IntPtr rawHtml0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitCustomElementDelegate _delVisitCustomElement;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionListStartDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitDefinitionListStartDelegate _delVisitDefinitionListStart;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionTermDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitDefinitionTermDelegate _delVisitDefinitionTerm;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionDescriptionDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitDefinitionDescriptionDelegate _delVisitDefinitionDescription;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDefinitionListEndDelegate(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitDefinitionListEndDelegate _delVisitDefinitionListEnd;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFormDelegate(IntPtr ctx, IntPtr userData, IntPtr rawAction0, IntPtr rawMethod0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitFormDelegate _delVisitForm;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitInputDelegate(IntPtr ctx, IntPtr userData, IntPtr rawInputType0, IntPtr rawName0, IntPtr rawValue0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitInputDelegate _delVisitInput;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitButtonDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitButtonDelegate _delVisitButton;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitAudioDelegate(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitAudioDelegate _delVisitAudio;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitVideoDelegate(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitVideoDelegate _delVisitVideo;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitIframeDelegate(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitIframeDelegate _delVisitIframe;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitDetailsDelegate(IntPtr ctx, IntPtr userData, int rawOpen0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitDetailsDelegate _delVisitDetails;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitSummaryDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitSummaryDelegate _delVisitSummary;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigureStartDelegate(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitFigureStartDelegate _delVisitFigureStart;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigcaptionDelegate(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitFigcaptionDelegate _delVisitFigcaption;\n    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n    private delegate int VisitFigureEndDelegate(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen);\n    private readonly VisitFigureEndDelegate _delVisitFigureEnd;\n\n    internal IntPtr NativePtr => _nativeStruct;\n\n    internal VisitorCallbacks(IVisitor visitor)\n    {\n        _visitor = visitor;\n\n        _delVisitText = new VisitTextDelegate(HandleVisitText);\n        _delVisitElementStart = new VisitElementStartDelegate(HandleVisitElementStart);\n        _delVisitElementEnd = new VisitElementEndDelegate(HandleVisitElementEnd);\n        _delVisitLink = new VisitLinkDelegate(HandleVisitLink);\n        _delVisitImage = new VisitImageDelegate(HandleVisitImage);\n        _delVisitHeading = new VisitHeadingDelegate(HandleVisitHeading);\n        _delVisitCodeBlock = new VisitCodeBlockDelegate(HandleVisitCodeBlock);\n        _delVisitCodeInline = new VisitCodeInlineDelegate(HandleVisitCodeInline);\n        _delVisitListItem = new VisitListItemDelegate(HandleVisitListItem);\n        _delVisitListStart = new VisitListStartDelegate(HandleVisitListStart);\n        _delVisitListEnd = new VisitListEndDelegate(HandleVisitListEnd);\n        _delVisitTableStart = new VisitTableStartDelegate(HandleVisitTableStart);\n        _delVisitTableRow = new VisitTableRowDelegate(HandleVisitTableRow);\n        _delVisitTableEnd = new VisitTableEndDelegate(HandleVisitTableEnd);\n        _delVisitBlockquote = new VisitBlockquoteDelegate(HandleVisitBlockquote);\n        _delVisitStrong = new VisitStrongDelegate(HandleVisitStrong);\n        _delVisitEmphasis = new VisitEmphasisDelegate(HandleVisitEmphasis);\n        _delVisitStrikethrough = new VisitStrikethroughDelegate(HandleVisitStrikethrough);\n        _delVisitUnderline = new VisitUnderlineDelegate(HandleVisitUnderline);\n        _delVisitSubscript = new VisitSubscriptDelegate(HandleVisitSubscript);\n        _delVisitSuperscript = new VisitSuperscriptDelegate(HandleVisitSuperscript);\n        _delVisitMark = new VisitMarkDelegate(HandleVisitMark);\n        _delVisitLineBreak = new VisitLineBreakDelegate(HandleVisitLineBreak);\n        _delVisitHorizontalRule = new VisitHorizontalRuleDelegate(HandleVisitHorizontalRule);\n        _delVisitCustomElement = new VisitCustomElementDelegate(HandleVisitCustomElement);\n        _delVisitDefinitionListStart = new VisitDefinitionListStartDelegate(HandleVisitDefinitionListStart);\n        _delVisitDefinitionTerm = new VisitDefinitionTermDelegate(HandleVisitDefinitionTerm);\n        _delVisitDefinitionDescription = new VisitDefinitionDescriptionDelegate(HandleVisitDefinitionDescription);\n        _delVisitDefinitionListEnd = new VisitDefinitionListEndDelegate(HandleVisitDefinitionListEnd);\n        _delVisitForm = new VisitFormDelegate(HandleVisitForm);\n        _delVisitInput = new VisitInputDelegate(HandleVisitInput);\n        _delVisitButton = new VisitButtonDelegate(HandleVisitButton);\n        _delVisitAudio = new VisitAudioDelegate(HandleVisitAudio);\n        _delVisitVideo = new VisitVideoDelegate(HandleVisitVideo);\n        _delVisitIframe = new VisitIframeDelegate(HandleVisitIframe);\n        _delVisitDetails = new VisitDetailsDelegate(HandleVisitDetails);\n        _delVisitSummary = new VisitSummaryDelegate(HandleVisitSummary);\n        _delVisitFigureStart = new VisitFigureStartDelegate(HandleVisitFigureStart);\n        _delVisitFigcaption = new VisitFigcaptionDelegate(HandleVisitFigcaption);\n        _delVisitFigureEnd = new VisitFigureEndDelegate(HandleVisitFigureEnd);\n\n        // HTMHtmVisitorCallbacks = user_data + 40 callback function pointers\n        _nativeStruct = Marshal.AllocHGlobal(IntPtr.Size * 41);\n        // Slot 0: user_data = IntPtr.Zero (visitor captured via delegate closure)\n        Marshal.WriteIntPtr(_nativeStruct, 0, IntPtr.Zero);\n        Marshal.WriteIntPtr(_nativeStruct, 8, Marshal.GetFunctionPointerForDelegate(_delVisitText));\n        Marshal.WriteIntPtr(_nativeStruct, 16, Marshal.GetFunctionPointerForDelegate(_delVisitElementStart));\n        Marshal.WriteIntPtr(_nativeStruct, 24, Marshal.GetFunctionPointerForDelegate(_delVisitElementEnd));\n        Marshal.WriteIntPtr(_nativeStruct, 32, Marshal.GetFunctionPointerForDelegate(_delVisitLink));\n        Marshal.WriteIntPtr(_nativeStruct, 40, Marshal.GetFunctionPointerForDelegate(_delVisitImage));\n        Marshal.WriteIntPtr(_nativeStruct, 48, Marshal.GetFunctionPointerForDelegate(_delVisitHeading));\n        Marshal.WriteIntPtr(_nativeStruct, 56, Marshal.GetFunctionPointerForDelegate(_delVisitCodeBlock));\n        Marshal.WriteIntPtr(_nativeStruct, 64, Marshal.GetFunctionPointerForDelegate(_delVisitCodeInline));\n        Marshal.WriteIntPtr(_nativeStruct, 72, Marshal.GetFunctionPointerForDelegate(_delVisitListItem));\n        Marshal.WriteIntPtr(_nativeStruct, 80, Marshal.GetFunctionPointerForDelegate(_delVisitListStart));\n        Marshal.WriteIntPtr(_nativeStruct, 88, Marshal.GetFunctionPointerForDelegate(_delVisitListEnd));\n        Marshal.WriteIntPtr(_nativeStruct, 96, Marshal.GetFunctionPointerForDelegate(_delVisitTableStart));\n        Marshal.WriteIntPtr(_nativeStruct, 104, Marshal.GetFunctionPointerForDelegate(_delVisitTableRow));\n        Marshal.WriteIntPtr(_nativeStruct, 112, Marshal.GetFunctionPointerForDelegate(_delVisitTableEnd));\n        Marshal.WriteIntPtr(_nativeStruct, 120, Marshal.GetFunctionPointerForDelegate(_delVisitBlockquote));\n        Marshal.WriteIntPtr(_nativeStruct, 128, Marshal.GetFunctionPointerForDelegate(_delVisitStrong));\n        Marshal.WriteIntPtr(_nativeStruct, 136, Marshal.GetFunctionPointerForDelegate(_delVisitEmphasis));\n        Marshal.WriteIntPtr(_nativeStruct, 144, Marshal.GetFunctionPointerForDelegate(_delVisitStrikethrough));\n        Marshal.WriteIntPtr(_nativeStruct, 152, Marshal.GetFunctionPointerForDelegate(_delVisitUnderline));\n        Marshal.WriteIntPtr(_nativeStruct, 160, Marshal.GetFunctionPointerForDelegate(_delVisitSubscript));\n        Marshal.WriteIntPtr(_nativeStruct, 168, Marshal.GetFunctionPointerForDelegate(_delVisitSuperscript));\n        Marshal.WriteIntPtr(_nativeStruct, 176, Marshal.GetFunctionPointerForDelegate(_delVisitMark));\n        Marshal.WriteIntPtr(_nativeStruct, 184, Marshal.GetFunctionPointerForDelegate(_delVisitLineBreak));\n        Marshal.WriteIntPtr(_nativeStruct, 192, Marshal.GetFunctionPointerForDelegate(_delVisitHorizontalRule));\n        Marshal.WriteIntPtr(_nativeStruct, 200, Marshal.GetFunctionPointerForDelegate(_delVisitCustomElement));\n        Marshal.WriteIntPtr(_nativeStruct, 208, Marshal.GetFunctionPointerForDelegate(_delVisitDefinitionListStart));\n        Marshal.WriteIntPtr(_nativeStruct, 216, Marshal.GetFunctionPointerForDelegate(_delVisitDefinitionTerm));\n        Marshal.WriteIntPtr(_nativeStruct, 224, Marshal.GetFunctionPointerForDelegate(_delVisitDefinitionDescription));\n        Marshal.WriteIntPtr(_nativeStruct, 232, Marshal.GetFunctionPointerForDelegate(_delVisitDefinitionListEnd));\n        Marshal.WriteIntPtr(_nativeStruct, 240, Marshal.GetFunctionPointerForDelegate(_delVisitForm));\n        Marshal.WriteIntPtr(_nativeStruct, 248, Marshal.GetFunctionPointerForDelegate(_delVisitInput));\n        Marshal.WriteIntPtr(_nativeStruct, 256, Marshal.GetFunctionPointerForDelegate(_delVisitButton));\n        Marshal.WriteIntPtr(_nativeStruct, 264, Marshal.GetFunctionPointerForDelegate(_delVisitAudio));\n        Marshal.WriteIntPtr(_nativeStruct, 272, Marshal.GetFunctionPointerForDelegate(_delVisitVideo));\n        Marshal.WriteIntPtr(_nativeStruct, 280, Marshal.GetFunctionPointerForDelegate(_delVisitIframe));\n        Marshal.WriteIntPtr(_nativeStruct, 288, Marshal.GetFunctionPointerForDelegate(_delVisitDetails));\n        Marshal.WriteIntPtr(_nativeStruct, 296, Marshal.GetFunctionPointerForDelegate(_delVisitSummary));\n        Marshal.WriteIntPtr(_nativeStruct, 304, Marshal.GetFunctionPointerForDelegate(_delVisitFigureStart));\n        Marshal.WriteIntPtr(_nativeStruct, 312, Marshal.GetFunctionPointerForDelegate(_delVisitFigcaption));\n        Marshal.WriteIntPtr(_nativeStruct, 320, Marshal.GetFunctionPointerForDelegate(_delVisitFigureEnd));\n    }\n\n    private int HandleVisitText(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitText(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitElementStart(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitElementStart(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitElementEnd(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var output = Marshal.PtrToStringAnsi(rawOutput0)!;\n            var result = _visitor.VisitElementEnd(context, output);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitLink(IntPtr ctx, IntPtr userData, IntPtr rawHref0, IntPtr rawText0, IntPtr rawTitle0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var href = Marshal.PtrToStringAnsi(rawHref0)!;\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var title = rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawTitle0);\n            var result = _visitor.VisitLink(context, href, text, title);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitImage(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr rawAlt0, IntPtr rawTitle0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var src = Marshal.PtrToStringAnsi(rawSrc0)!;\n            var alt = Marshal.PtrToStringAnsi(rawAlt0)!;\n            var title = rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawTitle0);\n            var result = _visitor.VisitImage(context, src, alt, title);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitHeading(IntPtr ctx, IntPtr userData, uint rawLevel0, IntPtr rawText0, IntPtr rawId0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var level = rawLevel0;\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var id = rawId0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawId0);\n            var result = _visitor.VisitHeading(context, level, text, id);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitCodeBlock(IntPtr ctx, IntPtr userData, IntPtr rawLang0, IntPtr rawCode0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var lang = rawLang0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawLang0);\n            var code = Marshal.PtrToStringAnsi(rawCode0)!;\n            var result = _visitor.VisitCodeBlock(context, lang, code);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitCodeInline(IntPtr ctx, IntPtr userData, IntPtr rawCode0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var code = Marshal.PtrToStringAnsi(rawCode0)!;\n            var result = _visitor.VisitCodeInline(context, code);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitListItem(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr rawMarker0, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var ordered = rawOrdered0 != 0;\n            var marker = Marshal.PtrToStringAnsi(rawMarker0)!;\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitListItem(context, ordered, marker, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitListStart(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var ordered = rawOrdered0 != 0;\n            var result = _visitor.VisitListStart(context, ordered);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitListEnd(IntPtr ctx, IntPtr userData, int rawOrdered0, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var ordered = rawOrdered0 != 0;\n            var output = Marshal.PtrToStringAnsi(rawOutput0)!;\n            var result = _visitor.VisitListEnd(context, ordered, output);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitTableStart(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitTableStart(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitTableRow(IntPtr ctx, IntPtr userData, IntPtr rawCells0, UIntPtr rawCells1, int isHeader, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var cells = DecodeCells(rawCells0, (long)(ulong)rawCells1);\n            var goIsHeader = isHeader != 0;\n            var result = _visitor.VisitTableRow(context, cells, goIsHeader);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitTableEnd(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var output = Marshal.PtrToStringAnsi(rawOutput0)!;\n            var result = _visitor.VisitTableEnd(context, output);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitBlockquote(IntPtr ctx, IntPtr userData, IntPtr rawContent0, UIntPtr rawDepth0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var content = Marshal.PtrToStringAnsi(rawContent0)!;\n            var depth = (ulong)rawDepth0;\n            var result = _visitor.VisitBlockquote(context, content, depth);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitStrong(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitStrong(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitEmphasis(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitEmphasis(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitStrikethrough(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitStrikethrough(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitUnderline(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitUnderline(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitSubscript(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitSubscript(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitSuperscript(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitSuperscript(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitMark(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitMark(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitLineBreak(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitLineBreak(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitHorizontalRule(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitHorizontalRule(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitCustomElement(IntPtr ctx, IntPtr userData, IntPtr rawTagName0, IntPtr rawHtml0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var tagName = Marshal.PtrToStringAnsi(rawTagName0)!;\n            var html = Marshal.PtrToStringAnsi(rawHtml0)!;\n            var result = _visitor.VisitCustomElement(context, tagName, html);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitDefinitionListStart(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitDefinitionListStart(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitDefinitionTerm(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitDefinitionTerm(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitDefinitionDescription(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitDefinitionDescription(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitDefinitionListEnd(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var output = Marshal.PtrToStringAnsi(rawOutput0)!;\n            var result = _visitor.VisitDefinitionListEnd(context, output);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitForm(IntPtr ctx, IntPtr userData, IntPtr rawAction0, IntPtr rawMethod0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var action = rawAction0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawAction0);\n            var method = rawMethod0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawMethod0);\n            var result = _visitor.VisitForm(context, action, method);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitInput(IntPtr ctx, IntPtr userData, IntPtr rawInputType0, IntPtr rawName0, IntPtr rawValue0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var inputType = Marshal.PtrToStringAnsi(rawInputType0)!;\n            var name = rawName0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawName0);\n            var value = rawValue0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawValue0);\n            var result = _visitor.VisitInput(context, inputType, name, value);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitButton(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitButton(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitAudio(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var src = rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0);\n            var result = _visitor.VisitAudio(context, src);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitVideo(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var src = rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0);\n            var result = _visitor.VisitVideo(context, src);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitIframe(IntPtr ctx, IntPtr userData, IntPtr rawSrc0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var src = rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0);\n            var result = _visitor.VisitIframe(context, src);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitDetails(IntPtr ctx, IntPtr userData, int rawOpen0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var open = rawOpen0 != 0;\n            var result = _visitor.VisitDetails(context, open);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitSummary(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitSummary(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitFigureStart(IntPtr ctx, IntPtr userData, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var result = _visitor.VisitFigureStart(context);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitFigcaption(IntPtr ctx, IntPtr userData, IntPtr rawText0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var text = Marshal.PtrToStringAnsi(rawText0)!;\n            var result = _visitor.VisitFigcaption(context, text);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private int HandleVisitFigureEnd(IntPtr ctx, IntPtr userData, IntPtr rawOutput0, IntPtr outCustom, IntPtr outLen)\n    {\n        try\n        {\n            var context = DecodeNodeContext(ctx);\n            var output = Marshal.PtrToStringAnsi(rawOutput0)!;\n            var result = _visitor.VisitFigureEnd(context, output);\n            return EncodeVisitResult(result, outCustom, outLen);\n        }\n        catch\n        {\n            return 0;\n        }\n    }\n\n    private static NodeContext DecodeNodeContext(IntPtr ctxPtr)\n    {\n        // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,\n        //                    uintptr index_in_parent, char* parent_tag, int32 is_inline\n        int nodeType = Marshal.ReadInt32(ctxPtr, 0);\n        var tagNamePtr = Marshal.ReadIntPtr(ctxPtr, 8);\n        string tagName = Marshal.PtrToStringAnsi(tagNamePtr) ?? string.Empty;\n        ulong depth = (ulong)(long)Marshal.ReadInt64(ctxPtr, 16);\n        ulong indexInParent = (ulong)(long)Marshal.ReadInt64(ctxPtr, 24);\n        var parentTagPtr = Marshal.ReadIntPtr(ctxPtr, 32);\n        string? parentTag = parentTagPtr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(parentTagPtr);\n        int isInlineRaw = Marshal.ReadInt32(ctxPtr, 40);\n        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);\n    }\n\n    private static string[] DecodeCells(IntPtr cellsPtr, long count)\n    {\n        var result = new string[count];\n        for (long i = 0; i < count; i++)\n        {\n            var ptr = Marshal.ReadIntPtr(cellsPtr, (int)(i * IntPtr.Size));\n            result[i] = Marshal.PtrToStringAnsi(ptr) ?? string.Empty;\n        }\n        return result;\n    }\n\n    private static int EncodeVisitResult(VisitResult result, IntPtr outCustom, IntPtr outLen)\n    {\n        return result switch\n        {\n            VisitResult.Continue => 0,\n            VisitResult.Skip => 1,\n            VisitResult.PreserveHtml => 2,\n            VisitResult.Custom c => EncodeString(c.Markdown, outCustom, outLen, 3),\n            VisitResult.Error e => EncodeString(e.Message, outCustom, outLen, 4),\n            _ => 0\n        };\n    }\n\n    private static int EncodeString(string text, IntPtr outCustom, IntPtr outLen, int code)\n    {\n        var bytes = System.Text.Encoding.UTF8.GetBytes(text);\n        var buf = Marshal.AllocHGlobal(bytes.Length + 1);\n        Marshal.Copy(bytes, 0, buf, bytes.Length);\n        Marshal.WriteByte(buf, bytes.Length, 0);\n        Marshal.WriteIntPtr(outCustom, buf);\n        Marshal.WriteInt64(outLen, (long)bytes.Length);\n        return code;\n    }\n\n    public void Dispose()\n    {\n        if (_disposed) return;\n        _disposed = true;\n        Marshal.FreeHGlobal(_nativeStruct);\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/VisitorHandle.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6ccaec0c39ae746641beb4942cfaf8146321552410ee5db149d8c8a8a5618475\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n///\n/// This allows visitors to be passed around and shared while still being mutable.\n/// </summary>\npublic sealed class VisitorHandle : IDisposable\n{\n    internal IntPtr Handle { get; }\n\n    internal VisitorHandle(IntPtr handle)\n    {\n        Handle = handle;\n    }\n\n    public void Dispose()\n    {\n        // Native free will be called by the runtime\n    }\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/WarningKind.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:260f3dab338a71a37cb07b0aa5cecf6700a29bae3c031565f7d32310c02fb03d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Categories of processing warnings.\n/// </summary>\npublic enum WarningKind\n{\n    /// <summary>\n    /// An image could not be extracted (e.g. invalid data URI, unsupported format).\n    /// </summary>\n    [JsonPropertyName(\"imageextractionfailed\")]\n    ImageExtractionFailed,\n    /// <summary>\n    /// The input encoding was not recognized; fell back to UTF-8.\n    /// </summary>\n    [JsonPropertyName(\"encodingfallback\")]\n    EncodingFallback,\n    /// <summary>\n    /// The input was truncated due to size limits.\n    /// </summary>\n    [JsonPropertyName(\"truncatedinput\")]\n    TruncatedInput,\n    /// <summary>\n    /// The HTML was malformed but processing continued with best effort.\n    /// </summary>\n    [JsonPropertyName(\"malformedhtml\")]\n    MalformedHtml,\n    /// <summary>\n    /// Sanitization was applied to remove potentially unsafe content.\n    /// </summary>\n    [JsonPropertyName(\"sanitizationapplied\")]\n    SanitizationApplied,\n    /// <summary>\n    /// DOM traversal was truncated because max_depth was exceeded.\n    /// </summary>\n    [JsonPropertyName(\"depthlimitexceeded\")]\n    DepthLimitExceeded,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown/WhitespaceMode.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bf2cef240f67adda664ba594d4a6c5aea0a06a6425da036985e30e53d6da7ee5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n#nullable enable\n\nusing System.Text.Json.Serialization;\n\nusing System;\nusing System.Text.Json;\n\nnamespace HtmlToMarkdown;\n\n/// <summary>\n/// Whitespace handling strategy during conversion.\n///\n/// Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n/// </summary>\npublic enum WhitespaceMode\n{\n    /// <summary>\n    /// Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior.\n    /// </summary>\n    [JsonPropertyName(\"normalized\")]\n    Normalized,\n    /// <summary>\n    /// Preserve all whitespace exactly as it appears in the HTML.\n    /// </summary>\n    [JsonPropertyName(\"strict\")]\n    Strict,\n}\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown.Tests/HtmlToMarkdown.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.4.0\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"../HtmlToMarkdown/HtmlToMarkdown.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "packages/csharp/HtmlToMarkdown.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>HtmlToMarkdown</RootNamespace>\n    <PackageId>KreuzbergDev.HtmlToMarkdown</PackageId>\n    <Version>3.4.0-rc.25</Version>\n    <Description>High-performance HTML to Markdown converter</Description>\n    <PackageLicenseFile>LICENSE</PackageLicenseFile>\n    <RepositoryUrl>https://github.com/kreuzberg-dev/html-to-markdown</RepositoryUrl>\n    <Authors>Kreuzberg Team</Authors>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"../../LICENSE\" Pack=\"true\" PackagePath=\"/\" />\n    <None Include=\"runtimes/**\" Pack=\"true\" PackagePath=\"runtimes/\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "packages/csharp/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with C#/.NET bindings using P/Invoke to the Rust core.\nProvides type-safe record-based APIs for metadata extraction, visitor patterns, and thread-safe concurrent conversion.\n\n## Installation\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n\nRequires .NET 8.0+ SDK.\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n\n## Performance Snapshot\n\n**Apple M4** · `Convert()` · Real Wikipedia documents\n\n| Document           | Size  | Latency | Throughput |\n| ------------------ | ----- | ------- | ---------- |\n| Lists (Timeline)   | 129KB |         | 392.9 MB/s |\n| Tables (Countries) | 360KB |         | 300.1 MB/s |\n| Mixed (Python)     | 656KB |         | 292.3 MB/s |\n\n## Quick Start\n\nBasic conversion:\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Hello World</h1><p>This is a paragraph.</p>\";\nvar result = HtmlToMarkdownConverter.Convert(html);\nConsole.WriteLine(result.Content);\n```\n\nWith conversion options:\n\n```csharp\nusing HtmlToMarkdown;\n\nvar options = new ConversionOptions\n{\n    HeadingStyle = \"atx\",\n    Wrap = true,\n    WrapWidth = 80,\n    ListIndentWidth = 4,\n};\n\nvar html = \"<h1>Hello</h1><p>This is <strong>formatted</strong> content.</p>\";\nvar result = HtmlToMarkdownConverter.Convert(html, options);\nConsole.WriteLine(result.Content);\n```\n\n## API Reference\n\n### Core Function\n\n**`HtmlToMarkdownConverter.Convert(string html, ConversionOptions? options = null) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```csharp\nvar result   = HtmlToMarkdownConverter.Convert(html);\nvar markdown = result.Content;    // Converted Markdown string\nvar metadata = result.Metadata;   // null unless ExtractMetadata = true\nvar tables   = result.Tables;     // empty unless ExtractTables = true\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nvar markdown = Converter.Convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nvar djot = Converter.Convert(html, new ConversionOptions { OutputFormat = \"djot\" });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nvar plain = Converter.Convert(html, new ConversionOptions { OutputFormat = \"plain\" });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **NuGet:** [nuget.org/packages/KreuzbergDev.HtmlToMarkdown](https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/elixir/.credo.exs",
    "content": "%{\n  configs: [\n    %{\n      name: \"default\",\n      files: %{\n        included: [\"lib/\", \"test/\"],\n        excluded: [\"test/support\"]\n      },\n      checks: [\n        # Enable all default checks\n        {Credo.Check.Consistency.ExceptionNames, []},\n        {Credo.Check.Consistency.LineEndings, []},\n        {Credo.Check.Consistency.ParameterPatternMatching, []},\n        {Credo.Check.Consistency.SpaceAroundOperators, []},\n        {Credo.Check.Consistency.SpaceInParentheses, []},\n        {Credo.Check.Consistency.TabsOrSpaces, []},\n\n        # Disable struct field count check for Options module\n        # The HtmlToMarkdown.Options struct has 32 fields matching the Rust API\n        {Credo.Check.Design.AliasUsage, false},\n\n        # Allow longer parameter lists for configuration structs\n        {Credo.Check.Refactor.FunctionArity, [max_arity: 35]},\n\n        # Readability checks\n        {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},\n        {Credo.Check.Readability.ModuleDoc, []},\n        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},\n        {Credo.Check.Readability.Semicolons, []},\n        {Credo.Check.Readability.SpaceAfterCommas, []},\n        {Credo.Check.Readability.TrailingBlankLine, []},\n\n        # Refactoring opportunities\n        {Credo.Check.Refactor.CondStatements, []},\n        {Credo.Check.Refactor.CyclomaticComplexity, []},\n        {Credo.Check.Refactor.NegatedConditionsInUnless, []},\n        {Credo.Check.Refactor.NegatedConditionsWithElse, []},\n        {Credo.Check.Refactor.Nesting, []},\n        {Credo.Check.Refactor.UnlessWithElse, []},\n\n        # Warnings\n        {Credo.Check.Warning.BoolOperationOnSameValues, []},\n        {Credo.Check.Warning.IExPry, []},\n        {Credo.Check.Warning.IoInspect, []},\n        # Allow up to 40 fields in structs (ConversionOptions matches Rust API surface)\n        {Credo.Check.Warning.StructFieldAmount, [max_fields: 40]},\n        {Credo.Check.Warning.UnusedEnumOperation, []},\n        {Credo.Check.Warning.UnusedKeywordOperation, []},\n        {Credo.Check.Warning.UnusedListOperation, []},\n        {Credo.Check.Warning.UnusedPathOperation, []},\n        {Credo.Check.Warning.UnusedRegexOperation, []},\n        {Credo.Check.Warning.UnusedStringOperation, []},\n        {Credo.Check.Warning.UnusedTupleOperation, []}\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/elixir/.formatter.exs",
    "content": "[\n  import_deps: [:rustler],\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  line_length: 120\n]\n"
  },
  {
    "path": "packages/elixir/.gitignore",
    "content": "/_build\n/deps\n/priv\n/native/*.so\n/native/*.dylib\n/native/*.dll\n"
  },
  {
    "path": "packages/elixir/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nElixir bindings for the Rust html-to-markdown engine. The package exposes a fast HTML to Markdown converter implemented with Rustler.\nShip identical Markdown across every runtime while enjoying native performance with Rustler NIF bindings.\n\n## Installation\n\n```bash\nAdd {:html_to_markdown, \"~> 3.0\"} to mix.exs deps\n```\n\nRequires Elixir 1.19+ and OTP 28. Add to your `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:html_to_markdown, \"~> 3.4.0-rc.25\"}\n  ]\nend\n```\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document           | Size  | Latency | Throughput |\n| ------------------ | ----- | ------- | ---------- |\n| Lists (Timeline)   | 129KB |         | 321.7 MB/s |\n| Tables (Countries) | 360KB |         | 293.8 MB/s |\n| Medium (Python)    | 656KB |         | 281.5 MB/s |\n| Large (Rust)       | 567KB |         | 268.7 MB/s |\n| Small (Intro)      | 463KB |         | 262.9 MB/s |\n\n## Quick Start\n\nBasic conversion:\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\")\nIO.puts(result.content)\n```\n\nWith conversion options:\n\n```elixir\nopts = %HtmlToMarkdown.Options{wrap: true, wrap_width: 40}\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\", opts)\nIO.puts(result.content)\n```\n\n## API Reference\n\n### Core Function\n\n**`HtmlToMarkdown.convert(html, options \\\\ nil) :: {:ok, ConversionResult.t()} | {:error, term()}`**\n\nConverts HTML to Markdown. Returns `{:ok, result}` where result is a struct with all results in a single call.\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(html)\nresult.content    # Converted Markdown string\nresult.metadata   # Metadata map (when extract_metadata: true)\nresult.tables     # Table data list (when extract_tables: true)\nresult.document   # Document-level info\nresult.images     # Extracted images\nresult.warnings   # Any conversion warnings\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```elixir\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\n{:ok, markdown} = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\n{:ok, djot} = HtmlToMarkdown.convert(html, %{output_format: \"djot\"})\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```elixir\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n{:ok, plain} = HtmlToMarkdown.convert(html, %{output_format: \"plain\"})\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```elixir\nhtml = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\"\nopts = %HtmlToMarkdown.Options{extract_metadata: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nIO.puts(result.content)                           # Converted Markdown\nIO.inspect(result.metadata[\"document\"][\"title\"])  # Document title\nIO.inspect(result.metadata[\"headers\"])            # All h1-h6 elements\nIO.inspect(result.metadata[\"links\"])              # All hyperlinks\nIO.inspect(result.metadata[\"images\"])             # All images with alt text\nIO.inspect(result.metadata[\"structured_data\"])    # JSON-LD, Microdata, RDFa\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```elixir\ndefmodule MyVisitor do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_ctx, href, text, _title) do\n    # Rewrite CDN URLs\n    href = if String.starts_with?(href, \"https://old-cdn.com\") do\n      String.replace(href, \"https://old-cdn.com\", \"https://new-cdn.com\")\n    else\n      href\n    end\n    {:custom, \"[#{text}](#{href})\"}\n  end\n\n  @impl true\n  def handle_image(_ctx, src, _alt, _title) do\n    # Skip tracking pixels\n    if String.contains?(src, \"tracking\"), do: :skip, else: :continue\n  end\nend\n\nhtml = \"<a href=\\\"https://old-cdn.com/file.pdf\\\">Download</a>\"\nopts = %HtmlToMarkdown.Options{visitor: MyVisitor}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\nresult.content\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Hex.pm:** [hex.pm/packages/html_to_markdown](https://hex.pm/packages/html_to_markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/elixir/checksum-Elixir.HtmlToMarkdown.Native.exs",
    "content": "%{}\n"
  },
  {
    "path": "packages/elixir/config/config.exs",
    "content": "import Config\n\nconfig :html_to_markdown, HtmlToMarkdown.Native,\n  crate: \"html_to_markdown_nif\",\n  path: \"native/html_to_markdown_nif\",\n  mode: if(Mix.env() == :prod, do: :release, else: :debug)\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/annotation_kind.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e507ae64bc0ebd803d753ca2cd0e4d13584a741e6b8c940d95803959ef5d8f07\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.AnnotationKind do\n  @moduledoc \"The type of an inline text annotation.\"\n\n  @type t :: term()\n\n  @type bold :: :bold\n  @type italic :: :italic\n  @type underline :: :underline\n  @type strikethrough :: :strikethrough\n  @type code :: :code\n  @type subscript :: :subscript\n  @type superscript :: :superscript\n  @type highlight :: :highlight\n  @type link :: %{type: :link, url: term(), title: term()}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/code_block_style.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:926f7a30e6183bf8ecd1031f1dc796d57416406ac705e385d9066faab769e491\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.CodeBlockStyle do\n  @moduledoc \"Code block fence style in Markdown output.\"\n\n  @type t :: :indented | :backticks | :tildes\n\n  @indented :indented\n  @backticks :backticks\n  @tildes :tildes\n\n  @spec indented() :: t()\n  def indented, do: @indented\n  @spec backticks() :: t()\n  def backticks, do: @backticks\n  @spec tildes() :: t()\n  def tildes, do: @tildes\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/conversion_options.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:399335f4c7ff81035a14e3286d3c28c6815157b236e9bffc6eb4cd22ebf1e727\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ConversionOptions do\n  @moduledoc \"Main conversion options for HTML to Markdown conversion.\"\n\n  defstruct heading_style: :atx,\n            list_indent_type: :spaces,\n            list_indent_width: 2,\n            bullets: \"-*+\",\n            strong_em_symbol: \"*\",\n            escape_asterisks: false,\n            escape_underscores: false,\n            escape_misc: false,\n            escape_ascii: false,\n            code_language: \"\",\n            autolinks: true,\n            default_title: false,\n            br_in_tables: false,\n            highlight_style: :double_equal,\n            extract_metadata: true,\n            whitespace_mode: :normalized,\n            strip_newlines: false,\n            wrap: false,\n            wrap_width: 80,\n            convert_as_inline: false,\n            sub_symbol: \"\",\n            sup_symbol: \"\",\n            newline_style: :spaces,\n            code_block_style: :backticks,\n            keep_inline_images_in: [],\n            preprocessing: nil,\n            encoding: \"utf-8\",\n            debug: false,\n            strip_tags: [],\n            preserve_tags: [],\n            skip_images: false,\n            link_style: :inline,\n            output_format: :markdown,\n            include_document_structure: false,\n            extract_images: false,\n            max_image_size: 5_242_880,\n            capture_svg: false,\n            infer_dimensions: true,\n            max_depth: nil,\n            exclude_selectors: [],\n            visitor: nil\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/conversion_options_update.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e87868ad7cb0ded93c60bd74e4858c15a9238019b0c5533ac760644046cc2e64\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ConversionOptionsUpdate do\n  @moduledoc \"Partial update for `ConversionOptions`.\"\n\n  defstruct heading_style: :atx,\n            list_indent_type: :spaces,\n            list_indent_width: 0,\n            bullets: \"\",\n            strong_em_symbol: \"\",\n            escape_asterisks: false,\n            escape_underscores: false,\n            escape_misc: false,\n            escape_ascii: false,\n            code_language: \"\",\n            autolinks: false,\n            default_title: false,\n            br_in_tables: false,\n            highlight_style: :double_equal,\n            extract_metadata: false,\n            whitespace_mode: :normalized,\n            strip_newlines: false,\n            wrap: false,\n            wrap_width: 0,\n            convert_as_inline: false,\n            sub_symbol: \"\",\n            sup_symbol: \"\",\n            newline_style: :spaces,\n            code_block_style: :backticks,\n            keep_inline_images_in: [],\n            preprocessing: nil,\n            encoding: \"\",\n            debug: false,\n            strip_tags: [],\n            preserve_tags: [],\n            skip_images: false,\n            link_style: :inline,\n            output_format: :markdown,\n            include_document_structure: false,\n            extract_images: false,\n            max_image_size: 0,\n            capture_svg: false,\n            infer_dimensions: false,\n            max_depth: nil,\n            exclude_selectors: [],\n            visitor: nil\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/conversion_result.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:ff6f3db12ca552179bc0d17075f15379c8f746bb61acd91d4690750b7d40487c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ConversionResult do\n  @moduledoc \"The primary result of HTML conversion and extraction.\"\n\n  defstruct content: \"\",\n            document: nil,\n            metadata: nil,\n            tables: [],\n            images: [],\n            warnings: []\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/document_metadata.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:3ce60c8f8467120bc28a139e39eeb273de03f3c4de9c162bd8f3dc5d0a0be527\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.DocumentMetadata do\n  @moduledoc \"Document-level metadata extracted from `<head>` and top-level elements.\"\n\n  defstruct title: \"\",\n            description: \"\",\n            keywords: [],\n            author: \"\",\n            canonical_url: \"\",\n            base_href: \"\",\n            language: \"\",\n            text_direction: :left_to_right,\n            open_graph: %{},\n            twitter_card: %{},\n            meta_tags: %{}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/document_node.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:735210186d707b357ddb39bc85a34005af68f82c3033251b5f856b7b737a0995\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.DocumentNode do\n  @moduledoc \"A single node in the document tree.\"\n\n  defstruct id: \"\",\n            content: :heading,\n            parent: nil,\n            children: [],\n            annotations: [],\n            attributes: nil\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/document_structure.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:80de70d185c47fc4fe80b38414336086a7748ad77c60822d9c094720aeb259ed\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.DocumentStructure do\n  @moduledoc \"A structured document tree representing the semantic content of an HTML document.\"\n\n  defstruct nodes: [],\n            source_format: nil\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/grid_cell.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6ecfdf008d50129f8581e9d1da2b92f93bc0530c4bc1dc739212b1071225fa57\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.GridCell do\n  @moduledoc \"A single cell in a table grid.\"\n\n  defstruct content: \"\",\n            row: 0,\n            col: 0,\n            row_span: 0,\n            col_span: 0,\n            is_header: false\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/header_metadata.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f27505744e2a898b811c50175fc5be04a0835d6d19a17ce57c8e77f996ee4361\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.HeaderMetadata do\n  @moduledoc \"Header element metadata with hierarchy tracking.\"\n\n  defstruct level: 0,\n            text: \"\",\n            id: nil,\n            depth: 0,\n            html_offset: 0\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/heading_style.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:4710e43f2e1b5c53a18935f24ed663198d4b833d5814505fff3e805321e7914d\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.HeadingStyle do\n  @moduledoc \"Heading style options for Markdown output.\"\n\n  @type t :: :underlined | :atx | :atx_closed\n\n  @underlined :underlined\n  @atx :atx\n  @atx_closed :atx_closed\n\n  @spec underlined() :: t()\n  def underlined, do: @underlined\n  @spec atx() :: t()\n  def atx, do: @atx\n  @spec atx_closed() :: t()\n  def atx_closed, do: @atx_closed\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/highlight_style.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f62e0526c197e9e2863c51112062bfad8dc11ff74519a94a996decd227a90bdd\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.HighlightStyle do\n  @moduledoc \"Highlight rendering style for `<mark>` elements.\"\n\n  @type t :: :double_equal | :html | :bold | :none\n\n  @double_equal :double_equal\n  @html :html\n  @bold :bold\n  @none :none\n\n  @spec double_equal() :: t()\n  def double_equal, do: @double_equal\n  @spec html() :: t()\n  def html, do: @html\n  @spec bold() :: t()\n  def bold, do: @bold\n  @spec none() :: t()\n  def none, do: @none\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/html_metadata.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:4592612df78c1e97250571947d39958c7a9ee828cb325b3ac84d8053e7473098\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.HtmlMetadata do\n  @moduledoc \"Comprehensive metadata extraction result from HTML document.\"\n\n  defstruct document: nil,\n            headers: [],\n            links: [],\n            images: [],\n            structured_data: []\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/html_visitor_bridge.ex",
    "content": "defmodule HtmlToMarkdownHtmlVisitorBridge do\n  @moduledoc \"\"\"\n  GenServer bridge for HtmlVisitor implementation in html_to_markdown.\n\n  Handles incoming trait method calls from Rust and dispatches them to an implementation module.\n  \"\"\"\n\n  use GenServer\n\n  require Logger\n\n  @doc \"\"\"\n  Start a GenServer linked to the current process.\n\n  impl_module should be a module that implements the HtmlVisitor trait methods.\n  \"\"\"\n  def start_link(impl_module) do\n    GenServer.start_link(__MODULE__, impl_module, name: __MODULE__)\n  end\n\n  @impl GenServer\n  def init(impl_module) do\n    {:ok, impl_module}\n  end\n\n  @doc \"\"\"\n  Handle an incoming trait call message.\n\n  Message format: {:trait_call, method_atom, args_json, reply_id}\n  \"\"\"\n  @impl GenServer\n  def handle_info({:trait_call, method, args_json, reply_id}, impl_module) do\n    try do\n      args = Jason.decode!(args_json)\n\n      # Dispatch to the implementation module\n      result = apply(impl_module, String.to_atom(method), args)\n\n      # Send result back to Rust\n      HtmlToMarkdown.Native.complete_trait_call(reply_id, Jason.encode!(result))\n    rescue\n      e ->\n        Logger.error(\"Error calling {impl_module}.{method}: {Exception.message(e)}\")\n        HtmlToMarkdown.Native.fail_trait_call(reply_id, Exception.message(e))\n    end\n\n    {:noreply, impl_module}\n  end\n\n  @doc \"\"\"\n  Register an implementation module, starting a GenServer to handle trait calls.\n  \"\"\"\n  def register(impl_module) do\n    {:ok, _pid} = start_link(impl_module)\n    HtmlToMarkdown.Native.register_html_visitor(self(), Atom.to_string(impl_module))\n  end\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/image_metadata.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:33e5170b1f64a4c3c0820e6e5c867fe37f8b0e0f83d5e3e794f719f235950840\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ImageMetadata do\n  @moduledoc \"Image metadata with source and dimensions.\"\n\n  defstruct src: \"\",\n            alt: nil,\n            title: nil,\n            dimensions: nil,\n            image_type: :data_uri,\n            attributes: %{}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/image_type.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:dbccb97912ff1ad62ec876e68f584370245d140649cbbda99ba9f09b864930d2\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ImageType do\n  @moduledoc \"Image source classification for proper handling and processing.\"\n\n  @type t :: :data_uri | :inline_svg | :external | :relative\n\n  @data_uri :data_uri\n  @inline_svg :inline_svg\n  @external :external\n  @relative :relative\n\n  @spec data_uri() :: t()\n  def data_uri, do: @data_uri\n  @spec inline_svg() :: t()\n  def inline_svg, do: @inline_svg\n  @spec external() :: t()\n  def external, do: @external\n  @spec relative() :: t()\n  def relative, do: @relative\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/link_metadata.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:cec54cdbde75766b4e33521994cf30ae0041e3c97c2db571d38a5fc719247c82\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.LinkMetadata do\n  @moduledoc \"Hyperlink metadata with categorization and attributes.\"\n\n  defstruct href: \"\",\n            text: \"\",\n            title: nil,\n            link_type: :anchor,\n            rel: [],\n            attributes: %{}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/link_style.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:ee58877c06720605c322b4618cbe104e101c684959b58c6ded91ca395e3ac12a\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.LinkStyle do\n  @moduledoc \"Link rendering style in Markdown output.\"\n\n  @type t :: :inline | :reference\n\n  @inline :inline\n  @reference :reference\n\n  @spec inline() :: t()\n  def inline, do: @inline\n  @spec reference() :: t()\n  def reference, do: @reference\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/link_type.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:83c6be11039d504e4a19c007a409faefa7e859b5c3e748823f23ed50e1dc984d\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.LinkType do\n  @moduledoc \"Link classification based on href value and document context.\"\n\n  @type t :: :anchor | :internal | :external | :email | :phone | :other\n\n  @anchor :anchor\n  @internal :internal\n  @external :external\n  @email :email\n  @phone :phone\n  @other :other\n\n  @spec anchor() :: t()\n  def anchor, do: @anchor\n  @spec internal() :: t()\n  def internal, do: @internal\n  @spec external() :: t()\n  def external, do: @external\n  @spec email() :: t()\n  def email, do: @email\n  @spec phone() :: t()\n  def phone, do: @phone\n  @spec other() :: t()\n  def other, do: @other\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/list_indent_type.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:382ea28911227bc1910652a0ef4c018585a7008484c3d206f66f0da208d09d65\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ListIndentType do\n  @moduledoc \"List indentation character type.\"\n\n  @type t :: :spaces | :tabs\n\n  @spaces :spaces\n  @tabs :tabs\n\n  @spec spaces() :: t()\n  def spaces, do: @spaces\n  @spec tabs() :: t()\n  def tabs, do: @tabs\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/native.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:1fad2a05165213cd657fc016581aed8adbd24051a87673f7c3e98789a25c4686\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.Native do\n  @moduledoc false\n\n  use RustlerPrecompiled,\n    otp_app: :html_to_markdown,\n    crate: \"html_to_markdown_nif\",\n    base_url:\n      \"https://github.com/kreuzberg-dev/html-to-markdown/releases/download/v#{Mix.Project.config()[:version]}\",\n    version: Mix.Project.config()[:version],\n    force_build:\n      System.get_env(\"HTML_TO_MARKDOWN_BUILD\") in [\"1\", \"true\"] or\n        Mix.env() in [:test, :dev],\n    targets: ~w(aarch64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu x86_64-pc-windows-gnu),\n    nif_versions: [\"2.16\", \"2.17\"]\n\n  def convert(_html, _options), do: :erlang.nif_error(:nif_not_loaded)\n  def convert_with_visitor(_html, _options, _visitor), do: :erlang.nif_error(:nif_not_loaded)\n  def visitor_reply(_ref_id, _result), do: :erlang.nif_error(:nif_not_loaded)\n  def headermetadata_is_valid(_obj), do: :erlang.nif_error(:nif_not_loaded)\n  def linkmetadata_classify_link(_href), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptions_default, do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptions_builder, do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptions_apply_update(_obj, _update), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptions_from_update(_update), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptions_from(_update), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_strip_tags(_obj, _tags), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_preserve_tags(_obj, _tags), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_keep_inline_images_in(_obj, _tags), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_exclude_selectors(_obj, _selectors), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_visitor(_obj, _visitor), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_preprocessing(_obj, _preprocessing), do: :erlang.nif_error(:nif_not_loaded)\n  def conversionoptionsbuilder_build(_obj), do: :erlang.nif_error(:nif_not_loaded)\n  def preprocessingoptions_default, do: :erlang.nif_error(:nif_not_loaded)\n  def preprocessingoptions_apply_update(_obj, _update), do: :erlang.nif_error(:nif_not_loaded)\n  def preprocessingoptions_from_update(_update), do: :erlang.nif_error(:nif_not_loaded)\n  def preprocessingoptions_from(_update), do: :erlang.nif_error(:nif_not_loaded)\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/newline_style.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:bf6ec1961d192373d942af8d11e3ee1e3188a492e5ed17fd9dede441741f7ca3\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.NewlineStyle do\n  @moduledoc \"Line break syntax in Markdown output.\"\n\n  @type t :: :spaces | :backslash\n\n  @spaces :spaces\n  @backslash :backslash\n\n  @spec spaces() :: t()\n  def spaces, do: @spaces\n  @spec backslash() :: t()\n  def backslash, do: @backslash\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/node_content.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:c07926f2d43fc63f3a26d701f0fda9370e68e0ff1f092b15a6abb1a77779f957\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.NodeContent do\n  @moduledoc \"The semantic content type of a document node.\"\n\n  @type t :: term()\n\n  @type heading :: %{type: :heading, level: term(), text: term()}\n  @type paragraph :: %{type: :paragraph, text: term()}\n  @type list_variant :: %{type: :list, ordered: term()}\n  @type list_item :: %{type: :list_item, text: term()}\n  @type table :: %{type: :table, grid: term()}\n  @type image :: %{type: :image, description: term(), src: term(), image_index: term()}\n  @type code :: %{type: :code, text: term(), language: term()}\n  @type quote :: :quote\n  @type definition_list :: :definition_list\n  @type definition_item :: %{type: :definition_item, term: term(), definition: term()}\n  @type raw_block :: %{type: :raw_block, format: term(), content: term()}\n  @type metadata_block :: %{type: :metadata_block, entries: term()}\n  @type group :: %{type: :group, label: term(), heading_level: term(), heading_text: term()}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/node_context.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:9a63da8e3857ed1025c8f55ee96ea30bc25c1983108cf71b089b39c57737eaa6\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.NodeContext do\n  @moduledoc \"Context information passed to all visitor methods.\"\n\n  defstruct node_type: :text,\n            tag_name: \"\",\n            attributes: %{},\n            depth: 0,\n            index_in_parent: 0,\n            parent_tag: nil,\n            is_inline: false\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/node_type.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:6220d32de3f426b21c7608d33a8cccd62be77c20243f77d39ce91fa6e1c454ae\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.NodeType do\n  @moduledoc \"Node type enumeration covering all HTML element types.\"\n\n  @type t ::\n          :text\n          | :element\n          | :heading\n          | :paragraph\n          | :div\n          | :blockquote\n          | :pre\n          | :hr\n          | :list\n          | :list_item\n          | :definition_list\n          | :definition_term\n          | :definition_description\n          | :table\n          | :table_row\n          | :table_cell\n          | :table_header\n          | :table_body\n          | :table_head\n          | :table_foot\n          | :link\n          | :image\n          | :strong\n          | :em\n          | :code\n          | :strikethrough\n          | :underline\n          | :subscript\n          | :superscript\n          | :mark\n          | :small\n          | :br\n          | :span\n          | :article\n          | :section\n          | :nav\n          | :aside\n          | :header\n          | :footer\n          | :main\n          | :figure\n          | :figcaption\n          | :time\n          | :details\n          | :summary\n          | :form\n          | :input\n          | :select\n          | :option\n          | :button\n          | :textarea\n          | :label\n          | :fieldset\n          | :legend\n          | :audio\n          | :video\n          | :picture\n          | :source\n          | :iframe\n          | :svg\n          | :canvas\n          | :ruby\n          | :rt\n          | :rp\n          | :abbr\n          | :kbd\n          | :samp\n          | :var\n          | :cite\n          | :q\n          | :del\n          | :ins\n          | :data\n          | :meter\n          | :progress\n          | :output\n          | :template\n          | :slot\n          | :html\n          | :head\n          | :body\n          | :title\n          | :meta\n          | :link_tag\n          | :style\n          | :script\n          | :base\n          | :custom\n\n  @text :text\n  @element :element\n  @heading :heading\n  @paragraph :paragraph\n  @div :div\n  @blockquote :blockquote\n  @pre :pre\n  @hr :hr\n  @list :list\n  @list_item :list_item\n  @definition_list :definition_list\n  @definition_term :definition_term\n  @definition_description :definition_description\n  @table :table\n  @table_row :table_row\n  @table_cell :table_cell\n  @table_header :table_header\n  @table_body :table_body\n  @table_head :table_head\n  @table_foot :table_foot\n  @link :link\n  @image :image\n  @strong :strong\n  @em :em\n  @code :code\n  @strikethrough :strikethrough\n  @underline :underline\n  @subscript :subscript\n  @superscript :superscript\n  @mark :mark\n  @small :small\n  @br :br\n  @span :span\n  @article :article\n  @section :section\n  @nav :nav\n  @aside :aside\n  @header :header\n  @footer :footer\n  @main :main\n  @figure :figure\n  @figcaption :figcaption\n  @time :time\n  @details :details\n  @summary :summary\n  @form :form\n  @input :input\n  @select :select\n  @option :option\n  @button :button\n  @textarea :textarea\n  @label :label\n  @fieldset :fieldset\n  @legend :legend\n  @audio :audio\n  @video :video\n  @picture :picture\n  @source :source\n  @iframe :iframe\n  @svg :svg\n  @canvas :canvas\n  @ruby :ruby\n  @rt :rt\n  @rp :rp\n  @abbr :abbr\n  @kbd :kbd\n  @samp :samp\n  @var :var\n  @cite :cite\n  @q :q\n  @del :del\n  @ins :ins\n  @data :data\n  @meter :meter\n  @progress :progress\n  @output :output\n  @template :template\n  @slot :slot\n  @html :html\n  @head :head\n  @body :body\n  @title :title\n  @meta :meta\n  @link_tag :link_tag\n  @style :style\n  @script :script\n  @base :base\n  @custom :custom\n\n  @spec text() :: t()\n  def text, do: @text\n  @spec element() :: t()\n  def element, do: @element\n  @spec heading() :: t()\n  def heading, do: @heading\n  @spec paragraph() :: t()\n  def paragraph, do: @paragraph\n  @spec div() :: t()\n  def div, do: @div\n  @spec blockquote() :: t()\n  def blockquote, do: @blockquote\n  @spec pre() :: t()\n  def pre, do: @pre\n  @spec hr() :: t()\n  def hr, do: @hr\n  @spec list() :: t()\n  def list, do: @list\n  @spec list_item() :: t()\n  def list_item, do: @list_item\n  @spec definition_list() :: t()\n  def definition_list, do: @definition_list\n  @spec definition_term() :: t()\n  def definition_term, do: @definition_term\n  @spec definition_description() :: t()\n  def definition_description, do: @definition_description\n  @spec table() :: t()\n  def table, do: @table\n  @spec table_row() :: t()\n  def table_row, do: @table_row\n  @spec table_cell() :: t()\n  def table_cell, do: @table_cell\n  @spec table_header() :: t()\n  def table_header, do: @table_header\n  @spec table_body() :: t()\n  def table_body, do: @table_body\n  @spec table_head() :: t()\n  def table_head, do: @table_head\n  @spec table_foot() :: t()\n  def table_foot, do: @table_foot\n  @spec link() :: t()\n  def link, do: @link\n  @spec image() :: t()\n  def image, do: @image\n  @spec strong() :: t()\n  def strong, do: @strong\n  @spec em() :: t()\n  def em, do: @em\n  @spec code() :: t()\n  def code, do: @code\n  @spec strikethrough() :: t()\n  def strikethrough, do: @strikethrough\n  @spec underline() :: t()\n  def underline, do: @underline\n  @spec subscript() :: t()\n  def subscript, do: @subscript\n  @spec superscript() :: t()\n  def superscript, do: @superscript\n  @spec mark() :: t()\n  def mark, do: @mark\n  @spec small() :: t()\n  def small, do: @small\n  @spec br() :: t()\n  def br, do: @br\n  @spec span() :: t()\n  def span, do: @span\n  @spec article() :: t()\n  def article, do: @article\n  @spec section() :: t()\n  def section, do: @section\n  @spec nav() :: t()\n  def nav, do: @nav\n  @spec aside() :: t()\n  def aside, do: @aside\n  @spec header() :: t()\n  def header, do: @header\n  @spec footer() :: t()\n  def footer, do: @footer\n  @spec main() :: t()\n  def main, do: @main\n  @spec figure() :: t()\n  def figure, do: @figure\n  @spec figcaption() :: t()\n  def figcaption, do: @figcaption\n  @spec time() :: t()\n  def time, do: @time\n  @spec details() :: t()\n  def details, do: @details\n  @spec summary() :: t()\n  def summary, do: @summary\n  @spec form() :: t()\n  def form, do: @form\n  @spec input() :: t()\n  def input, do: @input\n  @spec select() :: t()\n  def select, do: @select\n  @spec option() :: t()\n  def option, do: @option\n  @spec button() :: t()\n  def button, do: @button\n  @spec textarea() :: t()\n  def textarea, do: @textarea\n  @spec label() :: t()\n  def label, do: @label\n  @spec fieldset() :: t()\n  def fieldset, do: @fieldset\n  @spec legend() :: t()\n  def legend, do: @legend\n  @spec audio() :: t()\n  def audio, do: @audio\n  @spec video() :: t()\n  def video, do: @video\n  @spec picture() :: t()\n  def picture, do: @picture\n  @spec source() :: t()\n  def source, do: @source\n  @spec iframe() :: t()\n  def iframe, do: @iframe\n  @spec svg() :: t()\n  def svg, do: @svg\n  @spec canvas() :: t()\n  def canvas, do: @canvas\n  @spec ruby() :: t()\n  def ruby, do: @ruby\n  @spec rt() :: t()\n  def rt, do: @rt\n  @spec rp() :: t()\n  def rp, do: @rp\n  @spec abbr() :: t()\n  def abbr, do: @abbr\n  @spec kbd() :: t()\n  def kbd, do: @kbd\n  @spec samp() :: t()\n  def samp, do: @samp\n  @spec var() :: t()\n  def var, do: @var\n  @spec cite() :: t()\n  def cite, do: @cite\n  @spec q() :: t()\n  def q, do: @q\n  @spec del() :: t()\n  def del, do: @del\n  @spec ins() :: t()\n  def ins, do: @ins\n  @spec data() :: t()\n  def data, do: @data\n  @spec meter() :: t()\n  def meter, do: @meter\n  @spec progress() :: t()\n  def progress, do: @progress\n  @spec output() :: t()\n  def output, do: @output\n  @spec template() :: t()\n  def template, do: @template\n  @spec slot() :: t()\n  def slot, do: @slot\n  @spec html() :: t()\n  def html, do: @html\n  @spec head() :: t()\n  def head, do: @head\n  @spec body() :: t()\n  def body, do: @body\n  @spec title() :: t()\n  def title, do: @title\n  @spec meta() :: t()\n  def meta, do: @meta\n  @spec link_tag() :: t()\n  def link_tag, do: @link_tag\n  @spec style() :: t()\n  def style, do: @style\n  @spec script() :: t()\n  def script, do: @script\n  @spec base() :: t()\n  def base, do: @base\n  @spec custom() :: t()\n  def custom, do: @custom\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/output_format.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:26458fcf75ce5027d5b5c5a01bb1e666be00f69e433eb6b7f0e85b5c8b747fcc\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.OutputFormat do\n  @moduledoc \"Output format for conversion.\"\n\n  @type t :: :markdown | :djot | :plain\n\n  @markdown :markdown\n  @djot :djot\n  @plain :plain\n\n  @spec markdown() :: t()\n  def markdown, do: @markdown\n  @spec djot() :: t()\n  def djot, do: @djot\n  @spec plain() :: t()\n  def plain, do: @plain\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/preprocessing_options.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:1769ff56ca65bcf220a99eb327627b3e61869c99139e11a14269ff8daa8c7c6e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.PreprocessingOptions do\n  @moduledoc \"HTML preprocessing options for document cleanup before conversion.\"\n\n  defstruct enabled: true,\n            preset: :standard,\n            remove_navigation: true,\n            remove_forms: true\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/preprocessing_options_update.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:9c83d8646841d040d2690c215354426f82a855b7781afb3538014e3c5e8442f6\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.PreprocessingOptionsUpdate do\n  @moduledoc \"Partial update for `PreprocessingOptions`.\"\n\n  defstruct enabled: false,\n            preset: :standard,\n            remove_navigation: false,\n            remove_forms: false\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/preprocessing_preset.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:d0d72d1a1ca7980ae62305ce235f570892bf72fde41944b6cf721a94642e922e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.PreprocessingPreset do\n  @moduledoc \"HTML preprocessing aggressiveness level.\"\n\n  @type t :: :minimal | :standard | :aggressive\n\n  @minimal :minimal\n  @standard :standard\n  @aggressive :aggressive\n\n  @spec minimal() :: t()\n  def minimal, do: @minimal\n  @spec standard() :: t()\n  def standard, do: @standard\n  @spec aggressive() :: t()\n  def aggressive, do: @aggressive\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/processing_warning.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:7c299ff645533c508653ec7f0db43057a6f039e7398e1932905ae19b31378ca0\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.ProcessingWarning do\n  @moduledoc \"A non-fatal warning generated during HTML processing.\"\n\n  defstruct message: \"\",\n            kind: :image_extraction_failed\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/structured_data.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:c45ffdc7da968d28f702f1380982fe394816d2ed5ca2a557ff7b2dd54715f93b\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.StructuredData do\n  @moduledoc \"Structured data block (JSON-LD, Microdata, or RDFa).\"\n\n  defstruct data_type: :json_ld,\n            raw_json: \"\",\n            schema_type: nil\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/structured_data_type.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:b90744761097caff35ee0c0e05845c1e7cafa0e395a1c3604a739381bc80f536\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.StructuredDataType do\n  @moduledoc \"Structured data format type.\"\n\n  @type t :: :json_ld | :microdata | :rd_fa\n\n  @json_ld :json_ld\n  @microdata :microdata\n  @rd_fa :rd_fa\n\n  @spec json_ld() :: t()\n  def json_ld, do: @json_ld\n  @spec microdata() :: t()\n  def microdata, do: @microdata\n  @spec rd_fa() :: t()\n  def rd_fa, do: @rd_fa\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/table_data.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:5fc46c3859a4e435bf39b950907fcc47b95290956b24a6eb5d4fd897a4f22f5f\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.TableData do\n  @moduledoc \"A top-level extracted table with both structured data and markdown representation.\"\n\n  defstruct grid: nil,\n            markdown: \"\"\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/table_grid.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e847128892fbf6218508c9c7d1cf37ddc9d2e5e953e2c247a75a5ddd7ecd61ae\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.TableGrid do\n  @moduledoc \"A structured table grid with cell-level data including spans.\"\n\n  defstruct rows: 0,\n            cols: 0,\n            cells: []\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/text_annotation.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:9b653cf9ce52c6a82739c2aac0f87254221bc3bb2febe01b1e3100ab28e7dacc\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.TextAnnotation do\n  @moduledoc \"An inline text annotation with byte-range offsets.\"\n\n  defstruct start: 0,\n            end: 0,\n            kind: :bold\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/text_direction.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:e33d8427de14e694c6695035c439b10ae3d54493356a4d476371a4034128511f\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.TextDirection do\n  @moduledoc \"Text directionality of document content.\"\n\n  @type t :: :left_to_right | :right_to_left | :auto\n\n  @left_to_right :left_to_right\n  @right_to_left :right_to_left\n  @auto :auto\n\n  @spec left_to_right() :: t()\n  def left_to_right, do: @left_to_right\n  @spec right_to_left() :: t()\n  def right_to_left, do: @right_to_left\n  @spec auto() :: t()\n  def auto, do: @auto\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/visit_result.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f6208ae00481d7cdda138382455fa0ffcc1fdab5fec32788e2570ca812968581\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.VisitResult do\n  @moduledoc \"Result of a visitor callback.\"\n\n  @type t :: term()\n\n  @type continue :: :continue\n  @type custom :: %{type: :custom, value_0: term()}\n  @type skip :: :skip\n  @type preserve_html :: :preserve_html\n  @type error :: %{type: :error, value_0: term()}\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/warning_kind.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:00731056a48894114e3af4f2fbd24445774e037a99026a3ef1c7d7558716389d\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.WarningKind do\n  @moduledoc \"Categories of processing warnings.\"\n\n  @type t ::\n          :image_extraction_failed\n          | :encoding_fallback\n          | :truncated_input\n          | :malformed_html\n          | :sanitization_applied\n          | :depth_limit_exceeded\n\n  @image_extraction_failed :image_extraction_failed\n  @encoding_fallback :encoding_fallback\n  @truncated_input :truncated_input\n  @malformed_html :malformed_html\n  @sanitization_applied :sanitization_applied\n  @depth_limit_exceeded :depth_limit_exceeded\n\n  @spec image_extraction_failed() :: t()\n  def image_extraction_failed, do: @image_extraction_failed\n  @spec encoding_fallback() :: t()\n  def encoding_fallback, do: @encoding_fallback\n  @spec truncated_input() :: t()\n  def truncated_input, do: @truncated_input\n  @spec malformed_html() :: t()\n  def malformed_html, do: @malformed_html\n  @spec sanitization_applied() :: t()\n  def sanitization_applied, do: @sanitization_applied\n  @spec depth_limit_exceeded() :: t()\n  def depth_limit_exceeded, do: @depth_limit_exceeded\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown/whitespace_mode.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:dde2509cbad855c85abfa243b2ddeb4266633ef0f9622f435e3205d42f4dd4bf\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown.WhitespaceMode do\n  @moduledoc \"Whitespace handling strategy during conversion.\"\n\n  @type t :: :normalized | :strict\n\n  @normalized :normalized\n  @strict :strict\n\n  @spec normalized() :: t()\n  def normalized, do: @normalized\n  @spec strict() :: t()\n  def strict, do: @strict\nend\n"
  },
  {
    "path": "packages/elixir/lib/html_to_markdown.ex",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f575e2ec624371331608a05332feb176d52e8e09821c2244b8b1f5ecd1a5468c\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\ndefmodule HtmlToMarkdown do\n  @moduledoc \"High-level API for html_to_markdown.\"\n\n  @doc \"Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\"\n  @spec convert(String.t()) :: {:ok, String.t() | nil} | {:error, String.t()}\n  def convert(html) do\n    HtmlToMarkdown.Native.convert(html, nil)\n  end\n\n  @doc \"Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\"\n  @spec convert(String.t(), String.t() | nil) :: {:ok, String.t() | nil} | {:error, String.t()}\n  def convert(html, options) when is_map(options) do\n    {visitor, clean_opts} = Map.pop(options, :visitor)\n    if is_map(visitor) do\n      :ok = HtmlToMarkdown.Native.convert_with_visitor(html, if map_size(clean_opts) == 0, do: nil, else: Jason.encode!(clean_opts), visitor)\n      do_visitor_receive_loop(visitor)\n    else\n      HtmlToMarkdown.Native.convert(html, if map_size(options) == 0, do: nil, else: Jason.encode!(options))\n    end\n  end\n\n  @doc false\n  defp do_visitor_receive_loop(visitor) do\n    receive do\n      {:visitor_callback, ref_id, callback_name, args_json} ->\n        result =\n          case Map.get(visitor, callback_name) do\n            nil -> \"continue\"\n            fun -> apply_visitor_callback(fun, args_json)\n          end\n\n        HtmlToMarkdown.Native.visitor_reply(ref_id, result)\n        do_visitor_receive_loop(visitor)\n\n      {:ok, result} ->\n        {:ok, result}\n\n      {:error, reason} ->\n        {:error, reason}\n    after\n      30_000 ->\n        {:error, \"visitor callback timeout after 30s\"}\n    end\n  end\n\n  @doc false\n  defp apply_visitor_callback(fun, args_json) do\n    args = Jason.decode!(args_json)\n    result = fun.(args)\n    if is_binary(result), do: result, else: \"continue\"\n  end\n\n  @doc \"Validate that the header level is within valid range (1-6).\"\n  @spec headermetadata_is_valid(map()) :: boolean()\n  def headermetadata_is_valid(obj) do\n    HtmlToMarkdown.Native.headermetadata_is_valid(obj)\n  end\n\n  @doc \"Classify a link based on href value.\"\n  @spec linkmetadata_classify_link(String.t()) :: map()\n  def linkmetadata_classify_link(href) do\n    HtmlToMarkdown.Native.linkmetadata_classify_link(href)\n  end\n\n  @doc \"Method\"\n  @spec conversionoptions_default() :: String.t() | nil\n  def conversionoptions_default do\n    HtmlToMarkdown.Native.conversionoptions_default()\n  end\n\n  @doc \"Create a new builder with default values.\"\n  @spec conversionoptions_builder() :: reference()\n  def conversionoptions_builder do\n    HtmlToMarkdown.Native.conversionoptions_builder()\n  end\n\n  @doc \"Apply a partial update to these conversion options.\"\n  @spec conversionoptions_apply_update(map(), String.t() | nil) :: nil\n  def conversionoptions_apply_update(obj, update) do\n    HtmlToMarkdown.Native.conversionoptions_apply_update(obj, update)\n  end\n\n  @doc \"Create from a partial update, applying to defaults.\"\n  @spec conversionoptions_from_update(String.t() | nil) :: String.t() | nil\n  def conversionoptions_from_update(update) do\n    HtmlToMarkdown.Native.conversionoptions_from_update(update)\n  end\n\n  @doc \"Method\"\n  @spec conversionoptions_from(String.t() | nil) :: String.t() | nil\n  def conversionoptions_from(update) do\n    HtmlToMarkdown.Native.conversionoptions_from(update)\n  end\n\n  @doc \"Set the list of HTML tag names whose content is stripped from output.\"\n  @spec conversionoptionsbuilder_strip_tags(map(), [String.t()]) :: reference()\n  def conversionoptionsbuilder_strip_tags(obj, tags) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_strip_tags(obj, tags)\n  end\n\n  @doc \"Set the list of HTML tag names that are preserved verbatim in output.\"\n  @spec conversionoptionsbuilder_preserve_tags(map(), [String.t()]) :: reference()\n  def conversionoptionsbuilder_preserve_tags(obj, tags) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_preserve_tags(obj, tags)\n  end\n\n  @doc \"Set the list of HTML tag names whose `<img>` children are kept inline.\"\n  @spec conversionoptionsbuilder_keep_inline_images_in(map(), [String.t()]) :: reference()\n  def conversionoptionsbuilder_keep_inline_images_in(obj, tags) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_keep_inline_images_in(obj, tags)\n  end\n\n  @doc \"Set the list of CSS selectors for elements to exclude entirely from output.\"\n  @spec conversionoptionsbuilder_exclude_selectors(map(), [String.t()]) :: reference()\n  def conversionoptionsbuilder_exclude_selectors(obj, selectors) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_exclude_selectors(obj, selectors)\n  end\n\n  @doc \"Set the visitor used during conversion.\"\n  @spec conversionoptionsbuilder_visitor(map(), reference() | nil) :: reference()\n  def conversionoptionsbuilder_visitor(obj, visitor) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_visitor(obj, visitor)\n  end\n\n  @doc \"Set the pre-processing options applied to the HTML before conversion.\"\n  @spec conversionoptionsbuilder_preprocessing(map(), String.t() | nil) :: reference()\n  def conversionoptionsbuilder_preprocessing(obj, preprocessing) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_preprocessing(obj, preprocessing)\n  end\n\n  @doc \"Build the final [`ConversionOptions`].\"\n  @spec conversionoptionsbuilder_build(map()) :: String.t() | nil\n  def conversionoptionsbuilder_build(obj) do\n    HtmlToMarkdown.Native.conversionoptionsbuilder_build(obj)\n  end\n\n  @doc \"Method\"\n  @spec preprocessingoptions_default() :: String.t() | nil\n  def preprocessingoptions_default do\n    HtmlToMarkdown.Native.preprocessingoptions_default()\n  end\n\n  @doc \"Apply a partial update to these preprocessing options.\"\n  @spec preprocessingoptions_apply_update(map(), String.t() | nil) :: nil\n  def preprocessingoptions_apply_update(obj, update) do\n    HtmlToMarkdown.Native.preprocessingoptions_apply_update(obj, update)\n  end\n\n  @doc \"Create new preprocessing options from a partial update.\"\n  @spec preprocessingoptions_from_update(String.t() | nil) :: String.t() | nil\n  def preprocessingoptions_from_update(update) do\n    HtmlToMarkdown.Native.preprocessingoptions_from_update(update)\n  end\n\n  @doc \"Method\"\n  @spec preprocessingoptions_from(String.t() | nil) :: String.t() | nil\n  def preprocessingoptions_from(update) do\n    HtmlToMarkdown.Native.preprocessingoptions_from(update)\n  end\nend\n"
  },
  {
    "path": "packages/elixir/mix.exs",
    "content": "defmodule Html_to_markdown.MixProject do\n  use Mix.Project\n\n  @version \"3.4.0-rc.25\"\n  @source_url \"https://github.com/kreuzberg-dev/html-to-markdown\"\n\n  def project do\n    [\n      app: :html_to_markdown,\n      version: @version,\n      elixir: \"~> 1.14\",\n      description: \"High-performance HTML to Markdown converter\",\n      package: package(),\n      deps: deps(),\n      source_url: @source_url\n    ]\n  end\n\n  defp package do\n    [\n      licenses: [\"MIT\"],\n      links: %{\"GitHub\" => @source_url},\n      files: ~w(\n        lib\n        mix.exs\n        README.md\n        .formatter.exs\n        checksum-Elixir.HtmlToMarkdown.Native.exs\n      )\n    ]\n  end\n\n  defp deps do\n    [\n      {:rustler, \"~> 0.34\", optional: true, runtime: false},\n      {:rustler_precompiled, \"~> 0.9\"},\n      {:credo, \"~> 1.7\", only: [:dev, :test], runtime: false},\n      {:ex_doc, \"~> 0.40\", only: :dev, runtime: false}\n    ]\n  end\nend\n"
  },
  {
    "path": "packages/elixir/native/html_to_markdown_nif/Cargo.toml",
    "content": "[package]\nname = \"html_to_markdown_nif\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\"]\n\n[package.metadata.cargo-machete]\nignored = [\"async-trait\", \"tokio\"]\n\n[lib]\nname = \"html_to_markdown_nif\"\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../../../../crates/html-to-markdown\", features = [\n  \"full\",\n  \"metadata\",\n  \"visitor\",\n  \"serde\",\n  \"inline-images\",\n] }\nrustler = \"0.37\"\nasync-trait = \"0.1\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"sync\"] }\n"
  },
  {
    "path": "packages/elixir/native/html_to_markdown_nif/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:637468979147a6062efd722b2b068fbabad42cf70aeff17d4a44f68294bb448f\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unused_unit,\n    clippy::unnecessary_cast,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions\n)]\n\nuse rustler::Encoder;\nuse rustler::ResourceArc;\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::sync::Mutex;\nuse std::sync::atomic::{AtomicU64, Ordering};\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct DocumentMetadata {\n    pub title: Option<String>,\n    pub description: Option<String>,\n    pub keywords: Vec<String>,\n    pub author: Option<String>,\n    pub canonical_url: Option<String>,\n    pub base_href: Option<String>,\n    pub language: Option<String>,\n    pub text_direction: Option<TextDirection>,\n    pub open_graph: String,\n    pub twitter_card: String,\n    pub meta_tags: String,\n}\n\nimpl DocumentMetadata {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            title: opts.get(\"title\").and_then(|t| t.decode().ok()),\n            description: opts.get(\"description\").and_then(|t| t.decode().ok()),\n            keywords: opts.get(\"keywords\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            author: opts.get(\"author\").and_then(|t| t.decode().ok()),\n            canonical_url: opts.get(\"canonical_url\").and_then(|t| t.decode().ok()),\n            base_href: opts.get(\"base_href\").and_then(|t| t.decode().ok()),\n            language: opts.get(\"language\").and_then(|t| t.decode().ok()),\n            text_direction: opts.get(\"text_direction\").and_then(|t| t.decode().ok()),\n            open_graph: opts.get(\"open_graph\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            twitter_card: opts\n                .get(\"twitter_card\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            meta_tags: opts.get(\"meta_tags\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.HeaderMetadata\"]\npub struct HeaderMetadata {\n    pub level: u8,\n    pub text: String,\n    pub id: Option<String>,\n    pub depth: usize,\n    pub html_offset: usize,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.LinkMetadata\"]\npub struct LinkMetadata {\n    pub href: String,\n    pub text: String,\n    pub title: Option<String>,\n    pub link_type: LinkType,\n    pub rel: Vec<String>,\n    pub attributes: String,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.ImageMetadata\"]\npub struct ImageMetadata {\n    pub src: String,\n    pub alt: Option<String>,\n    pub title: Option<String>,\n    pub dimensions: Option<Vec<u32>>,\n    pub image_type: ImageType,\n    pub attributes: String,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.StructuredData\"]\npub struct StructuredData {\n    pub data_type: StructuredDataType,\n    pub raw_json: String,\n    pub schema_type: Option<String>,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct HtmlMetadata {\n    pub document: DocumentMetadata,\n    pub headers: Vec<HeaderMetadata>,\n    pub links: Vec<LinkMetadata>,\n    pub images: Vec<ImageMetadata>,\n    pub structured_data: Vec<StructuredData>,\n}\n\nimpl HtmlMetadata {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            document: opts.get(\"document\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            headers: opts.get(\"headers\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            links: opts.get(\"links\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            images: opts.get(\"images\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            structured_data: opts\n                .get(\"structured_data\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct ConversionOptions {\n    pub heading_style: HeadingStyle,\n    pub list_indent_type: ListIndentType,\n    pub list_indent_width: usize,\n    pub bullets: String,\n    pub strong_em_symbol: String,\n    pub escape_asterisks: bool,\n    pub escape_underscores: bool,\n    pub escape_misc: bool,\n    pub escape_ascii: bool,\n    pub code_language: String,\n    pub autolinks: bool,\n    pub default_title: bool,\n    pub br_in_tables: bool,\n    pub highlight_style: HighlightStyle,\n    pub extract_metadata: bool,\n    pub whitespace_mode: WhitespaceMode,\n    pub strip_newlines: bool,\n    pub wrap: bool,\n    pub wrap_width: usize,\n    pub convert_as_inline: bool,\n    pub sub_symbol: String,\n    pub sup_symbol: String,\n    pub newline_style: NewlineStyle,\n    pub code_block_style: CodeBlockStyle,\n    pub keep_inline_images_in: Vec<String>,\n    pub preprocessing: PreprocessingOptions,\n    pub encoding: String,\n    pub debug: bool,\n    pub strip_tags: Vec<String>,\n    pub preserve_tags: Vec<String>,\n    pub skip_images: bool,\n    pub link_style: LinkStyle,\n    pub output_format: OutputFormat,\n    pub include_document_structure: bool,\n    pub extract_images: bool,\n    pub max_image_size: u64,\n    pub capture_svg: bool,\n    pub infer_dimensions: bool,\n    pub max_depth: Option<usize>,\n    pub exclude_selectors: Vec<String>,\n    pub visitor: Option<VisitorHandle>,\n}\n\nimpl ConversionOptions {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            heading_style: opts\n                .get(\"heading_style\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            list_indent_type: opts\n                .get(\"list_indent_type\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            list_indent_width: opts.get(\"list_indent_width\").and_then(|t| t.decode().ok()).unwrap_or(2),\n            bullets: opts.get(\"bullets\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            strong_em_symbol: opts\n                .get(\"strong_em_symbol\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            escape_asterisks: opts\n                .get(\"escape_asterisks\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            escape_underscores: opts\n                .get(\"escape_underscores\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            escape_misc: opts.get(\"escape_misc\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            escape_ascii: opts.get(\"escape_ascii\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            code_language: opts\n                .get(\"code_language\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            autolinks: opts.get(\"autolinks\").and_then(|t| t.decode().ok()).unwrap_or(true),\n            default_title: opts.get(\"default_title\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            br_in_tables: opts.get(\"br_in_tables\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            highlight_style: opts\n                .get(\"highlight_style\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            extract_metadata: opts\n                .get(\"extract_metadata\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(true),\n            whitespace_mode: opts\n                .get(\"whitespace_mode\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            strip_newlines: opts\n                .get(\"strip_newlines\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            wrap: opts.get(\"wrap\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            wrap_width: opts.get(\"wrap_width\").and_then(|t| t.decode().ok()).unwrap_or(80),\n            convert_as_inline: opts\n                .get(\"convert_as_inline\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            sub_symbol: opts.get(\"sub_symbol\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            sup_symbol: opts.get(\"sup_symbol\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            newline_style: opts\n                .get(\"newline_style\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            code_block_style: opts\n                .get(\"code_block_style\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            keep_inline_images_in: opts\n                .get(\"keep_inline_images_in\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            preprocessing: opts\n                .get(\"preprocessing\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            encoding: opts.get(\"encoding\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            debug: opts.get(\"debug\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            strip_tags: opts.get(\"strip_tags\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            preserve_tags: opts\n                .get(\"preserve_tags\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            skip_images: opts.get(\"skip_images\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            link_style: opts.get(\"link_style\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            output_format: opts\n                .get(\"output_format\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            include_document_structure: opts\n                .get(\"include_document_structure\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            extract_images: opts\n                .get(\"extract_images\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(false),\n            max_image_size: opts\n                .get(\"max_image_size\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(5242880),\n            capture_svg: opts.get(\"capture_svg\").and_then(|t| t.decode().ok()).unwrap_or(false),\n            infer_dimensions: opts\n                .get(\"infer_dimensions\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(true),\n            max_depth: opts.get(\"max_depth\").and_then(|t| t.decode().ok()),\n            exclude_selectors: opts\n                .get(\"exclude_selectors\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or_default(),\n            visitor: opts.get(\"visitor\").and_then(|t| t.decode().ok()),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct ConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\n// SAFETY: See gen_opaque_resource in alef-backend-rustler for rationale.\nimpl std::panic::RefUnwindSafe for ConversionOptionsBuilder {}\n\nimpl rustler::Resource for ConversionOptionsBuilder {}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct ConversionOptionsUpdate {\n    pub heading_style: Option<HeadingStyle>,\n    pub list_indent_type: Option<ListIndentType>,\n    pub list_indent_width: Option<usize>,\n    pub bullets: Option<String>,\n    pub strong_em_symbol: Option<String>,\n    pub escape_asterisks: Option<bool>,\n    pub escape_underscores: Option<bool>,\n    pub escape_misc: Option<bool>,\n    pub escape_ascii: Option<bool>,\n    pub code_language: Option<String>,\n    pub autolinks: Option<bool>,\n    pub default_title: Option<bool>,\n    pub br_in_tables: Option<bool>,\n    pub highlight_style: Option<HighlightStyle>,\n    pub extract_metadata: Option<bool>,\n    pub whitespace_mode: Option<WhitespaceMode>,\n    pub strip_newlines: Option<bool>,\n    pub wrap: Option<bool>,\n    pub wrap_width: Option<usize>,\n    pub convert_as_inline: Option<bool>,\n    pub sub_symbol: Option<String>,\n    pub sup_symbol: Option<String>,\n    pub newline_style: Option<NewlineStyle>,\n    pub code_block_style: Option<CodeBlockStyle>,\n    pub keep_inline_images_in: Option<Vec<String>>,\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    pub encoding: Option<String>,\n    pub debug: Option<bool>,\n    pub strip_tags: Option<Vec<String>>,\n    pub preserve_tags: Option<Vec<String>>,\n    pub skip_images: Option<bool>,\n    pub link_style: Option<LinkStyle>,\n    pub output_format: Option<OutputFormat>,\n    pub include_document_structure: Option<bool>,\n    pub extract_images: Option<bool>,\n    pub max_image_size: Option<u64>,\n    pub capture_svg: Option<bool>,\n    pub infer_dimensions: Option<bool>,\n    pub max_depth: Option<usize>,\n    pub exclude_selectors: Option<Vec<String>>,\n    pub visitor: Option<VisitorHandle>,\n}\n\nimpl ConversionOptionsUpdate {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            heading_style: opts.get(\"heading_style\").and_then(|t| t.decode().ok()),\n            list_indent_type: opts.get(\"list_indent_type\").and_then(|t| t.decode().ok()),\n            list_indent_width: opts.get(\"list_indent_width\").and_then(|t| t.decode().ok()),\n            bullets: opts.get(\"bullets\").and_then(|t| t.decode().ok()),\n            strong_em_symbol: opts.get(\"strong_em_symbol\").and_then(|t| t.decode().ok()),\n            escape_asterisks: opts.get(\"escape_asterisks\").and_then(|t| t.decode().ok()),\n            escape_underscores: opts.get(\"escape_underscores\").and_then(|t| t.decode().ok()),\n            escape_misc: opts.get(\"escape_misc\").and_then(|t| t.decode().ok()),\n            escape_ascii: opts.get(\"escape_ascii\").and_then(|t| t.decode().ok()),\n            code_language: opts.get(\"code_language\").and_then(|t| t.decode().ok()),\n            autolinks: opts.get(\"autolinks\").and_then(|t| t.decode().ok()),\n            default_title: opts.get(\"default_title\").and_then(|t| t.decode().ok()),\n            br_in_tables: opts.get(\"br_in_tables\").and_then(|t| t.decode().ok()),\n            highlight_style: opts.get(\"highlight_style\").and_then(|t| t.decode().ok()),\n            extract_metadata: opts.get(\"extract_metadata\").and_then(|t| t.decode().ok()),\n            whitespace_mode: opts.get(\"whitespace_mode\").and_then(|t| t.decode().ok()),\n            strip_newlines: opts.get(\"strip_newlines\").and_then(|t| t.decode().ok()),\n            wrap: opts.get(\"wrap\").and_then(|t| t.decode().ok()),\n            wrap_width: opts.get(\"wrap_width\").and_then(|t| t.decode().ok()),\n            convert_as_inline: opts.get(\"convert_as_inline\").and_then(|t| t.decode().ok()),\n            sub_symbol: opts.get(\"sub_symbol\").and_then(|t| t.decode().ok()),\n            sup_symbol: opts.get(\"sup_symbol\").and_then(|t| t.decode().ok()),\n            newline_style: opts.get(\"newline_style\").and_then(|t| t.decode().ok()),\n            code_block_style: opts.get(\"code_block_style\").and_then(|t| t.decode().ok()),\n            keep_inline_images_in: opts.get(\"keep_inline_images_in\").and_then(|t| t.decode().ok()),\n            preprocessing: opts.get(\"preprocessing\").and_then(|t| t.decode().ok()),\n            encoding: opts.get(\"encoding\").and_then(|t| t.decode().ok()),\n            debug: opts.get(\"debug\").and_then(|t| t.decode().ok()),\n            strip_tags: opts.get(\"strip_tags\").and_then(|t| t.decode().ok()),\n            preserve_tags: opts.get(\"preserve_tags\").and_then(|t| t.decode().ok()),\n            skip_images: opts.get(\"skip_images\").and_then(|t| t.decode().ok()),\n            link_style: opts.get(\"link_style\").and_then(|t| t.decode().ok()),\n            output_format: opts.get(\"output_format\").and_then(|t| t.decode().ok()),\n            include_document_structure: opts.get(\"include_document_structure\").and_then(|t| t.decode().ok()),\n            extract_images: opts.get(\"extract_images\").and_then(|t| t.decode().ok()),\n            max_image_size: opts.get(\"max_image_size\").and_then(|t| t.decode().ok()),\n            capture_svg: opts.get(\"capture_svg\").and_then(|t| t.decode().ok()),\n            infer_dimensions: opts.get(\"infer_dimensions\").and_then(|t| t.decode().ok()),\n            max_depth: opts.get(\"max_depth\").and_then(|t| t.decode().ok()),\n            exclude_selectors: opts.get(\"exclude_selectors\").and_then(|t| t.decode().ok()),\n            visitor: opts.get(\"visitor\").and_then(|t| t.decode().ok()),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct PreprocessingOptions {\n    pub enabled: bool,\n    pub preset: PreprocessingPreset,\n    pub remove_navigation: bool,\n    pub remove_forms: bool,\n}\n\nimpl PreprocessingOptions {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),\n            preset: opts.get(\"preset\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            remove_navigation: opts\n                .get(\"remove_navigation\")\n                .and_then(|t| t.decode().ok())\n                .unwrap_or(true),\n            remove_forms: opts.get(\"remove_forms\").and_then(|t| t.decode().ok()).unwrap_or(true),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct PreprocessingOptionsUpdate {\n    pub enabled: Option<bool>,\n    pub preset: Option<PreprocessingPreset>,\n    pub remove_navigation: Option<bool>,\n    pub remove_forms: Option<bool>,\n}\n\nimpl PreprocessingOptionsUpdate {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()),\n            preset: opts.get(\"preset\").and_then(|t| t.decode().ok()),\n            remove_navigation: opts.get(\"remove_navigation\").and_then(|t| t.decode().ok()),\n            remove_forms: opts.get(\"remove_forms\").and_then(|t| t.decode().ok()),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.DocumentStructure\"]\npub struct DocumentStructure {\n    pub nodes: Vec<DocumentNode>,\n    pub source_format: Option<String>,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.DocumentNode\"]\npub struct DocumentNode {\n    pub id: String,\n    pub content: NodeContent,\n    pub parent: Option<u32>,\n    pub children: Vec<u32>,\n    pub annotations: Vec<TextAnnotation>,\n    pub attributes: Option<String>,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.TextAnnotation\"]\npub struct TextAnnotation {\n    pub start: u32,\n    pub end: u32,\n    pub kind: AnnotationKind,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct ConversionResult {\n    pub content: Option<String>,\n    pub document: Option<DocumentStructure>,\n    pub metadata: HtmlMetadata,\n    pub tables: Vec<TableData>,\n    pub images: Vec<String>,\n    pub warnings: Vec<ProcessingWarning>,\n}\n\nimpl ConversionResult {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            content: opts.get(\"content\").and_then(|t| t.decode().ok()),\n            document: opts.get(\"document\").and_then(|t| t.decode().ok()),\n            metadata: opts.get(\"metadata\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            tables: opts.get(\"tables\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            images: opts.get(\"images\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            warnings: opts.get(\"warnings\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]\npub struct TableGrid {\n    pub rows: u32,\n    pub cols: u32,\n    pub cells: Vec<GridCell>,\n}\n\nimpl TableGrid {\n    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {\n        Self {\n            rows: opts.get(\"rows\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            cols: opts.get(\"cols\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n            cells: opts.get(\"cells\").and_then(|t| t.decode().ok()).unwrap_or_default(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.GridCell\"]\npub struct GridCell {\n    pub content: String,\n    pub row: u32,\n    pub col: u32,\n    pub row_span: u32,\n    pub col_span: u32,\n    pub is_header: bool,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.TableData\"]\npub struct TableData {\n    pub grid: TableGrid,\n    pub markdown: String,\n}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.ProcessingWarning\"]\npub struct ProcessingWarning {\n    pub message: String,\n    pub kind: WarningKind,\n}\n\n#[derive(Clone)]\npub struct VisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n// SAFETY: See gen_opaque_resource in alef-backend-rustler for rationale.\nimpl std::panic::RefUnwindSafe for VisitorHandle {}\n\nimpl rustler::Resource for VisitorHandle {}\n\n#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]\n#[module = \"HtmlToMarkdown.NodeContext\"]\npub struct NodeContext {\n    pub node_type: NodeType,\n    pub tag_name: String,\n    pub attributes: String,\n    pub depth: usize,\n    pub index_in_parent: usize,\n    pub parent_tag: Option<String>,\n    pub is_inline: bool,\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum TextDirection {\n    LeftToRight,\n    RightToLeft,\n    Auto,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for TextDirection {\n    fn default() -> Self {\n        Self::LeftToRight\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum LinkType {\n    Anchor,\n    Internal,\n    External,\n    Email,\n    Phone,\n    Other,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for LinkType {\n    fn default() -> Self {\n        Self::Anchor\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum ImageType {\n    DataUri,\n    InlineSvg,\n    External,\n    Relative,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for ImageType {\n    fn default() -> Self {\n        Self::DataUri\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum StructuredDataType {\n    JsonLd,\n    Microdata,\n    RDFa,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for StructuredDataType {\n    fn default() -> Self {\n        Self::JsonLd\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum PreprocessingPreset {\n    Minimal,\n    Standard,\n    Aggressive,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for PreprocessingPreset {\n    fn default() -> Self {\n        Self::Minimal\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum HeadingStyle {\n    Underlined,\n    Atx,\n    AtxClosed,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for HeadingStyle {\n    fn default() -> Self {\n        Self::Underlined\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum ListIndentType {\n    Spaces,\n    Tabs,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for ListIndentType {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum WhitespaceMode {\n    Normalized,\n    Strict,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WhitespaceMode {\n    fn default() -> Self {\n        Self::Normalized\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum NewlineStyle {\n    Spaces,\n    Backslash,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for NewlineStyle {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum CodeBlockStyle {\n    Indented,\n    Backticks,\n    Tildes,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for CodeBlockStyle {\n    fn default() -> Self {\n        Self::Indented\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum HighlightStyle {\n    DoubleEqual,\n    Html,\n    Bold,\n    None,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for HighlightStyle {\n    fn default() -> Self {\n        Self::DoubleEqual\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum LinkStyle {\n    Inline,\n    Reference,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for LinkStyle {\n    fn default() -> Self {\n        Self::Inline\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum OutputFormat {\n    Markdown,\n    Djot,\n    Plain,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for OutputFormat {\n    fn default() -> Self {\n        Self::Markdown\n    }\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]\n#[serde(tag = \"node_type\")]\npub enum NodeContent {\n    Heading {\n        level: u8,\n        text: String,\n    },\n    Paragraph {\n        text: String,\n    },\n    List {\n        ordered: bool,\n    },\n    ListItem {\n        text: String,\n    },\n    Table {\n        grid: TableGrid,\n    },\n    Image {\n        description: Option<String>,\n        src: Option<String>,\n        image_index: Option<u32>,\n    },\n    Code {\n        text: String,\n        language: Option<String>,\n    },\n    Quote,\n    DefinitionList,\n    DefinitionItem {\n        term: String,\n        definition: String,\n    },\n    RawBlock {\n        format: String,\n        content: String,\n    },\n    MetadataBlock {\n        entries: Vec<String>,\n    },\n    Group {\n        label: Option<String>,\n        heading_level: Option<u8>,\n        heading_text: Option<String>,\n    },\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for NodeContent {\n    fn default() -> Self {\n        Self::Heading {\n            level: Default::default(),\n            text: Default::default(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]\n#[serde(tag = \"annotation_type\")]\npub enum AnnotationKind {\n    Bold,\n    Italic,\n    Underline,\n    Strikethrough,\n    Code,\n    Subscript,\n    Superscript,\n    Highlight,\n    Link { url: String, title: Option<String> },\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for AnnotationKind {\n    fn default() -> Self {\n        Self::Bold\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum WarningKind {\n    ImageExtractionFailed,\n    EncodingFallback,\n    TruncatedInput,\n    MalformedHtml,\n    SanitizationApplied,\n    DepthLimitExceeded,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for WarningKind {\n    fn default() -> Self {\n        Self::ImageExtractionFailed\n    }\n}\n\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]\npub enum NodeType {\n    Text,\n    Element,\n    Heading,\n    Paragraph,\n    Div,\n    Blockquote,\n    Pre,\n    Hr,\n    List,\n    ListItem,\n    DefinitionList,\n    DefinitionTerm,\n    DefinitionDescription,\n    Table,\n    TableRow,\n    TableCell,\n    TableHeader,\n    TableBody,\n    TableHead,\n    TableFoot,\n    Link,\n    Image,\n    Strong,\n    Em,\n    Code,\n    Strikethrough,\n    Underline,\n    Subscript,\n    Superscript,\n    Mark,\n    Small,\n    Br,\n    Span,\n    Article,\n    Section,\n    Nav,\n    Aside,\n    Header,\n    Footer,\n    Main,\n    Figure,\n    Figcaption,\n    Time,\n    Details,\n    Summary,\n    Form,\n    Input,\n    Select,\n    Option,\n    Button,\n    Textarea,\n    Label,\n    Fieldset,\n    Legend,\n    Audio,\n    Video,\n    Picture,\n    Source,\n    Iframe,\n    Svg,\n    Canvas,\n    Ruby,\n    Rt,\n    Rp,\n    Abbr,\n    Kbd,\n    Samp,\n    Var,\n    Cite,\n    Q,\n    Del,\n    Ins,\n    Data,\n    Meter,\n    Progress,\n    Output,\n    Template,\n    Slot,\n    Html,\n    Head,\n    Body,\n    Title,\n    Meta,\n    LinkTag,\n    Style,\n    Script,\n    Base,\n    Custom,\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for NodeType {\n    fn default() -> Self {\n        Self::Text\n    }\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]\npub enum VisitResult {\n    Continue,\n    Custom { _0: String },\n    Skip,\n    PreserveHtml,\n    Error { _0: String },\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for VisitResult {\n    fn default() -> Self {\n        Self::Continue\n    }\n}\n\nstatic TRAIT_REPLY_COUNTER: AtomicU64 = AtomicU64::new(1);\n\nstatic TRAIT_REPLY_CHANNELS: std::sync::LazyLock<\n    Mutex<HashMap<u64, tokio::sync::oneshot::Sender<std::result::Result<String, String>>>>,\n> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));\n\n/// Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n/// and warnings.\n///\n/// # Arguments\n///\n/// * `html` — the HTML string to convert.\n/// * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n///   When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n///   attached via the `visitor` field on `ConversionOptions`.\n///\n/// # Example\n///\n/// ```\n/// use html_to_markdown_rs::convert;\n///\n/// let html = \"<h1>Hello World</h1>\";\n/// let result = convert(html, None).unwrap();\n/// assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n/// ```\n///\n/// # Errors\n///\n/// Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n#[rustler::nif]\npub fn convert(html: String, options: Option<String>) -> Result<ConversionResult, String> {\n    let options_core: Option<html_to_markdown_rs::ConversionOptions> = options\n        .map(|s| serde_json::from_str::<html_to_markdown_rs::ConversionOptions>(&s))\n        .transpose()\n        .map_err(|e| e.to_string())?;\n    let result = html_to_markdown_rs::convert(&html, options_core).map_err(|e| e.to_string())?;\n    Ok(result.into())\n}\n\n// Options-field visitor variant: spawns a system thread, sends result as a message.\n#[rustler::nif]\npub fn convert_with_visitor(\n    env: rustler::Env<'_>,\n    html: String,\n    options: Option<String>,\n    visitor: Option<rustler::Term<'_>>,\n) -> Result<(), String> {\n    let pid = env.pid();\n    let options_core: Option<html_to_markdown_rs::ConversionOptions> = options\n        .map(|s| serde_json::from_str::<html_to_markdown_rs::ConversionOptions>(&s).map_err(|e| e.to_string()))\n        .transpose()\n        .map_err(|e| e.to_string())?;\n\n    let visitor_owned_env = rustler::OwnedEnv::new();\n    let visitor_saved = visitor_owned_env.save(visitor.expect(\"visitor present in _with_visitor\"));\n    let options_base = options_core.unwrap_or_default();\n    let html = html.clone();\n\n    std::thread::spawn(move || {\n        let bridge = ElixirHtmlVisitorBridge::new_from_saved(pid, visitor_owned_env, visitor_saved);\n        let visitor_handle: Option<html_to_markdown_rs::visitor::VisitorHandle> =\n            Some(std::rc::Rc::new(std::cell::RefCell::new(bridge)) as html_to_markdown_rs::visitor::VisitorHandle);\n        let mut options_with_visitor = options_base;\n        options_with_visitor.visitor = visitor_handle;\n        let mut result_env = rustler::OwnedEnv::new();\n        let _ = result_env.send_and_clear(&pid, |env| {\n            match html_to_markdown_rs::convert(&html, options_with_visitor) {\n                Ok(val) => {\n                    let result: ConversionResult = val.into();\n                    let ok_atom = rustler::types::atom::Atom::from_str(env, \"ok\").unwrap().to_term(env);\n                    let result_term = result.encode(env);\n                    rustler::types::tuple::make_tuple(env, &[ok_atom, result_term])\n                }\n                Err(e) => {\n                    let err_atom = rustler::types::atom::Atom::from_str(env, \"error\").unwrap().to_term(env);\n                    let reason = e.to_string().encode(env);\n                    rustler::types::tuple::make_tuple(env, &[err_atom, reason])\n                }\n            }\n        });\n    });\n    Ok(())\n}\n\nfn nodecontext_to_elixir_map<'a>(\n    env: rustler::Env<'a>,\n    ctx: &html_to_markdown_rs::visitor::NodeContext,\n) -> rustler::Term<'a> {\n    let mut pairs: Vec<(rustler::Term<'a>, rustler::Term<'a>)> = Vec::new();\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"node_type\")\n            .unwrap()\n            .to_term(env),\n        format!(\"{:?}\", ctx.node_type).encode(env),\n    ));\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"tag_name\")\n            .unwrap()\n            .to_term(env),\n        ctx.tag_name.encode(env),\n    ));\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"depth\").unwrap().to_term(env),\n        (ctx.depth as i64).encode(env),\n    ));\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"index_in_parent\")\n            .unwrap()\n            .to_term(env),\n        (ctx.index_in_parent as i64).encode(env),\n    ));\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"is_inline\")\n            .unwrap()\n            .to_term(env),\n        ctx.is_inline.encode(env),\n    ));\n    let parent_tag_term = match &ctx.parent_tag {\n        Some(s) => s.encode(env),\n        None => rustler::types::atom::Atom::from_str(env, \"nil\").unwrap().to_term(env),\n    };\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"parent_tag\")\n            .unwrap()\n            .to_term(env),\n        parent_tag_term,\n    ));\n    let attrs_pairs: Vec<(rustler::Term<'a>, rustler::Term<'a>)> = ctx\n        .attributes\n        .iter()\n        .map(|(k, v)| (k.encode(env), v.encode(env)))\n        .collect();\n    let attrs_map = rustler::Term::map_from_pairs(env, &attrs_pairs)\n        .unwrap_or_else(|_| rustler::types::atom::Atom::from_str(env, \"nil\").unwrap().to_term(env));\n    pairs.push((\n        rustler::types::atom::Atom::from_str(env, \"attributes\")\n            .unwrap()\n            .to_term(env),\n        attrs_map,\n    ));\n    rustler::Term::map_from_pairs(env, &pairs)\n        .unwrap_or_else(|_| rustler::types::atom::Atom::from_str(env, \"nil\").unwrap().to_term(env))\n}\n\nstatic VISITOR_REPLY_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);\nstatic VISITOR_CHANNELS: std::sync::LazyLock<\n    std::sync::Mutex<std::collections::HashMap<u64, std::sync::mpsc::SyncSender<Option<String>>>>,\n> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));\n\npub struct ElixirHtmlVisitorBridge {\n    caller_pid: rustler::types::LocalPid,\n    visitor_env: rustler::OwnedEnv,\n    visitor_saved: rustler::env::SavedTerm,\n}\n\nimpl std::fmt::Debug for ElixirHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"ElixirHtmlVisitorBridge\")\n    }\n}\n\nimpl ElixirHtmlVisitorBridge {\n    pub fn new(env: rustler::Env<'_>, caller_pid: rustler::types::LocalPid, visitor_term: rustler::Term<'_>) -> Self {\n        let owned = rustler::OwnedEnv::new();\n        let saved = owned.save(visitor_term);\n        Self {\n            caller_pid,\n            visitor_env: owned,\n            visitor_saved: saved,\n        }\n    }\n\n    pub fn new_from_saved(\n        caller_pid: rustler::types::LocalPid,\n        visitor_env: rustler::OwnedEnv,\n        visitor_saved: rustler::env::SavedTerm,\n    ) -> Self {\n        Self {\n            caller_pid,\n            visitor_env,\n            visitor_saved,\n        }\n    }\n}\n\nfn visitor_send_and_wait(bridge: &ElixirHtmlVisitorBridge, callback_name: &str, args_json: String) -> Option<String> {\n    let (tx, rx) = std::sync::mpsc::sync_channel::<Option<String>>(1);\n    let ref_id = VISITOR_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n    VISITOR_CHANNELS.lock().unwrap().insert(ref_id, tx);\n    let pid = bridge.caller_pid;\n    let cb_name = callback_name.to_string();\n    let mut msg_env = rustler::OwnedEnv::new();\n    let _ = msg_env.send_and_clear(&pid, |env| {\n        let tag = rustler::types::atom::Atom::from_str(env, \"visitor_callback\")\n            .unwrap()\n            .to_term(env);\n        let ref_term = ref_id.encode(env);\n        let name_term = rustler::types::atom::Atom::from_str(env, &cb_name)\n            .unwrap()\n            .to_term(env);\n        let args_term = args_json.encode(env);\n        rustler::types::tuple::make_tuple(env, &[tag, ref_term, name_term, args_term])\n    });\n    let result = rx.recv().ok().flatten();\n    VISITOR_CHANNELS.lock().unwrap().remove(&ref_id);\n    result\n}\n\n#[rustler::nif]\npub fn visitor_reply(ref_id: u64, result: Option<String>) {\n    if let Some(tx) = VISITOR_CHANNELS.lock().unwrap().get(&ref_id) {\n        let _ = tx.send(result);\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for ElixirHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_element_start\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_output\".to_string(), serde_json::Value::String(_output.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_element_end\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_text\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_href\".to_string(), serde_json::Value::String(_href.to_string()));\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        args_map.insert(\n            \"_title\".to_string(),\n            match _title {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_link\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_src\".to_string(), serde_json::Value::String(_src.to_string()));\n        args_map.insert(\"_alt\".to_string(), serde_json::Value::String(_alt.to_string()));\n        args_map.insert(\n            \"_title\".to_string(),\n            match _title {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_image\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_level\".to_string(),\n            serde_json::Value::Number(serde_json::Number::from(_level as u64)),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        args_map.insert(\n            \"_id\".to_string(),\n            match _id {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_heading\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_lang\".to_string(),\n            match _lang {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        args_map.insert(\"_code\".to_string(), serde_json::Value::String(_code.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_code_block\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_code\".to_string(), serde_json::Value::String(_code.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_code_inline\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_ordered\".to_string(), serde_json::Value::Bool(_ordered));\n        args_map.insert(\"_marker\".to_string(), serde_json::Value::String(_marker.to_string()));\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_list_item\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_ordered\".to_string(), serde_json::Value::Bool(_ordered));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_list_start\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_ordered\".to_string(), serde_json::Value::Bool(_ordered));\n        args_map.insert(\"_output\".to_string(), serde_json::Value::String(_output.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_list_end\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_table_start\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_cells\".to_string(),\n            serde_json::to_value(_cells).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_is_header\".to_string(), serde_json::Value::Bool(_is_header));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_table_row\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_output\".to_string(), serde_json::Value::String(_output.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_table_end\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_content\".to_string(), serde_json::Value::String(_content.to_string()));\n        args_map.insert(\n            \"_depth\".to_string(),\n            serde_json::Value::Number(serde_json::Number::from(_depth as u64)),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_blockquote\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_strong\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_emphasis\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_strikethrough\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_underline\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_subscript\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_superscript\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_mark\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_line_break\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_horizontal_rule\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_tag_name\".to_string(),\n            serde_json::Value::String(_tag_name.to_string()),\n        );\n        args_map.insert(\"_html\".to_string(), serde_json::Value::String(_html.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_custom_element\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_definition_list_start\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_definition_term\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_definition_description\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_output\".to_string(), serde_json::Value::String(_output.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_definition_list_end\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_action\".to_string(),\n            match _action {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        args_map.insert(\n            \"_method\".to_string(),\n            match _method {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_form\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_input_type\".to_string(),\n            serde_json::Value::String(_input_type.to_string()),\n        );\n        args_map.insert(\n            \"_name\".to_string(),\n            match _name {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        args_map.insert(\n            \"_value\".to_string(),\n            match _value {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_input\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_button\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_src\".to_string(),\n            match _src {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_audio\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_src\".to_string(),\n            match _src {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_video\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\n            \"_src\".to_string(),\n            match _src {\n                Some(s) => serde_json::Value::String(s.to_string()),\n                None => serde_json::Value::Null,\n            },\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_iframe\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_open\".to_string(), serde_json::Value::Bool(_open));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_details\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_summary\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_figure_start\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_text\".to_string(), serde_json::Value::String(_text.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_figcaption\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let mut args_map = serde_json::Map::new();\n        args_map.insert(\n            \"_ctx\".to_string(),\n            serde_json::to_value(_ctx).unwrap_or(serde_json::Value::Null),\n        );\n        args_map.insert(\"_output\".to_string(), serde_json::Value::String(_output.to_string()));\n        let args_json = serde_json::Value::Object(args_map).to_string();\n        let result = visitor_send_and_wait(self, \"visit_figure_end\", args_json);\n        match result {\n            None => html_to_markdown_rs::VisitResult::Continue,\n            Some(s) => match s.to_lowercase().as_str() {\n                \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n            },\n        }\n    }\n}\n\n/// Validate that the header level is within valid range (1-6).\n///\n/// # Returns\n///\n/// `true` if level is 1-6, `false` otherwise.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::HeaderMetadata;\n/// let valid = HeaderMetadata {\n///     level: 3,\n///     text: \"Title\".to_string(),\n///     id: None,\n///     depth: 2,\n///     html_offset: 100,\n/// };\n/// assert!(valid.is_valid());\n///\n/// let invalid = HeaderMetadata {\n///     level: 7,  // Invalid\n///     text: \"Title\".to_string(),\n///     id: None,\n///     depth: 2,\n///     html_offset: 100,\n/// };\n/// assert!(!invalid.is_valid());\n/// ```\n#[rustler::nif]\npub fn headermetadata_is_valid(obj: HeaderMetadata) -> bool {\n    html_to_markdown_rs::HeaderMetadata::from(obj).is_valid()\n}\n\n/// Classify a link based on href value.\n///\n/// # Arguments\n///\n/// * `href` - The href attribute value\n///\n/// # Returns\n///\n/// Appropriate [`LinkType`] based on protocol and content.\n///\n/// # Examples\n///\n/// ```\n/// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n/// assert_eq!(LinkMetadata::classify_link(\"#section\"), LinkType::Anchor);\n/// assert_eq!(LinkMetadata::classify_link(\"mailto:test@example.com\"), LinkType::Email);\n/// assert_eq!(LinkMetadata::classify_link(\"tel:+1234567890\"), LinkType::Phone);\n/// assert_eq!(LinkMetadata::classify_link(\"https://example.com\"), LinkType::External);\n/// ```\n#[rustler::nif]\npub fn linkmetadata_classify_link(href: String) -> LinkType {\n    html_to_markdown_rs::LinkMetadata::classify_link(&href).into()\n}\n\n#[rustler::nif]\npub fn conversionoptions_default() -> ConversionOptions {\n    html_to_markdown_rs::ConversionOptions::default().into()\n}\n\n/// Create a new builder with default values.\n#[rustler::nif]\npub fn conversionoptions_builder() -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new(html_to_markdown_rs::ConversionOptions::builder()),\n    })\n}\n\n/// Apply a partial update to these conversion options.\n#[rustler::nif]\npub fn conversionoptions_apply_update(obj: ConversionOptions, update: ConversionOptionsUpdate) -> () {\n    ()\n}\n\n/// Create from a partial update, applying to defaults.\n#[rustler::nif]\npub fn conversionoptions_from_update(update: ConversionOptionsUpdate) -> ConversionOptions {\n    let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n    html_to_markdown_rs::ConversionOptions::from_update(update_core).into()\n}\n\n#[rustler::nif]\npub fn conversionoptions_from(update: ConversionOptionsUpdate) -> ConversionOptions {\n    let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n    html_to_markdown_rs::ConversionOptions::from(update_core).into()\n}\n\n/// Set the list of HTML tag names whose content is stripped from output.\n#[rustler::nif]\npub fn conversionoptionsbuilder_strip_tags(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    tags: Vec<String>,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().strip_tags(tags)),\n    })\n}\n\n/// Set the list of HTML tag names that are preserved verbatim in output.\n#[rustler::nif]\npub fn conversionoptionsbuilder_preserve_tags(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    tags: Vec<String>,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().preserve_tags(tags)),\n    })\n}\n\n/// Set the list of HTML tag names whose `<img>` children are kept inline.\n#[rustler::nif]\npub fn conversionoptionsbuilder_keep_inline_images_in(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    tags: Vec<String>,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().keep_inline_images_in(tags)),\n    })\n}\n\n/// Set the list of CSS selectors for elements to exclude entirely from output.\n#[rustler::nif]\npub fn conversionoptionsbuilder_exclude_selectors(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    selectors: Vec<String>,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().exclude_selectors(selectors)),\n    })\n}\n\n/// Set the visitor used during conversion.\n#[rustler::nif]\npub fn conversionoptionsbuilder_visitor(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    visitor: rustler::ResourceArc<VisitorHandle>,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().visitor(&visitor.inner)),\n    })\n}\n\n/// Set the pre-processing options applied to the HTML before conversion.\n#[rustler::nif]\npub fn conversionoptionsbuilder_preprocessing(\n    resource: rustler::ResourceArc<ConversionOptionsBuilder>,\n    preprocessing: PreprocessingOptions,\n) -> ResourceArc<ConversionOptionsBuilder> {\n    ResourceArc::new(ConversionOptionsBuilder {\n        inner: Arc::new((*resource.inner).clone().preprocessing(preprocessing.into())),\n    })\n}\n\n/// Build the final [`ConversionOptions`].\n#[rustler::nif]\npub fn conversionoptionsbuilder_build(resource: rustler::ResourceArc<ConversionOptionsBuilder>) -> ConversionOptions {\n    (*resource.inner).clone().build().into()\n}\n\n#[rustler::nif]\npub fn preprocessingoptions_default() -> PreprocessingOptions {\n    html_to_markdown_rs::PreprocessingOptions::default().into()\n}\n\n/// Apply a partial update to these preprocessing options.\n///\n/// Any specified fields in the update will override the current values.\n/// Unspecified fields (None) are left unchanged.\n///\n/// # Arguments\n///\n/// * `update` - Partial preprocessing options update\n#[rustler::nif]\npub fn preprocessingoptions_apply_update(obj: PreprocessingOptions, update: PreprocessingOptionsUpdate) -> () {\n    ()\n}\n\n/// Create new preprocessing options from a partial update.\n///\n/// Creates a new `PreprocessingOptions` struct with defaults, then applies the update.\n/// Fields not specified in the update keep their default values.\n///\n/// # Arguments\n///\n/// * `update` - Partial preprocessing options update\n///\n/// # Returns\n///\n/// New `PreprocessingOptions` with specified updates applied to defaults\n#[rustler::nif]\npub fn preprocessingoptions_from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n    let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n    html_to_markdown_rs::PreprocessingOptions::from_update(update_core).into()\n}\n\n#[rustler::nif]\npub fn preprocessingoptions_from(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n    let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n    html_to_markdown_rs::PreprocessingOptions::from(update_core).into()\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: Default::default(),\n            twitter_card: Default::default(),\n            meta_tags: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for DocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: format!(\"{:?}\", val.open_graph),\n            twitter_card: format!(\"{:?}\", val.twitter_card),\n            meta_tags: format!(\"{:?}\", val.meta_tags),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for HeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<LinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for LinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: format!(\"{:?}\", val.attributes),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for ImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: format!(\"{:?}\", val.attributes),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<StructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for StructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for HtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.chars().next().unwrap_or('*'),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(Into::into),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for ConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth.map(|v| v as usize)).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(Into::into),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for ConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten(),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for PreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for PreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for DocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: Default::default(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for DocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.as_ref().map(|m| format!(\"{m:?}\")),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for TextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: Default::default(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for ConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for TableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<GridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for GridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableData> for html_to_markdown_rs::TableData {\n    fn from(val: TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for TableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for ProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for NodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: format!(\"{:?}\", val.attributes),\n            depth: val.depth,\n            index_in_parent: val.index_in_parent,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<TextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: TextDirection) -> Self {\n        match val {\n            TextDirection::LeftToRight => Self::LeftToRight,\n            TextDirection::RightToLeft => Self::RightToLeft,\n            TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for TextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<LinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: LinkType) -> Self {\n        match val {\n            LinkType::Anchor => Self::Anchor,\n            LinkType::Internal => Self::Internal,\n            LinkType::External => Self::External,\n            LinkType::Email => Self::Email,\n            LinkType::Phone => Self::Phone,\n            LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for LinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<ImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: ImageType) -> Self {\n        match val {\n            ImageType::DataUri => Self::DataUri,\n            ImageType::InlineSvg => Self::InlineSvg,\n            ImageType::External => Self::External,\n            ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for ImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<StructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: StructuredDataType) -> Self {\n        match val {\n            StructuredDataType::JsonLd => Self::JsonLd,\n            StructuredDataType::Microdata => Self::Microdata,\n            StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for StructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<PreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: PreprocessingPreset) -> Self {\n        match val {\n            PreprocessingPreset::Minimal => Self::Minimal,\n            PreprocessingPreset::Standard => Self::Standard,\n            PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for PreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<HeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: HeadingStyle) -> Self {\n        match val {\n            HeadingStyle::Underlined => Self::Underlined,\n            HeadingStyle::Atx => Self::Atx,\n            HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for HeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<ListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: ListIndentType) -> Self {\n        match val {\n            ListIndentType::Spaces => Self::Spaces,\n            ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for ListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<WhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: WhitespaceMode) -> Self {\n        match val {\n            WhitespaceMode::Normalized => Self::Normalized,\n            WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for WhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<NewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: NewlineStyle) -> Self {\n        match val {\n            NewlineStyle::Spaces => Self::Spaces,\n            NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for NewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<CodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: CodeBlockStyle) -> Self {\n        match val {\n            CodeBlockStyle::Indented => Self::Indented,\n            CodeBlockStyle::Backticks => Self::Backticks,\n            CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for CodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<HighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: HighlightStyle) -> Self {\n        match val {\n            HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            HighlightStyle::Html => Self::Html,\n            HighlightStyle::Bold => Self::Bold,\n            HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for HighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<LinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: LinkStyle) -> Self {\n        match val {\n            LinkStyle::Inline => Self::Inline,\n            LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for LinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<OutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: OutputFormat) -> Self {\n        match val {\n            OutputFormat::Markdown => Self::Markdown,\n            OutputFormat::Djot => Self::Djot,\n            OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for OutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<NodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: NodeContent) -> Self {\n        match val {\n            NodeContent::Heading { level, text } => Self::Heading { level, text },\n            NodeContent::Paragraph { text } => Self::Paragraph { text },\n            NodeContent::List { ordered } => Self::List { ordered },\n            NodeContent::ListItem { text } => Self::ListItem { text },\n            NodeContent::Table { grid } => Self::Table { grid: grid.into() },\n            NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self::Image {\n                description,\n                src,\n                image_index,\n            },\n            NodeContent::Code { text, language } => Self::Code { text, language },\n            NodeContent::Quote => Self::Quote,\n            NodeContent::DefinitionList => Self::DefinitionList,\n            NodeContent::DefinitionItem { term, definition } => Self::DefinitionItem { term, definition },\n            NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },\n            NodeContent::MetadataBlock { entries } => Self::MetadataBlock {\n                entries: entries.iter().filter_map(|s| serde_json::from_str(s).ok()).collect(),\n            },\n            NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self::Group {\n                label,\n                heading_level,\n                heading_text,\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for NodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { level, text } => Self::Heading { level, text },\n            html_to_markdown_rs::NodeContent::Paragraph { text } => Self::Paragraph { text },\n            html_to_markdown_rs::NodeContent::List { ordered } => Self::List { ordered },\n            html_to_markdown_rs::NodeContent::ListItem { text } => Self::ListItem { text },\n            html_to_markdown_rs::NodeContent::Table { grid } => Self::Table { grid: grid.into() },\n            html_to_markdown_rs::NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self::Image {\n                description,\n                src,\n                image_index,\n            },\n            html_to_markdown_rs::NodeContent::Code { text, language } => Self::Code { text, language },\n            html_to_markdown_rs::NodeContent::Quote => Self::Quote,\n            html_to_markdown_rs::NodeContent::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeContent::DefinitionItem { term, definition } => {\n                Self::DefinitionItem { term, definition }\n            }\n            html_to_markdown_rs::NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },\n            html_to_markdown_rs::NodeContent::MetadataBlock { entries } => Self::MetadataBlock {\n                entries: entries.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            },\n            html_to_markdown_rs::NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self::Group {\n                label,\n                heading_level,\n                heading_text,\n            },\n        }\n    }\n}\n\nimpl From<AnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: AnnotationKind) -> Self {\n        match val {\n            AnnotationKind::Bold => Self::Bold,\n            AnnotationKind::Italic => Self::Italic,\n            AnnotationKind::Underline => Self::Underline,\n            AnnotationKind::Strikethrough => Self::Strikethrough,\n            AnnotationKind::Code => Self::Code,\n            AnnotationKind::Subscript => Self::Subscript,\n            AnnotationKind::Superscript => Self::Superscript,\n            AnnotationKind::Highlight => Self::Highlight,\n            AnnotationKind::Link { url, title } => Self::Link { url, title },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for AnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self::Bold,\n            html_to_markdown_rs::AnnotationKind::Italic => Self::Italic,\n            html_to_markdown_rs::AnnotationKind::Underline => Self::Underline,\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::AnnotationKind::Code => Self::Code,\n            html_to_markdown_rs::AnnotationKind::Subscript => Self::Subscript,\n            html_to_markdown_rs::AnnotationKind::Superscript => Self::Superscript,\n            html_to_markdown_rs::AnnotationKind::Highlight => Self::Highlight,\n            html_to_markdown_rs::AnnotationKind::Link { url, title } => Self::Link { url, title },\n        }\n    }\n}\n\nimpl From<WarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: WarningKind) -> Self {\n        match val {\n            WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            WarningKind::EncodingFallback => Self::EncodingFallback,\n            WarningKind::TruncatedInput => Self::TruncatedInput,\n            WarningKind::MalformedHtml => Self::MalformedHtml,\n            WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for WarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for NodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for VisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        match val {\n            html_to_markdown_rs::VisitResult::Continue => Self::Continue,\n            html_to_markdown_rs::VisitResult::Custom(_0) => Self::Custom { _0 },\n            html_to_markdown_rs::VisitResult::Skip => Self::Skip,\n            html_to_markdown_rs::VisitResult::PreserveHtml => Self::PreserveHtml,\n            html_to_markdown_rs::VisitResult::Error(_0) => Self::Error { _0 },\n        }\n    }\n}\n\n/// Convert a `html_to_markdown_rs::error::ConversionError` error to a Rustler error string.\n#[allow(dead_code)]\nfn conversion_error_to_rustler_err(e: html_to_markdown_rs::error::ConversionError) -> String {\n    e.to_string()\n}\n\nfn on_load(env: rustler::Env, _info: rustler::Term) -> bool {\n    env.register::<ConversionOptionsBuilder>()\n        .expect(\"Failed to register resource type ConversionOptionsBuilder\");\n    env.register::<VisitorHandle>()\n        .expect(\"Failed to register resource type VisitorHandle\");\n    true\n}\n\nrustler::init!(\"Elixir.HtmlToMarkdown.Native\", load = on_load);\n"
  },
  {
    "path": "packages/elixir/test/test_helper.exs",
    "content": "ExUnit.configure(exclude: [:skip])\nExUnit.start()\n"
  },
  {
    "path": "packages/go/.golangci.yml",
    "content": "version: \"2\"\n\nrun:\n  timeout: 5m\n  issues-exit-code: 1\n  tests: true\n  concurrency: 4\n  modules-download-mode: readonly\n  allow-serial-runners: false\n  allow-parallel-runners: true\n\nlinters:\n  default: none\n  enable:\n    - errcheck\n    - govet\n    - ineffassign\n    - staticcheck\n    - unused\n    - revive\n    - gocyclo\n    - goconst\n    - gocritic\n    - gosec\n    - misspell\n    - nakedret\n  settings:\n    errcheck:\n      check-type-assertions: true\n      check-blank: true\n      exclude-functions:\n        - (net/http.ResponseWriter).Write\n        - (io.Closer).Close\n        - fmt.Fprintf\n        - fmt.Printf\n        - fmt.Println\n        - os.Setenv\n        - os.Unsetenv\n    goconst:\n      min-len: 3\n      min-occurrences: 3\n    gocyclo:\n      min-complexity: 50\n    govet:\n      enable-all: true\n      disable:\n        - shadow\n    gocritic:\n      disabled-checks:\n        - dupSubExpr\n    misspell:\n      locale: US\n    nakedret:\n      max-func-lines: 30\n    revive:\n      confidence: 0.8\n      severity: warning\n      enable-all-rules: false\n      rules:\n        - name: blank-imports\n        - name: context-keys-type\n        - name: time-naming\n        - name: var-declaration\n        - name: unexported-return\n        - name: errorf\n        - name: context-as-argument\n        - name: dot-imports\n        - name: error-return\n        - name: error-strings\n        - name: error-naming\n        - name: if-return\n        - name: increment-decrement\n        - name: var-naming\n        - name: range\n        - name: receiver-naming\n        - name: indent-error-flow\n        - name: exported\n          disabled: true\n        - name: package-comments\n          disabled: true\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - goconst\n        path: _test\\.go\n      - linters:\n          - gocyclo\n        path: _test\\.go\n      - linters:\n          - gosec\n        path: _test\\.go\n      - linters:\n          - revive\n        path: _test\\.go\n        text: \"context-as-argument\"\n      - linters:\n          - govet\n        text: \"fieldalignment:\"\n      - linters:\n          - govet\n        text: \"unsafeptr:\"\n    paths:\n      - vendor\n      - build\n      - third_party$\n\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\n  uniq-by-line: true\n  new: false\n\nformatters:\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n"
  },
  {
    "path": "packages/go/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-node\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-node?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.1.0\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with Go bindings to the Rust core library.\nSupports automatic downloading of prebuilt FFI libraries for Linux, macOS, and Windows with customizable caching.\n\n## Installation\n\n```bash\ngo get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\n```\n\nRequires Go 1.25+. After installing the package, run `go generate` to automatically download the platform-specific FFI library:\n\n```bash\ngo generate\n```\n\nThis downloads the native library from GitHub releases and generates the necessary CGO flags. The library is cached in `~/.html-to-markdown/` for subsequent builds.\n\nAlternatively, you can manually set `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables if you prefer to manage the FFI library yourself.\n\n## Performance Snapshot\n\n## Quick Start\n\nBasic conversion:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    html := \"<h1>Hello World</h1><p>This is a paragraph.</p>\"\n\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n\nWith conversion options:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    // Check library version\n    version := htmltomarkdown.Version()\n    fmt.Printf(\"html-to-markdown version: %s\\n\", version)\n\n    html := \"<h1>Hello</h1><p>Welcome</p>\"\n\n    // Convert with error handling\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatalf(\"Conversion failed: %v\", err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n\n## API Reference\n\n### Core Function\n\n**`Convert(html string, options ...ConversionOptions) (ConversionResult, error)`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` struct with all results in a single call.\n\n```go\nresult, err := htmltomarkdown.Convert(html)\nmarkdown  := result.Content    // *string — converted Markdown\nmetadata  := result.Metadata   // *Metadata — when ExtractMetadata: true\ntables    := result.Tables     // []TableData — when ExtractTables: true\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n// Default Markdown output\nmarkdown, _ := htmltomarkdown.Convert(html)\n// Result: \"This is **bold** and *italic* text.\"\n\n// Note: Djot output format configuration is not yet supported in Go bindings\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain, _ := htmltomarkdown.Convert(html, htmltomarkdown.WithOutputFormat(\"plain\"))\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Go Packages:** [pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2](https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/go/binding.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:42564e0f5d8bfa3e3aec287cc69c794edfbfba5e3e1107ecca85c637339f8623\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// Package htmltomarkdown provides Go bindings for the liter-llm library.\npackage htmltomarkdown\n\n/*\n#cgo CFLAGS: -I${SRCDIR}/../../crates/html-to-markdown-ffi/include\n#cgo LDFLAGS: -L${SRCDIR}/../../target/release -lhtml_to_markdown_ffi\n#include \"html_to_markdown.h\"\n*/\nimport \"C\"\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime/cgo\"\n\t\"unsafe\"\n)\n\n// lastError retrieves the last error from the FFI layer.\nfunc lastError() error {\n\tcode := int32(C.htm_last_error_code())\n\tif code == 0 {\n\t\treturn nil\n\t}\n\tctx := C.htm_last_error_context()\n\tmessage := C.GoString(ctx)\n\treturn fmt.Errorf(\"[%d] %s\", code, message)\n}\n\n// unmarshalBytes copies a C byte buffer into a Go []byte.\n//\n// The pointer is treated as a NUL-terminated C string; binary payloads\n// that may contain interior NULs should be exposed by the FFI with an\n// explicit length out-parameter instead.\nfunc unmarshalBytes(ptr *C.uint8_t) *[]byte {\n\tif ptr == nil {\n\t\treturn nil\n\t}\n\tv := []byte(C.GoString((*C.char)(unsafe.Pointer(ptr))))\n\treturn &v\n}\n\nvar (\n\t// ErrParseError is returned when hTML parsing error.\n\tErrParseError = errors.New(\"hTML parsing error\")\n\t// ErrSanitizationError is returned when sanitization error.\n\tErrSanitizationError = errors.New(\"sanitization error\")\n\t// ErrConfigError is returned when invalid configuration.\n\tErrConfigError = errors.New(\"invalid configuration\")\n\t// ErrIoError is returned when i/O error.\n\tErrIoError = errors.New(\"i/O error\")\n\t// ErrPanic is returned when internal panic.\n\tErrPanic = errors.New(\"internal panic\")\n\t// ErrInvalidInput is returned when invalid input.\n\tErrInvalidInput = errors.New(\"invalid input\")\n\t// ErrOther is returned when conversion error.\n\tErrOther = errors.New(\"conversion error\")\n)\n\n// ConversionError is a structured error type.\ntype ConversionError struct {\n\tCode    string\n\tMessage string\n}\n\nfunc (e *ConversionError) Error() string { return e.Message }\n\n// HtmlVisitor is the Go interface for the HtmlVisitor trait.\ntype HtmlVisitor interface {\n\t// VisitElementStart calls the VisitElementStart method.\n\tVisitElementStart(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitElementEnd calls the VisitElementEnd method.\n\tVisitElementEnd(ctx map[string]interface{}, output string) map[string]interface{}\n\n\t// VisitText calls the VisitText method.\n\tVisitText(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitLink calls the VisitLink method.\n\tVisitLink(ctx map[string]interface{}, href string, text string, title string) map[string]interface{}\n\n\t// VisitImage calls the VisitImage method.\n\tVisitImage(ctx map[string]interface{}, src string, alt string, title string) map[string]interface{}\n\n\t// VisitHeading calls the VisitHeading method.\n\tVisitHeading(ctx map[string]interface{}, level uint32, text string, id string) map[string]interface{}\n\n\t// VisitCodeBlock calls the VisitCodeBlock method.\n\tVisitCodeBlock(ctx map[string]interface{}, lang string, code string) map[string]interface{}\n\n\t// VisitCodeInline calls the VisitCodeInline method.\n\tVisitCodeInline(ctx map[string]interface{}, code string) map[string]interface{}\n\n\t// VisitListItem calls the VisitListItem method.\n\tVisitListItem(ctx map[string]interface{}, ordered bool, marker string, text string) map[string]interface{}\n\n\t// VisitListStart calls the VisitListStart method.\n\tVisitListStart(ctx map[string]interface{}, ordered bool) map[string]interface{}\n\n\t// VisitListEnd calls the VisitListEnd method.\n\tVisitListEnd(ctx map[string]interface{}, ordered bool, output string) map[string]interface{}\n\n\t// VisitTableStart calls the VisitTableStart method.\n\tVisitTableStart(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitTableRow calls the VisitTableRow method.\n\tVisitTableRow(ctx map[string]interface{}, cells []string, isHeader bool) map[string]interface{}\n\n\t// VisitTableEnd calls the VisitTableEnd method.\n\tVisitTableEnd(ctx map[string]interface{}, output string) map[string]interface{}\n\n\t// VisitBlockquote calls the VisitBlockquote method.\n\tVisitBlockquote(ctx map[string]interface{}, content string, depth uint) map[string]interface{}\n\n\t// VisitStrong calls the VisitStrong method.\n\tVisitStrong(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitEmphasis calls the VisitEmphasis method.\n\tVisitEmphasis(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitStrikethrough calls the VisitStrikethrough method.\n\tVisitStrikethrough(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitUnderline calls the VisitUnderline method.\n\tVisitUnderline(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitSubscript calls the VisitSubscript method.\n\tVisitSubscript(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitSuperscript calls the VisitSuperscript method.\n\tVisitSuperscript(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitMark calls the VisitMark method.\n\tVisitMark(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitLineBreak calls the VisitLineBreak method.\n\tVisitLineBreak(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitHorizontalRule calls the VisitHorizontalRule method.\n\tVisitHorizontalRule(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitCustomElement calls the VisitCustomElement method.\n\tVisitCustomElement(ctx map[string]interface{}, tagName string, html string) map[string]interface{}\n\n\t// VisitDefinitionListStart calls the VisitDefinitionListStart method.\n\tVisitDefinitionListStart(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitDefinitionTerm calls the VisitDefinitionTerm method.\n\tVisitDefinitionTerm(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitDefinitionDescription calls the VisitDefinitionDescription method.\n\tVisitDefinitionDescription(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitDefinitionListEnd calls the VisitDefinitionListEnd method.\n\tVisitDefinitionListEnd(ctx map[string]interface{}, output string) map[string]interface{}\n\n\t// VisitForm calls the VisitForm method.\n\tVisitForm(ctx map[string]interface{}, action string, method string) map[string]interface{}\n\n\t// VisitInput calls the VisitInput method.\n\tVisitInput(ctx map[string]interface{}, inputType string, name string, value string) map[string]interface{}\n\n\t// VisitButton calls the VisitButton method.\n\tVisitButton(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitAudio calls the VisitAudio method.\n\tVisitAudio(ctx map[string]interface{}, src string) map[string]interface{}\n\n\t// VisitVideo calls the VisitVideo method.\n\tVisitVideo(ctx map[string]interface{}, src string) map[string]interface{}\n\n\t// VisitIframe calls the VisitIframe method.\n\tVisitIframe(ctx map[string]interface{}, src string) map[string]interface{}\n\n\t// VisitDetails calls the VisitDetails method.\n\tVisitDetails(ctx map[string]interface{}, open bool) map[string]interface{}\n\n\t// VisitSummary calls the VisitSummary method.\n\tVisitSummary(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitFigureStart calls the VisitFigureStart method.\n\tVisitFigureStart(ctx map[string]interface{}) map[string]interface{}\n\n\t// VisitFigcaption calls the VisitFigcaption method.\n\tVisitFigcaption(ctx map[string]interface{}, text string) map[string]interface{}\n\n\t// VisitFigureEnd calls the VisitFigureEnd method.\n\tVisitFigureEnd(ctx map[string]interface{}, output string) map[string]interface{}\n}\n\n// TextDirection text directionality of document content.\n//\n// Corresponds to the HTML `dir` attribute and `bdi` element directionality.\ntype TextDirection string\n\nconst (\n\t// TextDirectionLeftToRight left-to-right text flow (default for Latin scripts)\n\tTextDirectionLeftToRight TextDirection = \"ltr\"\n\t// TextDirectionRightToLeft right-to-left text flow (Hebrew, Arabic, Urdu, etc.)\n\tTextDirectionRightToLeft TextDirection = \"rtl\"\n\t// TextDirectionAuto automatic directionality detection\n\tTextDirectionAuto TextDirection = \"auto\"\n)\n\n// LinkType link classification based on href value and document context.\n//\n// Used to categorize links during extraction for filtering and analysis.\ntype LinkType string\n\nconst (\n\t// LinkTypeAnchor anchor link within same document (href starts with #)\n\tLinkTypeAnchor LinkType = \"Anchor\"\n\t// LinkTypeInternal internal link within same domain\n\tLinkTypeInternal LinkType = \"Internal\"\n\t// LinkTypeExternal external link to different domain\n\tLinkTypeExternal LinkType = \"External\"\n\t// LinkTypeEmail email link (mailto:)\n\tLinkTypeEmail LinkType = \"Email\"\n\t// LinkTypePhone phone link (tel:)\n\tLinkTypePhone LinkType = \"Phone\"\n\t// LinkTypeOther other protocol or unclassifiable\n\tLinkTypeOther LinkType = \"Other\"\n)\n\n// ImageType image source classification for proper handling and processing.\n//\n// Determines whether an image is embedded (data URI), inline SVG, external, or relative.\ntype ImageType string\n\nconst (\n\t// ImageTypeDataURI data URI embedded image (base64 or other encoding)\n\tImageTypeDataURI ImageType = \"DataUri\"\n\t// ImageTypeInlineSvg inline SVG element\n\tImageTypeInlineSvg ImageType = \"InlineSvg\"\n\t// ImageTypeExternal external image URL (http/https)\n\tImageTypeExternal ImageType = \"External\"\n\t// ImageTypeRelative relative image path\n\tImageTypeRelative ImageType = \"Relative\"\n)\n\n// StructuredDataType structured data format type.\n//\n// Identifies the schema/format used for structured data markup.\ntype StructuredDataType string\n\nconst (\n\t// StructuredDataTypeJSONLd jSON-LD (JSON for Linking Data) script blocks\n\tStructuredDataTypeJSONLd StructuredDataType = \"json_ld\"\n\t// StructuredDataTypeMicrodata hTML5 Microdata attributes (itemscope, itemtype, itemprop)\n\tStructuredDataTypeMicrodata StructuredDataType = \"Microdata\"\n\t// StructuredDataTypeRdFa rDF in Attributes (RDFa) markup\n\tStructuredDataTypeRdFa StructuredDataType = \"rdfa\"\n)\n\n// PreprocessingPreset hTML preprocessing aggressiveness level.\n//\n// Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\ntype PreprocessingPreset string\n\nconst (\n\t// PreprocessingPresetMinimal minimal cleanup. Remove only essential noise (scripts, styles).\n\tPreprocessingPresetMinimal PreprocessingPreset = \"Minimal\"\n\t// PreprocessingPresetStandard standard cleanup. Default. Removes navigation, forms, and other auxiliary content.\n\tPreprocessingPresetStandard PreprocessingPreset = \"Standard\"\n\t// PreprocessingPresetAggressive aggressive cleanup. Remove extensive non-content elements and structure.\n\tPreprocessingPresetAggressive PreprocessingPreset = \"Aggressive\"\n)\n\n// HeadingStyle heading style options for Markdown output.\n//\n// Controls how headings (h1-h6) are rendered in the output Markdown.\ntype HeadingStyle string\n\nconst (\n\t// HeadingStyleUnderlined underlined style (=== for h1, --- for h2).\n\tHeadingStyleUnderlined HeadingStyle = \"Underlined\"\n\t// HeadingStyleAtx aTX style (# for h1, ## for h2, etc.). Default.\n\tHeadingStyleAtx HeadingStyle = \"Atx\"\n\t// HeadingStyleAtxClosed aTX closed style (# title #, with closing hashes).\n\tHeadingStyleAtxClosed HeadingStyle = \"AtxClosed\"\n)\n\n// ListIndentType list indentation character type.\n//\n// Controls whether list items are indented with spaces or tabs.\ntype ListIndentType string\n\nconst (\n\t// ListIndentTypeSpaces use spaces for indentation. Default. Width controlled by `list_indent_width`.\n\tListIndentTypeSpaces ListIndentType = \"Spaces\"\n\t// ListIndentTypeTabs use tabs for indentation.\n\tListIndentTypeTabs ListIndentType = \"Tabs\"\n)\n\n// WhitespaceMode whitespace handling strategy during conversion.\n//\n// Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\ntype WhitespaceMode string\n\nconst (\n\t// WhitespaceModeNormalized collapse multiple whitespace characters to single spaces. Default. Matches browser behavior.\n\tWhitespaceModeNormalized WhitespaceMode = \"Normalized\"\n\t// WhitespaceModeStrict preserve all whitespace exactly as it appears in the HTML.\n\tWhitespaceModeStrict WhitespaceMode = \"Strict\"\n)\n\n// NewlineStyle line break syntax in Markdown output.\n//\n// Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\ntype NewlineStyle string\n\nconst (\n\t// NewlineStyleSpaces two trailing spaces at end of line. Default. Standard Markdown syntax.\n\tNewlineStyleSpaces NewlineStyle = \"Spaces\"\n\t// NewlineStyleBackslash backslash at end of line. Alternative Markdown syntax.\n\tNewlineStyleBackslash NewlineStyle = \"Backslash\"\n)\n\n// CodeBlockStyle code block fence style in Markdown output.\n//\n// Determines how code blocks (`<pre><code>`) are rendered in Markdown.\ntype CodeBlockStyle string\n\nconst (\n\t// CodeBlockStyleIndented indented code blocks (4 spaces). `CommonMark` standard.\n\tCodeBlockStyleIndented CodeBlockStyle = \"Indented\"\n\t// CodeBlockStyleBackticks fenced code blocks with backticks (```). Default (GFM). Supports language hints.\n\tCodeBlockStyleBackticks CodeBlockStyle = \"Backticks\"\n\t// CodeBlockStyleTildes fenced code blocks with tildes (~~~). Supports language hints.\n\tCodeBlockStyleTildes CodeBlockStyle = \"Tildes\"\n)\n\n// HighlightStyle highlight rendering style for `<mark>` elements.\n//\n// Controls how highlighted text is rendered in Markdown output.\ntype HighlightStyle string\n\nconst (\n\t// HighlightStyleDoubleEqual double equals syntax (==text==). Default. Pandoc-compatible.\n\tHighlightStyleDoubleEqual HighlightStyle = \"DoubleEqual\"\n\t// HighlightStyleHTML preserve as HTML (==text==). Original HTML tag.\n\tHighlightStyleHTML HighlightStyle = \"Html\"\n\t// HighlightStyleBold render as bold (**text**). Uses strong emphasis.\n\tHighlightStyleBold HighlightStyle = \"Bold\"\n\t// HighlightStyleNone strip formatting, render as plain text. No markup.\n\tHighlightStyleNone HighlightStyle = \"None\"\n)\n\n// LinkStyle link rendering style in Markdown output.\n//\n// Controls whether links and images use inline `[text](url)` syntax or\n// reference-style `[text][1]` syntax with definitions collected at the end.\ntype LinkStyle string\n\nconst (\n\t// LinkStyleInline inline links: `[text](url)`. Default.\n\tLinkStyleInline LinkStyle = \"Inline\"\n\t// LinkStyleReference reference-style links: `[text][1]` with `[1]: url` at end of document.\n\tLinkStyleReference LinkStyle = \"Reference\"\n)\n\n// OutputFormat output format for conversion.\n//\n// Specifies the target markup language format for the conversion output.\ntype OutputFormat string\n\nconst (\n\t// OutputFormatMarkdown standard Markdown (CommonMark compatible). Default.\n\tOutputFormatMarkdown OutputFormat = \"Markdown\"\n\t// OutputFormatDjot djot lightweight markup language.\n\tOutputFormatDjot OutputFormat = \"Djot\"\n\t// OutputFormatPlain plain text output (no markup, visible text only).\n\tOutputFormatPlain OutputFormat = \"Plain\"\n)\n\n// NodeContent semantic content type of a document node.\n//\n// Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n// Variants: Heading, Paragraph, List, ListItem, Table, Image, Code, Quote, DefinitionList, DefinitionItem, RawBlock, MetadataBlock, Group\ntype NodeContent struct {\n\tNodeType string `json:\"node_type\"`\n\t// Heading level (1-6).\n\tLevel *uint8 `json:\"level,omitempty\"`\n\t// The heading text content.\n\tText *string `json:\"text,omitempty\"`\n\t// Whether this is an ordered list.\n\tOrdered *bool `json:\"ordered,omitempty\"`\n\t// The table grid structure.\n\tGrid *TableGrid `json:\"grid,omitempty\"`\n\t// Alt text or caption.\n\tDescription *string `json:\"description,omitempty\"`\n\t// Image source URL.\n\tSrc *string `json:\"src,omitempty\"`\n\t// Index into `ConversionResult.images` when image extraction is enabled.\n\tImageIndex *uint32 `json:\"image_index,omitempty\"`\n\t// Programming language (from class=\"language-*\" or similar).\n\tLanguage *string `json:\"language,omitempty\"`\n\t// The term being defined.\n\tTerm *string `json:\"term,omitempty\"`\n\t// The definition text.\n\tDefinition *string `json:\"definition,omitempty\"`\n\t// The format of the raw content (e.g. \"html\", \"css\", \"javascript\").\n\tFormat *string `json:\"format,omitempty\"`\n\t// The raw content.\n\tContent *string `json:\"content,omitempty\"`\n\t// Key-value metadata pairs.\n\tEntries *[]string `json:\"entries,omitempty\"`\n\t// Optional section label.\n\tLabel *string `json:\"label,omitempty\"`\n\t// The heading level that created this group.\n\tHeadingLevel *uint8 `json:\"heading_level,omitempty\"`\n\t// The heading text that created this group.\n\tHeadingText *string `json:\"heading_text,omitempty\"`\n}\n\n// AnnotationKind type of an inline text annotation.\n//\n// Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n// Variants: Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript, Highlight, Link\ntype AnnotationKind struct {\n\tAnnotationType string `json:\"annotation_type\"`\n\t// The link URL.\n\tURL *string `json:\"url,omitempty\"`\n\t// Optional link title attribute.\n\tTitle *string `json:\"title,omitempty\"`\n}\n\n// WarningKind categories of processing warnings.\ntype WarningKind string\n\nconst (\n\t// WarningKindImageExtractionFailed an image could not be extracted (e.g. invalid data URI, unsupported format).\n\tWarningKindImageExtractionFailed WarningKind = \"ImageExtractionFailed\"\n\t// WarningKindEncodingFallback the input encoding was not recognized; fell back to UTF-8.\n\tWarningKindEncodingFallback WarningKind = \"EncodingFallback\"\n\t// WarningKindTruncatedInput the input was truncated due to size limits.\n\tWarningKindTruncatedInput WarningKind = \"TruncatedInput\"\n\t// WarningKindMalformedHTML the HTML was malformed but processing continued with best effort.\n\tWarningKindMalformedHTML WarningKind = \"MalformedHtml\"\n\t// WarningKindSanitizationApplied sanitization was applied to remove potentially unsafe content.\n\tWarningKindSanitizationApplied WarningKind = \"SanitizationApplied\"\n\t// WarningKindDepthLimitExceeded dOM traversal was truncated because max_depth was exceeded.\n\tWarningKindDepthLimitExceeded WarningKind = \"DepthLimitExceeded\"\n)\n\n// NodeType node type enumeration covering all HTML element types.\n//\n// This enum categorizes all HTML elements that the converter recognizes,\n// providing a coarse-grained classification for visitor dispatch.\ntype NodeType string\n\nconst (\n\t// NodeTypeText text node (most frequent - 100+ per document)\n\tNodeTypeText NodeType = \"Text\"\n\t// NodeTypeElement generic element node\n\tNodeTypeElement NodeType = \"Element\"\n\t// NodeTypeHeading heading elements (h1-h6)\n\tNodeTypeHeading NodeType = \"Heading\"\n\t// NodeTypeParagraph paragraph element\n\tNodeTypeParagraph NodeType = \"Paragraph\"\n\t// NodeTypeDiv generic div container\n\tNodeTypeDiv NodeType = \"Div\"\n\t// NodeTypeBlockquote blockquote element\n\tNodeTypeBlockquote NodeType = \"Blockquote\"\n\t// NodeTypePre preformatted text block\n\tNodeTypePre NodeType = \"Pre\"\n\t// NodeTypeHr horizontal rule\n\tNodeTypeHr NodeType = \"Hr\"\n\t// NodeTypeList ordered or unordered list (ul, ol)\n\tNodeTypeList NodeType = \"List\"\n\t// NodeTypeListItem list item (li)\n\tNodeTypeListItem NodeType = \"ListItem\"\n\t// NodeTypeDefinitionList definition list (dl)\n\tNodeTypeDefinitionList NodeType = \"DefinitionList\"\n\t// NodeTypeDefinitionTerm definition term (dt)\n\tNodeTypeDefinitionTerm NodeType = \"DefinitionTerm\"\n\t// NodeTypeDefinitionDescription definition description (dd)\n\tNodeTypeDefinitionDescription NodeType = \"DefinitionDescription\"\n\t// NodeTypeTable table element\n\tNodeTypeTable NodeType = \"Table\"\n\t// NodeTypeTableRow table row (tr)\n\tNodeTypeTableRow NodeType = \"TableRow\"\n\t// NodeTypeTableCell table cell (td, th)\n\tNodeTypeTableCell NodeType = \"TableCell\"\n\t// NodeTypeTableHeader table header cell (th)\n\tNodeTypeTableHeader NodeType = \"TableHeader\"\n\t// NodeTypeTableBody table body (tbody)\n\tNodeTypeTableBody NodeType = \"TableBody\"\n\t// NodeTypeTableHead table head (thead)\n\tNodeTypeTableHead NodeType = \"TableHead\"\n\t// NodeTypeTableFoot table foot (tfoot)\n\tNodeTypeTableFoot NodeType = \"TableFoot\"\n\t// NodeTypeLink anchor link (a)\n\tNodeTypeLink NodeType = \"Link\"\n\t// NodeTypeImage image (img)\n\tNodeTypeImage NodeType = \"Image\"\n\t// NodeTypeStrong strong/bold (strong, b)\n\tNodeTypeStrong NodeType = \"Strong\"\n\t// NodeTypeEm emphasis/italic (em, i)\n\tNodeTypeEm NodeType = \"Em\"\n\t// NodeTypeCode inline code (code)\n\tNodeTypeCode NodeType = \"Code\"\n\t// NodeTypeStrikethrough strikethrough (s, del, strike)\n\tNodeTypeStrikethrough NodeType = \"Strikethrough\"\n\t// NodeTypeUnderline underline (u, ins)\n\tNodeTypeUnderline NodeType = \"Underline\"\n\t// NodeTypeSubscript subscript (sub)\n\tNodeTypeSubscript NodeType = \"Subscript\"\n\t// NodeTypeSuperscript superscript (sup)\n\tNodeTypeSuperscript NodeType = \"Superscript\"\n\t// NodeTypeMark mark/highlight (mark)\n\tNodeTypeMark NodeType = \"Mark\"\n\t// NodeTypeSmall small text (small)\n\tNodeTypeSmall NodeType = \"Small\"\n\t// NodeTypeBr line break (br)\n\tNodeTypeBr NodeType = \"Br\"\n\t// NodeTypeSpan span element\n\tNodeTypeSpan NodeType = \"Span\"\n\t// NodeTypeArticle article element\n\tNodeTypeArticle NodeType = \"Article\"\n\t// NodeTypeSection section element\n\tNodeTypeSection NodeType = \"Section\"\n\t// NodeTypeNav navigation element\n\tNodeTypeNav NodeType = \"Nav\"\n\t// NodeTypeAside aside element\n\tNodeTypeAside NodeType = \"Aside\"\n\t// NodeTypeHeader header element\n\tNodeTypeHeader NodeType = \"Header\"\n\t// NodeTypeFooter footer element\n\tNodeTypeFooter NodeType = \"Footer\"\n\t// NodeTypeMain main element\n\tNodeTypeMain NodeType = \"Main\"\n\t// NodeTypeFigure figure element\n\tNodeTypeFigure NodeType = \"Figure\"\n\t// NodeTypeFigcaption figure caption\n\tNodeTypeFigcaption NodeType = \"Figcaption\"\n\t// NodeTypeTime time element\n\tNodeTypeTime NodeType = \"Time\"\n\t// NodeTypeDetails details element\n\tNodeTypeDetails NodeType = \"Details\"\n\t// NodeTypeSummary summary element\n\tNodeTypeSummary NodeType = \"Summary\"\n\t// NodeTypeForm form element\n\tNodeTypeForm NodeType = \"Form\"\n\t// NodeTypeInput input element\n\tNodeTypeInput NodeType = \"Input\"\n\t// NodeTypeSelect select element\n\tNodeTypeSelect NodeType = \"Select\"\n\t// NodeTypeOption option element\n\tNodeTypeOption NodeType = \"Option\"\n\t// NodeTypeButton button element\n\tNodeTypeButton NodeType = \"Button\"\n\t// NodeTypeTextarea textarea element\n\tNodeTypeTextarea NodeType = \"Textarea\"\n\t// NodeTypeLabel label element\n\tNodeTypeLabel NodeType = \"Label\"\n\t// NodeTypeFieldset fieldset element\n\tNodeTypeFieldset NodeType = \"Fieldset\"\n\t// NodeTypeLegend legend element\n\tNodeTypeLegend NodeType = \"Legend\"\n\t// NodeTypeAudio audio element\n\tNodeTypeAudio NodeType = \"Audio\"\n\t// NodeTypeVideo video element\n\tNodeTypeVideo NodeType = \"Video\"\n\t// NodeTypePicture picture element\n\tNodeTypePicture NodeType = \"Picture\"\n\t// NodeTypeSource source element\n\tNodeTypeSource NodeType = \"Source\"\n\t// NodeTypeIframe iframe element\n\tNodeTypeIframe NodeType = \"Iframe\"\n\t// NodeTypeSvg sVG element\n\tNodeTypeSvg NodeType = \"Svg\"\n\t// NodeTypeCanvas canvas element\n\tNodeTypeCanvas NodeType = \"Canvas\"\n\t// NodeTypeRuby ruby annotation\n\tNodeTypeRuby NodeType = \"Ruby\"\n\t// NodeTypeRt ruby text\n\tNodeTypeRt NodeType = \"Rt\"\n\t// NodeTypeRp ruby parenthesis\n\tNodeTypeRp NodeType = \"Rp\"\n\t// NodeTypeAbbr abbreviation\n\tNodeTypeAbbr NodeType = \"Abbr\"\n\t// NodeTypeKbd keyboard input\n\tNodeTypeKbd NodeType = \"Kbd\"\n\t// NodeTypeSamp sample output\n\tNodeTypeSamp NodeType = \"Samp\"\n\t// NodeTypeVar variable\n\tNodeTypeVar NodeType = \"Var\"\n\t// NodeTypeCite citation\n\tNodeTypeCite NodeType = \"Cite\"\n\t// NodeTypeQ quote\n\tNodeTypeQ NodeType = \"Q\"\n\t// NodeTypeDel deleted text\n\tNodeTypeDel NodeType = \"Del\"\n\t// NodeTypeIns inserted text\n\tNodeTypeIns NodeType = \"Ins\"\n\t// NodeTypeData data element\n\tNodeTypeData NodeType = \"Data\"\n\t// NodeTypeMeter meter element\n\tNodeTypeMeter NodeType = \"Meter\"\n\t// NodeTypeProgress progress element\n\tNodeTypeProgress NodeType = \"Progress\"\n\t// NodeTypeOutput output element\n\tNodeTypeOutput NodeType = \"Output\"\n\t// NodeTypeTemplate template element\n\tNodeTypeTemplate NodeType = \"Template\"\n\t// NodeTypeSlot slot element\n\tNodeTypeSlot NodeType = \"Slot\"\n\t// NodeTypeHTML hTML root element\n\tNodeTypeHTML NodeType = \"Html\"\n\t// NodeTypeHead head element\n\tNodeTypeHead NodeType = \"Head\"\n\t// NodeTypeBody body element\n\tNodeTypeBody NodeType = \"Body\"\n\t// NodeTypeTitle title element\n\tNodeTypeTitle NodeType = \"Title\"\n\t// NodeTypeMeta meta element\n\tNodeTypeMeta NodeType = \"Meta\"\n\t// NodeTypeLinkTag link element (not anchor)\n\tNodeTypeLinkTag NodeType = \"LinkTag\"\n\t// NodeTypeStyle style element\n\tNodeTypeStyle NodeType = \"Style\"\n\t// NodeTypeScript script element\n\tNodeTypeScript NodeType = \"Script\"\n\t// NodeTypeBase base element\n\tNodeTypeBase NodeType = \"Base\"\n\t// NodeTypeCustom custom element (web components) or unknown tag\n\tNodeTypeCustom NodeType = \"Custom\"\n)\n\n// DocumentMetadata document-level metadata extracted from `<head>` and top-level elements.\n//\n// Contains all metadata typically used by search engines, social media platforms,\n// and browsers for document indexing and presentation.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::DocumentMetadata;\n// let doc = DocumentMetadata {\n// title: Some(\"My Article\".to_string()),\n// description: Some(\"A great article about Rust\".to_string()),\n// keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n// ..Default::default()\n// };\n//\n// assert_eq!(doc.title, Some(\"My Article\".to_string()));\n// ```\ntype DocumentMetadata struct {\n\t// Document title from `<title>` tag\n\tTitle *string `json:\"title,omitempty\"`\n\t// Document description from `<meta name=\"description\">` tag\n\tDescription *string `json:\"description,omitempty\"`\n\t// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n\tKeywords []string `json:\"keywords,omitempty\"`\n\t// Document author from `<meta name=\"author\">` tag\n\tAuthor *string `json:\"author,omitempty\"`\n\t// Canonical URL from `<link rel=\"canonical\">` tag\n\tCanonicalURL *string `json:\"canonical_url,omitempty\"`\n\t// Base URL from `<base href=\"\">` tag for resolving relative URLs\n\tBaseHref *string `json:\"base_href,omitempty\"`\n\t// Document language from `lang` attribute\n\tLanguage *string `json:\"language,omitempty\"`\n\t// Document text direction from `dir` attribute\n\tTextDirection *TextDirection `json:\"text_direction,omitempty\"`\n\t// Open Graph metadata (og:* properties) for social media\n\t// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n\tOpenGraph map[string]string `json:\"open_graph,omitempty\"`\n\t// Twitter Card metadata (twitter:* properties)\n\t// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n\tTwitterCard map[string]string `json:\"twitter_card,omitempty\"`\n\t// Additional meta tags not covered by specific fields\n\t// Keys are meta name/property attributes, values are content\n\tMetaTags map[string]string `json:\"meta_tags,omitempty\"`\n}\n\n// DocumentMetadataOption is an option function for DocumentMetadata.\ntype DocumentMetadataOption func(*DocumentMetadata)\n\n// WithDocumentMetadataTitle sets the title field.\nfunc WithDocumentMetadataTitle(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.Title = &v }\n}\n\n// WithDocumentMetadataDescription sets the description field.\nfunc WithDocumentMetadataDescription(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.Description = &v }\n}\n\n// WithDocumentMetadataKeywords sets the keywords field.\nfunc WithDocumentMetadataKeywords(v []string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.Keywords = v }\n}\n\n// WithDocumentMetadataAuthor sets the author field.\nfunc WithDocumentMetadataAuthor(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.Author = &v }\n}\n\n// WithDocumentMetadataCanonicalURL sets the canonical_url field.\nfunc WithDocumentMetadataCanonicalURL(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.CanonicalURL = &v }\n}\n\n// WithDocumentMetadataBaseHref sets the base_href field.\nfunc WithDocumentMetadataBaseHref(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.BaseHref = &v }\n}\n\n// WithDocumentMetadataLanguage sets the language field.\nfunc WithDocumentMetadataLanguage(v string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.Language = &v }\n}\n\n// WithDocumentMetadataTextDirection sets the text_direction field.\nfunc WithDocumentMetadataTextDirection(v TextDirection) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.TextDirection = &v }\n}\n\n// WithDocumentMetadataOpenGraph sets the open_graph field.\nfunc WithDocumentMetadataOpenGraph(v map[string]string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.OpenGraph = v }\n}\n\n// WithDocumentMetadataTwitterCard sets the twitter_card field.\nfunc WithDocumentMetadataTwitterCard(v map[string]string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.TwitterCard = v }\n}\n\n// WithDocumentMetadataMetaTags sets the meta_tags field.\nfunc WithDocumentMetadataMetaTags(v map[string]string) DocumentMetadataOption {\n\treturn func(c *DocumentMetadata) { c.MetaTags = v }\n}\n\n// NewDocumentMetadata creates a DocumentMetadata with optional parameters.\nfunc NewDocumentMetadata(opts ...DocumentMetadataOption) *DocumentMetadata {\n\tc := &DocumentMetadata{\n\t\tTitle:         nil,\n\t\tDescription:   nil,\n\t\tKeywords:      nil,\n\t\tAuthor:        nil,\n\t\tCanonicalURL:  nil,\n\t\tBaseHref:      nil,\n\t\tLanguage:      nil,\n\t\tTextDirection: nil,\n\t\tOpenGraph:     nil,\n\t\tTwitterCard:   nil,\n\t\tMetaTags:      nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// HeaderMetadata header element metadata with hierarchy tracking.\n//\n// Captures heading elements (h1-h6) with their text content, identifiers,\n// and position in the document structure.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::HeaderMetadata;\n// let header = HeaderMetadata {\n// level: 1,\n// text: \"Main Title\".to_string(),\n// id: Some(\"main-title\".to_string()),\n// depth: 0,\n// html_offset: 145,\n// };\n//\n// assert_eq!(header.level, 1);\n// assert!(header.is_valid());\n// ```\ntype HeaderMetadata struct {\n\t// Header level: 1 (h1) through 6 (h6)\n\tLevel uint8 `json:\"level\"`\n\t// Normalized text content of the header\n\tText string `json:\"text\"`\n\t// HTML id attribute if present\n\tID *string `json:\"id,omitempty\"`\n\t// Document tree depth at the header element\n\tDepth uint `json:\"depth\"`\n\t// Byte offset in original HTML document\n\tHTMLOffset uint `json:\"html_offset\"`\n}\n\n// LinkMetadata hyperlink metadata with categorization and attributes.\n//\n// Represents `<a>` elements with parsed href values, text content, and link type classification.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n// let link = LinkMetadata {\n// href: \"https://example.com\".to_string(),\n// text: \"Example\".to_string(),\n// title: Some(\"Visit Example\".to_string()),\n// link_type: LinkType::External,\n// rel: vec![\"nofollow\".to_string()],\n// attributes: Default::default(),\n// };\n//\n// assert_eq!(link.link_type, LinkType::External);\n// assert_eq!(link.text, \"Example\");\n// ```\ntype LinkMetadata struct {\n\t// The href URL value\n\tHref string `json:\"href\"`\n\t// Link text content (normalized, concatenated if mixed with elements)\n\tText string `json:\"text\"`\n\t// Optional title attribute (often shown as tooltip)\n\tTitle *string `json:\"title,omitempty\"`\n\t// Link type classification\n\tLinkType LinkType `json:\"link_type\"`\n\t// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n\tRel []string `json:\"rel,omitempty\"`\n\t// Additional HTML attributes\n\tAttributes map[string]string `json:\"attributes,omitempty\"`\n}\n\n// ImageMetadata image metadata with source and dimensions.\n//\n// Captures `<img>` elements and inline `<svg>` elements with metadata\n// for image analysis and optimization.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n// let img = ImageMetadata {\n// src: \"https://example.com/image.jpg\".to_string(),\n// alt: Some(\"An example image\".to_string()),\n// title: Some(\"Example\".to_string()),\n// dimensions: Some((800, 600)),\n// image_type: ImageType::External,\n// attributes: Default::default(),\n// };\n//\n// assert_eq!(img.image_type, ImageType::External);\n// ```\ntype ImageMetadata struct {\n\t// Image source (URL, data URI, or SVG content identifier)\n\tSrc string `json:\"src\"`\n\t// Alternative text from alt attribute (for accessibility)\n\tAlt *string `json:\"alt,omitempty\"`\n\t// Title attribute (often shown as tooltip)\n\tTitle *string `json:\"title,omitempty\"`\n\t// Image dimensions as (width, height) if available\n\tDimensions *[]uint32 `json:\"dimensions,omitempty\"`\n\t// Image type classification\n\tImageType ImageType `json:\"image_type\"`\n\t// Additional HTML attributes\n\tAttributes map[string]string `json:\"attributes,omitempty\"`\n}\n\n// StructuredData structured data block (JSON-LD, Microdata, or RDFa).\n//\n// Represents machine-readable structured data found in the document.\n// JSON-LD blocks are collected as raw JSON strings for flexibility.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n// let schema = StructuredData {\n// data_type: StructuredDataType::JsonLd,\n// raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n// schema_type: Some(\"Article\".to_string()),\n// };\n//\n// assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n// ```\ntype StructuredData struct {\n\t// Type of structured data (JSON-LD, Microdata, RDFa)\n\tDataType StructuredDataType `json:\"data_type\"`\n\t// Raw JSON string (for JSON-LD) or serialized representation\n\tRawJSON string `json:\"raw_json\"`\n\t// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n\tSchemaType *string `json:\"schema_type,omitempty\"`\n}\n\n// HTMLMetadata comprehensive metadata extraction result from HTML document.\n//\n// Contains all extracted metadata types in a single structure,\n// suitable for serialization and transmission across language boundaries.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::HtmlMetadata;\n// let metadata = HtmlMetadata {\n// document: Default::default(),\n// headers: Vec::new(),\n// links: Vec::new(),\n// images: Vec::new(),\n// structured_data: Vec::new(),\n// };\n//\n// assert!(metadata.headers.is_empty());\n// ```\ntype HTMLMetadata struct {\n\t// Document-level metadata (title, description, canonical, etc.)\n\tDocument DocumentMetadata `json:\"document\"`\n\t// Extracted header elements with hierarchy\n\tHeaders []HeaderMetadata `json:\"headers,omitempty\"`\n\t// Extracted hyperlinks with type classification\n\tLinks []LinkMetadata `json:\"links,omitempty\"`\n\t// Extracted images with source and dimensions\n\tImages []ImageMetadata `json:\"images,omitempty\"`\n\t// Extracted structured data blocks\n\tStructuredData []StructuredData `json:\"structured_data,omitempty\"`\n}\n\n// HTMLMetadataOption is an option function for HTMLMetadata.\ntype HTMLMetadataOption func(*HTMLMetadata)\n\n// WithHTMLMetadataDocument sets the document field.\nfunc WithHTMLMetadataDocument(v DocumentMetadata) HTMLMetadataOption {\n\treturn func(c *HTMLMetadata) { c.Document = v }\n}\n\n// WithHTMLMetadataHeaders sets the headers field.\nfunc WithHTMLMetadataHeaders(v []HeaderMetadata) HTMLMetadataOption {\n\treturn func(c *HTMLMetadata) { c.Headers = v }\n}\n\n// WithHTMLMetadataLinks sets the links field.\nfunc WithHTMLMetadataLinks(v []LinkMetadata) HTMLMetadataOption {\n\treturn func(c *HTMLMetadata) { c.Links = v }\n}\n\n// WithHTMLMetadataImages sets the images field.\nfunc WithHTMLMetadataImages(v []ImageMetadata) HTMLMetadataOption {\n\treturn func(c *HTMLMetadata) { c.Images = v }\n}\n\n// WithHTMLMetadataStructuredData sets the structured_data field.\nfunc WithHTMLMetadataStructuredData(v []StructuredData) HTMLMetadataOption {\n\treturn func(c *HTMLMetadata) { c.StructuredData = v }\n}\n\n// NewHTMLMetadata creates a HTMLMetadata with optional parameters.\nfunc NewHTMLMetadata(opts ...HTMLMetadataOption) *HTMLMetadata {\n\tc := &HTMLMetadata{\n\t\tDocument:       DocumentMetadata{},\n\t\tHeaders:        nil,\n\t\tLinks:          nil,\n\t\tImages:         nil,\n\t\tStructuredData: nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// ConversionOptions main conversion options for HTML to Markdown conversion.\n//\n// Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n//\n// # Example\n//\n// ```text\n// use html_to_markdown_rs::ConversionOptions;\n//\n// let options = ConversionOptions::builder()\n// .heading_style(HeadingStyle::Atx)\n// .wrap(true)\n// .wrap_width(100)\n// .build();\n// ```\ntype ConversionOptions struct {\n\t// Heading style to use in Markdown output (ATX `#` or Setext underline).\n\tHeadingStyle HeadingStyle `json:\"heading_style,omitempty\"`\n\t// How to indent nested list items (spaces or tab).\n\tListIndentType ListIndentType `json:\"list_indent_type,omitempty\"`\n\t// Number of spaces (or tabs) to use for each level of list indentation.\n\tListIndentWidth *uint `json:\"list_indent_width,omitempty\"`\n\t// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n\tBullets *string `json:\"bullets,omitempty\"`\n\t// Character used for bold/italic emphasis markers (`*` or `_`).\n\tStrongEmSymbol *string `json:\"strong_em_symbol,omitempty\"`\n\t// Escape `*` characters in plain text to avoid unintended bold/italic.\n\tEscapeAsterisks bool `json:\"escape_asterisks\"`\n\t// Escape `_` characters in plain text to avoid unintended bold/italic.\n\tEscapeUnderscores bool `json:\"escape_underscores\"`\n\t// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n\tEscapeMisc bool `json:\"escape_misc\"`\n\t// Escape ASCII characters that have special meaning in certain Markdown dialects.\n\tEscapeASCII bool `json:\"escape_ascii\"`\n\t// Default language annotation for fenced code blocks that have no language hint.\n\tCodeLanguage string `json:\"code_language\"`\n\t// Automatically convert bare URLs into Markdown autolinks.\n\tAutolinks *bool `json:\"autolinks,omitempty\"`\n\t// Emit a default title when no `<title>` tag is present.\n\tDefaultTitle bool `json:\"default_title\"`\n\t// Render `<br>` elements inside table cells as literal line breaks.\n\tBrInTables bool `json:\"br_in_tables\"`\n\t// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n\tHighlightStyle HighlightStyle `json:\"highlight_style,omitempty\"`\n\t// Extract `<meta>` and `<head>` information into the result metadata.\n\tExtractMetadata *bool `json:\"extract_metadata,omitempty\"`\n\t// Controls how whitespace is normalised during conversion.\n\tWhitespaceMode WhitespaceMode `json:\"whitespace_mode,omitempty\"`\n\t// Strip all newlines from the output, producing a single-line result.\n\tStripNewlines bool `json:\"strip_newlines\"`\n\t// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n\tWrap bool `json:\"wrap\"`\n\t// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n\tWrapWidth *uint `json:\"wrap_width,omitempty\"`\n\t// Treat the entire document as inline content (no block-level wrappers).\n\tConvertAsInline bool `json:\"convert_as_inline\"`\n\t// Markdown notation for subscript text (e.g. `\"~\"`).\n\tSubSymbol string `json:\"sub_symbol\"`\n\t// Markdown notation for superscript text (e.g. `\"^\"`).\n\tSupSymbol string `json:\"sup_symbol\"`\n\t// How to encode hard line breaks (`<br>`) in Markdown.\n\tNewlineStyle *NewlineStyle `json:\"newline_style,omitempty\"`\n\t// Style used for fenced code blocks (backticks or tilde).\n\tCodeBlockStyle CodeBlockStyle `json:\"code_block_style,omitempty\"`\n\t// HTML tag names whose `<img>` children are kept inline instead of block.\n\tKeepInlineImagesIn []string `json:\"keep_inline_images_in,omitempty\"`\n\t// Pre-processing options applied to the HTML before conversion.\n\tPreprocessing PreprocessingOptions `json:\"preprocessing\"`\n\t// Expected character encoding of the input HTML (default `\"utf-8\"`).\n\tEncoding *string `json:\"encoding,omitempty\"`\n\t// Emit debug information during conversion.\n\tDebug bool `json:\"debug\"`\n\t// HTML tag names whose content is stripped from the output entirely.\n\tStripTags []string `json:\"strip_tags,omitempty\"`\n\t// HTML tag names that are preserved verbatim in the output.\n\tPreserveTags []string `json:\"preserve_tags,omitempty\"`\n\t// Skip conversion of `<img>` elements (omit images from output).\n\tSkipImages bool `json:\"skip_images\"`\n\t// Link rendering style (inline or reference).\n\tLinkStyle LinkStyle `json:\"link_style,omitempty\"`\n\t// Target output format (Markdown, plain text, etc.).\n\tOutputFormat OutputFormat `json:\"output_format,omitempty\"`\n\t// Include structured document tree in result.\n\tIncludeDocumentStructure bool `json:\"include_document_structure\"`\n\t// Extract inline images from data URIs and SVGs.\n\tExtractImages bool `json:\"extract_images\"`\n\t// Maximum decoded image size in bytes (default 5MB).\n\tMaxImageSize *uint64 `json:\"max_image_size,omitempty\"`\n\t// Capture SVG elements as images.\n\tCaptureSvg bool `json:\"capture_svg\"`\n\t// Infer image dimensions from data.\n\tInferDimensions *bool `json:\"infer_dimensions,omitempty\"`\n\t// Maximum DOM traversal depth. `None` means unlimited.\n\t// When set, subtrees beyond this depth are silently truncated.\n\tMaxDepth *uint `json:\"max_depth,omitempty\"`\n\t// CSS selectors for elements to exclude entirely (element + all content).\n\t//\n\t// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n\t// excluded elements and all their descendants are dropped from the output.\n\t// Supports any CSS selector that `tl` supports: tag names, `.class`,\n\t// `#id`, `[attribute]`, etc.\n\t//\n\t// Invalid selectors are silently skipped at conversion time.\n\t//\n\t// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n\tExcludeSelectors []string `json:\"exclude_selectors,omitempty\"`\n\t// Visitor is the optional visitor implementation attached to these options.\n\tVisitor HtmlVisitor `json:\"-\"`\n}\n\n// ConversionOptionsOption is an option function for ConversionOptions.\ntype ConversionOptionsOption func(*ConversionOptions)\n\n// WithConversionOptionsHeadingStyle sets the heading_style field.\nfunc WithConversionOptionsHeadingStyle(v HeadingStyle) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.HeadingStyle = v }\n}\n\n// WithConversionOptionsListIndentType sets the list_indent_type field.\nfunc WithConversionOptionsListIndentType(v ListIndentType) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ListIndentType = v }\n}\n\n// WithConversionOptionsListIndentWidth sets the list_indent_width field.\nfunc WithConversionOptionsListIndentWidth(v uint) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ListIndentWidth = &v }\n}\n\n// WithConversionOptionsBullets sets the bullets field.\nfunc WithConversionOptionsBullets(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Bullets = &v }\n}\n\n// WithConversionOptionsStrongEmSymbol sets the strong_em_symbol field.\nfunc WithConversionOptionsStrongEmSymbol(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.StrongEmSymbol = &v }\n}\n\n// WithConversionOptionsEscapeAsterisks sets the escape_asterisks field.\nfunc WithConversionOptionsEscapeAsterisks(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.EscapeAsterisks = v }\n}\n\n// WithConversionOptionsEscapeUnderscores sets the escape_underscores field.\nfunc WithConversionOptionsEscapeUnderscores(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.EscapeUnderscores = v }\n}\n\n// WithConversionOptionsEscapeMisc sets the escape_misc field.\nfunc WithConversionOptionsEscapeMisc(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.EscapeMisc = v }\n}\n\n// WithConversionOptionsEscapeASCII sets the escape_ascii field.\nfunc WithConversionOptionsEscapeASCII(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.EscapeASCII = v }\n}\n\n// WithConversionOptionsCodeLanguage sets the code_language field.\nfunc WithConversionOptionsCodeLanguage(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.CodeLanguage = v }\n}\n\n// WithConversionOptionsAutolinks sets the autolinks field.\nfunc WithConversionOptionsAutolinks(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Autolinks = &v }\n}\n\n// WithConversionOptionsDefaultTitle sets the default_title field.\nfunc WithConversionOptionsDefaultTitle(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.DefaultTitle = v }\n}\n\n// WithConversionOptionsBrInTables sets the br_in_tables field.\nfunc WithConversionOptionsBrInTables(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.BrInTables = v }\n}\n\n// WithConversionOptionsHighlightStyle sets the highlight_style field.\nfunc WithConversionOptionsHighlightStyle(v HighlightStyle) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.HighlightStyle = v }\n}\n\n// WithConversionOptionsExtractMetadata sets the extract_metadata field.\nfunc WithConversionOptionsExtractMetadata(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ExtractMetadata = &v }\n}\n\n// WithConversionOptionsWhitespaceMode sets the whitespace_mode field.\nfunc WithConversionOptionsWhitespaceMode(v WhitespaceMode) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.WhitespaceMode = v }\n}\n\n// WithConversionOptionsStripNewlines sets the strip_newlines field.\nfunc WithConversionOptionsStripNewlines(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.StripNewlines = v }\n}\n\n// WithConversionOptionsWrap sets the wrap field.\nfunc WithConversionOptionsWrap(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Wrap = v }\n}\n\n// WithConversionOptionsWrapWidth sets the wrap_width field.\nfunc WithConversionOptionsWrapWidth(v uint) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.WrapWidth = &v }\n}\n\n// WithConversionOptionsConvertAsInline sets the convert_as_inline field.\nfunc WithConversionOptionsConvertAsInline(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ConvertAsInline = v }\n}\n\n// WithConversionOptionsSubSymbol sets the sub_symbol field.\nfunc WithConversionOptionsSubSymbol(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.SubSymbol = v }\n}\n\n// WithConversionOptionsSupSymbol sets the sup_symbol field.\nfunc WithConversionOptionsSupSymbol(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.SupSymbol = v }\n}\n\n// WithConversionOptionsNewlineStyle sets the newline_style field.\nfunc WithConversionOptionsNewlineStyle(v NewlineStyle) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.NewlineStyle = &v }\n}\n\n// WithConversionOptionsCodeBlockStyle sets the code_block_style field.\nfunc WithConversionOptionsCodeBlockStyle(v CodeBlockStyle) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.CodeBlockStyle = v }\n}\n\n// WithConversionOptionsKeepInlineImagesIn sets the keep_inline_images_in field.\nfunc WithConversionOptionsKeepInlineImagesIn(v []string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.KeepInlineImagesIn = v }\n}\n\n// WithConversionOptionsPreprocessing sets the preprocessing field.\nfunc WithConversionOptionsPreprocessing(v PreprocessingOptions) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Preprocessing = v }\n}\n\n// WithConversionOptionsEncoding sets the encoding field.\nfunc WithConversionOptionsEncoding(v string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Encoding = &v }\n}\n\n// WithConversionOptionsDebug sets the debug field.\nfunc WithConversionOptionsDebug(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Debug = v }\n}\n\n// WithConversionOptionsStripTags sets the strip_tags field.\nfunc WithConversionOptionsStripTags(v []string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.StripTags = v }\n}\n\n// WithConversionOptionsPreserveTags sets the preserve_tags field.\nfunc WithConversionOptionsPreserveTags(v []string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.PreserveTags = v }\n}\n\n// WithConversionOptionsSkipImages sets the skip_images field.\nfunc WithConversionOptionsSkipImages(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.SkipImages = v }\n}\n\n// WithConversionOptionsLinkStyle sets the link_style field.\nfunc WithConversionOptionsLinkStyle(v LinkStyle) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.LinkStyle = v }\n}\n\n// WithConversionOptionsOutputFormat sets the output_format field.\nfunc WithConversionOptionsOutputFormat(v OutputFormat) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.OutputFormat = v }\n}\n\n// WithConversionOptionsIncludeDocumentStructure sets the include_document_structure field.\nfunc WithConversionOptionsIncludeDocumentStructure(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.IncludeDocumentStructure = v }\n}\n\n// WithConversionOptionsExtractImages sets the extract_images field.\nfunc WithConversionOptionsExtractImages(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ExtractImages = v }\n}\n\n// WithConversionOptionsMaxImageSize sets the max_image_size field.\nfunc WithConversionOptionsMaxImageSize(v uint64) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.MaxImageSize = &v }\n}\n\n// WithConversionOptionsCaptureSvg sets the capture_svg field.\nfunc WithConversionOptionsCaptureSvg(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.CaptureSvg = v }\n}\n\n// WithConversionOptionsInferDimensions sets the infer_dimensions field.\nfunc WithConversionOptionsInferDimensions(v bool) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.InferDimensions = &v }\n}\n\n// WithConversionOptionsMaxDepth sets the max_depth field.\nfunc WithConversionOptionsMaxDepth(v uint) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.MaxDepth = &v }\n}\n\n// WithConversionOptionsExcludeSelectors sets the exclude_selectors field.\nfunc WithConversionOptionsExcludeSelectors(v []string) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.ExcludeSelectors = v }\n}\n\n// WithConversionOptionsHtmlVisitor sets the visitor field to the given visitor implementation.\nfunc WithConversionOptionsHtmlVisitor(v HtmlVisitor) ConversionOptionsOption {\n\treturn func(c *ConversionOptions) { c.Visitor = v }\n}\n\n// NewConversionOptions creates a ConversionOptions with optional parameters.\nfunc NewConversionOptions(opts ...ConversionOptionsOption) *ConversionOptions {\n\tc := &ConversionOptions{\n\t\tHeadingStyle:             \"\",\n\t\tListIndentType:           \"\",\n\t\tListIndentWidth:          nil,\n\t\tBullets:                  nil,\n\t\tStrongEmSymbol:           nil,\n\t\tEscapeAsterisks:          false,\n\t\tEscapeUnderscores:        false,\n\t\tEscapeMisc:               false,\n\t\tEscapeASCII:              false,\n\t\tCodeLanguage:             \"\",\n\t\tAutolinks:                nil,\n\t\tDefaultTitle:             false,\n\t\tBrInTables:               false,\n\t\tHighlightStyle:           \"\",\n\t\tExtractMetadata:          nil,\n\t\tWhitespaceMode:           \"\",\n\t\tStripNewlines:            false,\n\t\tWrap:                     false,\n\t\tWrapWidth:                nil,\n\t\tConvertAsInline:          false,\n\t\tSubSymbol:                \"\",\n\t\tSupSymbol:                \"\",\n\t\tNewlineStyle:             nil,\n\t\tCodeBlockStyle:           \"\",\n\t\tKeepInlineImagesIn:       nil,\n\t\tPreprocessing:            PreprocessingOptions{},\n\t\tEncoding:                 nil,\n\t\tDebug:                    false,\n\t\tStripTags:                nil,\n\t\tPreserveTags:             nil,\n\t\tSkipImages:               false,\n\t\tLinkStyle:                \"\",\n\t\tOutputFormat:             \"\",\n\t\tIncludeDocumentStructure: false,\n\t\tExtractImages:            false,\n\t\tMaxImageSize:             nil,\n\t\tCaptureSvg:               false,\n\t\tInferDimensions:          nil,\n\t\tMaxDepth:                 nil,\n\t\tExcludeSelectors:         nil,\n\t\tVisitor:                  nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// ConversionOptionsBuilder builder for [`ConversionOptions`].\n//\n// All fields start with default values. Call `.build()` to produce the final options.\ntype ConversionOptionsBuilder struct {\n\tptr unsafe.Pointer\n}\n\n// Free releases the resources held by this handle.\nfunc (h *ConversionOptionsBuilder) Free() {\n\tif h.ptr != nil {\n\t\tC.htm_conversion_options_builder_free((*C.HTMConversionOptionsBuilder)(h.ptr))\n\t\th.ptr = nil\n\t}\n}\n\n// ConversionOptionsUpdate partial update for `ConversionOptions`.\n//\n// Uses `Option<T>` fields for selective updates. Bindings use this to construct\n// options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\ntype ConversionOptionsUpdate struct {\n\t// Optional override for [`ConversionOptions::heading_style`].\n\tHeadingStyle *HeadingStyle `json:\"heading_style,omitempty\"`\n\t// Optional override for [`ConversionOptions::list_indent_type`].\n\tListIndentType *ListIndentType `json:\"list_indent_type,omitempty\"`\n\t// Optional override for [`ConversionOptions::list_indent_width`].\n\tListIndentWidth *uint `json:\"list_indent_width,omitempty\"`\n\t// Optional override for [`ConversionOptions::bullets`].\n\tBullets *string `json:\"bullets,omitempty\"`\n\t// Optional override for [`ConversionOptions::strong_em_symbol`].\n\tStrongEmSymbol *string `json:\"strong_em_symbol,omitempty\"`\n\t// Optional override for [`ConversionOptions::escape_asterisks`].\n\tEscapeAsterisks *bool `json:\"escape_asterisks,omitempty\"`\n\t// Optional override for [`ConversionOptions::escape_underscores`].\n\tEscapeUnderscores *bool `json:\"escape_underscores,omitempty\"`\n\t// Optional override for [`ConversionOptions::escape_misc`].\n\tEscapeMisc *bool `json:\"escape_misc,omitempty\"`\n\t// Optional override for [`ConversionOptions::escape_ascii`].\n\tEscapeASCII *bool `json:\"escape_ascii,omitempty\"`\n\t// Optional override for [`ConversionOptions::code_language`].\n\tCodeLanguage *string `json:\"code_language,omitempty\"`\n\t// Optional override for [`ConversionOptions::autolinks`].\n\tAutolinks *bool `json:\"autolinks,omitempty\"`\n\t// Optional override for [`ConversionOptions::default_title`].\n\tDefaultTitle *bool `json:\"default_title,omitempty\"`\n\t// Optional override for [`ConversionOptions::br_in_tables`].\n\tBrInTables *bool `json:\"br_in_tables,omitempty\"`\n\t// Optional override for [`ConversionOptions::highlight_style`].\n\tHighlightStyle *HighlightStyle `json:\"highlight_style,omitempty\"`\n\t// Optional override for [`ConversionOptions::extract_metadata`].\n\tExtractMetadata *bool `json:\"extract_metadata,omitempty\"`\n\t// Optional override for [`ConversionOptions::whitespace_mode`].\n\tWhitespaceMode *WhitespaceMode `json:\"whitespace_mode,omitempty\"`\n\t// Optional override for [`ConversionOptions::strip_newlines`].\n\tStripNewlines *bool `json:\"strip_newlines,omitempty\"`\n\t// Optional override for [`ConversionOptions::wrap`].\n\tWrap *bool `json:\"wrap,omitempty\"`\n\t// Optional override for [`ConversionOptions::wrap_width`].\n\tWrapWidth *uint `json:\"wrap_width,omitempty\"`\n\t// Optional override for [`ConversionOptions::convert_as_inline`].\n\tConvertAsInline *bool `json:\"convert_as_inline,omitempty\"`\n\t// Optional override for [`ConversionOptions::sub_symbol`].\n\tSubSymbol *string `json:\"sub_symbol,omitempty\"`\n\t// Optional override for [`ConversionOptions::sup_symbol`].\n\tSupSymbol *string `json:\"sup_symbol,omitempty\"`\n\t// Optional override for [`ConversionOptions::newline_style`].\n\tNewlineStyle *NewlineStyle `json:\"newline_style,omitempty\"`\n\t// Optional override for [`ConversionOptions::code_block_style`].\n\tCodeBlockStyle *CodeBlockStyle `json:\"code_block_style,omitempty\"`\n\t// Optional override for [`ConversionOptions::keep_inline_images_in`].\n\tKeepInlineImagesIn *[]string `json:\"keep_inline_images_in,omitempty\"`\n\t// Optional override for [`ConversionOptions::preprocessing`].\n\tPreprocessing *PreprocessingOptionsUpdate `json:\"preprocessing,omitempty\"`\n\t// Optional override for [`ConversionOptions::encoding`].\n\tEncoding *string `json:\"encoding,omitempty\"`\n\t// Optional override for [`ConversionOptions::debug`].\n\tDebug *bool `json:\"debug,omitempty\"`\n\t// Optional override for [`ConversionOptions::strip_tags`].\n\tStripTags *[]string `json:\"strip_tags,omitempty\"`\n\t// Optional override for [`ConversionOptions::preserve_tags`].\n\tPreserveTags *[]string `json:\"preserve_tags,omitempty\"`\n\t// Optional override for [`ConversionOptions::skip_images`].\n\tSkipImages *bool `json:\"skip_images,omitempty\"`\n\t// Optional override for [`ConversionOptions::link_style`].\n\tLinkStyle *LinkStyle `json:\"link_style,omitempty\"`\n\t// Optional override for [`ConversionOptions::output_format`].\n\tOutputFormat *OutputFormat `json:\"output_format,omitempty\"`\n\t// Optional override for [`ConversionOptions::include_document_structure`].\n\tIncludeDocumentStructure *bool `json:\"include_document_structure,omitempty\"`\n\t// Optional override for [`ConversionOptions::extract_images`].\n\tExtractImages *bool `json:\"extract_images,omitempty\"`\n\t// Optional override for [`ConversionOptions::max_image_size`].\n\tMaxImageSize *uint64 `json:\"max_image_size,omitempty\"`\n\t// Optional override for [`ConversionOptions::capture_svg`].\n\tCaptureSvg *bool `json:\"capture_svg,omitempty\"`\n\t// Optional override for [`ConversionOptions::infer_dimensions`].\n\tInferDimensions *bool `json:\"infer_dimensions,omitempty\"`\n\t// Optional override for [`ConversionOptions::max_depth`].\n\tMaxDepth *uint `json:\"max_depth,omitempty\"`\n\t// Optional override for [`ConversionOptions::exclude_selectors`].\n\tExcludeSelectors *[]string `json:\"exclude_selectors,omitempty\"`\n\t// Optional override for [`ConversionOptions::visitor`].\n\tVisitor *VisitorHandle `json:\"visitor,omitempty\"`\n}\n\n// PreprocessingOptions hTML preprocessing options for document cleanup before conversion.\ntype PreprocessingOptions struct {\n\t// Enable HTML preprocessing globally\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\t// Preprocessing preset level (Minimal, Standard, Aggressive)\n\tPreset PreprocessingPreset `json:\"preset,omitempty\"`\n\t// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n\tRemoveNavigation *bool `json:\"remove_navigation,omitempty\"`\n\t// Remove form elements (forms, inputs, buttons, etc.)\n\tRemoveForms *bool `json:\"remove_forms,omitempty\"`\n}\n\n// PreprocessingOptionsOption is an option function for PreprocessingOptions.\ntype PreprocessingOptionsOption func(*PreprocessingOptions)\n\n// WithPreprocessingOptionsEnabled sets the enabled field.\nfunc WithPreprocessingOptionsEnabled(v bool) PreprocessingOptionsOption {\n\treturn func(c *PreprocessingOptions) { c.Enabled = &v }\n}\n\n// WithPreprocessingOptionsPreset sets the preset field.\nfunc WithPreprocessingOptionsPreset(v PreprocessingPreset) PreprocessingOptionsOption {\n\treturn func(c *PreprocessingOptions) { c.Preset = v }\n}\n\n// WithPreprocessingOptionsRemoveNavigation sets the remove_navigation field.\nfunc WithPreprocessingOptionsRemoveNavigation(v bool) PreprocessingOptionsOption {\n\treturn func(c *PreprocessingOptions) { c.RemoveNavigation = &v }\n}\n\n// WithPreprocessingOptionsRemoveForms sets the remove_forms field.\nfunc WithPreprocessingOptionsRemoveForms(v bool) PreprocessingOptionsOption {\n\treturn func(c *PreprocessingOptions) { c.RemoveForms = &v }\n}\n\n// NewPreprocessingOptions creates a PreprocessingOptions with optional parameters.\nfunc NewPreprocessingOptions(opts ...PreprocessingOptionsOption) *PreprocessingOptions {\n\tc := &PreprocessingOptions{\n\t\tEnabled:          nil,\n\t\tPreset:           \"\",\n\t\tRemoveNavigation: nil,\n\t\tRemoveForms:      nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// PreprocessingOptionsUpdate partial update for `PreprocessingOptions`.\n//\n// This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n// Only specified fields (Some values) will override existing options; None values leave the\n// corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\ntype PreprocessingOptionsUpdate struct {\n\t// Optional global preprocessing enablement override\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\t// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n\tPreset *PreprocessingPreset `json:\"preset,omitempty\"`\n\t// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n\tRemoveNavigation *bool `json:\"remove_navigation,omitempty\"`\n\t// Optional form element removal override (forms, inputs, buttons, etc.)\n\tRemoveForms *bool `json:\"remove_forms,omitempty\"`\n}\n\n// DocumentStructure structured document tree representing the semantic content of an HTML document.\n//\n// Uses a flat node array with index-based parent/child references for efficient traversal.\ntype DocumentStructure struct {\n\t// All nodes in document reading order.\n\tNodes []DocumentNode `json:\"nodes,omitempty\"`\n\t// The source format (always \"html\" for this crate).\n\tSourceFormat *string `json:\"source_format,omitempty\"`\n}\n\n// DocumentNode single node in the document tree.\ntype DocumentNode struct {\n\t// Deterministic node identifier.\n\tID string `json:\"id\"`\n\t// The semantic content of this node.\n\tContent NodeContent `json:\"content\"`\n\t// Index of the parent node (None for root nodes).\n\tParent *uint32 `json:\"parent,omitempty\"`\n\t// Indices of child nodes in reading order.\n\tChildren []uint32 `json:\"children,omitempty\"`\n\t// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n\tAnnotations []TextAnnotation `json:\"annotations,omitempty\"`\n\t// Format-specific attributes (e.g. class, id, data-* attributes).\n\tAttributes *map[string]string `json:\"attributes,omitempty\"`\n}\n\n// TextAnnotation inline text annotation with byte-range offsets.\n//\n// Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\ntype TextAnnotation struct {\n\t// Start byte offset (inclusive) into the parent node's text.\n\tStart uint32 `json:\"start\"`\n\t// End byte offset (exclusive) into the parent node's text.\n\tEnd uint32 `json:\"end\"`\n\t// The type of annotation.\n\tKind AnnotationKind `json:\"kind\"`\n}\n\n// ConversionResult primary result of HTML conversion and extraction.\n//\n// Contains the converted text output, optional structured document tree,\n// metadata, extracted tables, images, and processing warnings.\n//\n// # Example\n//\n// ```text\n// use html_to_markdown_rs::{convert, ConversionOptions};\n//\n// let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n// assert!(result.content.is_some());\n// assert!(result.warnings.is_empty());\n// ```\ntype ConversionResult struct {\n\t// Converted text output (markdown, djot, or plain text).\n\t//\n\t// `None` when `output_format` is set to `OutputFormat::None`,\n\t// indicating extraction-only mode.\n\tContent *string `json:\"content,omitempty\"`\n\t// Structured document tree with semantic elements.\n\t//\n\t// Populated when `include_document_structure` is `true` in options.\n\tDocument *DocumentStructure `json:\"document,omitempty\"`\n\t// Extracted HTML metadata (title, OG, links, images, structured data).\n\tMetadata HTMLMetadata `json:\"metadata\"`\n\t// Extracted tables with structured cell data and markdown representation.\n\tTables []TableData `json:\"tables,omitempty\"`\n\t// Extracted inline images (data URIs and SVGs).\n\t//\n\t// Populated when `extract_images` is `true` in options.\n\tImages []string `json:\"images,omitempty\"`\n\t// Non-fatal processing warnings.\n\tWarnings []ProcessingWarning `json:\"warnings,omitempty\"`\n}\n\n// ConversionResultOption is an option function for ConversionResult.\ntype ConversionResultOption func(*ConversionResult)\n\n// WithConversionResultContent sets the content field.\nfunc WithConversionResultContent(v string) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Content = &v }\n}\n\n// WithConversionResultDocument sets the document field.\nfunc WithConversionResultDocument(v DocumentStructure) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Document = &v }\n}\n\n// WithConversionResultMetadata sets the metadata field.\nfunc WithConversionResultMetadata(v HTMLMetadata) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Metadata = v }\n}\n\n// WithConversionResultTables sets the tables field.\nfunc WithConversionResultTables(v []TableData) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Tables = v }\n}\n\n// WithConversionResultImages sets the images field.\nfunc WithConversionResultImages(v []string) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Images = v }\n}\n\n// WithConversionResultWarnings sets the warnings field.\nfunc WithConversionResultWarnings(v []ProcessingWarning) ConversionResultOption {\n\treturn func(c *ConversionResult) { c.Warnings = v }\n}\n\n// NewConversionResult creates a ConversionResult with optional parameters.\nfunc NewConversionResult(opts ...ConversionResultOption) *ConversionResult {\n\tc := &ConversionResult{\n\t\tContent:  nil,\n\t\tDocument: nil,\n\t\tMetadata: HTMLMetadata{},\n\t\tTables:   nil,\n\t\tImages:   nil,\n\t\tWarnings: nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// TableGrid structured table grid with cell-level data including spans.\ntype TableGrid struct {\n\t// Number of rows.\n\tRows uint32 `json:\"rows\"`\n\t// Number of columns.\n\tCols uint32 `json:\"cols\"`\n\t// All cells in the table (may be fewer than rows*cols due to spans).\n\tCells []GridCell `json:\"cells,omitempty\"`\n}\n\n// TableGridOption is an option function for TableGrid.\ntype TableGridOption func(*TableGrid)\n\n// WithTableGridRows sets the rows field.\nfunc WithTableGridRows(v uint32) TableGridOption {\n\treturn func(c *TableGrid) { c.Rows = v }\n}\n\n// WithTableGridCols sets the cols field.\nfunc WithTableGridCols(v uint32) TableGridOption {\n\treturn func(c *TableGrid) { c.Cols = v }\n}\n\n// WithTableGridCells sets the cells field.\nfunc WithTableGridCells(v []GridCell) TableGridOption {\n\treturn func(c *TableGrid) { c.Cells = v }\n}\n\n// NewTableGrid creates a TableGrid with optional parameters.\nfunc NewTableGrid(opts ...TableGridOption) *TableGrid {\n\tc := &TableGrid{\n\t\tRows:  0,\n\t\tCols:  0,\n\t\tCells: nil,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// GridCell single cell in a table grid.\ntype GridCell struct {\n\t// The text content of the cell.\n\tContent string `json:\"content\"`\n\t// 0-indexed row position.\n\tRow uint32 `json:\"row\"`\n\t// 0-indexed column position.\n\tCol uint32 `json:\"col\"`\n\t// Number of rows this cell spans (default 1).\n\tRowSpan uint32 `json:\"row_span\"`\n\t// Number of columns this cell spans (default 1).\n\tColSpan uint32 `json:\"col_span\"`\n\t// Whether this is a header cell (`<th>`).\n\tIsHeader bool `json:\"is_header\"`\n}\n\n// TableData top-level extracted table with both structured data and markdown representation.\ntype TableData struct {\n\t// The structured table grid.\n\tGrid TableGrid `json:\"grid\"`\n\t// The markdown rendering of this table.\n\tMarkdown string `json:\"markdown\"`\n}\n\n// ProcessingWarning non-fatal warning generated during HTML processing.\ntype ProcessingWarning struct {\n\t// Human-readable warning message.\n\tMessage string `json:\"message\"`\n\t// The category of warning.\n\tKind WarningKind `json:\"kind\"`\n}\n\n// VisitorHandle type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n//\n// This allows visitors to be passed around and shared while still being mutable.\ntype VisitorHandle struct {\n\tptr unsafe.Pointer\n}\n\n// Free releases the resources held by this handle.\nfunc (h *VisitorHandle) Free() {\n\tif h.ptr != nil {\n\t\tC.htm_visitor_handle_free((*C.HTMVisitorHandle)(h.ptr))\n\t\th.ptr = nil\n\t}\n}\n\n// Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n// and warnings.\n//\n// # Arguments\n//\n// * `html` — the HTML string to convert.\n// * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n// When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n// attached via the `visitor` field on `ConversionOptions`.\n//\n// # Example\n//\n// ```\n// use html_to_markdown_rs::convert;\n//\n// let html = \"<h1>Hello World</h1>\";\n// let result = convert(html, None).unwrap();\n// assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n// ```\n//\n// # Errors\n//\n// Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\nfunc Convert(html string, options *ConversionOptions) (*ConversionResult, error) {\n\tchtml := C.CString(html)\n\tdefer C.free(unsafe.Pointer(chtml))\n\n\tjsonBytescOptions, err := json.Marshal(options)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\ttmpStrcOptions := C.CString(string(jsonBytescOptions))\n\ttmpUpdatecOptions := C.htm_conversion_options_update_from_json(tmpStrcOptions)\n\tC.free(unsafe.Pointer(tmpStrcOptions))\n\tcOptions := C.htm_conversion_options_from(tmpUpdatecOptions)\n\tC.htm_conversion_options_update_free(tmpUpdatecOptions)\n\tdefer C.htm_conversion_options_free(cOptions)\n\n\tif options.Visitor != nil {\n\t\tvisitorHandle := cgo.NewHandle(options.Visitor)\n\t\tdefer visitorHandle.Delete()\n\t\tC.htm_options_set_visitor(cOptions, (*C.HTMHtmHtmlVisitorBridge)(unsafe.Pointer(uintptr(visitorHandle))))\n\t}\n\n\tptr := C.htm_convert(chtml, cOptions)\n\tif err := lastError(); err != nil {\n\t\tif ptr != nil {\n\t\t\tC.htm_conversion_result_free(ptr)\n\t\t}\n\t\treturn nil, err\n\t}\n\tdefer C.htm_conversion_result_free(ptr)\n\treturn func() *ConversionResult {\n\t\tjsonPtr := C.htm_conversion_result_to_json(ptr)\n\t\tif jsonPtr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tdefer C.htm_free_string(jsonPtr)\n\t\tvar result ConversionResult\n\t\tif err := json.Unmarshal([]byte(C.GoString(jsonPtr)), &result); err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &result\n\t}(), nil\n}\n\n// IsValid validate that the header level is within valid range (1-6).\n//\n// # Returns\n//\n// `true` if level is 1-6, `false` otherwise.\n//\n// # Examples\n//\n// ```\n// # use html_to_markdown_rs::metadata::HeaderMetadata;\n// let valid = HeaderMetadata {\n// level: 3,\n// text: \"Title\".to_string(),\n// id: None,\n// depth: 2,\n// html_offset: 100,\n// };\n// assert!(valid.is_valid());\n//\n// let invalid = HeaderMetadata {\n// level: 7,  // Invalid\n// text: \"Title\".to_string(),\n// id: None,\n// depth: 2,\n// html_offset: 100,\n// };\n// assert!(!invalid.is_valid());\n// ```\nfunc (r *HeaderMetadata) IsValid() (*bool, error) {\n\tjsonBytesRecv, err := json.Marshal(r)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal receiver: %w\", err)\n\t}\n\ttmpStrRecv := C.CString(string(jsonBytesRecv))\n\tcRecv := C.htm_header_metadata_from_json(tmpStrRecv)\n\tC.free(unsafe.Pointer(tmpStrRecv))\n\tdefer C.htm_header_metadata_free(cRecv)\n\tptr := C.htm_header_metadata_is_valid(cRecv)\n\treturn func() *bool { v := ptr != 0; return &v }(), nil\n}\n\n// ApplyUpdate apply a partial update to these conversion options.\nfunc (r *ConversionOptions) ApplyUpdate(update ConversionOptionsUpdate) error {\n\tjsonBytescUpdate, err := json.Marshal(update)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\ttmpStrcUpdate := C.CString(string(jsonBytescUpdate))\n\tcUpdate := C.htm_conversion_options_update_from_json(tmpStrcUpdate)\n\tC.free(unsafe.Pointer(tmpStrcUpdate))\n\tdefer C.htm_conversion_options_update_free(cUpdate)\n\n\tjsonBytesRecv, err := json.Marshal(r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal receiver: %w\", err)\n\t}\n\ttmpStrRecv := C.CString(string(jsonBytesRecv))\n\ttmpUpdateRecv := C.htm_conversion_options_update_from_json(tmpStrRecv)\n\tC.free(unsafe.Pointer(tmpStrRecv))\n\tcRecv := C.htm_conversion_options_from(tmpUpdateRecv)\n\tC.htm_conversion_options_update_free(tmpUpdateRecv)\n\tdefer C.htm_conversion_options_free(cRecv)\n\tC.htm_conversion_options_apply_update(cRecv, cUpdate)\n\tjsonPtrUpdated := C.htm_conversion_options_to_json(cRecv)\n\tif jsonPtrUpdated != nil {\n\t\t_ = json.Unmarshal([]byte(C.GoString(jsonPtrUpdated)), r)\n\t\tC.htm_free_string(jsonPtrUpdated)\n\t}\n\treturn nil\n}\n\n// StripTags set the list of HTML tag names whose content is stripped from output.\nfunc (h *ConversionOptionsBuilder) StripTags(tags []string) (*ConversionOptionsBuilder, error) {\n\tjsonBytescTags, err := json.Marshal(tags)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\tcTags := C.CString(string(jsonBytescTags))\n\tdefer C.free(unsafe.Pointer(cTags))\n\n\tptr := C.htm_conversion_options_builder_strip_tags((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cTags)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h, nil\n}\n\n// PreserveTags set the list of HTML tag names that are preserved verbatim in output.\nfunc (h *ConversionOptionsBuilder) PreserveTags(tags []string) (*ConversionOptionsBuilder, error) {\n\tjsonBytescTags, err := json.Marshal(tags)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\tcTags := C.CString(string(jsonBytescTags))\n\tdefer C.free(unsafe.Pointer(cTags))\n\n\tptr := C.htm_conversion_options_builder_preserve_tags((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cTags)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h, nil\n}\n\n// KeepInlineImagesIn set the list of HTML tag names whose `<img>` children are kept inline.\nfunc (h *ConversionOptionsBuilder) KeepInlineImagesIn(tags []string) (*ConversionOptionsBuilder, error) {\n\tjsonBytescTags, err := json.Marshal(tags)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\tcTags := C.CString(string(jsonBytescTags))\n\tdefer C.free(unsafe.Pointer(cTags))\n\n\tptr := C.htm_conversion_options_builder_keep_inline_images_in((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cTags)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h, nil\n}\n\n// ExcludeSelectors set the list of CSS selectors for elements to exclude entirely from output.\nfunc (h *ConversionOptionsBuilder) ExcludeSelectors(selectors []string) (*ConversionOptionsBuilder, error) {\n\tjsonBytescSelectors, err := json.Marshal(selectors)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\tcSelectors := C.CString(string(jsonBytescSelectors))\n\tdefer C.free(unsafe.Pointer(cSelectors))\n\n\tptr := C.htm_conversion_options_builder_exclude_selectors((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cSelectors)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h, nil\n}\n\n// Visitor set the visitor used during conversion.\nfunc (h *ConversionOptionsBuilder) Visitor(visitor *VisitorHandle) *ConversionOptionsBuilder {\n\tcVisitor := (*C.HTMVisitorHandle)(unsafe.Pointer(visitor.ptr))\n\n\tptr := C.htm_conversion_options_builder_visitor((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cVisitor)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h\n}\n\n// Preprocessing set the pre-processing options applied to the HTML before conversion.\nfunc (h *ConversionOptionsBuilder) Preprocessing(preprocessing PreprocessingOptions) (*ConversionOptionsBuilder, error) {\n\tjsonBytescPreprocessing, err := json.Marshal(preprocessing)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\ttmpStrcPreprocessing := C.CString(string(jsonBytescPreprocessing))\n\ttmpUpdatecPreprocessing := C.htm_preprocessing_options_update_from_json(tmpStrcPreprocessing)\n\tC.free(unsafe.Pointer(tmpStrcPreprocessing))\n\tcPreprocessing := C.htm_preprocessing_options_from(tmpUpdatecPreprocessing)\n\tC.htm_preprocessing_options_update_free(tmpUpdatecPreprocessing)\n\tdefer C.htm_preprocessing_options_free(cPreprocessing)\n\n\tptr := C.htm_conversion_options_builder_preprocessing((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)), cPreprocessing)\n\th.ptr = unsafe.Pointer(ptr)\n\treturn h, nil\n}\n\n// Build the final [`ConversionOptions`].\nfunc (h *ConversionOptionsBuilder) Build() *ConversionOptions {\n\tptr := C.htm_conversion_options_builder_build((*C.HTMConversionOptionsBuilder)(unsafe.Pointer(h.ptr)))\n\tdefer C.htm_conversion_options_free(ptr)\n\treturn func() *ConversionOptions {\n\t\tjsonPtr := C.htm_conversion_options_to_json(ptr)\n\t\tif jsonPtr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tdefer C.htm_free_string(jsonPtr)\n\t\tvar result ConversionOptions\n\t\tif err := json.Unmarshal([]byte(C.GoString(jsonPtr)), &result); err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &result\n\t}()\n}\n\n// ApplyUpdate apply a partial update to these preprocessing options.\n//\n// Any specified fields in the update will override the current values.\n// Unspecified fields (None) are left unchanged.\n//\n// # Arguments\n//\n// * `update` - Partial preprocessing options update\nfunc (r *PreprocessingOptions) ApplyUpdate(update PreprocessingOptionsUpdate) error {\n\tjsonBytescUpdate, err := json.Marshal(update)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal: %w\", err)\n\t}\n\ttmpStrcUpdate := C.CString(string(jsonBytescUpdate))\n\tcUpdate := C.htm_preprocessing_options_update_from_json(tmpStrcUpdate)\n\tC.free(unsafe.Pointer(tmpStrcUpdate))\n\tdefer C.htm_preprocessing_options_update_free(cUpdate)\n\n\tjsonBytesRecv, err := json.Marshal(r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal receiver: %w\", err)\n\t}\n\ttmpStrRecv := C.CString(string(jsonBytesRecv))\n\ttmpUpdateRecv := C.htm_preprocessing_options_update_from_json(tmpStrRecv)\n\tC.free(unsafe.Pointer(tmpStrRecv))\n\tcRecv := C.htm_preprocessing_options_from(tmpUpdateRecv)\n\tC.htm_preprocessing_options_update_free(tmpUpdateRecv)\n\tdefer C.htm_preprocessing_options_free(cRecv)\n\tC.htm_preprocessing_options_apply_update(cRecv, cUpdate)\n\tjsonPtrUpdated := C.htm_preprocessing_options_to_json(cRecv)\n\tif jsonPtrUpdated != nil {\n\t\t_ = json.Unmarshal([]byte(C.GoString(jsonPtrUpdated)), r)\n\t\tC.htm_free_string(jsonPtrUpdated)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "packages/go/go.mod",
    "content": "module github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\n\ngo 1.21\n"
  },
  {
    "path": "packages/go/v3/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with Go bindings to the Rust core library.\nSupports automatic downloading of prebuilt FFI libraries for Linux, macOS, and Windows with customizable caching.\n\n## Installation\n\n```bash\ngo get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\n```\n\nRequires Go 1.25+. After installing the package, run `go generate` to automatically download the platform-specific FFI library:\n\n```bash\ngo generate\n```\n\nThis downloads the native library from GitHub releases and generates the necessary CGO flags. The library is cached in `~/.html-to-markdown/` for subsequent builds.\n\nAlternatively, you can manually set `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables if you prefer to manage the FFI library yourself.\n\n## Performance Snapshot\n\n**Apple M4** · `Convert()` · Real Wikipedia documents\n\n| Document            | Size  | Latency | Throughput |\n| ------------------- | ----- | ------- | ---------- |\n| Lists (Timeline)    | 129KB | 0.46ms  | 277.5 MB/s |\n| Tables (Countries)  | 360KB | 1.37ms  | 262.1 MB/s |\n| Mixed (Python wiki) | 656KB | 2.75ms  | 237.9 MB/s |\n\n## Quick Start\n\nBasic conversion:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    html := \"<h1>Hello World</h1><p>This is a paragraph.</p>\"\n\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n\nWith conversion options:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    // Check library version\n    version := htmltomarkdown.Version()\n    fmt.Printf(\"html-to-markdown version: %s\\n\", version)\n\n    html := \"<h1>Hello</h1><p>Welcome</p>\"\n\n    // Convert with error handling\n    result, err := htmltomarkdown.Convert(html)\n    if err != nil {\n        log.Fatalf(\"Conversion failed: %v\", err)\n    }\n\n    if result.Content != nil {\n        fmt.Println(*result.Content)\n    }\n}\n```\n\n## API Reference\n\n### Core Function\n\n**`Convert(html string, options ...ConversionOptions) (ConversionResult, error)`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` struct with all results in a single call.\n\n```go\nresult, err := htmltomarkdown.Convert(html)\nmarkdown  := result.Content    // *string — converted Markdown\nmetadata  := result.Metadata   // *Metadata — when ExtractMetadata: true\ntables    := result.Tables     // []TableData — when ExtractTables: true\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n// Default Markdown output\nmarkdown, _ := htmltomarkdown.Convert(html)\n// Result: \"This is **bold** and *italic* text.\"\n\n// Note: Djot output format configuration is not yet supported in Go bindings\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain, _ := htmltomarkdown.Convert(html, htmltomarkdown.WithOutputFormat(\"plain\"))\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Go Packages:** [pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2](https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/java/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with Java Panama FFI bindings to the Rust core.\nUses Foreign Function & Memory API for zero-dependency, thread-safe conversion with full metadata extraction support.\n\n## Installation\n\n```bash\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.1.0</version>\n    <classifier>linux</classifier> <!-- or macos, windows -->\n</dependency>\n```\n\nRequires Java 25+ with Panama FFI support.\n\n**Maven:**\n\n```xml\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.4.0-rc.25</version>\n</dependency>\n```\n\n**Gradle (Kotlin DSL):**\n\n```kotlin\nimplementation(\"dev.kreuzberg:html-to-markdown:3.4.0-rc.25\")\n```\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document           | Size  | Latency | Throughput |\n| ------------------ | ----- | ------- | ---------- |\n| Lists (Timeline)   | 129KB |         | 291.5 MB/s |\n| Tables (Countries) | 360KB |         | 272.0 MB/s |\n| Mixed (Python)     | 656KB |         | 258.5 MB/s |\n\n## Quick Start\n\nBasic conversion:\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class Example {\n    public static void main(String[] args) {\n        String html = \"<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>\";\n        ConversionResult result = HtmlToMarkdown.convert(html);\n        System.out.println(result.content());\n    }\n}\n```\n\nWith conversion options:\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class MetadataExample {\n    public static void main(String[] args) {\n        String html = \"<html><head><title>My Page</title></head>\"\n            + \"<body><h1>Welcome</h1><a href=\\\"https://example.com\\\">Link</a></body></html>\";\n\n        ConversionOptions options = ConversionOptions.builder()\n            .extractMetadata(true)\n            .build();\n        ConversionResult result = HtmlToMarkdown.convert(html, options);\n\n        System.out.println(\"Markdown: \" + result.content());\n        System.out.println(\"Title: \" + result.metadata().document().title());\n        System.out.println(\"Headers: \" + result.metadata().headers().size());\n        System.out.println(\"Links: \" + result.metadata().links().size());\n    }\n}\n```\n\n## API Reference\n\n### Core Function\n\n**`HtmlToMarkdown.convert(String html) : ConversionResult`**\n**`HtmlToMarkdown.convert(String html, ConversionOptions options) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```java\nConversionResult result = HtmlToMarkdown.convert(html);\nString   markdown = result.content();   // Converted Markdown string\nMetadata metadata = result.metadata();  // null unless extractMetadata(true)\nList<?>  tables   = result.tables();    // empty unless extractTables(true)\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nString markdown = HtmlToMarkdown.convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nString djot = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.DJOT));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nString plain = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.PLAIN));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Maven Central:** [central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown](https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/java/checkstyle-suppressions.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE suppressions PUBLIC\n    \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\"\n    \"https://checkstyle.org/dtds/suppressions_1_2.dtd\">\n\n<suppressions>\n    <!-- Allow star imports in test files -->\n    <suppress checks=\"AvoidStarImport\" files=\".*Test\\.java\"/>\n\n    <!-- Suppress package-info requirement for test packages -->\n    <suppress checks=\"JavadocPackage\" files=\".*[\\\\/]test[\\\\/].*\"/>\n\n    <!-- Allow magic numbers in test files -->\n    <suppress checks=\"MagicNumber\" files=\".*Test\\.java\"/>\n</suppressions>\n"
  },
  {
    "path": "packages/java/checkstyle.properties",
    "content": "checkstyle.suppressions.file=packages/java/checkstyle-suppressions.xml\n"
  },
  {
    "path": "packages/java/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n    \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n    \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n    <property name=\"severity\" value=\"error\"/>\n    <property name=\"fileExtensions\" value=\"java\"/>\n\n    <!-- Suppressions for FFI code patterns -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${checkstyle.suppressions.file}\"/>\n        <property name=\"optional\" value=\"false\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"max\" value=\"120\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <!-- Naming Conventions -->\n        <module name=\"ConstantName\">\n            <!-- Allow C-style snake_case for FFI function handles -->\n            <property name=\"format\" value=\"^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|[a-z_]+)$\"/>\n        </module>\n        <module name=\"LocalFinalVariableName\"/>\n        <module name=\"LocalVariableName\"/>\n        <module name=\"MemberName\"/>\n        <module name=\"MethodName\"/>\n        <module name=\"PackageName\"/>\n        <module name=\"ParameterName\"/>\n        <module name=\"TypeName\"/>\n\n        <!-- Imports -->\n        <module name=\"AvoidStarImport\">\n            <property name=\"allowStaticMemberImports\" value=\"true\"/>\n        </module>\n        <module name=\"RedundantImport\"/>\n        <module name=\"UnusedImports\"/>\n\n        <!-- Size Violations -->\n        <module name=\"MethodLength\">\n            <property name=\"max\" value=\"150\"/>\n        </module>\n\n        <!-- Modifier Checks -->\n        <module name=\"ModifierOrder\"/>\n        <module name=\"RedundantModifier\"/>\n\n        <!-- Coding -->\n        <module name=\"EmptyStatement\"/>\n        <module name=\"EqualsHashCode\"/>\n        <module name=\"SimplifyBooleanExpression\"/>\n        <module name=\"SimplifyBooleanReturn\"/>\n\n        <!-- Misc -->\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"UpperEll\"/>\n    </module>\n</module>\n"
  },
  {
    "path": "packages/java/eclipse-formatter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<profiles version=\"21\">\n    <profile kind=\"CodeFormatterProfile\" name=\"Kreuzberg\" version=\"21\">\n        <setting id=\"org.eclipse.jdt.core.formatter.lineSplit\" value=\"120\"/>\n        <setting id=\"org.eclipse.jdt.core.formatter.tabulation.char\" value=\"space\"/>\n        <setting id=\"org.eclipse.jdt.core.formatter.tabulation.size\" value=\"4\"/>\n        <setting id=\"org.eclipse.jdt.core.formatter.indentation.size\" value=\"4\"/>\n        <setting id=\"org.eclipse.jdt.core.formatter.comment.line_length\" value=\"120\"/>\n    </profile>\n</profiles>\n"
  },
  {
    "path": "packages/java/pmd-ruleset.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"html-to-markdown Java Rules\"\n         xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd\">\n\n    <description>\n        PMD rules for html-to-markdown Java Panama FFI bindings.\n        Relaxed rules for FFI code that requires defensive programming patterns.\n    </description>\n\n    <!-- Use standard Java rulesets as base -->\n    <rule ref=\"category/java/bestpractices.xml\">\n        <!-- Allow throwing NullPointerException for explicit null checks -->\n        <exclude name=\"AvoidThrowingNullPointerException\"/>\n        <!-- Allow catching Throwable in FFI boundary code -->\n        <exclude name=\"AvoidCatchingGenericException\"/>\n    </rule>\n\n    <rule ref=\"category/java/codestyle.xml\">\n        <!-- Allow non-final local variables -->\n        <exclude name=\"LocalVariableCouldBeFinal\"/>\n        <exclude name=\"MethodArgumentCouldBeFinal\"/>\n        <!-- Allow multiple return statements for guard clauses -->\n        <exclude name=\"OnlyOneReturn\"/>\n        <!-- Allow detailed comments -->\n        <exclude name=\"CommentSize\"/>\n    </rule>\n\n    <rule ref=\"category/java/design.xml\">\n        <!-- FFI error handling may require higher cyclomatic complexity -->\n        <exclude name=\"CyclomaticComplexity\"/>\n        <!-- Exception classes don't need serialVersionUID -->\n        <exclude name=\"MissingSerialVersionUID\"/>\n        <!-- Allow throwing RuntimeException for FFI errors -->\n        <exclude name=\"AvoidThrowingRawExceptionTypes\"/>\n    </rule>\n\n    <rule ref=\"category/java/errorprone.xml\"/>\n    <rule ref=\"category/java/performance.xml\"/>\n</ruleset>\n"
  },
  {
    "path": "packages/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.4.0-rc.25</version>\n    <packaging>jar</packaging>\n\n    <name>html-to-markdown</name>\n    <description>High-performance HTML to Markdown converter</description>\n    <url>https://github.com/kreuzberg-dev/html-to-markdown</url>\n\n    <licenses>\n        <license>\n            <name>MIT</name>\n            <url>https://opensource.org/licenses/MIT</url>\n        </license>\n    </licenses>\n\n    <developers>\n        <developer>\n            <name>Na'aman Hirschfeld</name>\n            <email>nhirschfeld@gmail.com</email>\n        </developer>\n    </developers>\n\n    <scm>\n        <connection>scm:git:git://github.com/kreuzberg-dev/html-to-markdown.git</connection>\n        <developerConnection>scm:git:ssh://github.com:kreuzberg-dev/html-to-markdown.git</developerConnection>\n        <url>https://github.com/kreuzberg-dev/html-to-markdown</url>\n    </scm>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.release>25</maven.compiler.release>\n        <junit.version>6.0.3</junit.version>\n        <maven.version>3.9.11</maven.version>\n        <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>\n        <maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>\n        <maven-resources-plugin.version>3.5.0</maven-resources-plugin.version>\n        <maven-clean-plugin.version>3.5.0</maven-clean-plugin.version>\n        <maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>\n        <maven-install-plugin.version>3.1.4</maven-install-plugin.version>\n        <maven-deploy-plugin.version>3.1.4</maven-deploy-plugin.version>\n        <maven-site-plugin.version>4.0.0-M16</maven-site-plugin.version>\n        <maven-source-plugin.version>3.4.0</maven-source-plugin.version>\n        <maven-javadoc-plugin.version>3.12.0</maven-javadoc-plugin.version>\n        <maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>\n        <central-publishing-plugin.version>0.10.0</central-publishing-plugin.version>\n        <spotless-maven-plugin.version>3.4.0</spotless-maven-plugin.version>\n        <!-- Skip GPG signing by default, only enable for publish profile -->\n        <gpg.skip>true</gpg.skip>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>2.21.3</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <version>2.21.3</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-clean-plugin</artifactId>\n                    <version>${maven-clean-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-resources-plugin</artifactId>\n                    <version>${maven-resources-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-jar-plugin</artifactId>\n                    <version>${maven-jar-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-install-plugin</artifactId>\n                    <version>${maven-install-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-deploy-plugin</artifactId>\n                    <version>${maven-deploy-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-site-plugin</artifactId>\n                    <version>${maven-site-plugin.version}</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>3.6.0</version>\n                <configuration>\n                    <configLocation>checkstyle.xml</configLocation>\n                    <propertyExpansion>checkstyle.suppressions.file=${project.basedir}/checkstyle-suppressions.xml</propertyExpansion>\n                    <consoleOutput>true</consoleOutput>\n                    <failsOnError>true</failsOnError>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven-compiler-plugin.version}</version>\n                <configuration>\n                    <release>25</release>\n                    <compilerArgs>\n                        <arg>--enable-preview</arg>\n                    </compilerArgs>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven-surefire-plugin.version}</version>\n                <configuration>\n                    <argLine>@{argLine} --enable-native-access=ALL-UNNAMED --enable-preview -Djava.library.path=${project.basedir}/../../target/release</argLine>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>${maven-source-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar-no-fork</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>${maven-javadoc-plugin.version}</version>\n                <configuration>\n                    <doclint>none</doclint>\n                    <show>protected</show>\n                    <additionalOptions>--enable-preview</additionalOptions>\n                    <sourcepath>${project.basedir}/src/main/java</sourcepath>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>attach-javadocs</id>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <!-- Maven GPG Plugin for signing artifacts -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n                <version>${maven-gpg-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>sign-artifacts</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>sign</goal>\n                        </goals>\n                        <configuration>\n                            <passphraseEnvName>MAVEN_GPG_PASSPHRASE</passphraseEnvName>\n                            <gpgArguments>\n                                <arg>--batch</arg>\n                                <arg>--yes</arg>\n                                <arg>--pinentry-mode=loopback</arg>\n                            </gpgArguments>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>com.diffplug.spotless</groupId>\n                <artifactId>spotless-maven-plugin</artifactId>\n                <version>${spotless-maven-plugin.version}</version>\n                <configuration>\n                    <java>\n                        <eclipse>\n                            <version>4.31</version>\n                            <file>${project.basedir}/eclipse-formatter.xml</file>\n                        </eclipse>\n                    </java>\n                </configuration>\n            </plugin>\n        </plugins>\n\n        <resources>\n            <resource>\n                <directory>src/main/resources</directory>\n            </resource>\n        </resources>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>publish</id>\n            <properties>\n                <gpg.skip>false</gpg.skip>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-deploy-plugin</artifactId>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.sonatype.central</groupId>\n                        <artifactId>central-publishing-maven-plugin</artifactId>\n                        <version>${central-publishing-plugin.version}</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <publishingServerId>ossrh</publishingServerId>\n                            <autoPublish>true</autoPublish>\n                            <waitUntil>published</waitUntil>\n                            <waitMaxTime>7200</waitMaxTime>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "packages/java/pom.xml.versionsBackup",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>3.4.0-rc.22</version>\n    <packaging>jar</packaging>\n\n    <name>html-to-markdown</name>\n    <description>High-performance HTML to Markdown converter</description>\n    <url>https://github.com/kreuzberg-dev/html-to-markdown</url>\n\n    <licenses>\n        <license>\n            <name>MIT</name>\n            <url>https://opensource.org/licenses/MIT</url>\n        </license>\n    </licenses>\n\n    <developers>\n        <developer>\n            <name>Na'aman Hirschfeld</name>\n            <email>nhirschfeld@gmail.com</email>\n        </developer>\n    </developers>\n\n    <scm>\n        <connection>scm:git:git://github.com/kreuzberg-dev/html-to-markdown.git</connection>\n        <developerConnection>scm:git:ssh://github.com:kreuzberg-dev/html-to-markdown.git</developerConnection>\n        <url>https://github.com/kreuzberg-dev/html-to-markdown</url>\n    </scm>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.release>25</maven.compiler.release>\n        <junit.version>6.0.3</junit.version>\n        <maven.version>3.9.11</maven.version>\n        <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>\n        <maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>\n        <maven-resources-plugin.version>3.5.0</maven-resources-plugin.version>\n        <maven-clean-plugin.version>3.5.0</maven-clean-plugin.version>\n        <maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>\n        <maven-install-plugin.version>3.1.4</maven-install-plugin.version>\n        <maven-deploy-plugin.version>3.1.4</maven-deploy-plugin.version>\n        <maven-site-plugin.version>4.0.0-M16</maven-site-plugin.version>\n        <maven-source-plugin.version>3.4.0</maven-source-plugin.version>\n        <maven-javadoc-plugin.version>3.12.0</maven-javadoc-plugin.version>\n        <maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>\n        <central-publishing-plugin.version>0.10.0</central-publishing-plugin.version>\n        <spotless-maven-plugin.version>3.4.0</spotless-maven-plugin.version>\n        <!-- Skip GPG signing by default, only enable for publish profile -->\n        <gpg.skip>true</gpg.skip>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>2.21.2</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <version>2.21.2</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-clean-plugin</artifactId>\n                    <version>${maven-clean-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-resources-plugin</artifactId>\n                    <version>${maven-resources-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-jar-plugin</artifactId>\n                    <version>${maven-jar-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-install-plugin</artifactId>\n                    <version>${maven-install-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-deploy-plugin</artifactId>\n                    <version>${maven-deploy-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-site-plugin</artifactId>\n                    <version>${maven-site-plugin.version}</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>3.6.0</version>\n                <configuration>\n                    <configLocation>checkstyle.xml</configLocation>\n                    <propertyExpansion>checkstyle.suppressions.file=${project.basedir}/checkstyle-suppressions.xml</propertyExpansion>\n                    <consoleOutput>true</consoleOutput>\n                    <failsOnError>true</failsOnError>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven-compiler-plugin.version}</version>\n                <configuration>\n                    <release>25</release>\n                    <compilerArgs>\n                        <arg>--enable-preview</arg>\n                    </compilerArgs>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven-surefire-plugin.version}</version>\n                <configuration>\n                    <argLine>@{argLine} --enable-native-access=ALL-UNNAMED --enable-preview -Djava.library.path=${project.basedir}/../../target/release</argLine>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>${maven-source-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar-no-fork</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>${maven-javadoc-plugin.version}</version>\n                <configuration>\n                    <doclint>none</doclint>\n                    <show>protected</show>\n                    <additionalOptions>--enable-preview</additionalOptions>\n                    <sourcepath>${project.basedir}/src/main/java</sourcepath>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>attach-javadocs</id>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <!-- Maven GPG Plugin for signing artifacts -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n                <version>${maven-gpg-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>sign-artifacts</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>sign</goal>\n                        </goals>\n                        <configuration>\n                            <passphraseEnvName>MAVEN_GPG_PASSPHRASE</passphraseEnvName>\n                            <gpgArguments>\n                                <arg>--batch</arg>\n                                <arg>--yes</arg>\n                                <arg>--pinentry-mode=loopback</arg>\n                            </gpgArguments>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>com.diffplug.spotless</groupId>\n                <artifactId>spotless-maven-plugin</artifactId>\n                <version>${spotless-maven-plugin.version}</version>\n                <configuration>\n                    <java>\n                        <eclipse>\n                            <version>4.31</version>\n                            <file>${project.basedir}/eclipse-formatter.xml</file>\n                        </eclipse>\n                    </java>\n                </configuration>\n            </plugin>\n        </plugins>\n\n        <resources>\n            <resource>\n                <directory>src/main/resources</directory>\n            </resource>\n        </resources>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>publish</id>\n            <properties>\n                <gpg.skip>false</gpg.skip>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-deploy-plugin</artifactId>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.sonatype.central</groupId>\n                        <artifactId>central-publishing-maven-plugin</artifactId>\n                        <version>${central-publishing-plugin.version}</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <publishingServerId>ossrh</publishingServerId>\n                            <autoPublish>true</autoPublish>\n                            <waitUntil>published</waitUntil>\n                            <waitMaxTime>7200</waitMaxTime>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/AnnotationKind.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:aaeef52cfe38f57340313021ecc8db308b4896b6e71b2bf3ae446307e33cd10a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport java.util.Optional;\n\n/**\n * The type of an inline text annotation.\n *\n * Uses internally tagged representation ({@code \"annotation_type\": \"bold\"}) for JSON serialization.\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"annotation_type\", visible = false)\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = AnnotationKind.Bold.class, name = \"bold\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Italic.class, name = \"italic\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Underline.class, name = \"underline\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Strikethrough.class, name = \"strikethrough\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Code.class, name = \"code\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Subscript.class, name = \"subscript\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Superscript.class, name = \"superscript\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Highlight.class, name = \"highlight\"),\n    @JsonSubTypes.Type(value = AnnotationKind.Link.class, name = \"link\")\n})\npublic sealed interface AnnotationKind {\n\n    /** Bold / strong emphasis. */\n    record Bold() implements AnnotationKind {\n    }\n\n    /** Italic / emphasis. */\n    record Italic() implements AnnotationKind {\n    }\n\n    /** Underline. */\n    record Underline() implements AnnotationKind {\n    }\n\n    /** Strikethrough / deleted text. */\n    record Strikethrough() implements AnnotationKind {\n    }\n\n    /** Inline code. */\n    record Code() implements AnnotationKind {\n    }\n\n    /** Subscript text. */\n    record Subscript() implements AnnotationKind {\n    }\n\n    /** Superscript text. */\n    record Superscript() implements AnnotationKind {\n    }\n\n    /** Highlighted / marked text. */\n    record Highlight() implements AnnotationKind {\n    }\n\n    /** A hyperlink. */\n    record Link(\n        @JsonProperty(\"url\") String url,\n        @JsonProperty(\"title\") Optional<String> title\n    ) implements AnnotationKind {\n    }\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/CodeBlockStyle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c09bedd13ecc8b49ff7ba7e9036af1c083c78a97fada47075a01db5d3d12e7db\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Code block fence style in Markdown output.\n *\n * Determines how code blocks ({@code &lt;pre&gt;&lt;code&gt;}) are rendered in Markdown.\n */\npublic enum CodeBlockStyle {\n    /** Indented code blocks (4 spaces). {@code CommonMark} standard. */\n    Indented(\"indented\"),\n    /**\n     * Fenced code blocks with backticks ({@code }{@code ). Default (GFM). Supports language hints.}\n     */\n    Backticks(\"backticks\"),\n    /** Fenced code blocks with tildes (~~~). Supports language hints. */\n    Tildes(\"tildes\");\n\n    /** The string value. */\n    private final String value;\n\n    CodeBlockStyle(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static CodeBlockStyle fromValue(final String value) {\n        for (CodeBlockStyle e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConfigErrorException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:a6ceb1d6d59931a8525167e8e1f06cfd10b68a58cff95cb55a693a5a1cebe19f\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * Invalid configuration\n */\npublic class ConfigErrorException extends ConversionErrorException {\n    /** Creates a new ConfigErrorException with the given message. */\n    public ConfigErrorException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new ConfigErrorException with the given message and cause. */\n    public ConfigErrorException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionErrorException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:84e14f1634d6f6f85a0132706eec22cf05c672faab43305343c7bc4a9bb43c23\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * Errors that can occur during HTML to Markdown conversion.\n */\npublic class ConversionErrorException extends Exception {\n    /** Creates a new ConversionErrorException with the given message. */\n    public ConversionErrorException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new ConversionErrorException with the given message and cause. */\n    public ConversionErrorException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionOptions.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:cd82377fa573e967be1e73f0b3b9b28b1e4e042f1c506085453fe7a7560c28f5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\n\n/**\n * Main conversion options for HTML to Markdown conversion.\n *\n * Use [{@code ConversionOptions::builder()}] to construct, or [{@code Default::default()}] for defaults.\n *\n * # Example\n *\n * {@code }{@code text}\n * use html_to_markdown_rs::ConversionOptions;\n *\n * let options = ConversionOptions::builder()\n *     .heading_style(HeadingStyle::Atx)\n *     .wrap(true)\n *     .wrap_width(100)\n *     .build();\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record ConversionOptions(\n    /** Heading style to use in Markdown output (ATX {@code #} or Setext underline). */\n    @JsonProperty(\"heading_style\") HeadingStyle headingStyle,\n    /** How to indent nested list items (spaces or tab). */\n    @JsonProperty(\"list_indent_type\") ListIndentType listIndentType,\n    /** Number of spaces (or tabs) to use for each level of list indentation. */\n    @JsonProperty(\"list_indent_width\") long listIndentWidth,\n    /** Bullet character(s) to use for unordered list items (e.g. {@code \"-\"}, {@code \"*\"}). */\n    String bullets,\n    /** Character used for bold/italic emphasis markers ({@code *} or {@code _}). */\n    @JsonProperty(\"strong_em_symbol\") String strongEmSymbol,\n    /** Escape {@code *} characters in plain text to avoid unintended bold/italic. */\n    @JsonProperty(\"escape_asterisks\") boolean escapeAsterisks,\n    /** Escape {@code _} characters in plain text to avoid unintended bold/italic. */\n    @JsonProperty(\"escape_underscores\") boolean escapeUnderscores,\n    /** Escape miscellaneous Markdown metacharacters ({@code []()#} etc.) in plain text. */\n    @JsonProperty(\"escape_misc\") boolean escapeMisc,\n    /** Escape ASCII characters that have special meaning in certain Markdown dialects. */\n    @JsonProperty(\"escape_ascii\") boolean escapeAscii,\n    /** Default language annotation for fenced code blocks that have no language hint. */\n    @JsonProperty(\"code_language\") String codeLanguage,\n    /** Automatically convert bare URLs into Markdown autolinks. */\n    boolean autolinks,\n    /** Emit a default title when no {@code &lt;title&gt;} tag is present. */\n    @JsonProperty(\"default_title\") boolean defaultTitle,\n    /** Render {@code &lt;br&gt;} elements inside table cells as literal line breaks. */\n    @JsonProperty(\"br_in_tables\") boolean brInTables,\n    /** Style used for {@code &lt;mark&gt;} / highlighted text (e.g. {@code ==text==}). */\n    @JsonProperty(\"highlight_style\") HighlightStyle highlightStyle,\n    /** Extract {@code &lt;meta&gt;} and {@code &lt;head&gt;} information into the result metadata. */\n    @JsonProperty(\"extract_metadata\") boolean extractMetadata,\n    /** Controls how whitespace is normalised during conversion. */\n    @JsonProperty(\"whitespace_mode\") WhitespaceMode whitespaceMode,\n    /** Strip all newlines from the output, producing a single-line result. */\n    @JsonProperty(\"strip_newlines\") boolean stripNewlines,\n    /** Wrap long lines at [{@code wrap_width}](Self::wrap_width) characters. */\n    boolean wrap,\n    /** Maximum line width when [{@code wrap}](Self::wrap) is enabled (default {@code 80}). */\n    @JsonProperty(\"wrap_width\") long wrapWidth,\n    /** Treat the entire document as inline content (no block-level wrappers). */\n    @JsonProperty(\"convert_as_inline\") boolean convertAsInline,\n    /** Markdown notation for subscript text (e.g. {@code \"~\"}). */\n    @JsonProperty(\"sub_symbol\") String subSymbol,\n    /** Markdown notation for superscript text (e.g. {@code \"^\"}). */\n    @JsonProperty(\"sup_symbol\") String supSymbol,\n    /** How to encode hard line breaks ({@code &lt;br&gt;}) in Markdown. */\n    @JsonProperty(\"newline_style\") NewlineStyle newlineStyle,\n    /** Style used for fenced code blocks (backticks or tilde). */\n    @JsonProperty(\"code_block_style\") CodeBlockStyle codeBlockStyle,\n    /** HTML tag names whose {@code &lt;img&gt;} children are kept inline instead of block. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(\"keep_inline_images_in\") List<String> keepInlineImagesIn,\n    /** Pre-processing options applied to the HTML before conversion. */\n    PreprocessingOptions preprocessing,\n    /** Expected character encoding of the input HTML (default {@code \"utf-8\"}). */\n    String encoding,\n    /** Emit debug information during conversion. */\n    boolean debug,\n    /** HTML tag names whose content is stripped from the output entirely. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(\"strip_tags\") List<String> stripTags,\n    /** HTML tag names that are preserved verbatim in the output. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(\"preserve_tags\") List<String> preserveTags,\n    /** Skip conversion of {@code &lt;img&gt;} elements (omit images from output). */\n    @JsonProperty(\"skip_images\") boolean skipImages,\n    /** Link rendering style (inline or reference). */\n    @JsonProperty(\"link_style\") LinkStyle linkStyle,\n    /** Target output format (Markdown, plain text, etc.). */\n    @JsonProperty(\"output_format\") OutputFormat outputFormat,\n    /** Include structured document tree in result. */\n    @JsonProperty(\"include_document_structure\") boolean includeDocumentStructure,\n    /** Extract inline images from data URIs and SVGs. */\n    @JsonProperty(\"extract_images\") boolean extractImages,\n    /** Maximum decoded image size in bytes (default 5MB). */\n    @JsonProperty(\"max_image_size\") long maxImageSize,\n    /** Capture SVG elements as images. */\n    @JsonProperty(\"capture_svg\") boolean captureSvg,\n    /** Infer image dimensions from data. */\n    @JsonProperty(\"infer_dimensions\") boolean inferDimensions,\n    /** Maximum DOM traversal depth. {@code None} means unlimited. */\n    @JsonProperty(\"max_depth\") Optional<Long> maxDepth,\n    /** CSS selectors for elements to exclude entirely (element + all content). */\n    @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(\"exclude_selectors\") List<String> excludeSelectors,\n    /** Optional visitor for custom traversal logic. */\n    @JsonIgnore VisitorHandle visitor\n) {\n    public static ConversionOptionsBuilder builder() {\n        return new ConversionOptionsBuilder();\n    }\n    public ConversionOptions{\n        if (listIndentWidth == 0) listIndentWidth = 2;\n        if (wrapWidth == 0) wrapWidth = 80;\n        if (maxImageSize == 0) maxImageSize = 5242880;\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionOptionsBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6952efec15b4d286e614162c0ea53e16bcbaf27182ea52e3877e8182faffe2cf\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Main conversion options for HTML to Markdown conversion.\n *\n * Use [{@code ConversionOptions::builder()}] to construct, or [{@code Default::default()}] for defaults.\n *\n * # Example\n *\n * {@code }{@code text}\n * use html_to_markdown_rs::ConversionOptions;\n *\n * let options = ConversionOptions::builder()\n *     .heading_style(HeadingStyle::Atx)\n *     .wrap(true)\n *     .wrap_width(100)\n *     .build();\n * {@code }{@code }\n */\npublic class ConversionOptionsBuilder {\n\n    private HeadingStyle headingStyle = null;\n    private ListIndentType listIndentType = null;\n    private long listIndentWidth = 0;\n    private String bullets = \"\";\n    private String strongEmSymbol = \"\";\n    private boolean escapeAsterisks = false;\n    private boolean escapeUnderscores = false;\n    private boolean escapeMisc = false;\n    private boolean escapeAscii = false;\n    private String codeLanguage = \"\";\n    private boolean autolinks = false;\n    private boolean defaultTitle = false;\n    private boolean brInTables = false;\n    private HighlightStyle highlightStyle = null;\n    private boolean extractMetadata = false;\n    private WhitespaceMode whitespaceMode = null;\n    private boolean stripNewlines = false;\n    private boolean wrap = false;\n    private long wrapWidth = 0;\n    private boolean convertAsInline = false;\n    private String subSymbol = \"\";\n    private String supSymbol = \"\";\n    private NewlineStyle newlineStyle = null;\n    private CodeBlockStyle codeBlockStyle = null;\n    private List<String> keepInlineImagesIn = List.of();\n    private PreprocessingOptions preprocessing = null;\n    private String encoding = \"\";\n    private boolean debug = false;\n    private List<String> stripTags = List.of();\n    private List<String> preserveTags = List.of();\n    private boolean skipImages = false;\n    private LinkStyle linkStyle = null;\n    private OutputFormat outputFormat = null;\n    private boolean includeDocumentStructure = false;\n    private boolean extractImages = false;\n    private long maxImageSize = 0;\n    private boolean captureSvg = false;\n    private boolean inferDimensions = false;\n    private Optional<Long> maxDepth = Optional.empty();\n    private List<String> excludeSelectors = List.of();\n    private Optional<VisitorHandle> visitor = Optional.empty();\n\n    /** Sets the headingStyle field. */\n    public ConversionOptionsBuilder withHeadingStyle(final HeadingStyle value) {\n        this.headingStyle = value;\n        return this;\n    }\n\n    /** Sets the listIndentType field. */\n    public ConversionOptionsBuilder withListIndentType(final ListIndentType value) {\n        this.listIndentType = value;\n        return this;\n    }\n\n    /** Sets the listIndentWidth field. */\n    public ConversionOptionsBuilder withListIndentWidth(final long value) {\n        this.listIndentWidth = value;\n        return this;\n    }\n\n    /** Sets the bullets field. */\n    public ConversionOptionsBuilder withBullets(final String value) {\n        this.bullets = value;\n        return this;\n    }\n\n    /** Sets the strongEmSymbol field. */\n    public ConversionOptionsBuilder withStrongEmSymbol(final String value) {\n        this.strongEmSymbol = value;\n        return this;\n    }\n\n    /** Sets the escapeAsterisks field. */\n    public ConversionOptionsBuilder withEscapeAsterisks(final boolean value) {\n        this.escapeAsterisks = value;\n        return this;\n    }\n\n    /** Sets the escapeUnderscores field. */\n    public ConversionOptionsBuilder withEscapeUnderscores(final boolean value) {\n        this.escapeUnderscores = value;\n        return this;\n    }\n\n    /** Sets the escapeMisc field. */\n    public ConversionOptionsBuilder withEscapeMisc(final boolean value) {\n        this.escapeMisc = value;\n        return this;\n    }\n\n    /** Sets the escapeAscii field. */\n    public ConversionOptionsBuilder withEscapeAscii(final boolean value) {\n        this.escapeAscii = value;\n        return this;\n    }\n\n    /** Sets the codeLanguage field. */\n    public ConversionOptionsBuilder withCodeLanguage(final String value) {\n        this.codeLanguage = value;\n        return this;\n    }\n\n    /** Sets the autolinks field. */\n    public ConversionOptionsBuilder withAutolinks(final boolean value) {\n        this.autolinks = value;\n        return this;\n    }\n\n    /** Sets the defaultTitle field. */\n    public ConversionOptionsBuilder withDefaultTitle(final boolean value) {\n        this.defaultTitle = value;\n        return this;\n    }\n\n    /** Sets the brInTables field. */\n    public ConversionOptionsBuilder withBrInTables(final boolean value) {\n        this.brInTables = value;\n        return this;\n    }\n\n    /** Sets the highlightStyle field. */\n    public ConversionOptionsBuilder withHighlightStyle(final HighlightStyle value) {\n        this.highlightStyle = value;\n        return this;\n    }\n\n    /** Sets the extractMetadata field. */\n    public ConversionOptionsBuilder withExtractMetadata(final boolean value) {\n        this.extractMetadata = value;\n        return this;\n    }\n\n    /** Sets the whitespaceMode field. */\n    public ConversionOptionsBuilder withWhitespaceMode(final WhitespaceMode value) {\n        this.whitespaceMode = value;\n        return this;\n    }\n\n    /** Sets the stripNewlines field. */\n    public ConversionOptionsBuilder withStripNewlines(final boolean value) {\n        this.stripNewlines = value;\n        return this;\n    }\n\n    /** Sets the wrap field. */\n    public ConversionOptionsBuilder withWrap(final boolean value) {\n        this.wrap = value;\n        return this;\n    }\n\n    /** Sets the wrapWidth field. */\n    public ConversionOptionsBuilder withWrapWidth(final long value) {\n        this.wrapWidth = value;\n        return this;\n    }\n\n    /** Sets the convertAsInline field. */\n    public ConversionOptionsBuilder withConvertAsInline(final boolean value) {\n        this.convertAsInline = value;\n        return this;\n    }\n\n    /** Sets the subSymbol field. */\n    public ConversionOptionsBuilder withSubSymbol(final String value) {\n        this.subSymbol = value;\n        return this;\n    }\n\n    /** Sets the supSymbol field. */\n    public ConversionOptionsBuilder withSupSymbol(final String value) {\n        this.supSymbol = value;\n        return this;\n    }\n\n    /** Sets the newlineStyle field. */\n    public ConversionOptionsBuilder withNewlineStyle(final NewlineStyle value) {\n        this.newlineStyle = value;\n        return this;\n    }\n\n    /** Sets the codeBlockStyle field. */\n    public ConversionOptionsBuilder withCodeBlockStyle(final CodeBlockStyle value) {\n        this.codeBlockStyle = value;\n        return this;\n    }\n\n    /** Sets the keepInlineImagesIn field. */\n    public ConversionOptionsBuilder withKeepInlineImagesIn(final List<String> value) {\n        this.keepInlineImagesIn = value;\n        return this;\n    }\n\n    /** Sets the preprocessing field. */\n    public ConversionOptionsBuilder withPreprocessing(final PreprocessingOptions value) {\n        this.preprocessing = value;\n        return this;\n    }\n\n    /** Sets the encoding field. */\n    public ConversionOptionsBuilder withEncoding(final String value) {\n        this.encoding = value;\n        return this;\n    }\n\n    /** Sets the debug field. */\n    public ConversionOptionsBuilder withDebug(final boolean value) {\n        this.debug = value;\n        return this;\n    }\n\n    /** Sets the stripTags field. */\n    public ConversionOptionsBuilder withStripTags(final List<String> value) {\n        this.stripTags = value;\n        return this;\n    }\n\n    /** Sets the preserveTags field. */\n    public ConversionOptionsBuilder withPreserveTags(final List<String> value) {\n        this.preserveTags = value;\n        return this;\n    }\n\n    /** Sets the skipImages field. */\n    public ConversionOptionsBuilder withSkipImages(final boolean value) {\n        this.skipImages = value;\n        return this;\n    }\n\n    /** Sets the linkStyle field. */\n    public ConversionOptionsBuilder withLinkStyle(final LinkStyle value) {\n        this.linkStyle = value;\n        return this;\n    }\n\n    /** Sets the outputFormat field. */\n    public ConversionOptionsBuilder withOutputFormat(final OutputFormat value) {\n        this.outputFormat = value;\n        return this;\n    }\n\n    /** Sets the includeDocumentStructure field. */\n    public ConversionOptionsBuilder withIncludeDocumentStructure(final boolean value) {\n        this.includeDocumentStructure = value;\n        return this;\n    }\n\n    /** Sets the extractImages field. */\n    public ConversionOptionsBuilder withExtractImages(final boolean value) {\n        this.extractImages = value;\n        return this;\n    }\n\n    /** Sets the maxImageSize field. */\n    public ConversionOptionsBuilder withMaxImageSize(final long value) {\n        this.maxImageSize = value;\n        return this;\n    }\n\n    /** Sets the captureSvg field. */\n    public ConversionOptionsBuilder withCaptureSvg(final boolean value) {\n        this.captureSvg = value;\n        return this;\n    }\n\n    /** Sets the inferDimensions field. */\n    public ConversionOptionsBuilder withInferDimensions(final boolean value) {\n        this.inferDimensions = value;\n        return this;\n    }\n\n    /** Sets the maxDepth field. */\n    public ConversionOptionsBuilder withMaxDepth(final Optional<Long> value) {\n        this.maxDepth = value;\n        return this;\n    }\n\n    /** Sets the excludeSelectors field. */\n    public ConversionOptionsBuilder withExcludeSelectors(final List<String> value) {\n        this.excludeSelectors = value;\n        return this;\n    }\n\n    /** Sets the visitor field. */\n    public ConversionOptionsBuilder withVisitor(final Optional<VisitorHandle> value) {\n        this.visitor = value;\n        return this;\n    }\n\n    /** Builds the ConversionOptions instance. */\n    public ConversionOptions build() {\n        return new ConversionOptions(\n            headingStyle,\n            listIndentType,\n            listIndentWidth,\n            bullets,\n            strongEmSymbol,\n            escapeAsterisks,\n            escapeUnderscores,\n            escapeMisc,\n            escapeAscii,\n            codeLanguage,\n            autolinks,\n            defaultTitle,\n            brInTables,\n            highlightStyle,\n            extractMetadata,\n            whitespaceMode,\n            stripNewlines,\n            wrap,\n            wrapWidth,\n            convertAsInline,\n            subSymbol,\n            supSymbol,\n            newlineStyle,\n            codeBlockStyle,\n            keepInlineImagesIn,\n            preprocessing,\n            encoding,\n            debug,\n            stripTags,\n            preserveTags,\n            skipImages,\n            linkStyle,\n            outputFormat,\n            includeDocumentStructure,\n            extractImages,\n            maxImageSize,\n            captureSvg,\n            inferDimensions,\n            maxDepth,\n            excludeSelectors,\n            visitor.orElse(null)\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionOptionsUpdate.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1f5fefe86026cb3b330148144e752d96a579165143e754a0133e57808ceda12e\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Partial update for {@code ConversionOptions}.\n *\n * Uses {@code Option&lt;T&gt;} fields for selective updates. Bindings use this to construct\n * options from language-native types. Prefer [{@code ConversionOptionsBuilder}] for Rust code.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record ConversionOptionsUpdate(\n    /** Optional override for [{@code ConversionOptions::heading_style}]. */\n    @JsonProperty(\"heading_style\") Optional<HeadingStyle> headingStyle,\n    /** Optional override for [{@code ConversionOptions::list_indent_type}]. */\n    @JsonProperty(\"list_indent_type\") Optional<ListIndentType> listIndentType,\n    /** Optional override for [{@code ConversionOptions::list_indent_width}]. */\n    @JsonProperty(\"list_indent_width\") Optional<Long> listIndentWidth,\n    /** Optional override for [{@code ConversionOptions::bullets}]. */\n    Optional<String> bullets,\n    /** Optional override for [{@code ConversionOptions::strong_em_symbol}]. */\n    @JsonProperty(\"strong_em_symbol\") Optional<String> strongEmSymbol,\n    /** Optional override for [{@code ConversionOptions::escape_asterisks}]. */\n    @JsonProperty(\"escape_asterisks\") Optional<Boolean> escapeAsterisks,\n    /** Optional override for [{@code ConversionOptions::escape_underscores}]. */\n    @JsonProperty(\"escape_underscores\") Optional<Boolean> escapeUnderscores,\n    /** Optional override for [{@code ConversionOptions::escape_misc}]. */\n    @JsonProperty(\"escape_misc\") Optional<Boolean> escapeMisc,\n    /** Optional override for [{@code ConversionOptions::escape_ascii}]. */\n    @JsonProperty(\"escape_ascii\") Optional<Boolean> escapeAscii,\n    /** Optional override for [{@code ConversionOptions::code_language}]. */\n    @JsonProperty(\"code_language\") Optional<String> codeLanguage,\n    /** Optional override for [{@code ConversionOptions::autolinks}]. */\n    Optional<Boolean> autolinks,\n    /** Optional override for [{@code ConversionOptions::default_title}]. */\n    @JsonProperty(\"default_title\") Optional<Boolean> defaultTitle,\n    /** Optional override for [{@code ConversionOptions::br_in_tables}]. */\n    @JsonProperty(\"br_in_tables\") Optional<Boolean> brInTables,\n    /** Optional override for [{@code ConversionOptions::highlight_style}]. */\n    @JsonProperty(\"highlight_style\") Optional<HighlightStyle> highlightStyle,\n    /** Optional override for [{@code ConversionOptions::extract_metadata}]. */\n    @JsonProperty(\"extract_metadata\") Optional<Boolean> extractMetadata,\n    /** Optional override for [{@code ConversionOptions::whitespace_mode}]. */\n    @JsonProperty(\"whitespace_mode\") Optional<WhitespaceMode> whitespaceMode,\n    /** Optional override for [{@code ConversionOptions::strip_newlines}]. */\n    @JsonProperty(\"strip_newlines\") Optional<Boolean> stripNewlines,\n    /** Optional override for [{@code ConversionOptions::wrap}]. */\n    Optional<Boolean> wrap,\n    /** Optional override for [{@code ConversionOptions::wrap_width}]. */\n    @JsonProperty(\"wrap_width\") Optional<Long> wrapWidth,\n    /** Optional override for [{@code ConversionOptions::convert_as_inline}]. */\n    @JsonProperty(\"convert_as_inline\") Optional<Boolean> convertAsInline,\n    /** Optional override for [{@code ConversionOptions::sub_symbol}]. */\n    @JsonProperty(\"sub_symbol\") Optional<String> subSymbol,\n    /** Optional override for [{@code ConversionOptions::sup_symbol}]. */\n    @JsonProperty(\"sup_symbol\") Optional<String> supSymbol,\n    /** Optional override for [{@code ConversionOptions::newline_style}]. */\n    @JsonProperty(\"newline_style\") Optional<NewlineStyle> newlineStyle,\n    /** Optional override for [{@code ConversionOptions::code_block_style}]. */\n    @JsonProperty(\"code_block_style\") Optional<CodeBlockStyle> codeBlockStyle,\n    /** Optional override for [{@code ConversionOptions::keep_inline_images_in}]. */\n    @JsonProperty(\"keep_inline_images_in\") Optional<List<String>> keepInlineImagesIn,\n    /** Optional override for [{@code ConversionOptions::preprocessing}]. */\n    Optional<PreprocessingOptionsUpdate> preprocessing,\n    /** Optional override for [{@code ConversionOptions::encoding}]. */\n    Optional<String> encoding,\n    /** Optional override for [{@code ConversionOptions::debug}]. */\n    Optional<Boolean> debug,\n    /** Optional override for [{@code ConversionOptions::strip_tags}]. */\n    @JsonProperty(\"strip_tags\") Optional<List<String>> stripTags,\n    /** Optional override for [{@code ConversionOptions::preserve_tags}]. */\n    @JsonProperty(\"preserve_tags\") Optional<List<String>> preserveTags,\n    /** Optional override for [{@code ConversionOptions::skip_images}]. */\n    @JsonProperty(\"skip_images\") Optional<Boolean> skipImages,\n    /** Optional override for [{@code ConversionOptions::link_style}]. */\n    @JsonProperty(\"link_style\") Optional<LinkStyle> linkStyle,\n    /** Optional override for [{@code ConversionOptions::output_format}]. */\n    @JsonProperty(\"output_format\") Optional<OutputFormat> outputFormat,\n    /** Optional override for [{@code ConversionOptions::include_document_structure}]. */\n    @JsonProperty(\"include_document_structure\") Optional<Boolean> includeDocumentStructure,\n    /** Optional override for [{@code ConversionOptions::extract_images}]. */\n    @JsonProperty(\"extract_images\") Optional<Boolean> extractImages,\n    /** Optional override for [{@code ConversionOptions::max_image_size}]. */\n    @JsonProperty(\"max_image_size\") Optional<Long> maxImageSize,\n    /** Optional override for [{@code ConversionOptions::capture_svg}]. */\n    @JsonProperty(\"capture_svg\") Optional<Boolean> captureSvg,\n    /** Optional override for [{@code ConversionOptions::infer_dimensions}]. */\n    @JsonProperty(\"infer_dimensions\") Optional<Boolean> inferDimensions,\n    /** Optional override for [{@code ConversionOptions::max_depth}]. */\n    @JsonProperty(\"max_depth\") Optional<Long> maxDepth,\n    /** Optional override for [{@code ConversionOptions::exclude_selectors}]. */\n    @JsonProperty(\"exclude_selectors\") Optional<List<String>> excludeSelectors,\n    /** Optional override for [{@code ConversionOptions::visitor}]. */\n    Optional<VisitorHandle> visitor\n) {\n    public static ConversionOptionsUpdateBuilder builder() {\n        return new ConversionOptionsUpdateBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionOptionsUpdateBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ae542f2c61bc1efdd29ae1e90f92fafded342671bd52d3b5dc6fc901daa9a65b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Partial update for {@code ConversionOptions}.\n *\n * Uses {@code Option&lt;T&gt;} fields for selective updates. Bindings use this to construct\n * options from language-native types. Prefer [{@code ConversionOptionsBuilder}] for Rust code.\n */\npublic class ConversionOptionsUpdateBuilder {\n\n    private Optional<HeadingStyle> headingStyle = Optional.empty();\n    private Optional<ListIndentType> listIndentType = Optional.empty();\n    private Optional<Long> listIndentWidth = Optional.empty();\n    private Optional<String> bullets = Optional.empty();\n    private Optional<String> strongEmSymbol = Optional.empty();\n    private Optional<Boolean> escapeAsterisks = Optional.empty();\n    private Optional<Boolean> escapeUnderscores = Optional.empty();\n    private Optional<Boolean> escapeMisc = Optional.empty();\n    private Optional<Boolean> escapeAscii = Optional.empty();\n    private Optional<String> codeLanguage = Optional.empty();\n    private Optional<Boolean> autolinks = Optional.empty();\n    private Optional<Boolean> defaultTitle = Optional.empty();\n    private Optional<Boolean> brInTables = Optional.empty();\n    private Optional<HighlightStyle> highlightStyle = Optional.empty();\n    private Optional<Boolean> extractMetadata = Optional.empty();\n    private Optional<WhitespaceMode> whitespaceMode = Optional.empty();\n    private Optional<Boolean> stripNewlines = Optional.empty();\n    private Optional<Boolean> wrap = Optional.empty();\n    private Optional<Long> wrapWidth = Optional.empty();\n    private Optional<Boolean> convertAsInline = Optional.empty();\n    private Optional<String> subSymbol = Optional.empty();\n    private Optional<String> supSymbol = Optional.empty();\n    private Optional<NewlineStyle> newlineStyle = Optional.empty();\n    private Optional<CodeBlockStyle> codeBlockStyle = Optional.empty();\n    private Optional<List<String>> keepInlineImagesIn = Optional.empty();\n    private Optional<PreprocessingOptionsUpdate> preprocessing = Optional.empty();\n    private Optional<String> encoding = Optional.empty();\n    private Optional<Boolean> debug = Optional.empty();\n    private Optional<List<String>> stripTags = Optional.empty();\n    private Optional<List<String>> preserveTags = Optional.empty();\n    private Optional<Boolean> skipImages = Optional.empty();\n    private Optional<LinkStyle> linkStyle = Optional.empty();\n    private Optional<OutputFormat> outputFormat = Optional.empty();\n    private Optional<Boolean> includeDocumentStructure = Optional.empty();\n    private Optional<Boolean> extractImages = Optional.empty();\n    private Optional<Long> maxImageSize = Optional.empty();\n    private Optional<Boolean> captureSvg = Optional.empty();\n    private Optional<Boolean> inferDimensions = Optional.empty();\n    private Optional<Long> maxDepth = Optional.empty();\n    private Optional<List<String>> excludeSelectors = Optional.empty();\n    private Optional<VisitorHandle> visitor = Optional.empty();\n\n    /** Sets the headingStyle field. */\n    public ConversionOptionsUpdateBuilder withHeadingStyle(final Optional<HeadingStyle> value) {\n        this.headingStyle = value;\n        return this;\n    }\n\n    /** Sets the listIndentType field. */\n    public ConversionOptionsUpdateBuilder withListIndentType(final Optional<ListIndentType> value) {\n        this.listIndentType = value;\n        return this;\n    }\n\n    /** Sets the listIndentWidth field. */\n    public ConversionOptionsUpdateBuilder withListIndentWidth(final Optional<Long> value) {\n        this.listIndentWidth = value;\n        return this;\n    }\n\n    /** Sets the bullets field. */\n    public ConversionOptionsUpdateBuilder withBullets(final Optional<String> value) {\n        this.bullets = value;\n        return this;\n    }\n\n    /** Sets the strongEmSymbol field. */\n    public ConversionOptionsUpdateBuilder withStrongEmSymbol(final Optional<String> value) {\n        this.strongEmSymbol = value;\n        return this;\n    }\n\n    /** Sets the escapeAsterisks field. */\n    public ConversionOptionsUpdateBuilder withEscapeAsterisks(final Optional<Boolean> value) {\n        this.escapeAsterisks = value;\n        return this;\n    }\n\n    /** Sets the escapeUnderscores field. */\n    public ConversionOptionsUpdateBuilder withEscapeUnderscores(final Optional<Boolean> value) {\n        this.escapeUnderscores = value;\n        return this;\n    }\n\n    /** Sets the escapeMisc field. */\n    public ConversionOptionsUpdateBuilder withEscapeMisc(final Optional<Boolean> value) {\n        this.escapeMisc = value;\n        return this;\n    }\n\n    /** Sets the escapeAscii field. */\n    public ConversionOptionsUpdateBuilder withEscapeAscii(final Optional<Boolean> value) {\n        this.escapeAscii = value;\n        return this;\n    }\n\n    /** Sets the codeLanguage field. */\n    public ConversionOptionsUpdateBuilder withCodeLanguage(final Optional<String> value) {\n        this.codeLanguage = value;\n        return this;\n    }\n\n    /** Sets the autolinks field. */\n    public ConversionOptionsUpdateBuilder withAutolinks(final Optional<Boolean> value) {\n        this.autolinks = value;\n        return this;\n    }\n\n    /** Sets the defaultTitle field. */\n    public ConversionOptionsUpdateBuilder withDefaultTitle(final Optional<Boolean> value) {\n        this.defaultTitle = value;\n        return this;\n    }\n\n    /** Sets the brInTables field. */\n    public ConversionOptionsUpdateBuilder withBrInTables(final Optional<Boolean> value) {\n        this.brInTables = value;\n        return this;\n    }\n\n    /** Sets the highlightStyle field. */\n    public ConversionOptionsUpdateBuilder withHighlightStyle(final Optional<HighlightStyle> value) {\n        this.highlightStyle = value;\n        return this;\n    }\n\n    /** Sets the extractMetadata field. */\n    public ConversionOptionsUpdateBuilder withExtractMetadata(final Optional<Boolean> value) {\n        this.extractMetadata = value;\n        return this;\n    }\n\n    /** Sets the whitespaceMode field. */\n    public ConversionOptionsUpdateBuilder withWhitespaceMode(final Optional<WhitespaceMode> value) {\n        this.whitespaceMode = value;\n        return this;\n    }\n\n    /** Sets the stripNewlines field. */\n    public ConversionOptionsUpdateBuilder withStripNewlines(final Optional<Boolean> value) {\n        this.stripNewlines = value;\n        return this;\n    }\n\n    /** Sets the wrap field. */\n    public ConversionOptionsUpdateBuilder withWrap(final Optional<Boolean> value) {\n        this.wrap = value;\n        return this;\n    }\n\n    /** Sets the wrapWidth field. */\n    public ConversionOptionsUpdateBuilder withWrapWidth(final Optional<Long> value) {\n        this.wrapWidth = value;\n        return this;\n    }\n\n    /** Sets the convertAsInline field. */\n    public ConversionOptionsUpdateBuilder withConvertAsInline(final Optional<Boolean> value) {\n        this.convertAsInline = value;\n        return this;\n    }\n\n    /** Sets the subSymbol field. */\n    public ConversionOptionsUpdateBuilder withSubSymbol(final Optional<String> value) {\n        this.subSymbol = value;\n        return this;\n    }\n\n    /** Sets the supSymbol field. */\n    public ConversionOptionsUpdateBuilder withSupSymbol(final Optional<String> value) {\n        this.supSymbol = value;\n        return this;\n    }\n\n    /** Sets the newlineStyle field. */\n    public ConversionOptionsUpdateBuilder withNewlineStyle(final Optional<NewlineStyle> value) {\n        this.newlineStyle = value;\n        return this;\n    }\n\n    /** Sets the codeBlockStyle field. */\n    public ConversionOptionsUpdateBuilder withCodeBlockStyle(final Optional<CodeBlockStyle> value) {\n        this.codeBlockStyle = value;\n        return this;\n    }\n\n    /** Sets the keepInlineImagesIn field. */\n    public ConversionOptionsUpdateBuilder withKeepInlineImagesIn(final Optional<List<String>> value) {\n        this.keepInlineImagesIn = value;\n        return this;\n    }\n\n    /** Sets the preprocessing field. */\n    public ConversionOptionsUpdateBuilder withPreprocessing(final Optional<PreprocessingOptionsUpdate> value) {\n        this.preprocessing = value;\n        return this;\n    }\n\n    /** Sets the encoding field. */\n    public ConversionOptionsUpdateBuilder withEncoding(final Optional<String> value) {\n        this.encoding = value;\n        return this;\n    }\n\n    /** Sets the debug field. */\n    public ConversionOptionsUpdateBuilder withDebug(final Optional<Boolean> value) {\n        this.debug = value;\n        return this;\n    }\n\n    /** Sets the stripTags field. */\n    public ConversionOptionsUpdateBuilder withStripTags(final Optional<List<String>> value) {\n        this.stripTags = value;\n        return this;\n    }\n\n    /** Sets the preserveTags field. */\n    public ConversionOptionsUpdateBuilder withPreserveTags(final Optional<List<String>> value) {\n        this.preserveTags = value;\n        return this;\n    }\n\n    /** Sets the skipImages field. */\n    public ConversionOptionsUpdateBuilder withSkipImages(final Optional<Boolean> value) {\n        this.skipImages = value;\n        return this;\n    }\n\n    /** Sets the linkStyle field. */\n    public ConversionOptionsUpdateBuilder withLinkStyle(final Optional<LinkStyle> value) {\n        this.linkStyle = value;\n        return this;\n    }\n\n    /** Sets the outputFormat field. */\n    public ConversionOptionsUpdateBuilder withOutputFormat(final Optional<OutputFormat> value) {\n        this.outputFormat = value;\n        return this;\n    }\n\n    /** Sets the includeDocumentStructure field. */\n    public ConversionOptionsUpdateBuilder withIncludeDocumentStructure(final Optional<Boolean> value) {\n        this.includeDocumentStructure = value;\n        return this;\n    }\n\n    /** Sets the extractImages field. */\n    public ConversionOptionsUpdateBuilder withExtractImages(final Optional<Boolean> value) {\n        this.extractImages = value;\n        return this;\n    }\n\n    /** Sets the maxImageSize field. */\n    public ConversionOptionsUpdateBuilder withMaxImageSize(final Optional<Long> value) {\n        this.maxImageSize = value;\n        return this;\n    }\n\n    /** Sets the captureSvg field. */\n    public ConversionOptionsUpdateBuilder withCaptureSvg(final Optional<Boolean> value) {\n        this.captureSvg = value;\n        return this;\n    }\n\n    /** Sets the inferDimensions field. */\n    public ConversionOptionsUpdateBuilder withInferDimensions(final Optional<Boolean> value) {\n        this.inferDimensions = value;\n        return this;\n    }\n\n    /** Sets the maxDepth field. */\n    public ConversionOptionsUpdateBuilder withMaxDepth(final Optional<Long> value) {\n        this.maxDepth = value;\n        return this;\n    }\n\n    /** Sets the excludeSelectors field. */\n    public ConversionOptionsUpdateBuilder withExcludeSelectors(final Optional<List<String>> value) {\n        this.excludeSelectors = value;\n        return this;\n    }\n\n    /** Sets the visitor field. */\n    public ConversionOptionsUpdateBuilder withVisitor(final Optional<VisitorHandle> value) {\n        this.visitor = value;\n        return this;\n    }\n\n    /** Builds the ConversionOptionsUpdate instance. */\n    public ConversionOptionsUpdate build() {\n        return new ConversionOptionsUpdate(\n            headingStyle,\n            listIndentType,\n            listIndentWidth,\n            bullets,\n            strongEmSymbol,\n            escapeAsterisks,\n            escapeUnderscores,\n            escapeMisc,\n            escapeAscii,\n            codeLanguage,\n            autolinks,\n            defaultTitle,\n            brInTables,\n            highlightStyle,\n            extractMetadata,\n            whitespaceMode,\n            stripNewlines,\n            wrap,\n            wrapWidth,\n            convertAsInline,\n            subSymbol,\n            supSymbol,\n            newlineStyle,\n            codeBlockStyle,\n            keepInlineImagesIn,\n            preprocessing,\n            encoding,\n            debug,\n            stripTags,\n            preserveTags,\n            skipImages,\n            linkStyle,\n            outputFormat,\n            includeDocumentStructure,\n            extractImages,\n            maxImageSize,\n            captureSvg,\n            inferDimensions,\n            maxDepth,\n            excludeSelectors,\n            visitor\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionResult.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:9b17c13b623df0a77deebf38bf4aea3f03cfd3dc29b875f9a37975c77a031e01\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * The primary result of HTML conversion and extraction.\n *\n * Contains the converted text output, optional structured document tree,\n * metadata, extracted tables, images, and processing warnings.\n *\n * # Example\n *\n * {@code }{@code text}\n * use html_to_markdown_rs::{convert, ConversionOptions};\n *\n * let result = convert(\"&lt;h1&gt;Hello&lt;/h1&gt;&lt;p&gt;World&lt;/p&gt;\", None)?;\n * assert!(result.content.is_some());\n * assert!(result.warnings.is_empty());\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record ConversionResult(\n    /** Converted text output (markdown, djot, or plain text). */\n    Optional<String> content,\n    /** Structured document tree with semantic elements. */\n    Optional<DocumentStructure> document,\n    /** Extracted HTML metadata (title, OG, links, images, structured data). */\n    HtmlMetadata metadata,\n    /** Extracted tables with structured cell data and markdown representation. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<TableData> tables,\n    /** Extracted inline images (data URIs and SVGs). */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<String> images,\n    /** Non-fatal processing warnings. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<ProcessingWarning> warnings\n) {\n    public static ConversionResultBuilder builder() {\n        return new ConversionResultBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ConversionResultBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:f38861a298351e3a0a986cb3990db99998776f18a973fa250e2e0a744630eec8\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * The primary result of HTML conversion and extraction.\n *\n * Contains the converted text output, optional structured document tree,\n * metadata, extracted tables, images, and processing warnings.\n *\n * # Example\n *\n * {@code }{@code text}\n * use html_to_markdown_rs::{convert, ConversionOptions};\n *\n * let result = convert(\"&lt;h1&gt;Hello&lt;/h1&gt;&lt;p&gt;World&lt;/p&gt;\", None)?;\n * assert!(result.content.is_some());\n * assert!(result.warnings.is_empty());\n * {@code }{@code }\n */\npublic class ConversionResultBuilder {\n\n    private Optional<String> content = Optional.empty();\n    private Optional<DocumentStructure> document = Optional.empty();\n    private HtmlMetadata metadata = null;\n    private List<TableData> tables = List.of();\n    private List<String> images = List.of();\n    private List<ProcessingWarning> warnings = List.of();\n\n    /** Sets the content field. */\n    public ConversionResultBuilder withContent(final Optional<String> value) {\n        this.content = value;\n        return this;\n    }\n\n    /** Sets the document field. */\n    public ConversionResultBuilder withDocument(final Optional<DocumentStructure> value) {\n        this.document = value;\n        return this;\n    }\n\n    /** Sets the metadata field. */\n    public ConversionResultBuilder withMetadata(final HtmlMetadata value) {\n        this.metadata = value;\n        return this;\n    }\n\n    /** Sets the tables field. */\n    public ConversionResultBuilder withTables(final List<TableData> value) {\n        this.tables = value;\n        return this;\n    }\n\n    /** Sets the images field. */\n    public ConversionResultBuilder withImages(final List<String> value) {\n        this.images = value;\n        return this;\n    }\n\n    /** Sets the warnings field. */\n    public ConversionResultBuilder withWarnings(final List<ProcessingWarning> value) {\n        this.warnings = value;\n        return this;\n    }\n\n    /** Builds the ConversionResult instance. */\n    public ConversionResult build() {\n        return new ConversionResult(\n            content,\n            document,\n            metadata,\n            tables,\n            images,\n            warnings\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/DocumentMetadata.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:643233bb94564eecdb6839f453eb32fd2012ff4555abdd405b2c6a20fdc103b7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Document-level metadata extracted from {@code &lt;head&gt;} and top-level elements.\n *\n * Contains all metadata typically used by search engines, social media platforms,\n * and browsers for document indexing and presentation.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::DocumentMetadata;\n * let doc = DocumentMetadata {\n *     title: Some(\"My Article\".to_string()),\n *     description: Some(\"A great article about Rust\".to_string()),\n *     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n *     ..Default::default()\n * };\n *\n * assert_eq!(doc.title, Some(\"My Article\".to_string()));\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record DocumentMetadata(\n    /** Document title from {@code &lt;title&gt;} tag */\n    Optional<String> title,\n    /** Document description from {@code &lt;meta name=\"description\"&gt;} tag */\n    Optional<String> description,\n    /** Document keywords from {@code &lt;meta name=\"keywords\"&gt;} tag, split on commas */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<String> keywords,\n    /** Document author from {@code &lt;meta name=\"author\"&gt;} tag */\n    Optional<String> author,\n    /** Canonical URL from {@code &lt;link rel=\"canonical\"&gt;} tag */\n    @JsonProperty(\"canonical_url\") Optional<String> canonicalUrl,\n    /** Base URL from {@code &lt;base href=\"\"&gt;} tag for resolving relative URLs */\n    @JsonProperty(\"base_href\") Optional<String> baseHref,\n    /** Document language from {@code lang} attribute */\n    Optional<String> language,\n    /** Document text direction from {@code dir} attribute */\n    @JsonProperty(\"text_direction\") Optional<TextDirection> textDirection,\n    /** Open Graph metadata (og:* properties) for social media */\n    @JsonProperty(\"open_graph\") Map<String, String> openGraph,\n    /** Twitter Card metadata (twitter:* properties) */\n    @JsonProperty(\"twitter_card\") Map<String, String> twitterCard,\n    /** Additional meta tags not covered by specific fields */\n    @JsonProperty(\"meta_tags\") Map<String, String> metaTags\n) {\n    public static DocumentMetadataBuilder builder() {\n        return new DocumentMetadataBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/DocumentMetadataBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:59af31dc819f7eb1fb6de85d615d5c5c7b43fa6dbf7da566649cbbc00c5971ff\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Document-level metadata extracted from {@code &lt;head&gt;} and top-level elements.\n *\n * Contains all metadata typically used by search engines, social media platforms,\n * and browsers for document indexing and presentation.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::DocumentMetadata;\n * let doc = DocumentMetadata {\n *     title: Some(\"My Article\".to_string()),\n *     description: Some(\"A great article about Rust\".to_string()),\n *     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n *     ..Default::default()\n * };\n *\n * assert_eq!(doc.title, Some(\"My Article\".to_string()));\n * {@code }{@code }\n */\npublic class DocumentMetadataBuilder {\n\n    private Optional<String> title = Optional.empty();\n    private Optional<String> description = Optional.empty();\n    private List<String> keywords = List.of();\n    private Optional<String> author = Optional.empty();\n    private Optional<String> canonicalUrl = Optional.empty();\n    private Optional<String> baseHref = Optional.empty();\n    private Optional<String> language = Optional.empty();\n    private Optional<TextDirection> textDirection = Optional.empty();\n    private Map<String, String> openGraph = Map.of();\n    private Map<String, String> twitterCard = Map.of();\n    private Map<String, String> metaTags = Map.of();\n\n    /** Sets the title field. */\n    public DocumentMetadataBuilder withTitle(final Optional<String> value) {\n        this.title = value;\n        return this;\n    }\n\n    /** Sets the description field. */\n    public DocumentMetadataBuilder withDescription(final Optional<String> value) {\n        this.description = value;\n        return this;\n    }\n\n    /** Sets the keywords field. */\n    public DocumentMetadataBuilder withKeywords(final List<String> value) {\n        this.keywords = value;\n        return this;\n    }\n\n    /** Sets the author field. */\n    public DocumentMetadataBuilder withAuthor(final Optional<String> value) {\n        this.author = value;\n        return this;\n    }\n\n    /** Sets the canonicalUrl field. */\n    public DocumentMetadataBuilder withCanonicalUrl(final Optional<String> value) {\n        this.canonicalUrl = value;\n        return this;\n    }\n\n    /** Sets the baseHref field. */\n    public DocumentMetadataBuilder withBaseHref(final Optional<String> value) {\n        this.baseHref = value;\n        return this;\n    }\n\n    /** Sets the language field. */\n    public DocumentMetadataBuilder withLanguage(final Optional<String> value) {\n        this.language = value;\n        return this;\n    }\n\n    /** Sets the textDirection field. */\n    public DocumentMetadataBuilder withTextDirection(final Optional<TextDirection> value) {\n        this.textDirection = value;\n        return this;\n    }\n\n    /** Sets the openGraph field. */\n    public DocumentMetadataBuilder withOpenGraph(final Map<String, String> value) {\n        this.openGraph = value;\n        return this;\n    }\n\n    /** Sets the twitterCard field. */\n    public DocumentMetadataBuilder withTwitterCard(final Map<String, String> value) {\n        this.twitterCard = value;\n        return this;\n    }\n\n    /** Sets the metaTags field. */\n    public DocumentMetadataBuilder withMetaTags(final Map<String, String> value) {\n        this.metaTags = value;\n        return this;\n    }\n\n    /** Builds the DocumentMetadata instance. */\n    public DocumentMetadata build() {\n        return new DocumentMetadata(\n            title,\n            description,\n            keywords,\n            author,\n            canonicalUrl,\n            baseHref,\n            language,\n            textDirection,\n            openGraph,\n            twitterCard,\n            metaTags\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/DocumentNode.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:145e5c4570bd423a330ee7c5606298f1abcd20a5d48fe1cfec8927f704f28c15\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A single node in the document tree.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record DocumentNode(\n    /** Deterministic node identifier. */\n    String id,\n    /** The semantic content of this node. */\n    NodeContent content,\n    /** Index of the parent node (None for root nodes). */\n    Optional<Integer> parent,\n    /** Indices of child nodes in reading order. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<Integer> children,\n    /** Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<TextAnnotation> annotations,\n    /** Format-specific attributes (e.g. class, id, data-* attributes). */\n    Optional<Map<String, String>> attributes\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/DocumentStructure.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d49946c720cad31b5078db2338b0b8a10e66618ee4acb9850dffb2672d1c947b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A structured document tree representing the semantic content of an HTML document.\n *\n * Uses a flat node array with index-based parent/child references for efficient traversal.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record DocumentStructure(\n    /** All nodes in document reading order. */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<DocumentNode> nodes,\n    /** The source format (always \"html\" for this crate). */\n    @JsonProperty(\"source_format\") Optional<String> sourceFormat\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/GridCell.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6e13c8e5a520b506782f0f6b96c79bbcfc3ce4155fc80e4d9cf19e82e6b93955\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A single cell in a table grid.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record GridCell(\n    /** The text content of the cell. */\n    String content,\n    /** 0-indexed row position. */\n    int row,\n    /** 0-indexed column position. */\n    int col,\n    /** Number of rows this cell spans (default 1). */\n    @JsonProperty(\"row_span\") int rowSpan,\n    /** Number of columns this cell spans (default 1). */\n    @JsonProperty(\"col_span\") int colSpan,\n    /** Whether this is a header cell ({@code &lt;th&gt;}). */\n    @JsonProperty(\"is_header\") boolean isHeader\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HeaderMetadata.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:171bac31b99af7c5c4966b3ddf39cbd2e4588c1d358bcf09634fcea7dc2af86a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Header element metadata with hierarchy tracking.\n *\n * Captures heading elements (h1-h6) with their text content, identifiers,\n * and position in the document structure.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::HeaderMetadata;\n * let header = HeaderMetadata {\n *     level: 1,\n *     text: \"Main Title\".to_string(),\n *     id: Some(\"main-title\".to_string()),\n *     depth: 0,\n *     html_offset: 145,\n * };\n *\n * assert_eq!(header.level, 1);\n * assert!(header.is_valid());\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record HeaderMetadata(\n    /** Header level: 1 (h1) through 6 (h6) */\n    byte level,\n    /** Normalized text content of the header */\n    String text,\n    /** HTML id attribute if present */\n    Optional<String> id,\n    /** Document tree depth at the header element */\n    long depth,\n    /** Byte offset in original HTML document */\n    @JsonProperty(\"html_offset\") long htmlOffset\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HeadingStyle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:4b627f65a17f4526ad8c149b9fa21d4aaf38311474092f5caa1283b3d35acc55\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Heading style options for Markdown output.\n *\n * Controls how headings (h1-h6) are rendered in the output Markdown.\n */\npublic enum HeadingStyle {\n    /** Underlined style (=== for h1, --- for h2). */\n    Underlined(\"underlined\"),\n    /** ATX style (# for h1, ## for h2, etc.). Default. */\n    Atx(\"atx\"),\n    /** ATX closed style (# title #, with closing hashes). */\n    AtxClosed(\"atxclosed\");\n\n    /** The string value. */\n    private final String value;\n\n    HeadingStyle(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static HeadingStyle fromValue(final String value) {\n        for (HeadingStyle e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HighlightStyle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:847aa64b03bc6e99dbb0a0f5e97525826f0014d84c225000be2c704e9b858870\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Highlight rendering style for {@code &lt;mark&gt;} elements.\n *\n * Controls how highlighted text is rendered in Markdown output.\n */\npublic enum HighlightStyle {\n    /** Double equals syntax (==text==). Default. Pandoc-compatible. */\n    DoubleEqual(\"doubleequal\"),\n    /** Preserve as HTML (==text==). Original HTML tag. */\n    Html(\"html\"),\n    /** Render as bold (**text**). Uses strong emphasis. */\n    Bold(\"bold\"),\n    /** Strip formatting, render as plain text. No markup. */\n    None(\"none\");\n\n    /** The string value. */\n    private final String value;\n\n    HighlightStyle(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static HighlightStyle fromValue(final String value) {\n        for (HighlightStyle e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlMetadata.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b28637938bf60d85de97bca2f130500c5c6be1ec5fa7d607f7005b1e95e3a98b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Comprehensive metadata extraction result from HTML document.\n *\n * Contains all extracted metadata types in a single structure,\n * suitable for serialization and transmission across language boundaries.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::HtmlMetadata;\n * let metadata = HtmlMetadata {\n *     document: Default::default(),\n *     headers: Vec::new(),\n *     links: Vec::new(),\n *     images: Vec::new(),\n *     structured_data: Vec::new(),\n * };\n *\n * assert!(metadata.headers.is_empty());\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record HtmlMetadata(\n    /** Document-level metadata (title, description, canonical, etc.) */\n    DocumentMetadata document,\n    /** Extracted header elements with hierarchy */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<HeaderMetadata> headers,\n    /** Extracted hyperlinks with type classification */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<LinkMetadata> links,\n    /** Extracted images with source and dimensions */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<ImageMetadata> images,\n    /** Extracted structured data blocks */\n    @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(\"structured_data\") List<StructuredData> structuredData\n) {\n    public static HtmlMetadataBuilder builder() {\n        return new HtmlMetadataBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlMetadataBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c64bb8b30b50dcab1d7bcde36096189d6643a8058be8ff2e685f4e005ba0aa62\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\n\n/**\n * Comprehensive metadata extraction result from HTML document.\n *\n * Contains all extracted metadata types in a single structure,\n * suitable for serialization and transmission across language boundaries.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::HtmlMetadata;\n * let metadata = HtmlMetadata {\n *     document: Default::default(),\n *     headers: Vec::new(),\n *     links: Vec::new(),\n *     images: Vec::new(),\n *     structured_data: Vec::new(),\n * };\n *\n * assert!(metadata.headers.is_empty());\n * {@code }{@code }\n */\npublic class HtmlMetadataBuilder {\n\n    private DocumentMetadata document = null;\n    private List<HeaderMetadata> headers = List.of();\n    private List<LinkMetadata> links = List.of();\n    private List<ImageMetadata> images = List.of();\n    private List<StructuredData> structuredData = List.of();\n\n    /** Sets the document field. */\n    public HtmlMetadataBuilder withDocument(final DocumentMetadata value) {\n        this.document = value;\n        return this;\n    }\n\n    /** Sets the headers field. */\n    public HtmlMetadataBuilder withHeaders(final List<HeaderMetadata> value) {\n        this.headers = value;\n        return this;\n    }\n\n    /** Sets the links field. */\n    public HtmlMetadataBuilder withLinks(final List<LinkMetadata> value) {\n        this.links = value;\n        return this;\n    }\n\n    /** Sets the images field. */\n    public HtmlMetadataBuilder withImages(final List<ImageMetadata> value) {\n        this.images = value;\n        return this;\n    }\n\n    /** Sets the structuredData field. */\n    public HtmlMetadataBuilder withStructuredData(final List<StructuredData> value) {\n        this.structuredData = value;\n        return this;\n    }\n\n    /** Builds the HtmlMetadata instance. */\n    public HtmlMetadata build() {\n        return new HtmlMetadata(\n            document,\n            headers,\n            links,\n            images,\n            structuredData\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlToMarkdown.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:b106dbba0e4282d6bdc82bfbf0c8d6a740a35a2df7abbb32cc98baf5339c46a0\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\npublic final class HtmlToMarkdown {\n    private HtmlToMarkdown() { }\n\n    /**\n     * Convert HTML to Markdown, returning a [{@code ConversionResult}] with content, metadata, images,\n     * and warnings.\n     *\n     * # Arguments\n     *\n     * * {@code html} — the HTML string to convert.\n     * * {@code options} — optional conversion options. Defaults to [{@code ConversionOptions::default}].\n     *   When the {@code visitor} feature is enabled, a custom [{@code crate::visitor::HtmlVisitor}] can be\n     *   attached via the {@code visitor} field on {@code ConversionOptions}.\n     *\n     * # Example\n     *\n     * {@code }{@code }\n     * use html_to_markdown_rs::convert;\n     *\n     * let html = \"&lt;h1&gt;Hello World&lt;/h1&gt;\";\n     * let result = convert(html, None).unwrap();\n     * assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n     * {@code }{@code }\n     *\n     * # Errors\n     *\n     * Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n     */\n    public static ConversionResult convert(final String html, final ConversionOptions options) throws HtmlToMarkdownRsException {\n        java.util.Objects.requireNonNull(html, \"html must not be null\");\n        return HtmlToMarkdownRs.convert(html, options);\n    }\n\n    public static ConversionResult convert(final String html) throws HtmlToMarkdownRsException {\n        return HtmlToMarkdownRs.convert(html, null);\n    }\n\n    public static ConversionResult convert(final String html, final ConversionOptions options,\n            final TestVisitor visitor) throws HtmlToMarkdownRsException {\n        return HtmlToMarkdownRs.convertWithVisitor(html, options, new TestVisitorAdapter(visitor));\n    }\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlToMarkdownRs.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:519c66969fba0c7aefde529d1ae63700c4a67525ec7ff2f4bd3f67e5fedf10d1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.lang.foreign.Arena;\nimport java.lang.foreign.MemorySegment;\n\npublic final class HtmlToMarkdownRs {\n    private HtmlToMarkdownRs() { }\n\n    public static ConversionResult convert(final String html, final ConversionOptions options) throws HtmlToMarkdownRsException {\n        try (var arena = Arena.ofConfined()) {\n            var chtml = arena.allocateFrom(html);\n            var coptionsJson = options != null ? createObjectMapper().writeValueAsString(options) : null;\n            var coptionsJsonSeg = coptionsJson != null ? arena.allocateFrom(coptionsJson) : MemorySegment.NULL;\n            if (NativeLib.HTM_OPTIONS_SET_VISITOR != null && options.visitor() != null) {\n                NativeLib.HTM_OPTIONS_SET_VISITOR.invoke(coptionsJsonSeg, options.visitor().handle());\n            }\n            var resultPtr = (MemorySegment) NativeLib.HTM_CONVERT.invoke(chtml, coptionsJsonSeg);\n            if (resultPtr.equals(MemorySegment.NULL)) {\n                checkLastError();\n                return null;\n            }\n            var jsonPtr = (MemorySegment) NativeLib.HTM_CONVERSION_RESULT_TO_JSON.invoke(resultPtr);\n            NativeLib.HTM_CONVERSION_RESULT_FREE.invoke(resultPtr);\n            if (jsonPtr.equals(MemorySegment.NULL)) {\n                checkLastError();\n                return null;\n            }\n            String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);\n            NativeLib.HTM_FREE_STRING.invoke(jsonPtr);\n            return createObjectMapper().readValue(json, ConversionResult.class);\n        } catch (Throwable e) {\n            throw new HtmlToMarkdownRsException(\"FFI call failed\", e);\n        }\n    }\n\n    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor) throws HtmlToMarkdownRsException {\n        try (var arena = Arena.ofConfined();\n             var bridge = new VisitorBridge(visitor)) {\n            var cHtml = arena.allocateFrom(html);\n\n            MemorySegment optionsPtr = MemorySegment.NULL;\n            if (options != null) {\n                var optJson = arena.allocateFrom(createObjectMapper().writeValueAsString(options));\n                optionsPtr = (MemorySegment) NativeLib.HTM_CONVERSION_OPTIONS_FROM_JSON.invoke(optJson);\n            }\n\n            var visitorHandle = (MemorySegment) NativeLib.HTM_VISITOR_CREATE.invoke(bridge.callbacksStruct());\n            if (visitorHandle.equals(MemorySegment.NULL)) {\n                throw new HtmlToMarkdownRsException(\"Failed to create visitor handle\", null);\n            }\n\n            try {\n                var resultPtr = (MemorySegment) NativeLib.HTM_CONVERT_WITH_VISITOR.invoke(cHtml, optionsPtr, visitorHandle);\n                if (!optionsPtr.equals(MemorySegment.NULL)) {\n                    NativeLib.HTM_CONVERSION_OPTIONS_FREE.invoke(optionsPtr);\n                }\n                if (resultPtr.equals(MemorySegment.NULL)) {\n                    checkLastError();\n                    return null;\n                }\n                var markdown = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);\n                NativeLib.HTM_FREE_STRING.invoke(resultPtr);\n                return new ConversionResult(java.util.Optional.of(markdown), java.util.Optional.empty(), null, null, null, null);\n            } catch (Throwable e) {\n                throw new HtmlToMarkdownRsException(\"FFI call failed\", e);\n            } finally {\n                NativeLib.HTM_VISITOR_FREE.invoke(visitorHandle);\n            }\n        } catch (HtmlToMarkdownRsException e) {\n            throw e;\n        } catch (Throwable e) {\n            throw new HtmlToMarkdownRsException(\"FFI call failed\", e);\n        }\n    }\n\n    // Helper methods for FFI marshalling\n\n    private static void checkLastError() throws Throwable {\n        int errCode = (int) NativeLib.HTM_LAST_ERROR_CODE.invoke();\n        if (errCode != 0) {\n            var ctxPtr = (MemorySegment) NativeLib.HTM_LAST_ERROR_CONTEXT.invoke();\n            String msg = ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);\n            throw new HtmlToMarkdownRsException(errCode, msg);\n        }\n    }\n\n    private static com.fasterxml.jackson.databind.ObjectMapper createObjectMapper() {\n        return new com.fasterxml.jackson.databind.ObjectMapper()\n            .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n            .findAndRegisterModules()\n            .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)\n            .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);\n    }\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlToMarkdownRsException.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6da5fa37e8861e9853542d2a81904858172435f149d85c69e2c4e8ac993473a1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Exception thrown by HtmlToMarkdownRs. */\npublic class HtmlToMarkdownRsException extends Exception {\n    /** The error code. */\n    private final int code;\n\n    /** Creates a new HtmlToMarkdownRsException. */\n    public HtmlToMarkdownRsException(final int code, final String message) {\n        super(message);\n        this.code = code;\n    }\n\n    /** Creates a new HtmlToMarkdownRsException with a cause. */\n    public HtmlToMarkdownRsException(final String message, final Throwable cause) {\n        super(message, cause);\n        this.code = -1;\n    }\n\n    /** Returns the error code. */\n    public int getCode() {\n        return code;\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlVisitorBridge.java",
    "content": "package dev.kreuzberg.htmltomarkdown;\n\nimport java.lang.foreign.Arena;\nimport java.lang.foreign.FunctionDescriptor;\nimport java.lang.foreign.Linker;\nimport java.lang.foreign.MemorySegment;\nimport java.lang.foreign.ValueLayout;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n/**\n * Allocates Panama FFM upcall stubs for an IHtmlVisitor implementation,\n * assembles the C vtable in native memory, and provides static\n * registerHtmlVisitor/unregisterHtmlVisitor helpers.\n */\npublic final class HtmlVisitorBridge implements AutoCloseable {\n\n    private static final Linker LINKER = Linker.nativeLinker();\n    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();\n    private static final ObjectMapper JSON = new ObjectMapper();\n\n    /** Live registry — keeps Arenas and upcall stubs alive past the register call. */\n    private static final ConcurrentHashMap<String, HtmlVisitorBridge> HTML_VISITOR_BRIDGES = new ConcurrentHashMap<>();\n\n    // C vtable: 41 fields (0 plugin methods + 40 trait methods + free_user_data)\n    private static final long VTABLE_SIZE = (long) ValueLayout.ADDRESS.byteSize() * 41L;\n\n    private final Arena arena;\n    private final MemorySegment vtable;\n    private final IHtmlVisitor impl;\n\n    HtmlVisitorBridge(final IHtmlVisitor impl) {\n        this.impl = impl;\n        this.arena = Arena.ofShared();\n        this.vtable = arena.allocate(VTABLE_SIZE);\n\n        try {\n            long offset = 0L;\n\n            var stubVisitElementStart = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitElementStart\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitElementStart);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitElementEnd = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitElementEnd\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitElementEnd);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitText = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitText\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitText);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitLink = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitLink\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitLink);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitImage = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitImage\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitImage);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitHeading = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitHeading\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitHeading);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitCodeBlock = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitCodeBlock\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitCodeBlock);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitCodeInline = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitCodeInline\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitCodeInline);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitListItem = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitListItem\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, boolean.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitListItem);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitListStart = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitListStart\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, boolean.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitListStart);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitListEnd = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitListEnd\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, boolean.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitListEnd);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitTableStart = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitTableStart\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitTableStart);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitTableRow = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitTableRow\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, boolean.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitTableRow);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitTableEnd = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitTableEnd\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitTableEnd);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitBlockquote = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitBlockquote\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, long.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitBlockquote);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitStrong = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitStrong\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitStrong);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitEmphasis = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitEmphasis\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitEmphasis);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitStrikethrough = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitStrikethrough\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitStrikethrough);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitUnderline = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitUnderline\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitUnderline);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitSubscript = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitSubscript\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitSubscript);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitSuperscript = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitSuperscript\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitSuperscript);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitMark = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitMark\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitMark);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitLineBreak = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitLineBreak\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitLineBreak);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitHorizontalRule = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitHorizontalRule\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitHorizontalRule);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitCustomElement = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitCustomElement\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitCustomElement);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitDefinitionListStart = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitDefinitionListStart\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitDefinitionListStart);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitDefinitionTerm = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitDefinitionTerm\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitDefinitionTerm);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitDefinitionDescription = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitDefinitionDescription\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitDefinitionDescription);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitDefinitionListEnd = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitDefinitionListEnd\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitDefinitionListEnd);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitForm = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitForm\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitForm);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitInput = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitInput\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitInput);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitButton = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitButton\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitButton);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitAudio = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitAudio\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitAudio);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitVideo = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitVideo\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitVideo);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitIframe = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitIframe\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitIframe);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitDetails = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitDetails\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, boolean.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitDetails);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitSummary = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitSummary\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitSummary);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitFigureStart = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitFigureStart\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitFigureStart);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitFigcaption = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitFigcaption\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitFigcaption);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            var stubVisitFigureEnd = LINKER.upcallStub(LOOKUP.bind(this, \"handleVisitFigureEnd\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)),\n                FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n                arena);\n            vtable.set(ValueLayout.ADDRESS, offset, stubVisitFigureEnd);\n            offset += ValueLayout.ADDRESS.byteSize();\n\n            vtable.set(ValueLayout.ADDRESS, offset, MemorySegment.NULL);\n\n        } catch (ReflectiveOperationException e) {\n            arena.close();\n            throw new RuntimeException(\"Failed to create trait bridge stubs\", e);\n        }\n    }\n\n    MemorySegment vtableSegment() { return vtable; }\n\n    private int handleVisitElementStart(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_element_start(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitElementEnd(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _output_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _output = _output_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_element_end(_ctx, _output);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitText(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_text(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitLink(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _href_in, MemorySegment _text_in, MemorySegment _title_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _href = _href_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _title = _title_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_link(_ctx, _href, _text, _title);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitImage(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _src_in, MemorySegment _alt_in, MemorySegment _title_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _src = _src_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _alt = _alt_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _title = _title_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_image(_ctx, _src, _alt, _title);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitHeading(MemorySegment userData, MemorySegment _ctx_in, int _level, MemorySegment _text_in, MemorySegment _id_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _id = _id_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_heading(_ctx, _level, _text, _id);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitCodeBlock(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _lang_in, MemorySegment _code_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _lang = _lang_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _code = _code_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_code_block(_ctx, _lang, _code);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitCodeInline(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _code_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _code = _code_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_code_inline(_ctx, _code);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitListItem(MemorySegment userData, MemorySegment _ctx_in, boolean _ordered, MemorySegment _marker_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _marker = _marker_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_list_item(_ctx, _ordered, _marker, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitListStart(MemorySegment userData, MemorySegment _ctx_in, boolean _ordered, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_list_start(_ctx, _ordered);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitListEnd(MemorySegment userData, MemorySegment _ctx_in, boolean _ordered, MemorySegment _output_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _output = _output_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_list_end(_ctx, _ordered, _output);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitTableStart(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_table_start(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitTableRow(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _cells_in, boolean _is_header, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _cells_json = _cells_in.reinterpret(Long.MAX_VALUE).getString(0);\n            List<String> _cells = JSON.readValue(_cells_json, new com.fasterxml.jackson.core.type.TypeReference<List<String>>() { });\n            VisitResult result = impl.visit_table_row(_ctx, _cells, _is_header);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitTableEnd(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _output_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _output = _output_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_table_end(_ctx, _output);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitBlockquote(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _content_in, long _depth, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _content = _content_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_blockquote(_ctx, _content, _depth);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitStrong(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_strong(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitEmphasis(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_emphasis(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitStrikethrough(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_strikethrough(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitUnderline(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_underline(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitSubscript(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_subscript(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitSuperscript(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_superscript(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitMark(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_mark(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitLineBreak(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_line_break(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitHorizontalRule(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_horizontal_rule(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitCustomElement(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _tag_name_in, MemorySegment _html_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _tag_name = _tag_name_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _html = _html_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_custom_element(_ctx, _tag_name, _html);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitDefinitionListStart(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_definition_list_start(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitDefinitionTerm(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_definition_term(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitDefinitionDescription(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_definition_description(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitDefinitionListEnd(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _output_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _output = _output_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_definition_list_end(_ctx, _output);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitForm(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _action_in, MemorySegment _method_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _action = _action_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _method = _method_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_form(_ctx, _action, _method);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitInput(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _input_type_in, MemorySegment _name_in, MemorySegment _value_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _input_type = _input_type_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _name = _name_in.reinterpret(Long.MAX_VALUE).getString(0);\n            String _value = _value_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_input(_ctx, _input_type, _name, _value);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitButton(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_button(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitAudio(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _src_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _src = _src_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_audio(_ctx, _src);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitVideo(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _src_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _src = _src_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_video(_ctx, _src);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitIframe(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _src_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _src = _src_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_iframe(_ctx, _src);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitDetails(MemorySegment userData, MemorySegment _ctx_in, boolean _open, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_details(_ctx, _open);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitSummary(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_summary(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitFigureStart(MemorySegment userData, MemorySegment _ctx_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            VisitResult result = impl.visit_figure_start(_ctx);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitFigcaption(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _text_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _text = _text_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_figcaption(_ctx, _text);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private int handleVisitFigureEnd(MemorySegment userData, MemorySegment _ctx_in, MemorySegment _output_in, MemorySegment outResult, MemorySegment outError) {\n        try {\n            String _ctx_json = _ctx_in.reinterpret(Long.MAX_VALUE).getString(0);\n            NodeContext _ctx = JSON.readValue(_ctx_json, NodeContext.class);\n            String _output = _output_in.reinterpret(Long.MAX_VALUE).getString(0);\n            VisitResult result = impl.visit_figure_end(_ctx, _output);\n            String json = JSON.writeValueAsString(result);\n            MemorySegment jsonCs = arena.allocateFrom(json);\n            outResult.set(ValueLayout.ADDRESS, 0, jsonCs);\n            return 0;\n        } catch (Throwable e) {\n            writeError(outError, e);\n            return 1;\n        }\n    }\n\n    private void writeError(MemorySegment outError, Throwable e) {\n        try { outError.set(ValueLayout.ADDRESS, 0, arena.allocateFrom(e.getClass().getSimpleName() + \": \" + e.getMessage())); }\n        catch (Throwable ignored) { /* swallow */ }\n    }\n\n    @Override\n    public void close() { arena.close(); }\n\n    /** Register a HtmlVisitor implementation via Panama FFM upcall stubs. */\n    public static void registerHtmlVisitor(final IHtmlVisitor impl, String name) throws Exception {\n        var bridge = new HtmlVisitorBridge(impl);\n        try {\n            try (var nameArena = Arena.ofConfined()) {\n                var nameCs = nameArena.allocateFrom(name);\n                MemorySegment outErr = nameArena.allocate(ValueLayout.ADDRESS);\n                int rc = (int) NativeLib.HTM_REGISTER_HTML_VISITOR.invoke(nameCs, bridge.vtableSegment(), MemorySegment.NULL, outErr);\n                if (rc != 0) {\n                    MemorySegment errPtr = outErr.get(ValueLayout.ADDRESS, 0);\n                    String msg = errPtr.equals(MemorySegment.NULL) ? \"registration failed (rc=\" + rc + \")\" : errPtr.reinterpret(Long.MAX_VALUE).getString(0);\n                    throw new RuntimeException(\"registerHtmlVisitor: \" + msg);\n                }\n            }\n        } catch (Throwable t) {\n            bridge.close();\n            if (t instanceof Exception e) {\n                throw e;\n            } else {\n                throw new RuntimeException(\"Unexpected error during registration\", t);\n            }\n        }\n        HTML_VISITOR_BRIDGES.put(name, bridge);\n    }\n\n    /** Unregister a HtmlVisitor implementation by name. */\n    public static void unregisterHtmlVisitor(String name) throws Exception {\n        try {\n            try (var nameArena = Arena.ofConfined()) {\n                var nameCs = nameArena.allocateFrom(name);\n                MemorySegment outErr = nameArena.allocate(ValueLayout.ADDRESS);\n                int rc = (int) NativeLib.HTM_UNREGISTER_HTML_VISITOR.invoke(nameCs, outErr);\n                if (rc != 0) {\n                    MemorySegment errPtr = outErr.get(ValueLayout.ADDRESS, 0);\n                    String msg = errPtr.equals(MemorySegment.NULL) ? \"unregistration failed (rc=\" + rc + \")\" : errPtr.reinterpret(Long.MAX_VALUE).getString(0);\n                    throw new RuntimeException(\"unregisterHtmlVisitor: \" + msg);\n                }\n            }\n        } catch (Throwable t) {\n            if (t instanceof Exception e) {\n                throw e;\n            } else {\n                throw new RuntimeException(\"Unexpected error during unregistration\", t);\n            }\n        }\n        HtmlVisitorBridge old = HTML_VISITOR_BRIDGES.remove(name);\n        if (old != null) { old.close(); }\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/IHtmlVisitor.java",
    "content": "package dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Bridge interface for the HtmlVisitor plugin system.\n *\n * Implementations are wrapped by HtmlVisitorBridge and exposed to the native\n * runtime through Panama FFM upcall stubs.\n */\npublic interface IHtmlVisitor {\n\n    /** visit_element_start. */\n    VisitResult visit_element_start(NodeContext _ctx) throws Exception;\n\n    /** visit_element_end. */\n    VisitResult visit_element_end(NodeContext _ctx, String _output) throws Exception;\n\n    /** visit_text. */\n    VisitResult visit_text(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_link. */\n    VisitResult visit_link(NodeContext _ctx, String _href, String _text, String _title) throws Exception;\n\n    /** visit_image. */\n    VisitResult visit_image(NodeContext _ctx, String _src, String _alt, String _title) throws Exception;\n\n    /** visit_heading. */\n    VisitResult visit_heading(NodeContext _ctx, int _level, String _text, String _id) throws Exception;\n\n    /** visit_code_block. */\n    VisitResult visit_code_block(NodeContext _ctx, String _lang, String _code) throws Exception;\n\n    /** visit_code_inline. */\n    VisitResult visit_code_inline(NodeContext _ctx, String _code) throws Exception;\n\n    /** visit_list_item. */\n    VisitResult visit_list_item(NodeContext _ctx, boolean _ordered, String _marker, String _text) throws Exception;\n\n    /** visit_list_start. */\n    VisitResult visit_list_start(NodeContext _ctx, boolean _ordered) throws Exception;\n\n    /** visit_list_end. */\n    VisitResult visit_list_end(NodeContext _ctx, boolean _ordered, String _output) throws Exception;\n\n    /** visit_table_start. */\n    VisitResult visit_table_start(NodeContext _ctx) throws Exception;\n\n    /** visit_table_row. */\n    VisitResult visit_table_row(NodeContext _ctx, List<String> _cells, boolean _is_header) throws Exception;\n\n    /** visit_table_end. */\n    VisitResult visit_table_end(NodeContext _ctx, String _output) throws Exception;\n\n    /** visit_blockquote. */\n    VisitResult visit_blockquote(NodeContext _ctx, String _content, long _depth) throws Exception;\n\n    /** visit_strong. */\n    VisitResult visit_strong(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_emphasis. */\n    VisitResult visit_emphasis(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_strikethrough. */\n    VisitResult visit_strikethrough(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_underline. */\n    VisitResult visit_underline(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_subscript. */\n    VisitResult visit_subscript(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_superscript. */\n    VisitResult visit_superscript(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_mark. */\n    VisitResult visit_mark(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_line_break. */\n    VisitResult visit_line_break(NodeContext _ctx) throws Exception;\n\n    /** visit_horizontal_rule. */\n    VisitResult visit_horizontal_rule(NodeContext _ctx) throws Exception;\n\n    /** visit_custom_element. */\n    VisitResult visit_custom_element(NodeContext _ctx, String _tag_name, String _html) throws Exception;\n\n    /** visit_definition_list_start. */\n    VisitResult visit_definition_list_start(NodeContext _ctx) throws Exception;\n\n    /** visit_definition_term. */\n    VisitResult visit_definition_term(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_definition_description. */\n    VisitResult visit_definition_description(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_definition_list_end. */\n    VisitResult visit_definition_list_end(NodeContext _ctx, String _output) throws Exception;\n\n    /** visit_form. */\n    VisitResult visit_form(NodeContext _ctx, String _action, String _method) throws Exception;\n\n    /** visit_input. */\n    VisitResult visit_input(NodeContext _ctx, String _input_type, String _name, String _value) throws Exception;\n\n    /** visit_button. */\n    VisitResult visit_button(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_audio. */\n    VisitResult visit_audio(NodeContext _ctx, String _src) throws Exception;\n\n    /** visit_video. */\n    VisitResult visit_video(NodeContext _ctx, String _src) throws Exception;\n\n    /** visit_iframe. */\n    VisitResult visit_iframe(NodeContext _ctx, String _src) throws Exception;\n\n    /** visit_details. */\n    VisitResult visit_details(NodeContext _ctx, boolean _open) throws Exception;\n\n    /** visit_summary. */\n    VisitResult visit_summary(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_figure_start. */\n    VisitResult visit_figure_start(NodeContext _ctx) throws Exception;\n\n    /** visit_figcaption. */\n    VisitResult visit_figcaption(NodeContext _ctx, String _text) throws Exception;\n\n    /** visit_figure_end. */\n    VisitResult visit_figure_end(NodeContext _ctx, String _output) throws Exception;\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ImageMetadata.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:52823a239c0101f1c40edb24d9e930629c67109c571c40d1d35caf94185592ad\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Image metadata with source and dimensions.\n *\n * Captures {@code &lt;img&gt;} elements and inline {@code &lt;svg&gt;} elements with metadata\n * for image analysis and optimization.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n * let img = ImageMetadata {\n *     src: \"https://example.com/image.jpg\".to_string(),\n *     alt: Some(\"An example image\".to_string()),\n *     title: Some(\"Example\".to_string()),\n *     dimensions: Some((800, 600)),\n *     image_type: ImageType::External,\n *     attributes: Default::default(),\n * };\n *\n * assert_eq!(img.image_type, ImageType::External);\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record ImageMetadata(\n    /** Image source (URL, data URI, or SVG content identifier) */\n    String src,\n    /** Alternative text from alt attribute (for accessibility) */\n    Optional<String> alt,\n    /** Title attribute (often shown as tooltip) */\n    Optional<String> title,\n    /** Image dimensions as (width, height) if available */\n    Optional<List<Integer>> dimensions,\n    /** Image type classification */\n    @JsonProperty(\"image_type\") ImageType imageType,\n    /** Additional HTML attributes */\n    Map<String, String> attributes\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ImageType.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:20b2d62a618029fe30b869d900b4dd54c76d4b41245d1a6dcc12c51597f21c09\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Image source classification for proper handling and processing.\n *\n * Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n */\npublic enum ImageType {\n    /** Data URI embedded image (base64 or other encoding) */\n    DataUri(\"datauri\"),\n    /** Inline SVG element */\n    InlineSvg(\"inlinesvg\"),\n    /** External image URL (http/https) */\n    External(\"external\"),\n    /** Relative image path */\n    Relative(\"relative\");\n\n    /** The string value. */\n    private final String value;\n\n    ImageType(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static ImageType fromValue(final String value) {\n        for (ImageType e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/InvalidInputException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:a299c9ba8b5e9607338092a3f498d8073e8c5b4ab7044184aefcbd758d8c4a6f\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * Invalid input data\n */\npublic class InvalidInputException extends ConversionErrorException {\n    /** Creates a new InvalidInputException with the given message. */\n    public InvalidInputException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new InvalidInputException with the given message and cause. */\n    public InvalidInputException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/IoErrorException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:edcd0a319564c0fe5438ffbfab52bbeb92f64cd2827c4f5322f2dcc0820a0b7c\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * I/O error\n */\npublic class IoErrorException extends ConversionErrorException {\n    /** Creates a new IoErrorException with the given message. */\n    public IoErrorException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new IoErrorException with the given message and cause. */\n    public IoErrorException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/LinkMetadata.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:75358257cf5edf37757368d142b832725b13f792309f86061c09718e84261d62\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Hyperlink metadata with categorization and attributes.\n *\n * Represents {@code &lt;a&gt;} elements with parsed href values, text content, and link type classification.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n * let link = LinkMetadata {\n *     href: \"https://example.com\".to_string(),\n *     text: \"Example\".to_string(),\n *     title: Some(\"Visit Example\".to_string()),\n *     link_type: LinkType::External,\n *     rel: vec![\"nofollow\".to_string()],\n *     attributes: Default::default(),\n * };\n *\n * assert_eq!(link.link_type, LinkType::External);\n * assert_eq!(link.text, \"Example\");\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record LinkMetadata(\n    /** The href URL value */\n    String href,\n    /** Link text content (normalized, concatenated if mixed with elements) */\n    String text,\n    /** Optional title attribute (often shown as tooltip) */\n    Optional<String> title,\n    /** Link type classification */\n    @JsonProperty(\"link_type\") LinkType linkType,\n    /** Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\") */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<String> rel,\n    /** Additional HTML attributes */\n    Map<String, String> attributes\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/LinkStyle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:8ca0057762ea53d14a448079e6fb398d61ba76ca6642e64b0420b59d671789f4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Link rendering style in Markdown output.\n *\n * Controls whether links and images use inline {@code [text](url)} syntax or\n * reference-style {@code [text][1]} syntax with definitions collected at the end.\n */\npublic enum LinkStyle {\n    /** Inline links: {@code [text](url)}. Default. */\n    Inline(\"inline\"),\n    /**\n     * Reference-style links: {@code [text][1]} with {@code [1]: url} at end of document.\n     */\n    Reference(\"reference\");\n\n    /** The string value. */\n    private final String value;\n\n    LinkStyle(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static LinkStyle fromValue(final String value) {\n        for (LinkStyle e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/LinkType.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a1bafcee8f2beb05805b0b7646b6e8e49221ef6ac812466ec95d690904dd33df\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Link classification based on href value and document context.\n *\n * Used to categorize links during extraction for filtering and analysis.\n */\npublic enum LinkType {\n    /** Anchor link within same document (href starts with #) */\n    Anchor(\"anchor\"),\n    /** Internal link within same domain */\n    Internal(\"internal\"),\n    /** External link to different domain */\n    External(\"external\"),\n    /** Email link (mailto:) */\n    Email(\"email\"),\n    /** Phone link (tel:) */\n    Phone(\"phone\"),\n    /** Other protocol or unclassifiable */\n    Other(\"other\");\n\n    /** The string value. */\n    private final String value;\n\n    LinkType(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static LinkType fromValue(final String value) {\n        for (LinkType e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ListIndentType.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e28d7a0f7ce08d9ece6a523460f4ffa9a973d65fab333b805d6f0c2f0f6bf1c3\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * List indentation character type.\n *\n * Controls whether list items are indented with spaces or tabs.\n */\npublic enum ListIndentType {\n    /**\n     * Use spaces for indentation. Default. Width controlled by {@code list_indent_width}.\n     */\n    Spaces(\"spaces\"),\n    /** Use tabs for indentation. */\n    Tabs(\"tabs\");\n\n    /** The string value. */\n    private final String value;\n\n    ListIndentType(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static ListIndentType fromValue(final String value) {\n        for (ListIndentType e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/NativeLib.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e5f32e974799349847a1117e88ca798e6ef9000013c8ccc25fc421c4082a9af7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.lang.foreign.FunctionDescriptor;\nimport java.lang.foreign.Linker;\nimport java.lang.foreign.SymbolLookup;\nimport java.lang.foreign.ValueLayout;\nimport java.lang.invoke.MethodHandle;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.jar.JarEntry;\nimport java.util.jar.JarFile;\n\nfinal class NativeLib {\n    private static final Linker LINKER = Linker.nativeLinker();\n    private static final SymbolLookup LIB;\n    private static final String NATIVES_RESOURCE_ROOT = \"/natives\";\n    private static final Object NATIVE_EXTRACT_LOCK = new Object();\n    private static String cachedExtractKey;\n    private static Path cachedExtractDir;\n\n    static {\n        loadNativeLibrary();\n        LIB = SymbolLookup.loaderLookup();\n    }\n\n    private static void loadNativeLibrary() {\n        String osName = System.getProperty(\"os.name\", \"\").toLowerCase(java.util.Locale.ROOT);\n        String osArch = System.getProperty(\"os.arch\", \"\").toLowerCase(java.util.Locale.ROOT);\n\n        String libName;\n        String libExt;\n        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {\n            libName = \"libhtml_to_markdown_ffi\";\n            libExt = \".dylib\";\n        } else if (osName.contains(\"win\")) {\n            libName = \"html_to_markdown_ffi\";\n            libExt = \".dll\";\n        } else {\n            libName = \"libhtml_to_markdown_ffi\";\n            libExt = \".so\";\n        }\n\n        String nativesRid = resolveNativesRid(osName, osArch);\n        String nativesDir = NATIVES_RESOURCE_ROOT + \"/\" + nativesRid;\n\n        Path extracted = tryExtractAndLoadFromResources(nativesDir, libName, libExt);\n        if (extracted != null) {\n            return;\n        }\n\n        try {\n            System.loadLibrary(\"html_to_markdown_ffi\");\n        } catch (UnsatisfiedLinkError e) {\n            String msg = \"Failed to load html_to_markdown_ffi native library. Expected resource: \" + nativesDir + \"/\" + libName\n                    + libExt + \" (RID: \" + nativesRid + \"). \"\n                    + \"Ensure the library is bundled in the JAR under natives/{os-arch}/, \"\n                    + \"or place it on the system library path (java.library.path).\";\n            UnsatisfiedLinkError out = new UnsatisfiedLinkError(msg + \" Original error: \" + e.getMessage());\n            out.initCause(e);\n            throw out;\n        }\n    }\n\n    private static Path tryExtractAndLoadFromResources(String nativesDir, String libName, String libExt) {\n        String resourcePath = nativesDir + \"/\" + libName + libExt;\n        URL resource = NativeLib.class.getResource(resourcePath);\n        if (resource == null) {\n            return null;\n        }\n\n        try {\n            Path tempDir = extractOrReuseNativeDirectory(nativesDir);\n            Path libPath = tempDir.resolve(libName + libExt);\n            if (!Files.exists(libPath)) {\n                throw new UnsatisfiedLinkError(\"Missing extracted native library: \" + libPath);\n            }\n            System.load(libPath.toAbsolutePath().toString());\n            return libPath;\n        } catch (Exception e) {\n            System.err.println(\"[NativeLib] Failed to extract and load native library from resources: \" + e.getMessage());\n            return null;\n        }\n    }\n\n    private static Path extractOrReuseNativeDirectory(String nativesDir) throws Exception {\n        URL location = NativeLib.class.getProtectionDomain().getCodeSource().getLocation();\n        if (location == null) {\n            throw new IllegalStateException(\"Missing code source location for html_to_markdown_ffi JAR\");\n        }\n\n        Path codePath = Path.of(location.toURI());\n        String key = codePath.toAbsolutePath() + \"::\" + nativesDir;\n\n        synchronized (NATIVE_EXTRACT_LOCK) {\n            if (cachedExtractDir != null && key.equals(cachedExtractKey)) {\n                return cachedExtractDir;\n            }\n            Path tempDir = Files.createTempDirectory(\"html_to_markdown_ffi_native\");\n            tempDir.toFile().deleteOnExit();\n            List<Path> extracted = extractNativeDirectory(codePath, nativesDir, tempDir);\n            if (extracted.isEmpty()) {\n                throw new IllegalStateException(\"No native files extracted from resources dir: \" + nativesDir);\n            }\n            cachedExtractKey = key;\n            cachedExtractDir = tempDir;\n            return tempDir;\n        }\n    }\n\n    private static List<Path> extractNativeDirectory(Path codePath, String nativesDir, Path destDir) throws Exception {\n        if (!Files.exists(destDir) || !Files.isDirectory(destDir)) {\n            throw new IllegalArgumentException(\"Destination directory does not exist: \" + destDir);\n        }\n\n        String prefix = nativesDir.startsWith(\"/\") ? nativesDir.substring(1) : nativesDir;\n        if (!prefix.endsWith(\"/\")) {\n            prefix = prefix + \"/\";\n        }\n\n        if (Files.isDirectory(codePath)) {\n            Path nativesPath = codePath.resolve(prefix);\n            if (!Files.exists(nativesPath) || !Files.isDirectory(nativesPath)) {\n                return List.of();\n            }\n            return copyDirectory(nativesPath, destDir);\n        }\n\n        List<Path> extracted = new ArrayList<>();\n        try (JarFile jar = new JarFile(codePath.toFile())) {\n            Enumeration<JarEntry> entries = jar.entries();\n            while (entries.hasMoreElements()) {\n                JarEntry entry = entries.nextElement();\n                String name = entry.getName();\n                if (!name.startsWith(prefix) || entry.isDirectory()) {\n                    continue;\n                }\n                String relative = name.substring(prefix.length());\n                Path out = safeResolve(destDir, relative);\n                Files.createDirectories(out.getParent());\n                try (var in = jar.getInputStream(entry)) {\n                    Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);\n                }\n                out.toFile().deleteOnExit();\n                extracted.add(out);\n            }\n        }\n        return extracted;\n    }\n\n    private static List<Path> copyDirectory(Path srcDir, Path destDir) throws Exception {\n        List<Path> copied = new ArrayList<>();\n        try (var paths = Files.walk(srcDir)) {\n            for (Path src : (Iterable<Path>) paths::iterator) {\n                if (Files.isDirectory(src)) {\n                    continue;\n                }\n                Path relative = srcDir.relativize(src);\n                Path out = safeResolve(destDir, relative.toString());\n                Files.createDirectories(out.getParent());\n                Files.copy(src, out, StandardCopyOption.REPLACE_EXISTING);\n                out.toFile().deleteOnExit();\n                copied.add(out);\n            }\n        }\n        return copied;\n    }\n\n    private static Path safeResolve(Path destDir, String relative) throws Exception {\n        Path normalizedDest = destDir.toAbsolutePath().normalize();\n        Path out = normalizedDest.resolve(relative).normalize();\n        if (!out.startsWith(normalizedDest)) {\n            throw new SecurityException(\"Blocked extracting native file outside destination directory: \" + relative);\n        }\n        return out;\n    }\n\n    private static String resolveNativesRid(String osName, String osArch) {\n        String arch;\n        if (osArch.contains(\"aarch64\") || osArch.contains(\"arm64\")) {\n            arch = \"arm64\";\n        } else if (osArch.contains(\"x86_64\") || osArch.contains(\"amd64\")) {\n            arch = \"x86_64\";\n        } else {\n            arch = osArch.replaceAll(\"[^a-z0-9_]+\", \"\");\n        }\n\n        String os;\n        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {\n            os = \"macos\";\n        } else if (osName.contains(\"win\")) {\n            os = \"windows\";\n        } else {\n            os = \"linux\";\n        }\n\n        return os + \"-\" + arch;\n    }\n\n    static final MethodHandle HTM_CONVERT = LINKER.downcallHandle(\n        LIB.find(\"htm_convert\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_FREE_STRING = LINKER.downcallHandle(\n        LIB.find(\"htm_free_string\").orElseThrow(),\n        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)\n    );\n    static final MethodHandle HTM_LAST_ERROR_CODE = LINKER.downcallHandle(\n        LIB.find(\"htm_last_error_code\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.JAVA_INT)\n    );\n    static final MethodHandle HTM_LAST_ERROR_CONTEXT = LINKER.downcallHandle(\n        LIB.find(\"htm_last_error_context\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_CONVERSION_RESULT_TO_JSON = LINKER.downcallHandle(\n        LIB.find(\"htm_conversion_result_to_json\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_CONVERSION_RESULT_FREE = LINKER.downcallHandle(\n        LIB.find(\"htm_conversion_result_free\").orElseThrow(),\n        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_CONVERSION_OPTIONS_FROM_JSON = LINKER.downcallHandle(\n        LIB.find(\"htm_conversion_options_from_json\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_CONVERSION_OPTIONS_FREE = LINKER.downcallHandle(\n        LIB.find(\"htm_conversion_options_free\").orElseThrow(),\n        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_VISITOR_HANDLE_FREE = LINKER.downcallHandle(\n        LIB.find(\"htm_visitor_handle_free\").orElseThrow(),\n        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_REGISTER_HTML_VISITOR = LIB.find(\"htm_register_html_visitor\").map(s -> LINKER.downcallHandle(s,\n        FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS))).orElse(null);\n\n    static final MethodHandle HTM_UNREGISTER_HTML_VISITOR = LIB.find(\"htm_unregister_html_visitor\").map(s -> LINKER.downcallHandle(s,\n        FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS))).orElse(null);\n\n    // Visitor FFI handles\n    static final MethodHandle HTM_VISITOR_CREATE = LINKER.downcallHandle(\n        LIB.find(\"htm_visitor_create\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_VISITOR_FREE = LINKER.downcallHandle(\n        LIB.find(\"htm_visitor_free\").orElseThrow(),\n        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_CONVERT_WITH_VISITOR = LINKER.downcallHandle(\n        LIB.find(\"htm_convert_with_visitor\").orElseThrow(),\n        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)\n    );\n\n    static final MethodHandle HTM_OPTIONS_SET_VISITOR = LIB.find(\"htm_conversion_options_set_visitor\")\n        .map(s -> LINKER.downcallHandle(s, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS))).orElse(null);\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/NewlineStyle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:801855c90c5ff89935c04eba92cad61e785aefaa1cf06894a843216e5f979eca\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Line break syntax in Markdown output.\n *\n * Controls how soft line breaks (from {@code &lt;br&gt;} or line breaks in source) are rendered.\n */\npublic enum NewlineStyle {\n    /**\n     * Two trailing spaces at end of line. Default. Standard Markdown syntax.\n     */\n    Spaces(\"spaces\"),\n    /** Backslash at end of line. Alternative Markdown syntax. */\n    Backslash(\"backslash\");\n\n    /** The string value. */\n    private final String value;\n\n    NewlineStyle(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static NewlineStyle fromValue(final String value) {\n        for (NewlineStyle e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/NodeContent.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6560d21e74e512454aa7359011e015778ed620cf91f3787ff2faf055318d3e12\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport java.util.Optional;\n\n/**\n * The semantic content type of a document node.\n *\n * Uses internally tagged representation ({@code \"node_type\": \"heading\"}) for JSON serialization.\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"node_type\", visible = false)\n@JsonSubTypes({\n    @JsonSubTypes.Type(value = NodeContent.Heading.class, name = \"heading\"),\n    @JsonSubTypes.Type(value = NodeContent.Paragraph.class, name = \"paragraph\"),\n    @JsonSubTypes.Type(value = NodeContent.List.class, name = \"list\"),\n    @JsonSubTypes.Type(value = NodeContent.ListItem.class, name = \"listitem\"),\n    @JsonSubTypes.Type(value = NodeContent.Table.class, name = \"table\"),\n    @JsonSubTypes.Type(value = NodeContent.Image.class, name = \"image\"),\n    @JsonSubTypes.Type(value = NodeContent.Code.class, name = \"code\"),\n    @JsonSubTypes.Type(value = NodeContent.Quote.class, name = \"quote\"),\n    @JsonSubTypes.Type(value = NodeContent.DefinitionList.class, name = \"definitionlist\"),\n    @JsonSubTypes.Type(value = NodeContent.DefinitionItem.class, name = \"definitionitem\"),\n    @JsonSubTypes.Type(value = NodeContent.RawBlock.class, name = \"rawblock\"),\n    @JsonSubTypes.Type(value = NodeContent.MetadataBlock.class, name = \"metadatablock\"),\n    @JsonSubTypes.Type(value = NodeContent.Group.class, name = \"group\")\n})\npublic sealed interface NodeContent {\n\n    /** A heading element (h1-h6). */\n    record Heading(\n        @JsonProperty(\"level\") byte level,\n        @JsonProperty(\"text\") String text\n    ) implements NodeContent {\n    }\n\n    /** A paragraph of text. */\n    record Paragraph(@JsonProperty(\"text\") String text) implements NodeContent { }\n\n    /** A list container (ordered or unordered). Children are {@code ListItem} nodes. */\n    record List(@JsonProperty(\"ordered\") boolean ordered) implements NodeContent { }\n\n    /** A single list item. */\n    record ListItem(@JsonProperty(\"text\") String text) implements NodeContent { }\n\n    /** A table with structured cell data. */\n    record Table(@JsonProperty(\"grid\") TableGrid grid) implements NodeContent { }\n\n    /** An image element. */\n    record Image(\n        @JsonProperty(\"description\") Optional<String> description,\n        @JsonProperty(\"src\") Optional<String> src,\n        @JsonProperty(\"image_index\") Optional<Integer> imageIndex\n    ) implements NodeContent {\n    }\n\n    /** A code block or inline code. */\n    record Code(\n        @JsonProperty(\"text\") String text,\n        @JsonProperty(\"language\") Optional<String> language\n    ) implements NodeContent {\n    }\n\n    /** A block quote container. */\n    record Quote() implements NodeContent {\n    }\n\n    /** A definition list container. */\n    record DefinitionList() implements NodeContent {\n    }\n\n    /** A definition list entry with term and description. */\n    record DefinitionItem(\n        @JsonProperty(\"term\") String term,\n        @JsonProperty(\"definition\") String definition\n    ) implements NodeContent {\n    }\n\n    /** A raw block preserved as-is (e.g. {@code &lt;script&gt;}, {@code &lt;style&gt;} content). */\n    record RawBlock(\n        @JsonProperty(\"format\") String format,\n        @JsonProperty(\"content\") String content\n    ) implements NodeContent {\n    }\n\n    /** A block of key-value metadata pairs (from {@code &lt;head&gt;} meta tags). */\n    record MetadataBlock(@JsonProperty(\"entries\") java.util.List<String> entries) implements NodeContent { }\n\n    /** A section grouping container (auto-generated from heading hierarchy). */\n    record Group(\n        @JsonProperty(\"label\") Optional<String> label,\n        @JsonProperty(\"heading_level\") Optional<Byte> headingLevel,\n        @JsonProperty(\"heading_text\") Optional<String> headingText\n    ) implements NodeContent {\n    }\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/NodeContext.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ba535ba8d5d0940f2639e402dff02bca50ba72dfe7a8338e35b356b68863197a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Context passed to every visitor callback. */\npublic record NodeContext(\n        /** Coarse-grained node type tag. */\n        int nodeType,\n        /** HTML element tag name (e.g. \"div\"). */\n        String tagName,\n        /** DOM depth (0 = root). */\n        long depth,\n        /** 0-based sibling index. */\n        long indexInParent,\n        /** Parent element tag name, or null at the root. */\n        String parentTag,\n        /** True when this element is treated as inline. */\n        boolean isInline\n) {}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/NodeType.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:f1a7b3cdcd2fe8351d94460add45248803dff19219438c692c8daf6caa0fafa3\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Node type enumeration covering all HTML element types.\n *\n * This enum categorizes all HTML elements that the converter recognizes,\n * providing a coarse-grained classification for visitor dispatch.\n */\npublic enum NodeType {\n    /** Text node (most frequent - 100+ per document) */\n    Text(\"text\"),\n    /** Generic element node */\n    Element(\"element\"),\n    /** Heading elements (h1-h6) */\n    Heading(\"heading\"),\n    /** Paragraph element */\n    Paragraph(\"paragraph\"),\n    /** Generic div container */\n    Div(\"div\"),\n    /** Blockquote element */\n    Blockquote(\"blockquote\"),\n    /** Preformatted text block */\n    Pre(\"pre\"),\n    /** Horizontal rule */\n    Hr(\"hr\"),\n    /** Ordered or unordered list (ul, ol) */\n    List(\"list\"),\n    /** List item (li) */\n    ListItem(\"listitem\"),\n    /** Definition list (dl) */\n    DefinitionList(\"definitionlist\"),\n    /** Definition term (dt) */\n    DefinitionTerm(\"definitionterm\"),\n    /** Definition description (dd) */\n    DefinitionDescription(\"definitiondescription\"),\n    /** Table element */\n    Table(\"table\"),\n    /** Table row (tr) */\n    TableRow(\"tablerow\"),\n    /** Table cell (td, th) */\n    TableCell(\"tablecell\"),\n    /** Table header cell (th) */\n    TableHeader(\"tableheader\"),\n    /** Table body (tbody) */\n    TableBody(\"tablebody\"),\n    /** Table head (thead) */\n    TableHead(\"tablehead\"),\n    /** Table foot (tfoot) */\n    TableFoot(\"tablefoot\"),\n    /** Anchor link (a) */\n    Link(\"link\"),\n    /** Image (img) */\n    Image(\"image\"),\n    /** Strong/bold (strong, b) */\n    Strong(\"strong\"),\n    /** Emphasis/italic (em, i) */\n    Em(\"em\"),\n    /** Inline code (code) */\n    Code(\"code\"),\n    /** Strikethrough (s, del, strike) */\n    Strikethrough(\"strikethrough\"),\n    /** Underline (u, ins) */\n    Underline(\"underline\"),\n    /** Subscript (sub) */\n    Subscript(\"subscript\"),\n    /** Superscript (sup) */\n    Superscript(\"superscript\"),\n    /** Mark/highlight (mark) */\n    Mark(\"mark\"),\n    /** Small text (small) */\n    Small(\"small\"),\n    /** Line break (br) */\n    Br(\"br\"),\n    /** Span element */\n    Span(\"span\"),\n    /** Article element */\n    Article(\"article\"),\n    /** Section element */\n    Section(\"section\"),\n    /** Navigation element */\n    Nav(\"nav\"),\n    /** Aside element */\n    Aside(\"aside\"),\n    /** Header element */\n    Header(\"header\"),\n    /** Footer element */\n    Footer(\"footer\"),\n    /** Main element */\n    Main(\"main\"),\n    /** Figure element */\n    Figure(\"figure\"),\n    /** Figure caption */\n    Figcaption(\"figcaption\"),\n    /** Time element */\n    Time(\"time\"),\n    /** Details element */\n    Details(\"details\"),\n    /** Summary element */\n    Summary(\"summary\"),\n    /** Form element */\n    Form(\"form\"),\n    /** Input element */\n    Input(\"input\"),\n    /** Select element */\n    Select(\"select\"),\n    /** Option element */\n    Option(\"option\"),\n    /** Button element */\n    Button(\"button\"),\n    /** Textarea element */\n    Textarea(\"textarea\"),\n    /** Label element */\n    Label(\"label\"),\n    /** Fieldset element */\n    Fieldset(\"fieldset\"),\n    /** Legend element */\n    Legend(\"legend\"),\n    /** Audio element */\n    Audio(\"audio\"),\n    /** Video element */\n    Video(\"video\"),\n    /** Picture element */\n    Picture(\"picture\"),\n    /** Source element */\n    Source(\"source\"),\n    /** Iframe element */\n    Iframe(\"iframe\"),\n    /** SVG element */\n    Svg(\"svg\"),\n    /** Canvas element */\n    Canvas(\"canvas\"),\n    /** Ruby annotation */\n    Ruby(\"ruby\"),\n    /** Ruby text */\n    Rt(\"rt\"),\n    /** Ruby parenthesis */\n    Rp(\"rp\"),\n    /** Abbreviation */\n    Abbr(\"abbr\"),\n    /** Keyboard input */\n    Kbd(\"kbd\"),\n    /** Sample output */\n    Samp(\"samp\"),\n    /** Variable */\n    Var(\"var\"),\n    /** Citation */\n    Cite(\"cite\"),\n    /** Quote */\n    Q(\"q\"),\n    /** Deleted text */\n    Del(\"del\"),\n    /** Inserted text */\n    Ins(\"ins\"),\n    /** Data element */\n    Data(\"data\"),\n    /** Meter element */\n    Meter(\"meter\"),\n    /** Progress element */\n    Progress(\"progress\"),\n    /** Output element */\n    Output(\"output\"),\n    /** Template element */\n    Template(\"template\"),\n    /** Slot element */\n    Slot(\"slot\"),\n    /** HTML root element */\n    Html(\"html\"),\n    /** Head element */\n    Head(\"head\"),\n    /** Body element */\n    Body(\"body\"),\n    /** Title element */\n    Title(\"title\"),\n    /** Meta element */\n    Meta(\"meta\"),\n    /** Link element (not anchor) */\n    LinkTag(\"linktag\"),\n    /** Style element */\n    Style(\"style\"),\n    /** Script element */\n    Script(\"script\"),\n    /** Base element */\n    Base(\"base\"),\n    /** Custom element (web components) or unknown tag */\n    Custom(\"custom\");\n\n    /** The string value. */\n    private final String value;\n\n    NodeType(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static NodeType fromValue(final String value) {\n        for (NodeType e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/OtherException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:0c4094f5ed0eb41814b3c4df3a65f1e397a333b98a8c096f585518baa17888e4\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * Generic conversion error\n */\npublic class OtherException extends ConversionErrorException {\n    /** Creates a new OtherException with the given message. */\n    public OtherException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new OtherException with the given message and cause. */\n    public OtherException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/OutputFormat.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:64452f97729c08958dbefb30aec56024630c143e709264e2b4df47b3c4e2fa3f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Output format for conversion.\n *\n * Specifies the target markup language format for the conversion output.\n */\npublic enum OutputFormat {\n    /** Standard Markdown (CommonMark compatible). Default. */\n    Markdown(\"markdown\"),\n    /** Djot lightweight markup language. */\n    Djot(\"djot\"),\n    /** Plain text output (no markup, visible text only). */\n    Plain(\"plain\");\n\n    /** The string value. */\n    private final String value;\n\n    OutputFormat(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static OutputFormat fromValue(final String value) {\n        for (OutputFormat e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PanicException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:d21ee154d920c8ce846f63ddeb100c520c9081cfbb82cd6edbb053dc20b7b2f0\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * Panic caught during conversion to prevent unwinding across FFI boundaries\n */\npublic class PanicException extends ConversionErrorException {\n    /** Creates a new PanicException with the given message. */\n    public PanicException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new PanicException with the given message and cause. */\n    public PanicException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ParseErrorException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:1a6fb3e3a52492585056696e3848026a2b6c016670ebb1af3d289bb3b8df1d95\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * HTML parsing error\n */\npublic class ParseErrorException extends ConversionErrorException {\n    /** Creates a new ParseErrorException with the given message. */\n    public ParseErrorException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new ParseErrorException with the given message and cause. */\n    public ParseErrorException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PreprocessingOptions.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:55bafbb77a9598822d22e855219929fe9511f55454a40702dc0fe9c0a21da97a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * HTML preprocessing options for document cleanup before conversion.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record PreprocessingOptions(\n    /** Enable HTML preprocessing globally */\n    boolean enabled,\n    /** Preprocessing preset level (Minimal, Standard, Aggressive) */\n    PreprocessingPreset preset,\n    /** Remove navigation elements (nav, breadcrumbs, menus, sidebars) */\n    @JsonProperty(\"remove_navigation\") boolean removeNavigation,\n    /** Remove form elements (forms, inputs, buttons, etc.) */\n    @JsonProperty(\"remove_forms\") boolean removeForms\n) {\n    public static PreprocessingOptionsBuilder builder() {\n        return new PreprocessingOptionsBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PreprocessingOptionsBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6c887cd77e339b72758a429d2cf3b33446d80447baae5bee142996f3463461be\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n\n/**\n * HTML preprocessing options for document cleanup before conversion.\n */\npublic class PreprocessingOptionsBuilder {\n\n    private boolean enabled = false;\n    private PreprocessingPreset preset = null;\n    private boolean removeNavigation = false;\n    private boolean removeForms = false;\n\n    /** Sets the enabled field. */\n    public PreprocessingOptionsBuilder withEnabled(final boolean value) {\n        this.enabled = value;\n        return this;\n    }\n\n    /** Sets the preset field. */\n    public PreprocessingOptionsBuilder withPreset(final PreprocessingPreset value) {\n        this.preset = value;\n        return this;\n    }\n\n    /** Sets the removeNavigation field. */\n    public PreprocessingOptionsBuilder withRemoveNavigation(final boolean value) {\n        this.removeNavigation = value;\n        return this;\n    }\n\n    /** Sets the removeForms field. */\n    public PreprocessingOptionsBuilder withRemoveForms(final boolean value) {\n        this.removeForms = value;\n        return this;\n    }\n\n    /** Builds the PreprocessingOptions instance. */\n    public PreprocessingOptions build() {\n        return new PreprocessingOptions(\n            enabled,\n            preset,\n            removeNavigation,\n            removeForms\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PreprocessingOptionsUpdate.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:157a2812066077455cd6642c4a5c14f6800010867371ec7a6907501baf6e4286\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Partial update for {@code PreprocessingOptions}.\n *\n * This struct uses {@code Option&lt;T&gt;} to represent optional fields that can be selectively updated.\n * Only specified fields (Some values) will override existing options; None values leave the\n * corresponding fields unchanged when applied via [{@code PreprocessingOptions::apply_update}].\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record PreprocessingOptionsUpdate(\n    /** Optional global preprocessing enablement override */\n    Optional<Boolean> enabled,\n    /** Optional preprocessing preset level override (Minimal, Standard, Aggressive) */\n    Optional<PreprocessingPreset> preset,\n    /** Optional navigation element removal override (nav, breadcrumbs, menus, sidebars) */\n    @JsonProperty(\"remove_navigation\") Optional<Boolean> removeNavigation,\n    /** Optional form element removal override (forms, inputs, buttons, etc.) */\n    @JsonProperty(\"remove_forms\") Optional<Boolean> removeForms\n) {\n    public static PreprocessingOptionsUpdateBuilder builder() {\n        return new PreprocessingOptionsUpdateBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PreprocessingOptionsUpdateBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6b738ac81545f75316e7739df4cfb5011ddea428c41add2cfe0ac732bdf9d504\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.Optional;\n\n/**\n * Partial update for {@code PreprocessingOptions}.\n *\n * This struct uses {@code Option&lt;T&gt;} to represent optional fields that can be selectively updated.\n * Only specified fields (Some values) will override existing options; None values leave the\n * corresponding fields unchanged when applied via [{@code PreprocessingOptions::apply_update}].\n */\npublic class PreprocessingOptionsUpdateBuilder {\n\n    private Optional<Boolean> enabled = Optional.empty();\n    private Optional<PreprocessingPreset> preset = Optional.empty();\n    private Optional<Boolean> removeNavigation = Optional.empty();\n    private Optional<Boolean> removeForms = Optional.empty();\n\n    /** Sets the enabled field. */\n    public PreprocessingOptionsUpdateBuilder withEnabled(final Optional<Boolean> value) {\n        this.enabled = value;\n        return this;\n    }\n\n    /** Sets the preset field. */\n    public PreprocessingOptionsUpdateBuilder withPreset(final Optional<PreprocessingPreset> value) {\n        this.preset = value;\n        return this;\n    }\n\n    /** Sets the removeNavigation field. */\n    public PreprocessingOptionsUpdateBuilder withRemoveNavigation(final Optional<Boolean> value) {\n        this.removeNavigation = value;\n        return this;\n    }\n\n    /** Sets the removeForms field. */\n    public PreprocessingOptionsUpdateBuilder withRemoveForms(final Optional<Boolean> value) {\n        this.removeForms = value;\n        return this;\n    }\n\n    /** Builds the PreprocessingOptionsUpdate instance. */\n    public PreprocessingOptionsUpdate build() {\n        return new PreprocessingOptionsUpdate(\n            enabled,\n            preset,\n            removeNavigation,\n            removeForms\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/PreprocessingPreset.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2d09768c80b9d4440535b5013baf7930fa547b76902c1c120e9f1c7308d53cf7\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * HTML preprocessing aggressiveness level.\n *\n * Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n */\npublic enum PreprocessingPreset {\n    /** Minimal cleanup. Remove only essential noise (scripts, styles). */\n    Minimal(\"minimal\"),\n    /**\n     * Standard cleanup. Default. Removes navigation, forms, and other auxiliary content.\n     */\n    Standard(\"standard\"),\n    /**\n     * Aggressive cleanup. Remove extensive non-content elements and structure.\n     */\n    Aggressive(\"aggressive\");\n\n    /** The string value. */\n    private final String value;\n\n    PreprocessingPreset(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static PreprocessingPreset fromValue(final String value) {\n        for (PreprocessingPreset e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/ProcessingWarning.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:bec1c19579bc04d2d0d9f8eb821948c1899898c87180c6d5b5fa10877d37f25d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A non-fatal warning generated during HTML processing.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record ProcessingWarning(String message, WarningKind kind) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/SanitizationErrorException.java",
    "content": "// DO NOT EDIT - auto-generated by alef\n// alef:hash:2f8de78a1d39fcd16f91b58428d0a14495a7c4cd3da31d334771bcdafd3b6f3d\npackage dev.kreuzberg.htmltomarkdown;\n\n/**\n * HTML sanitization error\n */\npublic class SanitizationErrorException extends ConversionErrorException {\n    /** Creates a new SanitizationErrorException with the given message. */\n    public SanitizationErrorException(final String message) {\n        super(message);\n    }\n\n    /** Creates a new SanitizationErrorException with the given message and cause. */\n    public SanitizationErrorException(final String message, final Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/StructuredData.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:26bd00a95a57803cb522c0b551965c93c5f08cf40106c30228699c6ffb5f5de5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.Optional;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * Structured data block (JSON-LD, Microdata, or RDFa).\n *\n * Represents machine-readable structured data found in the document.\n * JSON-LD blocks are collected as raw JSON strings for flexibility.\n *\n * # Examples\n *\n * {@code }{@code }\n * # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n * let schema = StructuredData {\n *     data_type: StructuredDataType::JsonLd,\n *     raw_json: r#\"{\"{@literal @}context\":\"https://schema.org\",\"{@literal @}type\":\"Article\"}\"#.to_string(),\n *     schema_type: Some(\"Article\".to_string()),\n * };\n *\n * assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n * {@code }{@code }\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record StructuredData(\n    /** Type of structured data (JSON-LD, Microdata, RDFa) */\n    @JsonProperty(\"data_type\") StructuredDataType dataType,\n    /** Raw JSON string (for JSON-LD) or serialized representation */\n    @JsonProperty(\"raw_json\") String rawJson,\n    /** Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\") */\n    @JsonProperty(\"schema_type\") Optional<String> schemaType\n) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/StructuredDataType.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:98f695d57675d2044887858293a3f495a4a96dacfac0eb28c48dae24c8bf595f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Structured data format type.\n *\n * Identifies the schema/format used for structured data markup.\n */\npublic enum StructuredDataType {\n    /** JSON-LD (JSON for Linking Data) script blocks */\n    JsonLd(\"json_ld\"),\n    /** HTML5 Microdata attributes (itemscope, itemtype, itemprop) */\n    Microdata(\"microdata\"),\n    /** RDF in Attributes (RDFa) markup */\n    RDFa(\"rdfa\");\n\n    /** The string value. */\n    private final String value;\n\n    StructuredDataType(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static StructuredDataType fromValue(final String value) {\n        for (StructuredDataType e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TableData.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:db24e14bcaeaf9fa9bc710b4c0b6e407ea86d4f92824397b65cbdf3af3207dde\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A top-level extracted table with both structured data and markdown representation.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record TableData(TableGrid grid, String markdown) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TableGrid.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:befa45f41984f3c738d88a856769c2cd3d6066f672ef298a9d6f04292f4e09fd\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * A structured table grid with cell-level data including spans.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record TableGrid(\n    /** Number of rows. */\n    int rows,\n    /** Number of columns. */\n    int cols,\n    /** All cells in the table (may be fewer than rows*cols due to spans). */\n    @JsonInclude(JsonInclude.Include.NON_NULL) List<GridCell> cells\n) {\n    public static TableGridBuilder builder() {\n        return new TableGridBuilder();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TableGridBuilder.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:15ffd415302bb1b3935cdaa7958dec0a7386badc7728659e0c82bf764055930f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.util.List;\n\n/**\n * A structured table grid with cell-level data including spans.\n */\npublic class TableGridBuilder {\n\n    private int rows = 0;\n    private int cols = 0;\n    private List<GridCell> cells = List.of();\n\n    /** Sets the rows field. */\n    public TableGridBuilder withRows(final int value) {\n        this.rows = value;\n        return this;\n    }\n\n    /** Sets the cols field. */\n    public TableGridBuilder withCols(final int value) {\n        this.cols = value;\n        return this;\n    }\n\n    /** Sets the cells field. */\n    public TableGridBuilder withCells(final List<GridCell> value) {\n        this.cells = value;\n        return this;\n    }\n\n    /** Builds the TableGrid instance. */\n    public TableGrid build() {\n        return new TableGrid(\n            rows,\n            cols,\n            cells\n        );\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TestVisitor.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ea3da02c7661af8f1dd6747b3212e8479477dab2b2881803cf2fab870de5b6cd\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Test-friendly visitor interface using VisitContext instead of NodeContext. */\npublic interface TestVisitor {\n    /** Called for text nodes. */\n    default VisitResult visitText(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called before entering any element. */\n    default VisitResult visitElementStart(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called after exiting any element; receives the default markdown output. */\n    default VisitResult visitElementEnd(final VisitContext ctx, final String output) { return VisitResult.continueDefault(); }\n    /** Called for anchor links. title is null when the attribute is absent. */\n    default VisitResult visitLink(final VisitContext ctx, final String href, final String text, final String title) { return VisitResult.continueDefault(); }\n    /** Called for images. title is null when absent. */\n    default VisitResult visitImage(final VisitContext ctx, final String src, final String alt, final String title) { return VisitResult.continueDefault(); }\n    /** Called for heading elements h1-h6. id is null when absent. */\n    default VisitResult visitHeading(final VisitContext ctx, final int level, final String text, final String id) { return VisitResult.continueDefault(); }\n    /** Called for code blocks. lang is null when absent. */\n    default VisitResult visitCodeBlock(final VisitContext ctx, final String lang, final String code) { return VisitResult.continueDefault(); }\n    /** Called for inline code elements. */\n    default VisitResult visitCodeInline(final VisitContext ctx, final String code) { return VisitResult.continueDefault(); }\n    /** Called for list items. */\n    default VisitResult visitListItem(final VisitContext ctx, final boolean ordered, final String marker, final String text) { return VisitResult.continueDefault(); }\n    /** Called before processing a list. */\n    default VisitResult visitListStart(final VisitContext ctx, final boolean ordered) { return VisitResult.continueDefault(); }\n    /** Called after processing a list. */\n    default VisitResult visitListEnd(final VisitContext ctx, final boolean ordered, final String output) { return VisitResult.continueDefault(); }\n    /** Called before processing a table. */\n    default VisitResult visitTableStart(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called for table rows. cells contains the cell text values. */\n    default VisitResult visitTableRow(final VisitContext ctx, final java.util.List<String> cells, final boolean isHeader) { return VisitResult.continueDefault(); }\n    /** Called after processing a table. */\n    default VisitResult visitTableEnd(final VisitContext ctx, final String output) { return VisitResult.continueDefault(); }\n    /** Called for blockquote elements. */\n    default VisitResult visitBlockquote(final VisitContext ctx, final String content, final long depth) { return VisitResult.continueDefault(); }\n    /** Called for strong/bold elements. */\n    default VisitResult visitStrong(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for emphasis/italic elements. */\n    default VisitResult visitEmphasis(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for strikethrough elements. */\n    default VisitResult visitStrikethrough(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for underline elements. */\n    default VisitResult visitUnderline(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for subscript elements. */\n    default VisitResult visitSubscript(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for superscript elements. */\n    default VisitResult visitSuperscript(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for mark/highlight elements. */\n    default VisitResult visitMark(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for line break elements. */\n    default VisitResult visitLineBreak(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called for horizontal rule elements. */\n    default VisitResult visitHorizontalRule(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called for custom or unknown elements. */\n    default VisitResult visitCustomElement(final VisitContext ctx, final String tagName, final String html) { return VisitResult.continueDefault(); }\n    /** Called before a definition list. */\n    default VisitResult visitDefinitionListStart(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called for definition term elements. */\n    default VisitResult visitDefinitionTerm(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for definition description elements. */\n    default VisitResult visitDefinitionDescription(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called after a definition list. */\n    default VisitResult visitDefinitionListEnd(final VisitContext ctx, final String output) { return VisitResult.continueDefault(); }\n    /** Called for form elements. action and method may be null. */\n    default VisitResult visitForm(final VisitContext ctx, final String action, final String method) { return VisitResult.continueDefault(); }\n    /** Called for input elements. name and value may be null. */\n    default VisitResult visitInput(final VisitContext ctx, final String inputType, final String name, final String value) { return VisitResult.continueDefault(); }\n    /** Called for button elements. */\n    default VisitResult visitButton(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called for audio elements. src may be null. */\n    default VisitResult visitAudio(final VisitContext ctx, final String src) { return VisitResult.continueDefault(); }\n    /** Called for video elements. src may be null. */\n    default VisitResult visitVideo(final VisitContext ctx, final String src) { return VisitResult.continueDefault(); }\n    /** Called for iframe elements. src may be null. */\n    default VisitResult visitIframe(final VisitContext ctx, final String src) { return VisitResult.continueDefault(); }\n    /** Called for details elements. */\n    default VisitResult visitDetails(final VisitContext ctx, final boolean open) { return VisitResult.continueDefault(); }\n    /** Called for summary elements. */\n    default VisitResult visitSummary(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called before a figure element. */\n    default VisitResult visitFigureStart(final VisitContext ctx) { return VisitResult.continueDefault(); }\n    /** Called for figcaption elements. */\n    default VisitResult visitFigcaption(final VisitContext ctx, final String text) { return VisitResult.continueDefault(); }\n    /** Called after a figure element. */\n    default VisitResult visitFigureEnd(final VisitContext ctx, final String output) { return VisitResult.continueDefault(); }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TestVisitorAdapter.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:97fd276058fc462747855453b17ea6842828a3c505ede5324c84345707d74296\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Adapts a {@link TestVisitor} to the {@link Visitor} interface. */\nfinal class TestVisitorAdapter implements Visitor {\n    private final TestVisitor delegate;\n\n    TestVisitorAdapter(final TestVisitor delegate) {\n        java.util.Objects.requireNonNull(delegate, \"delegate must not be null\");\n        this.delegate = delegate;\n    }\n\n    private static VisitContext toVisitContext(final NodeContext ctx) {\n        return new VisitContext(ctx.nodeType(), ctx.tagName(), ctx.depth(), ctx.indexInParent(), ctx.parentTag(), ctx.isInline());\n    }\n\n    @Override\n    public VisitResult visitText(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitText(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitElementStart(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitElementStart(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitElementEnd(final NodeContext context, final String output) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitElementEnd(visitCtx, output);\n    }\n\n    @Override\n    public VisitResult visitLink(\n            final NodeContext context, final String href, final String text, final String title) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitLink(visitCtx, href, text, title);\n    }\n\n    @Override\n    public VisitResult visitImage(\n            final NodeContext context, final String src, final String alt, final String title) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitImage(visitCtx, src, alt, title);\n    }\n\n    @Override\n    public VisitResult visitHeading(\n            final NodeContext context, final int level, final String text, final String id) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitHeading(visitCtx, level, text, id);\n    }\n\n    @Override\n    public VisitResult visitCodeBlock(\n            final NodeContext context, final String lang, final String code) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitCodeBlock(visitCtx, lang, code);\n    }\n\n    @Override\n    public VisitResult visitCodeInline(final NodeContext context, final String code) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitCodeInline(visitCtx, code);\n    }\n\n    @Override\n    public VisitResult visitListItem(\n            final NodeContext context, final boolean ordered, final String marker, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitListItem(visitCtx, ordered, marker, text);\n    }\n\n    @Override\n    public VisitResult visitListStart(final NodeContext context, final boolean ordered) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitListStart(visitCtx, ordered);\n    }\n\n    @Override\n    public VisitResult visitListEnd(\n            final NodeContext context, final boolean ordered, final String output) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitListEnd(visitCtx, ordered, output);\n    }\n\n    @Override\n    public VisitResult visitTableStart(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitTableStart(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitTableRow(\n            final NodeContext context, final java.util.List<String> cells, final boolean isHeader) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitTableRow(visitCtx, cells, isHeader);\n    }\n\n    @Override\n    public VisitResult visitTableEnd(final NodeContext context, final String output) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitTableEnd(visitCtx, output);\n    }\n\n    @Override\n    public VisitResult visitBlockquote(\n            final NodeContext context, final String content, final long depth) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitBlockquote(visitCtx, content, depth);\n    }\n\n    @Override\n    public VisitResult visitStrong(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitStrong(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitEmphasis(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitEmphasis(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitStrikethrough(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitStrikethrough(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitUnderline(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitUnderline(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitSubscript(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitSubscript(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitSuperscript(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitSuperscript(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitMark(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitMark(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitLineBreak(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitLineBreak(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitHorizontalRule(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitHorizontalRule(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitCustomElement(\n            final NodeContext context, final String tagName, final String html) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitCustomElement(visitCtx, tagName, html);\n    }\n\n    @Override\n    public VisitResult visitDefinitionListStart(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitDefinitionListStart(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitDefinitionTerm(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitDefinitionTerm(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitDefinitionDescription(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitDefinitionDescription(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitDefinitionListEnd(final NodeContext context, final String output) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitDefinitionListEnd(visitCtx, output);\n    }\n\n    @Override\n    public VisitResult visitForm(\n            final NodeContext context, final String action, final String method) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitForm(visitCtx, action, method);\n    }\n\n    @Override\n    public VisitResult visitInput(\n            final NodeContext context, final String inputType, final String name, final String value) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitInput(visitCtx, inputType, name, value);\n    }\n\n    @Override\n    public VisitResult visitButton(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitButton(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitAudio(final NodeContext context, final String src) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitAudio(visitCtx, src);\n    }\n\n    @Override\n    public VisitResult visitVideo(final NodeContext context, final String src) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitVideo(visitCtx, src);\n    }\n\n    @Override\n    public VisitResult visitIframe(final NodeContext context, final String src) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitIframe(visitCtx, src);\n    }\n\n    @Override\n    public VisitResult visitDetails(final NodeContext context, final boolean open) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitDetails(visitCtx, open);\n    }\n\n    @Override\n    public VisitResult visitSummary(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitSummary(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitFigureStart(final NodeContext context) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitFigureStart(visitCtx);\n    }\n\n    @Override\n    public VisitResult visitFigcaption(final NodeContext context, final String text) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitFigcaption(visitCtx, text);\n    }\n\n    @Override\n    public VisitResult visitFigureEnd(final NodeContext context, final String output) {\n        var visitCtx = toVisitContext(context);\n        return delegate.visitFigureEnd(visitCtx, output);\n    }\n\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TextAnnotation.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:4abb9641b14fd404a6c3fdb835ca23e5bfa36317eae6376aa8d9a3903699b059\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\n/**\n * An inline text annotation with byte-range offsets.\n *\n * Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n */\n@JsonInclude(JsonInclude.Include.NON_ABSENT)\npublic record TextAnnotation(int start, int end, AnnotationKind kind) {\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/TextDirection.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:aacfc9873f37a1dddf9eeac3187e38383ed9fa8aa2a7299bb8aefc8c6aa20321\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Text directionality of document content.\n *\n * Corresponds to the HTML {@code dir} attribute and {@code bdi} element directionality.\n */\npublic enum TextDirection {\n    /** Left-to-right text flow (default for Latin scripts) */\n    LeftToRight(\"ltr\"),\n    /** Right-to-left text flow (Hebrew, Arabic, Urdu, etc.) */\n    RightToLeft(\"rtl\"),\n    /** Automatic directionality detection */\n    Auto(\"auto\");\n\n    /** The string value. */\n    private final String value;\n\n    TextDirection(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static TextDirection fromValue(final String value) {\n        for (TextDirection e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/VisitContext.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:8e831cfb4ce762309998fa85d64854d84f564ee5dd5a91eb2afb84274972fca5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Context passed to every visitor callback. */\npublic record VisitContext(\n        /** Coarse-grained node type tag. */\n        int nodeType,\n        /** HTML element tag name (e.g. \"div\"). */\n        String tagName,\n        /** DOM depth (0 = root). */\n        long depth,\n        /** 0-based sibling index. */\n        long indexInParent,\n        /** Parent element tag name, or null at the root. */\n        String parentTag,\n        /** True when this element is treated as inline. */\n        boolean isInline\n) {}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/VisitResult.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:9e7ee5230b69ee3c6a33772ac12b8bd4be4ba7fed47fdc258bbb4a809815992d\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Controls how the visitor affects the conversion pipeline. */\npublic sealed interface VisitResult\n        permits VisitResult.Continue, VisitResult.Skip, VisitResult.PreserveHtml,\n                VisitResult.Custom, VisitResult.Error {\n\n    /** Proceed with default conversion. */\n    record Continue() implements VisitResult {}\n\n    /** Omit this element from output entirely. */\n    record Skip() implements VisitResult {}\n\n    /** Keep original HTML verbatim. */\n    record PreserveHtml() implements VisitResult {}\n\n    /** Replace with custom Markdown. */\n    record Custom(String markdown) implements VisitResult {}\n\n    /** Abort conversion with an error message. */\n    record Error(String message) implements VisitResult {}\n\n    /** Convenience: continue with default conversion. */\n    static VisitResult continueDefault() { return new Continue(); }\n\n    /** Convenience: skip this element. */\n    static VisitResult skip() { return new Skip(); }\n\n    /** Convenience: preserve original HTML. */\n    static VisitResult preserveHtml() { return new PreserveHtml(); }\n\n    /** Convenience: emit custom Markdown. */\n    static VisitResult custom(String markdown) { return new Custom(markdown); }\n\n    /** Convenience: abort with error. */\n    static VisitResult error(String message) { return new Error(message); }\n\n    /** Alias for {@link #continueDefault()}. */\n    static VisitResult continue_() { return new Continue(); }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/Visitor.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:f556450b52a9391ecf23953b2baf6194d2c0b1a7a29b70b174331600163da85a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\n/** Visitor interface for the HTML-to-Markdown conversion pipeline. */\npublic interface Visitor {\n    /** Called for text nodes. */\n    default VisitResult visitText(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called before entering any element. */\n    default VisitResult visitElementStart(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called after exiting any element; receives the default markdown output. */\n    default VisitResult visitElementEnd(final NodeContext context, final String output) { return VisitResult.continueDefault(); }\n    /** Called for anchor links. title is null when the attribute is absent. */\n    default VisitResult visitLink(final NodeContext context, final String href, final String text, final String title) { return VisitResult.continueDefault(); }\n    /** Called for images. title is null when absent. */\n    default VisitResult visitImage(final NodeContext context, final String src, final String alt, final String title) { return VisitResult.continueDefault(); }\n    /** Called for heading elements h1-h6. id is null when absent. */\n    default VisitResult visitHeading(final NodeContext context, final int level, final String text, final String id) { return VisitResult.continueDefault(); }\n    /** Called for code blocks. lang is null when absent. */\n    default VisitResult visitCodeBlock(final NodeContext context, final String lang, final String code) { return VisitResult.continueDefault(); }\n    /** Called for inline code elements. */\n    default VisitResult visitCodeInline(final NodeContext context, final String code) { return VisitResult.continueDefault(); }\n    /** Called for list items. */\n    default VisitResult visitListItem(final NodeContext context, final boolean ordered, final String marker, final String text) { return VisitResult.continueDefault(); }\n    /** Called before processing a list. */\n    default VisitResult visitListStart(final NodeContext context, final boolean ordered) { return VisitResult.continueDefault(); }\n    /** Called after processing a list. */\n    default VisitResult visitListEnd(final NodeContext context, final boolean ordered, final String output) { return VisitResult.continueDefault(); }\n    /** Called before processing a table. */\n    default VisitResult visitTableStart(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called for table rows. cells contains the cell text values. */\n    default VisitResult visitTableRow(final NodeContext context, final java.util.List<String> cells, final boolean isHeader) { return VisitResult.continueDefault(); }\n    /** Called after processing a table. */\n    default VisitResult visitTableEnd(final NodeContext context, final String output) { return VisitResult.continueDefault(); }\n    /** Called for blockquote elements. */\n    default VisitResult visitBlockquote(final NodeContext context, final String content, final long depth) { return VisitResult.continueDefault(); }\n    /** Called for strong/bold elements. */\n    default VisitResult visitStrong(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for emphasis/italic elements. */\n    default VisitResult visitEmphasis(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for strikethrough elements. */\n    default VisitResult visitStrikethrough(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for underline elements. */\n    default VisitResult visitUnderline(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for subscript elements. */\n    default VisitResult visitSubscript(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for superscript elements. */\n    default VisitResult visitSuperscript(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for mark/highlight elements. */\n    default VisitResult visitMark(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for line break elements. */\n    default VisitResult visitLineBreak(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called for horizontal rule elements. */\n    default VisitResult visitHorizontalRule(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called for custom or unknown elements. */\n    default VisitResult visitCustomElement(final NodeContext context, final String tagName, final String html) { return VisitResult.continueDefault(); }\n    /** Called before a definition list. */\n    default VisitResult visitDefinitionListStart(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called for definition term elements. */\n    default VisitResult visitDefinitionTerm(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for definition description elements. */\n    default VisitResult visitDefinitionDescription(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called after a definition list. */\n    default VisitResult visitDefinitionListEnd(final NodeContext context, final String output) { return VisitResult.continueDefault(); }\n    /** Called for form elements. action and method may be null. */\n    default VisitResult visitForm(final NodeContext context, final String action, final String method) { return VisitResult.continueDefault(); }\n    /** Called for input elements. name and value may be null. */\n    default VisitResult visitInput(final NodeContext context, final String inputType, final String name, final String value) { return VisitResult.continueDefault(); }\n    /** Called for button elements. */\n    default VisitResult visitButton(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called for audio elements. src may be null. */\n    default VisitResult visitAudio(final NodeContext context, final String src) { return VisitResult.continueDefault(); }\n    /** Called for video elements. src may be null. */\n    default VisitResult visitVideo(final NodeContext context, final String src) { return VisitResult.continueDefault(); }\n    /** Called for iframe elements. src may be null. */\n    default VisitResult visitIframe(final NodeContext context, final String src) { return VisitResult.continueDefault(); }\n    /** Called for details elements. */\n    default VisitResult visitDetails(final NodeContext context, final boolean open) { return VisitResult.continueDefault(); }\n    /** Called for summary elements. */\n    default VisitResult visitSummary(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called before a figure element. */\n    default VisitResult visitFigureStart(final NodeContext context) { return VisitResult.continueDefault(); }\n    /** Called for figcaption elements. */\n    default VisitResult visitFigcaption(final NodeContext context, final String text) { return VisitResult.continueDefault(); }\n    /** Called after a figure element. */\n    default VisitResult visitFigureEnd(final NodeContext context, final String output) { return VisitResult.continueDefault(); }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/VisitorBridge.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:fcd8f1b9ad43549ad4748a07d19c9e0357379d7100788979404257b09f8c20a4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.lang.foreign.Arena;\nimport java.lang.foreign.FunctionDescriptor;\nimport java.lang.foreign.Linker;\nimport java.lang.foreign.MemoryLayout;\nimport java.lang.foreign.MemorySegment;\nimport java.lang.foreign.ValueLayout;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Allocates Panama FFM upcall stubs for a Visitor and assembles\n * the C HTMHtmVisitorCallbacks struct in native memory.\n */\nfinal class VisitorBridge implements AutoCloseable {\n    private static final Linker LINKER = Linker.nativeLinker();\n    private static final MethodHandles.Lookup LOOKUP =\n        MethodHandles.lookup();\n\n    // VisitResult discriminant codes returned to C\n    private static final int VISIT_RESULT_CONTINUE = 0;\n    private static final int VISIT_RESULT_SKIP = 1;\n    private static final int VISIT_RESULT_PRESERVE_HTML = 2;\n    private static final int VISIT_RESULT_CUSTOM = 3;\n    private static final int VISIT_RESULT_ERROR = 4;\n\n    // HTMHtmVisitorCallbacks: user_data + 40 callbacks\n    // = 41 pointer-sized slots\n    private static final long CALLBACKS_STRUCT_SIZE =\n        (long) ValueLayout.ADDRESS.byteSize() * 41L;\n\n    // HTMHtmNodeContext field offsets\n    private static final long CTX_OFFSET_TAG_NAME = 8L;\n    private static final long CTX_OFFSET_DEPTH = 16L;\n    private static final long CTX_OFFSET_INDEX_IN_PARENT = 24L;\n    private static final long CTX_OFFSET_PARENT_TAG = 32L;\n    private static final long CTX_OFFSET_IS_INLINE = 40L;\n\n    private final Arena arena;\n    private final MemorySegment struct;\n    private final Visitor visitor;\n\n    VisitorBridge(Visitor visitor) {\n        this.visitor = visitor;\n        this.arena = Arena.ofConfined();\n        this.struct = arena.allocate(CALLBACKS_STRUCT_SIZE);\n        // Slot 0: user_data = NULL\n        // (visitor captured via lambda closure)\n        struct.set(ValueLayout.ADDRESS, 0L, MemorySegment.NULL);\n        try {\n            long offset = ValueLayout.ADDRESS.byteSize();\n            offset = registerStubs1(offset);\n            offset = registerStubs2(offset);\n            offset = registerStubs3(offset);\n            registerStubs4(offset);\n        } catch (ReflectiveOperationException e) {\n            arena.close();\n            throw new RuntimeException(\n                \"Failed to create visitor upcall stubs\", e);\n        }\n    }\n\n    private long registerStubs1(\n            final long offset)\n            throws ReflectiveOperationException {\n        long off = offset;\n        // visit_text\n        var stubVisitText = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitText\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitText);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_element_start\n        var stubVisitElementStart = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitElementStart\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitElementStart);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_element_end\n        var stubVisitElementEnd = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitElementEnd\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitElementEnd);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_link\n        var stubVisitLink = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitLink\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitLink);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_image\n        var stubVisitImage = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitImage\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitImage);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_heading\n        var stubVisitHeading = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitHeading\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitHeading);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_code_block\n        var stubVisitCodeBlock = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitCodeBlock\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitCodeBlock);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_code_inline\n        var stubVisitCodeInline = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitCodeInline\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitCodeInline);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_list_item\n        var stubVisitListItem = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitListItem\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitListItem);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_list_start\n        var stubVisitListStart = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitListStart\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitListStart);\n        off += ValueLayout.ADDRESS.byteSize();\n        return off;\n    }\n\n    private long registerStubs2(\n            final long offset)\n            throws ReflectiveOperationException {\n        long off = offset;\n        // visit_list_end\n        var stubVisitListEnd = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitListEnd\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitListEnd);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_table_start\n        var stubVisitTableStart = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitTableStart\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitTableStart);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_table_row\n        var stubVisitTableRow = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitTableRow\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    long.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_LONG,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitTableRow);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_table_end\n        var stubVisitTableEnd = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitTableEnd\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitTableEnd);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_blockquote\n        var stubVisitBlockquote = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitBlockquote\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    long.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_LONG,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitBlockquote);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_strong\n        var stubVisitStrong = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitStrong\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitStrong);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_emphasis\n        var stubVisitEmphasis = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitEmphasis\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitEmphasis);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_strikethrough\n        var stubVisitStrikethrough = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitStrikethrough\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitStrikethrough);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_underline\n        var stubVisitUnderline = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitUnderline\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitUnderline);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_subscript\n        var stubVisitSubscript = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitSubscript\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitSubscript);\n        off += ValueLayout.ADDRESS.byteSize();\n        return off;\n    }\n\n    private long registerStubs3(\n            final long offset)\n            throws ReflectiveOperationException {\n        long off = offset;\n        // visit_superscript\n        var stubVisitSuperscript = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitSuperscript\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitSuperscript);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_mark\n        var stubVisitMark = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitMark\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitMark);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_line_break\n        var stubVisitLineBreak = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitLineBreak\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitLineBreak);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_horizontal_rule\n        var stubVisitHorizontalRule = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitHorizontalRule\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitHorizontalRule);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_custom_element\n        var stubVisitCustomElement = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitCustomElement\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitCustomElement);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_definition_list_start\n        var stubVisitDefinitionListStart = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitDefinitionListStart\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitDefinitionListStart);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_definition_term\n        var stubVisitDefinitionTerm = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitDefinitionTerm\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitDefinitionTerm);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_definition_description\n        var stubVisitDefinitionDescription = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitDefinitionDescription\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitDefinitionDescription);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_definition_list_end\n        var stubVisitDefinitionListEnd = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitDefinitionListEnd\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitDefinitionListEnd);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_form\n        var stubVisitForm = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitForm\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitForm);\n        off += ValueLayout.ADDRESS.byteSize();\n        return off;\n    }\n\n    private long registerStubs4(\n            final long offset)\n            throws ReflectiveOperationException {\n        long off = offset;\n        // visit_input\n        var stubVisitInput = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitInput\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitInput);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_button\n        var stubVisitButton = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitButton\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitButton);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_audio\n        var stubVisitAudio = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitAudio\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitAudio);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_video\n        var stubVisitVideo = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitVideo\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitVideo);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_iframe\n        var stubVisitIframe = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitIframe\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitIframe);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_details\n        var stubVisitDetails = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitDetails\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitDetails);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_summary\n        var stubVisitSummary = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitSummary\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitSummary);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_figure_start\n        var stubVisitFigureStart = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitFigureStart\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitFigureStart);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_figcaption\n        var stubVisitFigcaption = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitFigcaption\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitFigcaption);\n        off += ValueLayout.ADDRESS.byteSize();\n        // visit_figure_end\n        var stubVisitFigureEnd = LINKER.upcallStub(\n                LOOKUP.bind(\n                    this, \"handleVisitFigureEnd\",\n                    MethodType.methodType(\n                    int.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class,\n                    MemorySegment.class)),\n                FunctionDescriptor.of(\n                    ValueLayout.JAVA_INT,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS,\n                    ValueLayout.ADDRESS),\n                arena);\n        struct.set(ValueLayout.ADDRESS, off, stubVisitFigureEnd);\n        off += ValueLayout.ADDRESS.byteSize();\n        return off;\n    }\n\n\n    /** Returns the native HTMHtmVisitorCallbacks struct pointer. */\n    MemorySegment callbacksStruct() {\n        return struct;\n    }\n\n    int handleVisitText(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitText(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitElementStart(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitElementStart(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitElementEnd(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawOutput0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var output = rawOutput0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitElementEnd(context, output);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitLink(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawHref0,\n            final MemorySegment rawText0,\n            final MemorySegment rawTitle0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var href = rawHref0.reinterpret(Long.MAX_VALUE).getString(0);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var title = rawTitle0.equals(MemorySegment.NULL) ? null : rawTitle0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitLink(context, href, text, title);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitImage(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawSrc0,\n            final MemorySegment rawAlt0,\n            final MemorySegment rawTitle0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var src = rawSrc0.reinterpret(Long.MAX_VALUE).getString(0);\n            var alt = rawAlt0.reinterpret(Long.MAX_VALUE).getString(0);\n            var title = rawTitle0.equals(MemorySegment.NULL) ? null : rawTitle0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitImage(context, src, alt, title);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitHeading(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final int rawLevel0,\n            final MemorySegment rawText0,\n            final MemorySegment rawId0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var level = (int) rawLevel0;\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var id = rawId0.equals(MemorySegment.NULL) ? null : rawId0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitHeading(context, level, text, id);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitCodeBlock(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawLang0,\n            final MemorySegment rawCode0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var lang = rawLang0.equals(MemorySegment.NULL) ? null : rawLang0.reinterpret(Long.MAX_VALUE).getString(0);\n            var code = rawCode0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitCodeBlock(context, lang, code);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitCodeInline(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawCode0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var code = rawCode0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitCodeInline(context, code);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitListItem(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final int rawOrdered0,\n            final MemorySegment rawMarker0,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var ordered = ((int) rawOrdered0) != 0;\n            var marker = rawMarker0.reinterpret(Long.MAX_VALUE).getString(0);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitListItem(context, ordered, marker, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitListStart(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final int rawOrdered0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var ordered = ((int) rawOrdered0) != 0;\n            var result = visitor.visitListStart(context, ordered);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitListEnd(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final int rawOrdered0,\n            final MemorySegment rawOutput0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var ordered = ((int) rawOrdered0) != 0;\n            var output = rawOutput0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitListEnd(context, ordered, output);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitTableStart(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitTableStart(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitTableRow(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawCells0,\n            final long rawCells1,\n            final int isHeader,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var cells = decodeCells(rawCells0, (long) rawCells1);\n            var goIsHeader = isHeader != 0;\n            var result = visitor.visitTableRow(context, cells, goIsHeader);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitTableEnd(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawOutput0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var output = rawOutput0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitTableEnd(context, output);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitBlockquote(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawContent0,\n            final long rawDepth0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var content = rawContent0.reinterpret(Long.MAX_VALUE).getString(0);\n            var depth = (long) rawDepth0;\n            var result = visitor.visitBlockquote(context, content, depth);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitStrong(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitStrong(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitEmphasis(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitEmphasis(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitStrikethrough(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitStrikethrough(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitUnderline(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitUnderline(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitSubscript(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitSubscript(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitSuperscript(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitSuperscript(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitMark(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitMark(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitLineBreak(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitLineBreak(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitHorizontalRule(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitHorizontalRule(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitCustomElement(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawTagName0,\n            final MemorySegment rawHtml0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var tagName = rawTagName0.reinterpret(Long.MAX_VALUE).getString(0);\n            var html = rawHtml0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitCustomElement(context, tagName, html);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitDefinitionListStart(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitDefinitionListStart(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitDefinitionTerm(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitDefinitionTerm(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitDefinitionDescription(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitDefinitionDescription(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitDefinitionListEnd(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawOutput0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var output = rawOutput0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitDefinitionListEnd(context, output);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitForm(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawAction0,\n            final MemorySegment rawMethod0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var action = rawAction0.equals(MemorySegment.NULL) ? null : rawAction0.reinterpret(Long.MAX_VALUE).getString(0);\n            var method = rawMethod0.equals(MemorySegment.NULL) ? null : rawMethod0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitForm(context, action, method);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitInput(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawInputType0,\n            final MemorySegment rawName0,\n            final MemorySegment rawValue0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var inputType = rawInputType0.reinterpret(Long.MAX_VALUE).getString(0);\n            var name = rawName0.equals(MemorySegment.NULL) ? null : rawName0.reinterpret(Long.MAX_VALUE).getString(0);\n            var value = rawValue0.equals(MemorySegment.NULL) ? null : rawValue0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitInput(context, inputType, name, value);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitButton(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitButton(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitAudio(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawSrc0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var src = rawSrc0.equals(MemorySegment.NULL) ? null : rawSrc0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitAudio(context, src);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitVideo(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawSrc0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var src = rawSrc0.equals(MemorySegment.NULL) ? null : rawSrc0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitVideo(context, src);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitIframe(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawSrc0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var src = rawSrc0.equals(MemorySegment.NULL) ? null : rawSrc0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitIframe(context, src);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitDetails(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final int rawOpen0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var open = ((int) rawOpen0) != 0;\n            var result = visitor.visitDetails(context, open);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitSummary(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitSummary(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitFigureStart(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var result = visitor.visitFigureStart(context);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitFigcaption(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawText0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var text = rawText0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitFigcaption(context, text);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    int handleVisitFigureEnd(\n            final MemorySegment ctx,\n            final MemorySegment userData,\n            final MemorySegment rawOutput0,\n            final MemorySegment outCustom,\n            final MemorySegment outLen) {\n        try {\n            var context = decodeNodeContext(ctx);\n            var output = rawOutput0.reinterpret(Long.MAX_VALUE).getString(0);\n            var result = visitor.visitFigureEnd(context, output);\n            return encodeVisitResult(result, outCustom, outLen);\n        } catch (Throwable ignored) {\n            return 0;\n        }\n    }\n\n    // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,\n    //   uintptr index_in_parent, char* parent_tag,\n    //   int32 is_inline\n    private static final MemoryLayout CTX_LAYOUT =\n        MemoryLayout.structLayout(\n            ValueLayout.JAVA_INT.withName(\"node_type\"),\n            MemoryLayout.paddingLayout(4),\n            ValueLayout.ADDRESS.withName(\"tag_name\"),\n            ValueLayout.JAVA_LONG.withName(\"depth\"),\n            ValueLayout.JAVA_LONG.withName(\"index_in_parent\"),\n            ValueLayout.ADDRESS.withName(\"parent_tag\"),\n            ValueLayout.JAVA_INT.withName(\"is_inline\"),\n            MemoryLayout.paddingLayout(4)\n    );\n\n    private static NodeContext decodeNodeContext(\n            final MemorySegment ctxPtr) {\n        var ctx = ctxPtr.reinterpret(\n            CTX_LAYOUT.byteSize());\n        int nodeType = ctx.get(\n            ValueLayout.JAVA_INT, 0L);\n        var tagNamePtr = ctx.get(\n            ValueLayout.ADDRESS, CTX_OFFSET_TAG_NAME);\n        String tagName = tagNamePtr\n            .reinterpret(Long.MAX_VALUE).getString(0);\n        long depth = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_DEPTH);\n        long indexInParent = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_INDEX_IN_PARENT);\n        var parentTagPtr = ctx.get(ValueLayout.ADDRESS, CTX_OFFSET_PARENT_TAG);\n        String parentTag = parentTagPtr.equals(MemorySegment.NULL) ? null\n                : parentTagPtr.reinterpret(Long.MAX_VALUE).getString(0);\n        int isInlineRaw = ctx.get(ValueLayout.JAVA_INT, CTX_OFFSET_IS_INLINE);\n        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);\n    }\n\n    private static List<String> decodeCells(MemorySegment cellsPtr, long count) {\n        var result = new ArrayList<String>((int) count);\n        for (long i = 0; i < count; i++) {\n            var ptr = cellsPtr.get(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize());\n            result.add(ptr.reinterpret(Long.MAX_VALUE).getString(0));\n        }\n        return result;\n    }\n\n    private static int encodeVisitResult(VisitResult result, MemorySegment outCustom, MemorySegment outLen) {\n        return switch (result) {\n            case VisitResult.Continue ignored -> VISIT_RESULT_CONTINUE;\n            case VisitResult.Skip ignored -> VISIT_RESULT_SKIP;\n            case VisitResult.PreserveHtml ignored -> VISIT_RESULT_PRESERVE_HTML;\n            case VisitResult.Custom c -> {\n                var buf = Arena.global().allocateFrom(c.markdown());\n                outCustom.reinterpret(ValueLayout.ADDRESS.byteSize()).set(ValueLayout.ADDRESS, 0L, buf);\n                outLen.reinterpret(ValueLayout.JAVA_LONG.byteSize()).set(ValueLayout.JAVA_LONG, 0L, (long) c.markdown().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);\n                yield VISIT_RESULT_CUSTOM;\n            }\n            case VisitResult.Error e -> {\n                var buf = Arena.global().allocateFrom(e.message());\n                outCustom.reinterpret(ValueLayout.ADDRESS.byteSize()).set(ValueLayout.ADDRESS, 0L, buf);\n                outLen.reinterpret(ValueLayout.JAVA_LONG.byteSize()).set(ValueLayout.JAVA_LONG, 0L, (long) e.message().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);\n                yield VISIT_RESULT_ERROR;\n            }\n        };\n    }\n\n    @Override\n    public void close() {\n        arena.close();\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/VisitorHandle.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:cf4ae99df23cacf689c3ff1a66d8f546c2811fcfd04803592aeed6a387ad4ed1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport java.lang.foreign.MemorySegment;\n\n/**\n * Type alias for a visitor handle (Rc-wrapped {@code RefCell} for interior mutability).\n *\n * This allows visitors to be passed around and shared while still being mutable.\n */\npublic class VisitorHandle implements AutoCloseable {\n    private final MemorySegment handle;\n\n    VisitorHandle(MemorySegment handle) {\n        this.handle = handle;\n    }\n\n    MemorySegment handle() {\n        return this.handle;\n    }\n\n    @Override\n    public void close() {\n        if (handle != null && !handle.equals(MemorySegment.NULL)) {\n            try {\n                NativeLib.HTM_VISITOR_HANDLE_FREE.invoke(handle);\n            } catch (Throwable e) {\n                throw new RuntimeException(\"Failed to free VisitorHandle: \" + e.getMessage(), e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/WarningKind.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1ad8f7ac6cc132833216767f90b0127a87f0e2aa8bae2aabbc7de066fd3be547\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Categories of processing warnings.\n */\npublic enum WarningKind {\n    /**\n     * An image could not be extracted (e.g. invalid data URI, unsupported format).\n     */\n    ImageExtractionFailed(\"imageextractionfailed\"),\n    /** The input encoding was not recognized; fell back to UTF-8. */\n    EncodingFallback(\"encodingfallback\"),\n    /** The input was truncated due to size limits. */\n    TruncatedInput(\"truncatedinput\"),\n    /** The HTML was malformed but processing continued with best effort. */\n    MalformedHtml(\"malformedhtml\"),\n    /** Sanitization was applied to remove potentially unsafe content. */\n    SanitizationApplied(\"sanitizationapplied\"),\n    /** DOM traversal was truncated because max_depth was exceeded. */\n    DepthLimitExceeded(\"depthlimitexceeded\");\n\n    /** The string value. */\n    private final String value;\n\n    WarningKind(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static WarningKind fromValue(final String value) {\n        for (WarningKind e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/WhitespaceMode.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2922cf593d783ac12c5da074c0316d788c9d6f97e8bb4b6fc75f70d82c2ea8f1\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\n\n/**\n * Whitespace handling strategy during conversion.\n *\n * Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n */\npublic enum WhitespaceMode {\n    /**\n     * Collapse multiple whitespace characters to single spaces. Default. Matches browser behavior.\n     */\n    Normalized(\"normalized\"),\n    /** Preserve all whitespace exactly as it appears in the HTML. */\n    Strict(\"strict\");\n\n    /** The string value. */\n    private final String value;\n\n    WhitespaceMode(final String value) {\n        this.value = value;\n    }\n\n    /** Returns the string value. */\n    @JsonValue\n    public String getValue() {\n        return value;\n    }\n\n    /** Creates an instance from a string value. */\n    @JsonCreator\n    public static WhitespaceMode fromValue(final String value) {\n        for (WhitespaceMode e : values()) {\n            if (e.value.equalsIgnoreCase(value)) {\n                return e;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown value: \" + value);\n    }\n}\n"
  },
  {
    "path": "packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/package-info.java",
    "content": "/**\n * High-performance HTML to Markdown converter\n */\npackage dev.kreuzberg.htmltomarkdown;\n"
  },
  {
    "path": "packages/java/src/main/resources/.gitkeep",
    "content": ""
  },
  {
    "path": "packages/java/versions-rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ruleset xmlns=\"http://mojo.codehaus.org/versions-maven-plugin/rules/2.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://mojo.codehaus.org/versions-maven-plugin/rules/2.0.0\n                             https://www.mojohaus.org/versions/versions-maven-plugin/xsd/rule-2.0.0.xsd\"\n         comparisonMethod=\"maven\">\n    <ignoreVersions>\n        <ignoreVersion type=\"regex\">(?i).*[.-](alpha|beta|rc|cr|milestone|preview|ea|eap|snapshot).*</ignoreVersion>\n        <ignoreVersion type=\"regex\">(?i).*[.-]m\\d+.*</ignoreVersion>\n    </ignoreVersions>\n</ruleset>\n"
  },
  {
    "path": "packages/node/.oxfmtrc.json",
    "content": "{\n\t\"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n\t\"printWidth\": 120,\n\t\"useTabs\": true,\n\t\"tabWidth\": 4,\n\t\"semi\": true,\n\t\"singleQuote\": false,\n\t\"trailingComma\": \"all\",\n\t\"arrowParens\": \"always\",\n\t\"endOfLine\": \"lf\",\n\t\"bracketSpacing\": true,\n\t\"sortImports\": true,\n\t\"sortPackageJson\": true\n}\n"
  },
  {
    "path": "packages/node/.oxlintrc.json",
    "content": "{\n\t\"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n\t\"categories\": {\n\t\t\"correctness\": \"error\",\n\t\t\"suspicious\": \"warn\",\n\t\t\"pedantic\": \"off\",\n\t\t\"perf\": \"warn\",\n\t\t\"style\": \"off\",\n\t\t\"restriction\": \"off\"\n\t},\n\t\"plugins\": [\"typescript\", \"import\"],\n\t\"env\": {\n\t\t\"es6\": true,\n\t\t\"node\": true\n\t},\n\t\"rules\": {\n\t\t\"no-console\": \"warn\",\n\t\t\"no-unused-vars\": \"warn\",\n\t\t\"typescript/no-explicit-any\": \"warn\"\n\t},\n\t\"overrides\": [\n\t\t{\n\t\t\t\"files\": [\"**/tests/**\", \"**/*.test.ts\", \"**/*.spec.ts\"],\n\t\t\t\"rules\": {\n\t\t\t\t\"no-console\": \"off\",\n\t\t\t\t\"no-unused-vars\": \"off\",\n\t\t\t\t\"typescript/no-explicit-any\": \"off\"\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "packages/node/biome.json",
    "content": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.0.0/schema.json\",\n\t\"formatter\": {\n\t\t\"enabled\": true,\n\t\t\"indentStyle\": \"tab\",\n\t\t\"lineWidth\": 120\n\t},\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": true,\n\t\t\t\"suspicious\": {\n\t\t\t\t\"noExplicitAny\": \"warn\",\n\t\t\t\t\"noImplicitAnyLet\": \"warn\"\n\t\t\t}\n\t\t}\n\t},\n\t\"javascript\": {\n\t\t\"formatter\": {\n\t\t\t\"quoteStyle\": \"double\",\n\t\t\t\"trailingCommas\": \"all\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/node/index.d.ts",
    "content": "export * from \"../../crates/html-to-markdown-node/index\";\n"
  },
  {
    "path": "packages/node/package.json",
    "content": "{\n\t\"name\": \"@kreuzberg/html-to-markdown\",\n\t\"version\": \"3.3.2\",\n\t\"description\": \"High-performance HTML to Markdown converter\",\n\t\"keywords\": [\n\t\t\"converter\",\n\t\t\"html\",\n\t\t\"markdown\"\n\t],\n\t\"license\": \"MIT\",\n\t\"author\": \"Kreuzberg Team\",\n\t\"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n\t\"files\": [\n\t\t\"index.js\",\n\t\t\"index.d.ts\",\n\t\t\"**/*.node\"\n\t],\n\t\"main\": \"index.js\",\n\t\"types\": \"index.d.ts\",\n\t\"scripts\": {\n\t\t\"build\": \"napi build --release\",\n\t\t\"build:debug\": \"napi build\",\n\t\t\"build:ts\": \"echo 'No TypeScript wrapper to build'\",\n\t\t\"test\": \"node -e \\\"console.log('Add test command')\\\"\",\n\t\t\"lint\": \"biome check src\",\n\t\t\"lint:fix\": \"biome check --write src\",\n\t\t\"format\": \"biome format --write src\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@biomejs/biome\": \"^2.4.12\",\n\t\t\"@napi-rs/cli\": \"^3.0.0\",\n\t\t\"typescript\": \"^6.0.3\"\n\t},\n\t\"napi\": {\n\t\t\"name\": \"html-to-markdown-rs\",\n\t\t\"triples\": [\n\t\t\t\"x86_64-unknown-linux-gnu\",\n\t\t\t\"x86_64-apple-darwin\",\n\t\t\t\"aarch64-apple-darwin\",\n\t\t\t\"x86_64-pc-windows-msvc\"\n\t\t]\n\t}\n}\n"
  },
  {
    "path": "packages/node/src/index.d.ts",
    "content": "export * from \"../../crates/html-to-markdown-node/index\";\n"
  },
  {
    "path": "packages/node/tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ES2022\",\n\t\t\"lib\": [\"ES2022\"],\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"strict\": true,\n\t\t\"noImplicitAny\": true,\n\t\t\"strictNullChecks\": true,\n\t\t\"strictFunctionTypes\": true,\n\t\t\"strictBindCallApply\": true,\n\t\t\"strictPropertyInitialization\": true,\n\t\t\"noImplicitThis\": true,\n\t\t\"useUnknownInCatchVariables\": true,\n\t\t\"alwaysStrict\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"noImplicitReturns\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"noUncheckedIndexedAccess\": true,\n\t\t\"noImplicitOverride\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"allowSyntheticDefaultImports\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"noEmit\": true\n\t},\n\t\"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/php/.gitignore",
    "content": "/vendor/\n/var/\n/.phpunit.result.cache\ntests/test_apps/elixir/deps/\n"
  },
  {
    "path": "packages/php/.php-cs-fixer.dist.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\nreturn new Config()\n    ->setRiskyAllowed(false)\n    ->setRules([\n        '@auto' => true,\n    ])\n    // 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config\n    ->setFinder(\n        new Finder()\n            // 💡 root folder to check\n            ->in(__DIR__)\n        // 💡 additional files, eg bin entry file\n        // ->append([__DIR__.'/bin-entry-file'])\n        // 💡 folders to exclude, if any\n        // ->exclude([/* ... */])\n        // 💡 path patterns to exclude, if any\n        // ->notPath([/* ... */])\n        // 💡 extra configs\n        // ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode\n        // ->ignoreVCS(true) // true by default\n    )\n;\n"
  },
  {
    "path": "packages/php/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with typed PHP bindings powered by a Rust core.\nProvides a type-safe API with full PHPStan level 9 support, modern PHP 8.2+ features, and comprehensive metadata extraction.\n\nNote: The package was previously published as `goldziher/html-to-markdown`, which still works for backward compatibility.\n\n## Installation\n\n```bash\ncomposer require kreuzberg-dev/html-to-markdown\n```\n\nRequires PHP 8.2+. Install the native extension via PIE:\n\n```bash\npie install kreuzberg-dev/html-to-markdown\n```\n\nOr use Composer (requires ext-html_to_markdown):\n\n```bash\ncomposer require kreuzberg-dev/html-to-markdown\n```\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document           | Size  | Ops/sec |\n| ------------------ | ----- | ------- |\n| Lists (Timeline)   | 129KB | 3346    |\n| Tables (Countries) | 360KB | 973     |\n| Medium (Python)    | 657KB | 485     |\n\n## Quick Start\n\nBasic conversion:\n\n```php\nuse HtmlToMarkdown\\Service\\Converter;\nuse function HtmlToMarkdown\\convert;\n\n// Object-oriented usage\n$converter = Converter::create();\n$result = $converter->convert('<h1>Hello</h1><p>This is <strong>fast</strong>!</p>');\n$markdown = $result['content'];\n\n// Procedural helper\n$result = convert('<h1>Hello</h1>');\n$markdown = $result['content'];\n```\n\nWith conversion options:\n\n```php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$converter = Converter::create();\n\n$options = new ConversionOptions(\n    headingStyle: 'Atx',\n    listIndentWidth: 2,\n);\n\n$result = $converter->convert('<h1>Hello</h1>', $options);\n$markdown = $result['content'];\n```\n\n## API Reference\n\n### Core Function\n\n**`Converter::convert(string $html, ?ConversionOptions $options = null, ?VisitorInterface $visitor = null): array`**\n\nConverts HTML to Markdown. Returns an array `ConversionResult` with all results in a single call.\n\n```php\n<?php\nuse HtmlToMarkdown\\Service\\Converter;\n\n$result  = Converter::create()->convert($html);\n$markdown = $result['content'];    // Converted Markdown string\n$metadata = $result['metadata'];   // Metadata (when extractMetadata: true)\n$tables   = $result['tables'];     // Structured table data (when extractTables: true)\n$document = $result['document'];   // Document-level info\n$images   = $result['images'];     // Extracted images\n$warnings = $result['warnings'];   // Any conversion warnings\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\n$markdown = Converter::convert($html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\n$djot = Converter::convert($html, new ConversionOptions(outputFormat: 'djot'));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n$plain = Converter::convert($html, new ConversionOptions(outputFormat: 'plain'));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(extractMetadata: true)\n);\n\necho $result['content'];                          // Converted Markdown\necho $result['metadata']->document->title;        // Document title\nprint_r($result['metadata']->headers);            // All h1-h6 elements\nprint_r($result['metadata']->links);              // All hyperlinks\nprint_r($result['metadata']->images);             // All images with alt text\nprint_r($result['metadata']->structured_data);    // JSON-LD, Microdata, RDFa\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\nuse HtmlToMarkdown\\Visitor\\AbstractVisitor;\nuse HtmlToMarkdown\\Visitor\\NodeContext;\nuse HtmlToMarkdown\\Visitor\\VisitResult;\n\nclass MyVisitor extends AbstractVisitor\n{\n    public function visitLink(NodeContext $ctx, string $href, string $text, ?string $title): array\n    {\n        // Rewrite CDN URLs\n        if (str_starts_with($href, 'https://old-cdn.com')) {\n            $href = str_replace('https://old-cdn.com', 'https://new-cdn.com', $href);\n        }\n        return VisitResult::custom(\"[{$text}]({$href})\");\n    }\n\n    public function visitImage(NodeContext $ctx, string $src, ?string $alt, ?string $title): array\n    {\n        // Skip tracking pixels\n        return str_contains($src, 'tracking') ? VisitResult::skip() : VisitResult::continue();\n    }\n}\n\n$html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(visitor: new MyVisitor())\n);\n$markdown = $result['content'];\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Packagist:** [packagist.org/packages/kreuzberg-dev/html-to-markdown](https://packagist.org/packages/kreuzberg-dev/html-to-markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/php/composer.json",
    "content": "{\n  \"name\": \"kreuzberg-dev/html-to-markdown-rs\",\n  \"description\": \"High-performance HTML to Markdown converter\",\n  \"license\": \"MIT\",\n  \"type\": \"php-ext\",\n  \"require\": {\n    \"php\": \">=8.2\"\n  },\n  \"require-dev\": {\n    \"phpstan/phpstan\": \"^2.1\",\n    \"friendsofphp/php-cs-fixer\": \"^3.95\",\n    \"phpunit/phpunit\": \"^13.1\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"HtmlToMarkdown\\\\\": \"src/\"\n    }\n  },\n  \"scripts\": {\n    \"phpstan\": \"php -d detect_unicode=0 vendor/bin/phpstan --configuration=phpstan.neon --memory-limit=512M\",\n    \"format\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config php-cs-fixer.php src tests\",\n    \"format:check\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config php-cs-fixer.php --dry-run src tests\",\n    \"test\": \"php vendor/bin/phpunit\",\n    \"lint\": \"@phpstan\",\n    \"lint:fix\": \"PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer fix --config php-cs-fixer.php src tests && php -d detect_unicode=0 vendor/bin/phpstan --configuration=phpstan.neon --memory-limit=512M\"\n  },\n  \"extra\": {\n    \"ext-name\": \"html_to_markdown_rs\"\n  },\n  \"keywords\": [\n    \"html\",\n    \"markdown\",\n    \"converter\"\n  ]\n}\n"
  },
  {
    "path": "packages/php/php-cs-fixer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\n$finder = Finder::create()\n    ->in(__DIR__)\n    ->exclude('vendor')\n    ->exclude('var')\n    ->exclude('pecl')\n    ->exclude('bin')\n    ->files()\n    ->name('*.php')\n    ->path('#^src/#')\n    ->path('#^tests/#')\n    ->notName('*.blade.php');\n\nreturn new Config()\n    ->setRiskyAllowed(true)\n    ->setRules([\n        '@PSR12' => true,\n        '@PSR12:risky' => true,\n        'array_syntax' => ['syntax' => 'short'],\n        'declare_strict_types' => true,\n        'final_class' => false,\n        'final_public_method_for_abstract_class' => false,\n        'single_quote' => true,\n        'phpdoc_to_comment' => false,\n        'php_unit_method_casing' => false,\n        'ordered_imports' => [\n            'imports_order' => ['class', 'function', 'const'],\n            'sort_algorithm' => 'alpha',\n        ],\n        'no_superfluous_phpdoc_tags' => true,\n        'native_function_invocation' => ['include' => ['@internal'], 'exclude' => ['html_to_markdown_convert']],\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "packages/php/phpstan-baseline.neon",
    "content": "parameters:\n    ignoreErrors: []\n"
  },
  {
    "path": "packages/php/phpstan-test.neon",
    "content": "parameters:\n    level: max\n    treatPhpDocTypesAsCertain: false\n    reportUnmatchedIgnoredErrors: false\n    paths:\n        - src\n        - tests\n    tmpDir: var/cache/phpstan\n    stubFiles:\n        - stubs/html_to_markdown_extension.php\n"
  },
  {
    "path": "packages/php/phpstan.neon",
    "content": "includes:\n    - phpstan-baseline.neon\n\nparameters:\n    level: max\n    treatPhpDocTypesAsCertain: false\n    reportUnmatchedIgnoredErrors: false\n    paths:\n        - src\n        - tests\n    excludePaths:\n        - stubs/html_to_markdown_extension.php\n    tmpDir: var/cache/phpstan\n    scanFiles:\n        - stubs/html_to_markdown_extension.php\n"
  },
  {
    "path": "packages/php/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/11.3/phpunit.xsd\"\n         colors=\"true\"\n         stopOnFailure=\"false\">\n    <testsuites>\n        <testsuite name=\"HtmlToMarkdown PHP\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "packages/php/src/HtmlToMarkdown.php",
    "content": "<?php\n\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:100d2a7d2ddc0ab944776d4ad68104a26ca1f0a49f4bdc6a424def836542e729\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\ndeclare(strict_types=1);\n\nnamespace HtmlToMarkdown;\n\nfinal class HtmlToMarkdown\n{\n    /**\n     * Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n     * and warnings.\n     *\n     * # Arguments\n     *\n     * * `html` — the HTML string to convert.\n     * * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n     *   When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n     *   attached via the `visitor` field on `ConversionOptions`.\n     *\n     * # Example\n     *\n     * ```\n     * use html_to_markdown_rs::convert;\n     *\n     * let html = \"<h1>Hello World</h1>\";\n     * let result = convert(html, None).unwrap();\n     * assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n     * ```\n     *\n     * # Errors\n     *\n     * Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n     *\n     * @param string $html\n     * @param ?ConversionOptions $options\n     * @return ConversionResult\n     * @throws \\HtmlToMarkdown\\HtmlToMarkdownException\n     */\n    public static function convert(string $html, ?ConversionOptions $options = null): ConversionResult\n    {\n        return \\HtmlToMarkdown\\HtmlToMarkdownApi::convert($html, $options, $options?->visitor); // delegate to native extension class\n    }\n\n}\n"
  },
  {
    "path": "packages/php/src/functions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace {\n\n    use HtmlToMarkdown\\HtmlToMarkdown;\n\n    if (!\\function_exists('html_to_markdown_convert')) {\n        /**\n         * Convert HTML to Markdown and return the content string.\n         *\n         * Delegates to the native Rust extension via the HtmlToMarkdown facade.\n         * Options are not currently supported in this convenience wrapper — call\n         * HtmlToMarkdown::convert() directly for full control.\n         *\n         * @param string               $html    The HTML string to convert.\n         * @param array<string, mixed> $options Reserved for future use.\n         *\n         * @throws \\HtmlToMarkdown\\HtmlToMarkdownException on conversion error.\n         */\n        function html_to_markdown_convert(string $html, array $options = []): string\n        {\n            $result = HtmlToMarkdown::convert($html, null);\n\n            return $result->content ?? '';\n        }\n    }\n\n} // end namespace\n"
  },
  {
    "path": "packages/php/stubs/html_to_markdown_extension.php",
    "content": "<?php\n\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:ad8d9846b72a4e5f8b7ad297797870203176c7981d90d6ed7174f408a3ea6f3e\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n// Type stubs for the native PHP extension — declares classes\n// provided at runtime by the compiled Rust extension (.so/.dll).\n// Include this in phpstan.neon scanFiles for static analysis.\n\ndeclare(strict_types=1);\n\nnamespace HtmlToMarkdown {\n\n    class HtmlToMarkdownException extends \\RuntimeException\n    {\n        public function getErrorCode(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Builder for [`ConversionOptions`].\n     *\n     * All fields start with default values. Call `.build()` to produce the final options.\n     */\n    class ConversionOptionsBuilder {}\n\n    /**\n     * Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n     *\n     * This allows visitors to be passed around and shared while still being mutable.\n     */\n    class VisitorHandle {}\n\n    /**\n     * Document-level metadata extracted from `<head>` and top-level elements.\n     *\n     * Contains all metadata typically used by search engines, social media platforms,\n     * and browsers for document indexing and presentation.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::DocumentMetadata;\n     * let doc = DocumentMetadata {\n     *     title: Some(\"My Article\".to_string()),\n     *     description: Some(\"A great article about Rust\".to_string()),\n     *     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n     *     ..Default::default()\n     * };\n     *\n     * assert_eq!(doc.title, Some(\"My Article\".to_string()));\n     * ```\n     */\n    class DocumentMetadata\n    {\n        public ?string $title;\n        public ?string $description;\n        /** @var array<string> */\n        public array $keywords;\n        public ?string $author;\n        public ?string $canonical_url;\n        public ?string $base_href;\n        public ?string $language;\n        public ?TextDirection $text_direction;\n        /** @var array<string, string> */\n        public array $open_graph;\n        /** @var array<string, string> */\n        public array $twitter_card;\n        /** @var array<string, string> */\n        public array $meta_tags;\n\n        /**\n         * @param array<string> $keywords\n         * @param array<string, string> $open_graph\n         * @param array<string, string> $twitter_card\n         * @param array<string, string> $meta_tags\n         */\n        public function __construct(\n            array $keywords,\n            array $open_graph,\n            array $twitter_card,\n            array $meta_tags,\n            ?string $title = null,\n            ?string $description = null,\n            ?string $author = null,\n            ?string $canonical_url = null,\n            ?string $base_href = null,\n            ?string $language = null,\n            ?TextDirection $text_direction = null\n        ) {}\n\n        public function getTitle(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDescription(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getKeywords(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getAuthor(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCanonicalUrl(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getBaseHref(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getLanguage(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getTextDirection(): ?TextDirection\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getOpenGraph(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getTwitterCard(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getMetaTags(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Header element metadata with hierarchy tracking.\n     *\n     * Captures heading elements (h1-h6) with their text content, identifiers,\n     * and position in the document structure.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::HeaderMetadata;\n     * let header = HeaderMetadata {\n     *     level: 1,\n     *     text: \"Main Title\".to_string(),\n     *     id: Some(\"main-title\".to_string()),\n     *     depth: 0,\n     *     html_offset: 145,\n     * };\n     *\n     * assert_eq!(header.level, 1);\n     * assert!(header.is_valid());\n     * ```\n     */\n    class HeaderMetadata\n    {\n        public int $level;\n        public string $text;\n        public ?string $id;\n        public int $depth;\n        public int $html_offset;\n\n        public function __construct(\n            int $level,\n            string $text,\n            int $depth,\n            int $html_offset,\n            ?string $id = null\n        ) {}\n\n        public function getLevel(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getText(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getId(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDepth(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getHtmlOffset(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Hyperlink metadata with categorization and attributes.\n     *\n     * Represents `<a>` elements with parsed href values, text content, and link type classification.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n     * let link = LinkMetadata {\n     *     href: \"https://example.com\".to_string(),\n     *     text: \"Example\".to_string(),\n     *     title: Some(\"Visit Example\".to_string()),\n     *     link_type: LinkType::External,\n     *     rel: vec![\"nofollow\".to_string()],\n     *     attributes: Default::default(),\n     * };\n     *\n     * assert_eq!(link.link_type, LinkType::External);\n     * assert_eq!(link.text, \"Example\");\n     * ```\n     */\n    class LinkMetadata\n    {\n        public string $href;\n        public string $text;\n        public ?string $title;\n        public LinkType $link_type;\n        /** @var array<string> */\n        public array $rel;\n        /** @var array<string, string> */\n        public array $attributes;\n\n        /**\n         * @param array<string> $rel\n         * @param array<string, string> $attributes\n         */\n        public function __construct(\n            string $href,\n            string $text,\n            LinkType $link_type,\n            array $rel,\n            array $attributes,\n            ?string $title = null\n        ) {}\n\n        public function getHref(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getText(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getTitle(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getLinkType(): LinkType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getRel(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getAttributes(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Image metadata with source and dimensions.\n     *\n     * Captures `<img>` elements and inline `<svg>` elements with metadata\n     * for image analysis and optimization.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n     * let img = ImageMetadata {\n     *     src: \"https://example.com/image.jpg\".to_string(),\n     *     alt: Some(\"An example image\".to_string()),\n     *     title: Some(\"Example\".to_string()),\n     *     dimensions: Some((800, 600)),\n     *     image_type: ImageType::External,\n     *     attributes: Default::default(),\n     * };\n     *\n     * assert_eq!(img.image_type, ImageType::External);\n     * ```\n     */\n    class ImageMetadata\n    {\n        public string $src;\n        public ?string $alt;\n        public ?string $title;\n        /** @var ?array<int> */\n        public ?array $dimensions;\n        public ImageType $image_type;\n        /** @var array<string, string> */\n        public array $attributes;\n\n        /**\n         * @param array<string, string> $attributes\n         * @param ?array<int> $dimensions\n         */\n        public function __construct(\n            string $src,\n            ImageType $image_type,\n            array $attributes,\n            ?string $alt = null,\n            ?string $title = null,\n            ?array $dimensions = null\n        ) {}\n\n        public function getSrc(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getAlt(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getTitle(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<int> */\n        public function getDimensions(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getImageType(): ImageType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getAttributes(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Structured data block (JSON-LD, Microdata, or RDFa).\n     *\n     * Represents machine-readable structured data found in the document.\n     * JSON-LD blocks are collected as raw JSON strings for flexibility.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n     * let schema = StructuredData {\n     *     data_type: StructuredDataType::JsonLd,\n     *     raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n     *     schema_type: Some(\"Article\".to_string()),\n     * };\n     *\n     * assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n     * ```\n     */\n    class StructuredData\n    {\n        public StructuredDataType $data_type;\n        public string $raw_json;\n        public ?string $schema_type;\n\n        public function __construct(\n            StructuredDataType $data_type,\n            string $raw_json,\n            ?string $schema_type = null\n        ) {}\n\n        public function getDataType(): StructuredDataType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRawJson(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSchemaType(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Comprehensive metadata extraction result from HTML document.\n     *\n     * Contains all extracted metadata types in a single structure,\n     * suitable for serialization and transmission across language boundaries.\n     *\n     * # Examples\n     *\n     * ```\n     * # use html_to_markdown_rs::metadata::HtmlMetadata;\n     * let metadata = HtmlMetadata {\n     *     document: Default::default(),\n     *     headers: Vec::new(),\n     *     links: Vec::new(),\n     *     images: Vec::new(),\n     *     structured_data: Vec::new(),\n     * };\n     *\n     * assert!(metadata.headers.is_empty());\n     * ```\n     */\n    class HtmlMetadata\n    {\n        public DocumentMetadata $document;\n        /** @var array<HeaderMetadata> */\n        public array $headers;\n        /** @var array<LinkMetadata> */\n        public array $links;\n        /** @var array<ImageMetadata> */\n        public array $images;\n        /** @var array<StructuredData> */\n        public array $structured_data;\n\n        /**\n         * @param array<HeaderMetadata> $headers\n         * @param array<LinkMetadata> $links\n         * @param array<ImageMetadata> $images\n         * @param array<StructuredData> $structured_data\n         */\n        public function __construct(\n            DocumentMetadata $document,\n            array $headers,\n            array $links,\n            array $images,\n            array $structured_data\n        ) {}\n\n        public function getDocument(): DocumentMetadata\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<HeaderMetadata> */\n        public function getHeaders(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<LinkMetadata> */\n        public function getLinks(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<ImageMetadata> */\n        public function getImages(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<StructuredData> */\n        public function getStructuredData(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Main conversion options for HTML to Markdown conversion.\n     *\n     * Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n     *\n     * # Example\n     *\n     * ```text\n     * use html_to_markdown_rs::ConversionOptions;\n     *\n     * let options = ConversionOptions::builder()\n     *     .heading_style(HeadingStyle::Atx)\n     *     .wrap(true)\n     *     .wrap_width(100)\n     *     .build();\n     * ```\n     */\n    class ConversionOptions\n    {\n        public HeadingStyle $heading_style;\n        public ListIndentType $list_indent_type;\n        public int $list_indent_width;\n        public string $bullets;\n        public string $strong_em_symbol;\n        public bool $escape_asterisks;\n        public bool $escape_underscores;\n        public bool $escape_misc;\n        public bool $escape_ascii;\n        public string $code_language;\n        public bool $autolinks;\n        public bool $default_title;\n        public bool $br_in_tables;\n        public HighlightStyle $highlight_style;\n        public bool $extract_metadata;\n        public WhitespaceMode $whitespace_mode;\n        public bool $strip_newlines;\n        public bool $wrap;\n        public int $wrap_width;\n        public bool $convert_as_inline;\n        public string $sub_symbol;\n        public string $sup_symbol;\n        public NewlineStyle $newline_style;\n        public CodeBlockStyle $code_block_style;\n        /** @var array<string> */\n        public array $keep_inline_images_in;\n        public PreprocessingOptions $preprocessing;\n        public string $encoding;\n        public bool $debug;\n        /** @var array<string> */\n        public array $strip_tags;\n        /** @var array<string> */\n        public array $preserve_tags;\n        public bool $skip_images;\n        public LinkStyle $link_style;\n        public OutputFormat $output_format;\n        public bool $include_document_structure;\n        public bool $extract_images;\n        public int $max_image_size;\n        public bool $capture_svg;\n        public bool $infer_dimensions;\n        public ?int $max_depth;\n        /** @var array<string> */\n        public array $exclude_selectors;\n        public ?VisitorHandle $visitor;\n\n        /**\n         * @param array<string> $keep_inline_images_in\n         * @param array<string> $strip_tags\n         * @param array<string> $preserve_tags\n         * @param array<string> $exclude_selectors\n         */\n        public function __construct(\n            HeadingStyle $heading_style,\n            ListIndentType $list_indent_type,\n            int $list_indent_width,\n            string $bullets,\n            string $strong_em_symbol,\n            bool $escape_asterisks,\n            bool $escape_underscores,\n            bool $escape_misc,\n            bool $escape_ascii,\n            string $code_language,\n            bool $autolinks,\n            bool $default_title,\n            bool $br_in_tables,\n            HighlightStyle $highlight_style,\n            bool $extract_metadata,\n            WhitespaceMode $whitespace_mode,\n            bool $strip_newlines,\n            bool $wrap,\n            int $wrap_width,\n            bool $convert_as_inline,\n            string $sub_symbol,\n            string $sup_symbol,\n            NewlineStyle $newline_style,\n            CodeBlockStyle $code_block_style,\n            array $keep_inline_images_in,\n            PreprocessingOptions $preprocessing,\n            string $encoding,\n            bool $debug,\n            array $strip_tags,\n            array $preserve_tags,\n            bool $skip_images,\n            LinkStyle $link_style,\n            OutputFormat $output_format,\n            bool $include_document_structure,\n            bool $extract_images,\n            int $max_image_size,\n            bool $capture_svg,\n            bool $infer_dimensions,\n            array $exclude_selectors,\n            ?int $max_depth = null,\n            ?VisitorHandle $visitor = null\n        ) {}\n\n        public function getHeadingStyle(): HeadingStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getListIndentType(): ListIndentType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getListIndentWidth(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getBullets(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getStrongEmSymbol(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeAsterisks(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeUnderscores(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeMisc(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeAscii(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCodeLanguage(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getAutolinks(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDefaultTitle(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getBrInTables(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getHighlightStyle(): HighlightStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getExtractMetadata(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWhitespaceMode(): WhitespaceMode\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getStripNewlines(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWrap(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWrapWidth(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getConvertAsInline(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSubSymbol(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSupSymbol(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getNewlineStyle(): NewlineStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCodeBlockStyle(): CodeBlockStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getKeepInlineImagesIn(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getPreprocessing(): PreprocessingOptions\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEncoding(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDebug(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getStripTags(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getPreserveTags(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSkipImages(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getLinkStyle(): LinkStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getOutputFormat(): OutputFormat\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getIncludeDocumentStructure(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getExtractImages(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMaxImageSize(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCaptureSvg(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getInferDimensions(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMaxDepth(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getExcludeSelectors(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getVisitor(): ?VisitorHandle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Partial update for `ConversionOptions`.\n     *\n     * Uses `Option<T>` fields for selective updates. Bindings use this to construct\n     * options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n     */\n    class ConversionOptionsUpdate\n    {\n        public ?HeadingStyle $heading_style;\n        public ?ListIndentType $list_indent_type;\n        public ?int $list_indent_width;\n        public ?string $bullets;\n        public ?string $strong_em_symbol;\n        public ?bool $escape_asterisks;\n        public ?bool $escape_underscores;\n        public ?bool $escape_misc;\n        public ?bool $escape_ascii;\n        public ?string $code_language;\n        public ?bool $autolinks;\n        public ?bool $default_title;\n        public ?bool $br_in_tables;\n        public ?HighlightStyle $highlight_style;\n        public ?bool $extract_metadata;\n        public ?WhitespaceMode $whitespace_mode;\n        public ?bool $strip_newlines;\n        public ?bool $wrap;\n        public ?int $wrap_width;\n        public ?bool $convert_as_inline;\n        public ?string $sub_symbol;\n        public ?string $sup_symbol;\n        public ?NewlineStyle $newline_style;\n        public ?CodeBlockStyle $code_block_style;\n        /** @var ?array<string> */\n        public ?array $keep_inline_images_in;\n        public ?PreprocessingOptionsUpdate $preprocessing;\n        public ?string $encoding;\n        public ?bool $debug;\n        /** @var ?array<string> */\n        public ?array $strip_tags;\n        /** @var ?array<string> */\n        public ?array $preserve_tags;\n        public ?bool $skip_images;\n        public ?LinkStyle $link_style;\n        public ?OutputFormat $output_format;\n        public ?bool $include_document_structure;\n        public ?bool $extract_images;\n        public ?int $max_image_size;\n        public ?bool $capture_svg;\n        public ?bool $infer_dimensions;\n        public ?int $max_depth;\n        /** @var ?array<string> */\n        public ?array $exclude_selectors;\n        public ?VisitorHandle $visitor;\n\n        /**\n         * @param ?array<string> $keep_inline_images_in\n         * @param ?array<string> $strip_tags\n         * @param ?array<string> $preserve_tags\n         * @param ?array<string> $exclude_selectors\n         */\n        public function __construct(\n            ?HeadingStyle $heading_style = null,\n            ?ListIndentType $list_indent_type = null,\n            ?int $list_indent_width = null,\n            ?string $bullets = null,\n            ?string $strong_em_symbol = null,\n            ?bool $escape_asterisks = null,\n            ?bool $escape_underscores = null,\n            ?bool $escape_misc = null,\n            ?bool $escape_ascii = null,\n            ?string $code_language = null,\n            ?bool $autolinks = null,\n            ?bool $default_title = null,\n            ?bool $br_in_tables = null,\n            ?HighlightStyle $highlight_style = null,\n            ?bool $extract_metadata = null,\n            ?WhitespaceMode $whitespace_mode = null,\n            ?bool $strip_newlines = null,\n            ?bool $wrap = null,\n            ?int $wrap_width = null,\n            ?bool $convert_as_inline = null,\n            ?string $sub_symbol = null,\n            ?string $sup_symbol = null,\n            ?NewlineStyle $newline_style = null,\n            ?CodeBlockStyle $code_block_style = null,\n            ?array $keep_inline_images_in = null,\n            ?PreprocessingOptionsUpdate $preprocessing = null,\n            ?string $encoding = null,\n            ?bool $debug = null,\n            ?array $strip_tags = null,\n            ?array $preserve_tags = null,\n            ?bool $skip_images = null,\n            ?LinkStyle $link_style = null,\n            ?OutputFormat $output_format = null,\n            ?bool $include_document_structure = null,\n            ?bool $extract_images = null,\n            ?int $max_image_size = null,\n            ?bool $capture_svg = null,\n            ?bool $infer_dimensions = null,\n            ?int $max_depth = null,\n            ?array $exclude_selectors = null,\n            ?VisitorHandle $visitor = null\n        ) {}\n\n        public function getHeadingStyle(): ?HeadingStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getListIndentType(): ?ListIndentType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getListIndentWidth(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getBullets(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getStrongEmSymbol(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeAsterisks(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeUnderscores(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeMisc(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEscapeAscii(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCodeLanguage(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getAutolinks(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDefaultTitle(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getBrInTables(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getHighlightStyle(): ?HighlightStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getExtractMetadata(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWhitespaceMode(): ?WhitespaceMode\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getStripNewlines(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWrap(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getWrapWidth(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getConvertAsInline(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSubSymbol(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSupSymbol(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getNewlineStyle(): ?NewlineStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCodeBlockStyle(): ?CodeBlockStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<string> */\n        public function getKeepInlineImagesIn(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getPreprocessing(): ?PreprocessingOptionsUpdate\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEncoding(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDebug(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<string> */\n        public function getStripTags(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<string> */\n        public function getPreserveTags(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSkipImages(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getLinkStyle(): ?LinkStyle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getOutputFormat(): ?OutputFormat\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getIncludeDocumentStructure(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getExtractImages(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMaxImageSize(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCaptureSvg(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getInferDimensions(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMaxDepth(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<string> */\n        public function getExcludeSelectors(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getVisitor(): ?VisitorHandle\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * HTML preprocessing options for document cleanup before conversion.\n     */\n    class PreprocessingOptions\n    {\n        public bool $enabled;\n        public PreprocessingPreset $preset;\n        public bool $remove_navigation;\n        public bool $remove_forms;\n\n        public function __construct(\n            bool $enabled,\n            PreprocessingPreset $preset,\n            bool $remove_navigation,\n            bool $remove_forms\n        ) {}\n\n        public function getEnabled(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getPreset(): PreprocessingPreset\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRemoveNavigation(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRemoveForms(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Partial update for `PreprocessingOptions`.\n     *\n     * This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n     * Only specified fields (Some values) will override existing options; None values leave the\n     * corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n     */\n    class PreprocessingOptionsUpdate\n    {\n        public ?bool $enabled;\n        public ?PreprocessingPreset $preset;\n        public ?bool $remove_navigation;\n        public ?bool $remove_forms;\n\n        public function __construct(\n            ?bool $enabled = null,\n            ?PreprocessingPreset $preset = null,\n            ?bool $remove_navigation = null,\n            ?bool $remove_forms = null\n        ) {}\n\n        public function getEnabled(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getPreset(): ?PreprocessingPreset\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRemoveNavigation(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRemoveForms(): ?bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A structured document tree representing the semantic content of an HTML document.\n     *\n     * Uses a flat node array with index-based parent/child references for efficient traversal.\n     */\n    class DocumentStructure\n    {\n        /** @var array<DocumentNode> */\n        public array $nodes;\n        public ?string $source_format;\n\n        /**\n         * @param array<DocumentNode> $nodes\n         */\n        public function __construct(\n            array $nodes,\n            ?string $source_format = null\n        ) {}\n\n        /** @return array<DocumentNode> */\n        public function getNodes(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getSourceFormat(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A single node in the document tree.\n     */\n    class DocumentNode\n    {\n        public string $id;\n        public NodeContent $content;\n        public ?int $parent;\n        /** @var array<int> */\n        public array $children;\n        /** @var array<TextAnnotation> */\n        public array $annotations;\n        /** @var ?array<string, string> */\n        public ?array $attributes;\n\n        /**\n         * @param array<int> $children\n         * @param array<TextAnnotation> $annotations\n         * @param ?array<string, string> $attributes\n         */\n        public function __construct(\n            string $id,\n            NodeContent $content,\n            array $children,\n            array $annotations,\n            ?int $parent = null,\n            ?array $attributes = null\n        ) {}\n\n        public function getId(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getContent(): NodeContent\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getParent(): ?int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<int> */\n        public function getChildren(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<TextAnnotation> */\n        public function getAnnotations(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return ?array<string, string> */\n        public function getAttributes(): ?array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * An inline text annotation with byte-range offsets.\n     *\n     * Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n     */\n    class TextAnnotation\n    {\n        public int $start;\n        public int $end;\n        public AnnotationKind $kind;\n\n        public function __construct(\n            int $start,\n            int $end,\n            AnnotationKind $kind\n        ) {}\n\n        public function getStart(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getEnd(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getKind(): AnnotationKind\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * The primary result of HTML conversion and extraction.\n     *\n     * Contains the converted text output, optional structured document tree,\n     * metadata, extracted tables, images, and processing warnings.\n     *\n     * # Example\n     *\n     * ```text\n     * use html_to_markdown_rs::{convert, ConversionOptions};\n     *\n     * let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n     * assert!(result.content.is_some());\n     * assert!(result.warnings.is_empty());\n     * ```\n     */\n    class ConversionResult\n    {\n        public ?string $content;\n        public ?DocumentStructure $document;\n        public HtmlMetadata $metadata;\n        /** @var array<TableData> */\n        public array $tables;\n        /** @var array<string> */\n        public array $images;\n        /** @var array<ProcessingWarning> */\n        public array $warnings;\n\n        /**\n         * @param array<TableData> $tables\n         * @param array<string> $images\n         * @param array<ProcessingWarning> $warnings\n         */\n        public function __construct(\n            HtmlMetadata $metadata,\n            array $tables,\n            array $images,\n            array $warnings,\n            ?string $content = null,\n            ?DocumentStructure $document = null\n        ) {}\n\n        public function getContent(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDocument(): ?DocumentStructure\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMetadata(): HtmlMetadata\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<TableData> */\n        public function getTables(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string> */\n        public function getImages(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<ProcessingWarning> */\n        public function getWarnings(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A structured table grid with cell-level data including spans.\n     */\n    class TableGrid\n    {\n        public int $rows;\n        public int $cols;\n        /** @var array<GridCell> */\n        public array $cells;\n\n        /**\n         * @param array<GridCell> $cells\n         */\n        public function __construct(\n            int $rows,\n            int $cols,\n            array $cells\n        ) {}\n\n        public function getRows(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCols(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<GridCell> */\n        public function getCells(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A single cell in a table grid.\n     */\n    class GridCell\n    {\n        public string $content;\n        public int $row;\n        public int $col;\n        public int $row_span;\n        public int $col_span;\n        public bool $is_header;\n\n        public function __construct(\n            string $content,\n            int $row,\n            int $col,\n            int $row_span,\n            int $col_span,\n            bool $is_header\n        ) {}\n\n        public function getContent(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRow(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getCol(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getRowSpan(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getColSpan(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getIsHeader(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A top-level extracted table with both structured data and markdown representation.\n     */\n    class TableData\n    {\n        public TableGrid $grid;\n        public string $markdown;\n\n        public function __construct(\n            TableGrid $grid,\n            string $markdown\n        ) {}\n\n        public function getGrid(): TableGrid\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getMarkdown(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * A non-fatal warning generated during HTML processing.\n     */\n    class ProcessingWarning\n    {\n        public string $message;\n        public WarningKind $kind;\n\n        public function __construct(\n            string $message,\n            WarningKind $kind\n        ) {}\n\n        public function getMessage(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getKind(): WarningKind\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    /**\n     * Context information passed to all visitor methods.\n     *\n     * Provides comprehensive metadata about the current node being visited,\n     * including its type, attributes, position in the DOM tree, and parent context.\n     */\n    class NodeContext\n    {\n        public NodeType $node_type;\n        public string $tag_name;\n        /** @var array<string, string> */\n        public array $attributes;\n        public int $depth;\n        public int $index_in_parent;\n        public ?string $parent_tag;\n        public bool $is_inline;\n\n        /**\n         * @param array<string, string> $attributes\n         */\n        public function __construct(\n            NodeType $node_type,\n            string $tag_name,\n            array $attributes,\n            int $depth,\n            int $index_in_parent,\n            bool $is_inline,\n            ?string $parent_tag = null\n        ) {}\n\n        public function getNodeType(): NodeType\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getTagName(): string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        /** @return array<string, string> */\n        public function getAttributes(): array\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getDepth(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getIndexInParent(): int\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getParentTag(): ?string\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n        public function getIsInline(): bool\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n    enum TextDirection: string\n    {\n        case LeftToRight = 'LeftToRight';\n        case RightToLeft = 'RightToLeft';\n        case Auto = 'Auto';\n    }\n\n    enum LinkType: string\n    {\n        case Anchor = 'Anchor';\n        case Internal = 'Internal';\n        case External = 'External';\n        case Email = 'Email';\n        case Phone = 'Phone';\n        case Other = 'Other';\n    }\n\n    enum ImageType: string\n    {\n        case DataUri = 'DataUri';\n        case InlineSvg = 'InlineSvg';\n        case External = 'External';\n        case Relative = 'Relative';\n    }\n\n    enum StructuredDataType: string\n    {\n        case JsonLd = 'JsonLd';\n        case Microdata = 'Microdata';\n        case RDFa = 'RDFa';\n    }\n\n    enum PreprocessingPreset: string\n    {\n        case Minimal = 'Minimal';\n        case Standard = 'Standard';\n        case Aggressive = 'Aggressive';\n    }\n\n    enum HeadingStyle: string\n    {\n        case Underlined = 'Underlined';\n        case Atx = 'Atx';\n        case AtxClosed = 'AtxClosed';\n    }\n\n    enum ListIndentType: string\n    {\n        case Spaces = 'Spaces';\n        case Tabs = 'Tabs';\n    }\n\n    enum WhitespaceMode: string\n    {\n        case Normalized = 'Normalized';\n        case Strict = 'Strict';\n    }\n\n    enum NewlineStyle: string\n    {\n        case Spaces = 'Spaces';\n        case Backslash = 'Backslash';\n    }\n\n    enum CodeBlockStyle: string\n    {\n        case Indented = 'Indented';\n        case Backticks = 'Backticks';\n        case Tildes = 'Tildes';\n    }\n\n    enum HighlightStyle: string\n    {\n        case DoubleEqual = 'DoubleEqual';\n        case Html = 'Html';\n        case Bold = 'Bold';\n        case None = 'None';\n    }\n\n    enum LinkStyle: string\n    {\n        case Inline = 'Inline';\n        case Reference = 'Reference';\n    }\n\n    enum OutputFormat: string\n    {\n        case Markdown = 'Markdown';\n        case Djot = 'Djot';\n        case Plain = 'Plain';\n    }\n\n    enum NodeContent: string\n    {\n        case Heading = 'Heading';\n        case Paragraph = 'Paragraph';\n        case List = 'List';\n        case ListItem = 'ListItem';\n        case Table = 'Table';\n        case Image = 'Image';\n        case Code = 'Code';\n        case Quote = 'Quote';\n        case DefinitionList = 'DefinitionList';\n        case DefinitionItem = 'DefinitionItem';\n        case RawBlock = 'RawBlock';\n        case MetadataBlock = 'MetadataBlock';\n        case Group = 'Group';\n    }\n\n    enum AnnotationKind: string\n    {\n        case Bold = 'Bold';\n        case Italic = 'Italic';\n        case Underline = 'Underline';\n        case Strikethrough = 'Strikethrough';\n        case Code = 'Code';\n        case Subscript = 'Subscript';\n        case Superscript = 'Superscript';\n        case Highlight = 'Highlight';\n        case Link = 'Link';\n    }\n\n    enum WarningKind: string\n    {\n        case ImageExtractionFailed = 'ImageExtractionFailed';\n        case EncodingFallback = 'EncodingFallback';\n        case TruncatedInput = 'TruncatedInput';\n        case MalformedHtml = 'MalformedHtml';\n        case SanitizationApplied = 'SanitizationApplied';\n        case DepthLimitExceeded = 'DepthLimitExceeded';\n    }\n\n    enum NodeType: string\n    {\n        case Text = 'Text';\n        case Element = 'Element';\n        case Heading = 'Heading';\n        case Paragraph = 'Paragraph';\n        case Div = 'Div';\n        case Blockquote = 'Blockquote';\n        case Pre = 'Pre';\n        case Hr = 'Hr';\n        case List = 'List';\n        case ListItem = 'ListItem';\n        case DefinitionList = 'DefinitionList';\n        case DefinitionTerm = 'DefinitionTerm';\n        case DefinitionDescription = 'DefinitionDescription';\n        case Table = 'Table';\n        case TableRow = 'TableRow';\n        case TableCell = 'TableCell';\n        case TableHeader = 'TableHeader';\n        case TableBody = 'TableBody';\n        case TableHead = 'TableHead';\n        case TableFoot = 'TableFoot';\n        case Link = 'Link';\n        case Image = 'Image';\n        case Strong = 'Strong';\n        case Em = 'Em';\n        case Code = 'Code';\n        case Strikethrough = 'Strikethrough';\n        case Underline = 'Underline';\n        case Subscript = 'Subscript';\n        case Superscript = 'Superscript';\n        case Mark = 'Mark';\n        case Small = 'Small';\n        case Br = 'Br';\n        case Span = 'Span';\n        case Article = 'Article';\n        case Section = 'Section';\n        case Nav = 'Nav';\n        case Aside = 'Aside';\n        case Header = 'Header';\n        case Footer = 'Footer';\n        case Main = 'Main';\n        case Figure = 'Figure';\n        case Figcaption = 'Figcaption';\n        case Time = 'Time';\n        case Details = 'Details';\n        case Summary = 'Summary';\n        case Form = 'Form';\n        case Input = 'Input';\n        case Select = 'Select';\n        case Option = 'Option';\n        case Button = 'Button';\n        case Textarea = 'Textarea';\n        case Label = 'Label';\n        case Fieldset = 'Fieldset';\n        case Legend = 'Legend';\n        case Audio = 'Audio';\n        case Video = 'Video';\n        case Picture = 'Picture';\n        case Source = 'Source';\n        case Iframe = 'Iframe';\n        case Svg = 'Svg';\n        case Canvas = 'Canvas';\n        case Ruby = 'Ruby';\n        case Rt = 'Rt';\n        case Rp = 'Rp';\n        case Abbr = 'Abbr';\n        case Kbd = 'Kbd';\n        case Samp = 'Samp';\n        case Var = 'Var';\n        case Cite = 'Cite';\n        case Q = 'Q';\n        case Del = 'Del';\n        case Ins = 'Ins';\n        case Data = 'Data';\n        case Meter = 'Meter';\n        case Progress = 'Progress';\n        case Output = 'Output';\n        case Template = 'Template';\n        case Slot = 'Slot';\n        case Html = 'Html';\n        case Head = 'Head';\n        case Body = 'Body';\n        case Title = 'Title';\n        case Meta = 'Meta';\n        case LinkTag = 'LinkTag';\n        case Style = 'Style';\n        case Script = 'Script';\n        case Base = 'Base';\n        case Custom = 'Custom';\n    }\n\n    enum VisitResult: string\n    {\n        case Continue = 'Continue';\n        case Custom = 'Custom';\n        case Skip = 'Skip';\n        case PreserveHtml = 'PreserveHtml';\n        case Error = 'Error';\n    }\n\n    class HtmlToMarkdownApi\n    {\n        public static function convert(string $html, ?\\HtmlToMarkdown\\ConversionOptions $options = null, ?VisitorHandle $visitor_obj = null): \\HtmlToMarkdown\\ConversionResult\n        {\n            throw new \\RuntimeException('Not implemented.');\n        }\n    }\n\n} // end namespace\n"
  },
  {
    "path": "packages/php/tests/.gitkeep",
    "content": ""
  },
  {
    "path": "packages/python/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2024-2025 Na'aman Hirschfeld\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/python/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with a clean Python API (powered by a Rust core).\nThe same engine also drives the Node.js, Ruby, PHP, and WebAssembly bindings, so rendered Markdown\nstays identical across runtimes. Wheels are published for Linux, macOS, and Windows.\n\n## Installation\n\n```bash\npip install html-to-markdown\n```\n\nRequires Python 3.10+. Wheels are published for Linux, macOS, and Windows on PyPI.\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document            | Size  | Latency | Throughput |\n| ------------------- | ----- | ------- | ---------- |\n| Lists (Timeline)    | 129KB | 0.62ms  | 208 MB/s   |\n| Tables (Countries)  | 360KB | 2.02ms  | 178 MB/s   |\n| Mixed (Python wiki) | 656KB | 4.56ms  | 144 MB/s   |\n\n## Quick Start\n\nBasic conversion:\n\n```python\nfrom html_to_markdown import convert\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = convert(html)\nmarkdown = result.content\n```\n\nWith conversion options:\n\n```python\nfrom html_to_markdown import ConversionOptions, convert\n\nhtml = \"<h1>Hello</h1><p>This is <strong>formatted</strong> content.</p>\"\noptions = ConversionOptions(\n    heading_style=\"atx\",\n    list_indent_width=2,\n)\nresult = convert(html, options)\nmarkdown = result.content\n```\n\n## API Reference\n\n### Core Function\n\n**`convert(html: str, options?: ConversionOptions, visitor?: object) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nresult = convert(html)\nmarkdown = result.content           # Converted Markdown string\nmetadata = result.metadata          # Metadata (when extract_metadata=True)\ntables   = result.tables            # Structured table data\ndocument = result.document          # Document-level info\nimages   = result.images            # Extracted images\nwarnings = result.warnings          # Any conversion warnings\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = convert(html, ConversionOptions(output_format=\"djot\"))\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = convert(html, ConversionOptions(output_format=\"plain\"))\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = convert(html, ConversionOptions(extract_metadata=True))\n\nprint(result.content)                          # Converted Markdown\nprint(result.metadata.document.title)          # Document title\nprint(result.metadata.headers)                 # All h1-h6 elements\nprint(result.metadata.links)                   # All hyperlinks\nprint(result.metadata.images)                  # All images with alt text\nprint(result.metadata.structured_data)         # JSON-LD, Microdata, RDFa\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```python\nfrom html_to_markdown import convert\n\nclass MyVisitor:\n    def visit_link(self, ctx, href, text, title):\n        # Rewrite CDN URLs\n        if href.startswith(\"https://old-cdn.com\"):\n            href = href.replace(\"https://old-cdn.com\", \"https://new-cdn.com\")\n        return {\"type\": \"custom\", \"output\": f\"[{text}]({href})\"}\n\n    def visit_image(self, ctx, src, alt, title):\n        # Skip tracking pixels\n        if \"tracking\" in src:\n            return {\"type\": \"skip\"}\n        return {\"type\": \"continue\"}\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = convert(html, visitor=MyVisitor())\nmarkdown = result.content\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **PyPI:** [pypi.org/project/html-to-markdown](https://pypi.org/project/html-to-markdown/)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/python/html_to_markdown/__init__.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f8985d897e35307da5f2671142df08591e0b52652d9a2e1e85c17fa3429498ca\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Public API for the conversion library.\n\nVersion: 3.4.0-rc.25\n\"\"\"\n\nfrom ._html_to_markdown import (\n    AnnotationKind,\n    CodeBlockStyle,\n    ConversionOptionsBuilder,\n    ConversionResult,\n    DocumentNode,\n    DocumentStructure,\n    GridCell,\n    HeaderMetadata,\n    HeadingStyle,\n    HighlightStyle,\n    ImageMetadata,\n    ImageType,\n    LinkMetadata,\n    LinkStyle,\n    LinkType,\n    ListIndentType,\n    NewlineStyle,\n    NodeContent,\n    NodeContext,\n    NodeType,\n    OutputFormat,\n    PreprocessingPreset,\n    ProcessingWarning,\n    StructuredData,\n    StructuredDataType,\n    TableData,\n    TextAnnotation,\n    TextDirection,\n    VisitorHandle,\n    VisitResult,\n    WarningKind,\n    WhitespaceMode,\n)\nfrom .api import convert\nfrom .exceptions import (\n    ConfigError,\n    ConversionError,\n    InvalidInputError,\n    IoError,\n    OtherError,\n    PanicError,\n    ParseError,\n    SanitizationError,\n)\nfrom .options import (\n    ConversionOptions,\n    DocumentMetadata,\n    HtmlMetadata,\n    PreprocessingOptions,\n    TableGrid,\n)\n\n__all__ = [\n    \"AnnotationKind\",\n    \"CodeBlockStyle\",\n    \"ConfigError\",\n    \"ConversionError\",\n    \"ConversionOptions\",\n    \"ConversionOptionsBuilder\",\n    \"ConversionResult\",\n    \"DocumentMetadata\",\n    \"DocumentNode\",\n    \"DocumentStructure\",\n    \"GridCell\",\n    \"HeaderMetadata\",\n    \"HeadingStyle\",\n    \"HighlightStyle\",\n    \"HtmlMetadata\",\n    \"ImageMetadata\",\n    \"ImageType\",\n    \"InvalidInputError\",\n    \"IoError\",\n    \"LinkMetadata\",\n    \"LinkStyle\",\n    \"LinkType\",\n    \"ListIndentType\",\n    \"NewlineStyle\",\n    \"NodeContent\",\n    \"NodeContext\",\n    \"NodeType\",\n    \"OtherError\",\n    \"OutputFormat\",\n    \"PanicError\",\n    \"ParseError\",\n    \"PreprocessingOptions\",\n    \"PreprocessingPreset\",\n    \"ProcessingWarning\",\n    \"SanitizationError\",\n    \"StructuredData\",\n    \"StructuredDataType\",\n    \"TableData\",\n    \"TableGrid\",\n    \"TextAnnotation\",\n    \"TextDirection\",\n    \"VisitResult\",\n    \"VisitorHandle\",\n    \"WarningKind\",\n    \"WhitespaceMode\",\n    \"convert\",\n]\n\n__version__ = \"3.4.0-rc.25\"\n"
  },
  {
    "path": "packages/python/html_to_markdown/_html_to_markdown.pyi",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:45bc16366a6181aeedb6f03eaa0e5a90e1557b6eea363572b6234bf31b9972ce\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\nfrom typing import Literal, TypeAlias, TypedDict\n\nclass DocumentMetadata:\n    title: str | None\n    description: str | None\n    keywords: list[str]\n    author: str | None\n    canonical_url: str | None\n    base_href: str | None\n    language: str | None\n    text_direction: TextDirection | None\n    open_graph: dict[str, str]\n    twitter_card: dict[str, str]\n    meta_tags: dict[str, str]\n    def __init__(\n        self,\n        title: str | None = None,\n        description: str | None = None,\n        keywords: list[str] | None = None,\n        author: str | None = None,\n        canonical_url: str | None = None,\n        base_href: str | None = None,\n        language: str | None = None,\n        text_direction: TextDirection | str | None = None,\n        open_graph: dict[str, str] | None = None,\n        twitter_card: dict[str, str] | None = None,\n        meta_tags: dict[str, str] | None = None,\n    ) -> None: ...\n\nclass HeaderMetadata:\n    level: int\n    text: str\n    id: str | None\n    depth: int\n    html_offset: int\n    def __init__(\n        self,\n        level: int,\n        text: str,\n        depth: int,\n        html_offset: int,\n        id: str | None = None,  # noqa: A002\n    ) -> None: ...\n    def is_valid(self) -> bool: ...\n\nclass LinkMetadata:\n    href: str\n    text: str\n    title: str | None\n    link_type: LinkType\n    rel: list[str]\n    attributes: dict[str, str]\n    def __init__(\n        self,\n        href: str,\n        text: str,\n        link_type: LinkType | str,\n        rel: list[str],\n        attributes: dict[str, str],\n        title: str | None = None,\n    ) -> None: ...\n    @staticmethod\n    def classify_link(href: str) -> LinkType: ...\n\nclass ImageMetadata:\n    src: str\n    alt: str | None\n    title: str | None\n    dimensions: list[int] | None\n    image_type: ImageType\n    attributes: dict[str, str]\n    def __init__(\n        self,\n        src: str,\n        image_type: ImageType | str,\n        attributes: dict[str, str],\n        alt: str | None = None,\n        title: str | None = None,\n        dimensions: list[int] | None = None,\n    ) -> None: ...\n\nclass StructuredData:\n    data_type: StructuredDataType\n    raw_json: str\n    schema_type: str | None\n    def __init__(\n        self,\n        data_type: StructuredDataType | str,\n        raw_json: str,\n        schema_type: str | None = None,\n    ) -> None: ...\n\nclass HtmlMetadata:\n    document: DocumentMetadata\n    headers: list[HeaderMetadata]\n    links: list[LinkMetadata]\n    images: list[ImageMetadata]\n    structured_data: list[StructuredData]\n    def __init__(\n        self,\n        document: DocumentMetadata | None = None,\n        headers: list[HeaderMetadata] | None = None,\n        links: list[LinkMetadata] | None = None,\n        images: list[ImageMetadata] | None = None,\n        structured_data: list[StructuredData] | None = None,\n    ) -> None: ...\n\nclass ConversionOptions:\n    heading_style: HeadingStyle\n    list_indent_type: ListIndentType\n    list_indent_width: int\n    bullets: str\n    strong_em_symbol: str\n    escape_asterisks: bool\n    escape_underscores: bool\n    escape_misc: bool\n    escape_ascii: bool\n    code_language: str\n    autolinks: bool\n    default_title: bool\n    br_in_tables: bool\n    highlight_style: HighlightStyle\n    extract_metadata: bool\n    whitespace_mode: WhitespaceMode\n    strip_newlines: bool\n    wrap: bool\n    wrap_width: int\n    convert_as_inline: bool\n    sub_symbol: str\n    sup_symbol: str\n    newline_style: NewlineStyle\n    code_block_style: CodeBlockStyle\n    keep_inline_images_in: list[str]\n    preprocessing: PreprocessingOptions\n    encoding: str\n    debug: bool\n    strip_tags: list[str]\n    preserve_tags: list[str]\n    skip_images: bool\n    link_style: LinkStyle\n    output_format: OutputFormat\n    include_document_structure: bool\n    extract_images: bool\n    max_image_size: int\n    capture_svg: bool\n    infer_dimensions: bool\n    max_depth: int | None\n    exclude_selectors: list[str]\n    visitor: object | None\n    def __init__(\n        self,\n        heading_style: HeadingStyle | str | None = None,\n        list_indent_type: ListIndentType | str | None = None,\n        list_indent_width: int | None = None,\n        bullets: str | None = None,\n        strong_em_symbol: str | None = None,\n        escape_asterisks: bool | None = None,\n        escape_underscores: bool | None = None,\n        escape_misc: bool | None = None,\n        escape_ascii: bool | None = None,\n        code_language: str | None = None,\n        autolinks: bool | None = None,\n        default_title: bool | None = None,\n        br_in_tables: bool | None = None,\n        highlight_style: HighlightStyle | str | None = None,\n        extract_metadata: bool | None = None,\n        whitespace_mode: WhitespaceMode | str | None = None,\n        strip_newlines: bool | None = None,\n        wrap: bool | None = None,\n        wrap_width: int | None = None,\n        convert_as_inline: bool | None = None,\n        sub_symbol: str | None = None,\n        sup_symbol: str | None = None,\n        newline_style: NewlineStyle | str | None = None,\n        code_block_style: CodeBlockStyle | str | None = None,\n        keep_inline_images_in: list[str] | None = None,\n        preprocessing: PreprocessingOptions | None = None,\n        encoding: str | None = None,\n        debug: bool | None = None,\n        strip_tags: list[str] | None = None,\n        preserve_tags: list[str] | None = None,\n        skip_images: bool | None = None,\n        link_style: LinkStyle | str | None = None,\n        output_format: OutputFormat | str | None = None,\n        include_document_structure: bool | None = None,\n        extract_images: bool | None = None,\n        max_image_size: int | None = None,\n        capture_svg: bool | None = None,\n        infer_dimensions: bool | None = None,\n        max_depth: int | None = None,\n        exclude_selectors: list[str] | None = None,\n        visitor: object | None = None,\n    ) -> None: ...\n    def apply_update(self, update: ConversionOptionsUpdate) -> None: ...\n    @staticmethod\n    def default() -> ConversionOptions: ...\n    @staticmethod\n    def builder() -> ConversionOptionsBuilder: ...\n    @staticmethod\n    def from_update(update: ConversionOptionsUpdate) -> ConversionOptions: ...\n    @staticmethod\n    def from_(update: ConversionOptionsUpdate) -> ConversionOptions: ...\n\nclass ConversionOptionsUpdate:\n    heading_style: HeadingStyle | None\n    list_indent_type: ListIndentType | None\n    list_indent_width: int | None\n    bullets: str | None\n    strong_em_symbol: str | None\n    escape_asterisks: bool | None\n    escape_underscores: bool | None\n    escape_misc: bool | None\n    escape_ascii: bool | None\n    code_language: str | None\n    autolinks: bool | None\n    default_title: bool | None\n    br_in_tables: bool | None\n    highlight_style: HighlightStyle | None\n    extract_metadata: bool | None\n    whitespace_mode: WhitespaceMode | None\n    strip_newlines: bool | None\n    wrap: bool | None\n    wrap_width: int | None\n    convert_as_inline: bool | None\n    sub_symbol: str | None\n    sup_symbol: str | None\n    newline_style: NewlineStyle | None\n    code_block_style: CodeBlockStyle | None\n    keep_inline_images_in: list[str] | None\n    preprocessing: PreprocessingOptionsUpdate | None\n    encoding: str | None\n    debug: bool | None\n    strip_tags: list[str] | None\n    preserve_tags: list[str] | None\n    skip_images: bool | None\n    link_style: LinkStyle | None\n    output_format: OutputFormat | None\n    include_document_structure: bool | None\n    extract_images: bool | None\n    max_image_size: int | None\n    capture_svg: bool | None\n    infer_dimensions: bool | None\n    max_depth: int | None\n    exclude_selectors: list[str] | None\n    visitor: VisitorHandle | None\n    def __init__(\n        self,\n        heading_style: HeadingStyle | str | None = None,\n        list_indent_type: ListIndentType | str | None = None,\n        list_indent_width: int | None = None,\n        bullets: str | None = None,\n        strong_em_symbol: str | None = None,\n        escape_asterisks: bool | None = None,\n        escape_underscores: bool | None = None,\n        escape_misc: bool | None = None,\n        escape_ascii: bool | None = None,\n        code_language: str | None = None,\n        autolinks: bool | None = None,\n        default_title: bool | None = None,\n        br_in_tables: bool | None = None,\n        highlight_style: HighlightStyle | str | None = None,\n        extract_metadata: bool | None = None,\n        whitespace_mode: WhitespaceMode | str | None = None,\n        strip_newlines: bool | None = None,\n        wrap: bool | None = None,\n        wrap_width: int | None = None,\n        convert_as_inline: bool | None = None,\n        sub_symbol: str | None = None,\n        sup_symbol: str | None = None,\n        newline_style: NewlineStyle | str | None = None,\n        code_block_style: CodeBlockStyle | str | None = None,\n        keep_inline_images_in: list[str] | None = None,\n        preprocessing: PreprocessingOptionsUpdate | None = None,\n        encoding: str | None = None,\n        debug: bool | None = None,\n        strip_tags: list[str] | None = None,\n        preserve_tags: list[str] | None = None,\n        skip_images: bool | None = None,\n        link_style: LinkStyle | str | None = None,\n        output_format: OutputFormat | str | None = None,\n        include_document_structure: bool | None = None,\n        extract_images: bool | None = None,\n        max_image_size: int | None = None,\n        capture_svg: bool | None = None,\n        infer_dimensions: bool | None = None,\n        max_depth: int | None = None,\n        exclude_selectors: list[str] | None = None,\n        visitor: VisitorHandle | None = None,\n    ) -> None: ...\n\nclass PreprocessingOptions:\n    enabled: bool\n    preset: PreprocessingPreset\n    remove_navigation: bool\n    remove_forms: bool\n    def __init__(\n        self,\n        enabled: bool | None = None,\n        preset: PreprocessingPreset | str | None = None,\n        remove_navigation: bool | None = None,\n        remove_forms: bool | None = None,\n    ) -> None: ...\n    def apply_update(self, update: PreprocessingOptionsUpdate) -> None: ...\n    @staticmethod\n    def default() -> PreprocessingOptions: ...\n    @staticmethod\n    def from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions: ...\n    @staticmethod\n    def from_(update: PreprocessingOptionsUpdate) -> PreprocessingOptions: ...\n\nclass PreprocessingOptionsUpdate:\n    enabled: bool | None\n    preset: PreprocessingPreset | None\n    remove_navigation: bool | None\n    remove_forms: bool | None\n    def __init__(\n        self,\n        enabled: bool | None = None,\n        preset: PreprocessingPreset | str | None = None,\n        remove_navigation: bool | None = None,\n        remove_forms: bool | None = None,\n    ) -> None: ...\n\nclass DocumentStructure:\n    nodes: list[DocumentNode]\n    source_format: str | None\n    def __init__(self, nodes: list[DocumentNode], source_format: str | None = None) -> None: ...\n\nclass DocumentNode:\n    id: str\n    content: NodeContent\n    parent: int | None\n    children: list[int]\n    annotations: list[TextAnnotation]\n    attributes: dict[str, str] | None\n    def __init__(\n        self,\n        id: str,  # noqa: A002\n        content: NodeContent,\n        children: list[int],\n        annotations: list[TextAnnotation],\n        parent: int | None = None,\n        attributes: dict[str, str] | None = None,\n    ) -> None: ...\n\nclass TextAnnotation:\n    start: int\n    end: int\n    kind: AnnotationKind\n    def __init__(self, start: int, end: int, kind: AnnotationKind) -> None: ...\n\nclass ConversionResult:\n    content: str | None\n    document: DocumentStructure | None\n    metadata: HtmlMetadata\n    tables: list[TableData]\n    images: list[str]\n    warnings: list[ProcessingWarning]\n    def __init__(\n        self,\n        content: str | None = None,\n        document: DocumentStructure | None = None,\n        metadata: HtmlMetadata | None = None,\n        tables: list[TableData] | None = None,\n        images: list[str] | None = None,\n        warnings: list[ProcessingWarning] | None = None,\n    ) -> None: ...\n\nclass TableGrid:\n    rows: int\n    cols: int\n    cells: list[GridCell]\n    def __init__(\n        self,\n        rows: int | None = None,\n        cols: int | None = None,\n        cells: list[GridCell] | None = None,\n    ) -> None: ...\n\nclass GridCell:\n    content: str\n    row: int\n    col: int\n    row_span: int\n    col_span: int\n    is_header: bool\n    def __init__(\n        self,\n        content: str,\n        row: int,\n        col: int,\n        row_span: int,\n        col_span: int,\n        is_header: bool,\n    ) -> None: ...\n\nclass TableData:\n    grid: TableGrid\n    markdown: str\n    def __init__(self, grid: TableGrid, markdown: str) -> None: ...\n\nclass ProcessingWarning:\n    message: str\n    kind: WarningKind\n    def __init__(self, message: str, kind: WarningKind | str) -> None: ...\n\nclass NodeContext:\n    node_type: NodeType\n    tag_name: str\n    attributes: dict[str, str]\n    depth: int\n    index_in_parent: int\n    parent_tag: str | None\n    is_inline: bool\n    def __init__(\n        self,\n        node_type: NodeType | str,\n        tag_name: str,\n        attributes: dict[str, str],\n        depth: int,\n        index_in_parent: int,\n        is_inline: bool,\n        parent_tag: str | None = None,\n    ) -> None: ...\n\nclass ConversionOptionsBuilder:\n    def strip_tags(self, tags: list[str]) -> ConversionOptionsBuilder: ...\n    def preserve_tags(self, tags: list[str]) -> ConversionOptionsBuilder: ...\n    def keep_inline_images_in(self, tags: list[str]) -> ConversionOptionsBuilder: ...\n    def exclude_selectors(self, selectors: list[str]) -> ConversionOptionsBuilder: ...\n    def visitor(self, visitor: VisitorHandle | None = None) -> ConversionOptionsBuilder: ...\n    def preprocessing(self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder: ...\n    def build(self) -> ConversionOptions: ...\n\nclass VisitorHandle: ...\n\nclass TextDirection:\n    LeftToRight: TextDirection = ...\n    RightToLeft: TextDirection = ...\n    Auto: TextDirection = ...\n    LEFT_TO_RIGHT: TextDirection = ...\n    RIGHT_TO_LEFT: TextDirection = ...\n    AUTO: TextDirection = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass LinkType:\n    Anchor: LinkType = ...\n    Internal: LinkType = ...\n    External: LinkType = ...\n    Email: LinkType = ...\n    Phone: LinkType = ...\n    Other: LinkType = ...\n    ANCHOR: LinkType = ...\n    INTERNAL: LinkType = ...\n    EXTERNAL: LinkType = ...\n    EMAIL: LinkType = ...\n    PHONE: LinkType = ...\n    OTHER: LinkType = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass ImageType:\n    DataUri: ImageType = ...\n    InlineSvg: ImageType = ...\n    External: ImageType = ...\n    Relative: ImageType = ...\n    DATA_URI: ImageType = ...\n    INLINE_SVG: ImageType = ...\n    EXTERNAL: ImageType = ...\n    RELATIVE: ImageType = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass StructuredDataType:\n    JsonLd: StructuredDataType = ...\n    Microdata: StructuredDataType = ...\n    RDFa: StructuredDataType = ...\n    JSON_LD: StructuredDataType = ...\n    MICRODATA: StructuredDataType = ...\n    RD_FA: StructuredDataType = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass PreprocessingPreset:\n    Minimal: PreprocessingPreset = ...\n    Standard: PreprocessingPreset = ...\n    Aggressive: PreprocessingPreset = ...\n    MINIMAL: PreprocessingPreset = ...\n    STANDARD: PreprocessingPreset = ...\n    AGGRESSIVE: PreprocessingPreset = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass HeadingStyle:\n    Underlined: HeadingStyle = ...\n    Atx: HeadingStyle = ...\n    AtxClosed: HeadingStyle = ...\n    UNDERLINED: HeadingStyle = ...\n    ATX: HeadingStyle = ...\n    ATX_CLOSED: HeadingStyle = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass ListIndentType:\n    Spaces: ListIndentType = ...\n    Tabs: ListIndentType = ...\n    SPACES: ListIndentType = ...\n    TABS: ListIndentType = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass WhitespaceMode:\n    Normalized: WhitespaceMode = ...\n    Strict: WhitespaceMode = ...\n    NORMALIZED: WhitespaceMode = ...\n    STRICT: WhitespaceMode = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass NewlineStyle:\n    Spaces: NewlineStyle = ...\n    Backslash: NewlineStyle = ...\n    SPACES: NewlineStyle = ...\n    BACKSLASH: NewlineStyle = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass CodeBlockStyle:\n    Indented: CodeBlockStyle = ...\n    Backticks: CodeBlockStyle = ...\n    Tildes: CodeBlockStyle = ...\n    INDENTED: CodeBlockStyle = ...\n    BACKTICKS: CodeBlockStyle = ...\n    TILDES: CodeBlockStyle = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass HighlightStyle:\n    DoubleEqual: HighlightStyle = ...\n    Html: HighlightStyle = ...\n    Bold: HighlightStyle = ...\n    None_: HighlightStyle = ...\n    DOUBLE_EQUAL: HighlightStyle = ...\n    HTML: HighlightStyle = ...\n    BOLD: HighlightStyle = ...\n    NONE: HighlightStyle = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass LinkStyle:\n    Inline: LinkStyle = ...\n    Reference: LinkStyle = ...\n    INLINE: LinkStyle = ...\n    REFERENCE: LinkStyle = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass OutputFormat:\n    Markdown: OutputFormat = ...\n    Djot: OutputFormat = ...\n    Plain: OutputFormat = ...\n    MARKDOWN: OutputFormat = ...\n    DJOT: OutputFormat = ...\n    PLAIN: OutputFormat = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass NodeContentHeadingVariant(TypedDict):\n    node_type: Literal[\"Heading\"]\n    level: int\n    text: str\n\nclass NodeContentParagraphVariant(TypedDict):\n    node_type: Literal[\"Paragraph\"]\n    text: str\n\nclass NodeContentListVariant(TypedDict):\n    node_type: Literal[\"List\"]\n    ordered: bool\n\nclass NodeContentListItemVariant(TypedDict):\n    node_type: Literal[\"ListItem\"]\n    text: str\n\nclass NodeContentTableVariant(TypedDict):\n    node_type: Literal[\"Table\"]\n    grid: TableGrid\n\nclass NodeContentImageVariant(TypedDict):\n    node_type: Literal[\"Image\"]\n    description: str | None\n    src: str | None\n    image_index: int | None\n\nclass NodeContentCodeVariant(TypedDict):\n    node_type: Literal[\"Code\"]\n    text: str\n    language: str | None\n\nclass NodeContentQuoteVariant(TypedDict):\n    node_type: Literal[\"Quote\"]\n\nclass NodeContentDefinitionListVariant(TypedDict):\n    node_type: Literal[\"DefinitionList\"]\n\nclass NodeContentDefinitionItemVariant(TypedDict):\n    node_type: Literal[\"DefinitionItem\"]\n    term: str\n    definition: str\n\nclass NodeContentRawBlockVariant(TypedDict):\n    node_type: Literal[\"RawBlock\"]\n    format: str\n    content: str\n\nclass NodeContentMetadataBlockVariant(TypedDict):\n    node_type: Literal[\"MetadataBlock\"]\n    entries: list[str]\n\nclass NodeContentGroupVariant(TypedDict):\n    node_type: Literal[\"Group\"]\n    label: str | None\n    heading_level: int | None\n    heading_text: str | None\n\nNodeContent: TypeAlias = (\n    NodeContentHeadingVariant\n    | NodeContentParagraphVariant\n    | NodeContentListVariant\n    | NodeContentListItemVariant\n    | NodeContentTableVariant\n    | NodeContentImageVariant\n    | NodeContentCodeVariant\n    | NodeContentQuoteVariant\n    | NodeContentDefinitionListVariant\n    | NodeContentDefinitionItemVariant\n    | NodeContentRawBlockVariant\n    | NodeContentMetadataBlockVariant\n    | NodeContentGroupVariant\n)\n\nclass AnnotationKindBoldVariant(TypedDict):\n    annotation_type: Literal[\"Bold\"]\n\nclass AnnotationKindItalicVariant(TypedDict):\n    annotation_type: Literal[\"Italic\"]\n\nclass AnnotationKindUnderlineVariant(TypedDict):\n    annotation_type: Literal[\"Underline\"]\n\nclass AnnotationKindStrikethroughVariant(TypedDict):\n    annotation_type: Literal[\"Strikethrough\"]\n\nclass AnnotationKindCodeVariant(TypedDict):\n    annotation_type: Literal[\"Code\"]\n\nclass AnnotationKindSubscriptVariant(TypedDict):\n    annotation_type: Literal[\"Subscript\"]\n\nclass AnnotationKindSuperscriptVariant(TypedDict):\n    annotation_type: Literal[\"Superscript\"]\n\nclass AnnotationKindHighlightVariant(TypedDict):\n    annotation_type: Literal[\"Highlight\"]\n\nclass AnnotationKindLinkVariant(TypedDict):\n    annotation_type: Literal[\"Link\"]\n    url: str\n    title: str | None\n\nAnnotationKind: TypeAlias = (\n    AnnotationKindBoldVariant\n    | AnnotationKindItalicVariant\n    | AnnotationKindUnderlineVariant\n    | AnnotationKindStrikethroughVariant\n    | AnnotationKindCodeVariant\n    | AnnotationKindSubscriptVariant\n    | AnnotationKindSuperscriptVariant\n    | AnnotationKindHighlightVariant\n    | AnnotationKindLinkVariant\n)\n\nclass WarningKind:\n    ImageExtractionFailed: WarningKind = ...\n    EncodingFallback: WarningKind = ...\n    TruncatedInput: WarningKind = ...\n    MalformedHtml: WarningKind = ...\n    SanitizationApplied: WarningKind = ...\n    DepthLimitExceeded: WarningKind = ...\n    IMAGE_EXTRACTION_FAILED: WarningKind = ...\n    ENCODING_FALLBACK: WarningKind = ...\n    TRUNCATED_INPUT: WarningKind = ...\n    MALFORMED_HTML: WarningKind = ...\n    SANITIZATION_APPLIED: WarningKind = ...\n    DEPTH_LIMIT_EXCEEDED: WarningKind = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass NodeType:\n    Text: NodeType = ...\n    Element: NodeType = ...\n    Heading: NodeType = ...\n    Paragraph: NodeType = ...\n    Div: NodeType = ...\n    Blockquote: NodeType = ...\n    Pre: NodeType = ...\n    Hr: NodeType = ...\n    List: NodeType = ...\n    ListItem: NodeType = ...\n    DefinitionList: NodeType = ...\n    DefinitionTerm: NodeType = ...\n    DefinitionDescription: NodeType = ...\n    Table: NodeType = ...\n    TableRow: NodeType = ...\n    TableCell: NodeType = ...\n    TableHeader: NodeType = ...\n    TableBody: NodeType = ...\n    TableHead: NodeType = ...\n    TableFoot: NodeType = ...\n    Link: NodeType = ...\n    Image: NodeType = ...\n    Strong: NodeType = ...\n    Em: NodeType = ...\n    Code: NodeType = ...\n    Strikethrough: NodeType = ...\n    Underline: NodeType = ...\n    Subscript: NodeType = ...\n    Superscript: NodeType = ...\n    Mark: NodeType = ...\n    Small: NodeType = ...\n    Br: NodeType = ...\n    Span: NodeType = ...\n    Article: NodeType = ...\n    Section: NodeType = ...\n    Nav: NodeType = ...\n    Aside: NodeType = ...\n    Header: NodeType = ...\n    Footer: NodeType = ...\n    Main: NodeType = ...\n    Figure: NodeType = ...\n    Figcaption: NodeType = ...\n    Time: NodeType = ...\n    Details: NodeType = ...\n    Summary: NodeType = ...\n    Form: NodeType = ...\n    Input: NodeType = ...\n    Select: NodeType = ...\n    Option: NodeType = ...\n    Button: NodeType = ...\n    Textarea: NodeType = ...\n    Label: NodeType = ...\n    Fieldset: NodeType = ...\n    Legend: NodeType = ...\n    Audio: NodeType = ...\n    Video: NodeType = ...\n    Picture: NodeType = ...\n    Source: NodeType = ...\n    Iframe: NodeType = ...\n    Svg: NodeType = ...\n    Canvas: NodeType = ...\n    Ruby: NodeType = ...\n    Rt: NodeType = ...\n    Rp: NodeType = ...\n    Abbr: NodeType = ...\n    Kbd: NodeType = ...\n    Samp: NodeType = ...\n    Var: NodeType = ...\n    Cite: NodeType = ...\n    Q: NodeType = ...\n    Del: NodeType = ...\n    Ins: NodeType = ...\n    Data: NodeType = ...\n    Meter: NodeType = ...\n    Progress: NodeType = ...\n    Output: NodeType = ...\n    Template: NodeType = ...\n    Slot: NodeType = ...\n    Html: NodeType = ...\n    Head: NodeType = ...\n    Body: NodeType = ...\n    Title: NodeType = ...\n    Meta: NodeType = ...\n    LinkTag: NodeType = ...\n    Style: NodeType = ...\n    Script: NodeType = ...\n    Base: NodeType = ...\n    Custom: NodeType = ...\n    TEXT: NodeType = ...\n    ELEMENT: NodeType = ...\n    HEADING: NodeType = ...\n    PARAGRAPH: NodeType = ...\n    DIV: NodeType = ...\n    BLOCKQUOTE: NodeType = ...\n    PRE: NodeType = ...\n    HR: NodeType = ...\n    LIST: NodeType = ...\n    LIST_ITEM: NodeType = ...\n    DEFINITION_LIST: NodeType = ...\n    DEFINITION_TERM: NodeType = ...\n    DEFINITION_DESCRIPTION: NodeType = ...\n    TABLE: NodeType = ...\n    TABLE_ROW: NodeType = ...\n    TABLE_CELL: NodeType = ...\n    TABLE_HEADER: NodeType = ...\n    TABLE_BODY: NodeType = ...\n    TABLE_HEAD: NodeType = ...\n    TABLE_FOOT: NodeType = ...\n    LINK: NodeType = ...\n    IMAGE: NodeType = ...\n    STRONG: NodeType = ...\n    EM: NodeType = ...\n    CODE: NodeType = ...\n    STRIKETHROUGH: NodeType = ...\n    UNDERLINE: NodeType = ...\n    SUBSCRIPT: NodeType = ...\n    SUPERSCRIPT: NodeType = ...\n    MARK: NodeType = ...\n    SMALL: NodeType = ...\n    BR: NodeType = ...\n    SPAN: NodeType = ...\n    ARTICLE: NodeType = ...\n    SECTION: NodeType = ...\n    NAV: NodeType = ...\n    ASIDE: NodeType = ...\n    HEADER: NodeType = ...\n    FOOTER: NodeType = ...\n    MAIN: NodeType = ...\n    FIGURE: NodeType = ...\n    FIGCAPTION: NodeType = ...\n    TIME: NodeType = ...\n    DETAILS: NodeType = ...\n    SUMMARY: NodeType = ...\n    FORM: NodeType = ...\n    INPUT: NodeType = ...\n    SELECT: NodeType = ...\n    OPTION: NodeType = ...\n    BUTTON: NodeType = ...\n    TEXTAREA: NodeType = ...\n    LABEL: NodeType = ...\n    FIELDSET: NodeType = ...\n    LEGEND: NodeType = ...\n    AUDIO: NodeType = ...\n    VIDEO: NodeType = ...\n    PICTURE: NodeType = ...\n    SOURCE: NodeType = ...\n    IFRAME: NodeType = ...\n    SVG: NodeType = ...\n    CANVAS: NodeType = ...\n    RUBY: NodeType = ...\n    RT: NodeType = ...\n    RP: NodeType = ...\n    ABBR: NodeType = ...\n    KBD: NodeType = ...\n    SAMP: NodeType = ...\n    VAR: NodeType = ...\n    CITE: NodeType = ...\n    DEL: NodeType = ...\n    INS: NodeType = ...\n    DATA: NodeType = ...\n    METER: NodeType = ...\n    PROGRESS: NodeType = ...\n    OUTPUT: NodeType = ...\n    TEMPLATE: NodeType = ...\n    SLOT: NodeType = ...\n    HTML: NodeType = ...\n    HEAD: NodeType = ...\n    BODY: NodeType = ...\n    TITLE: NodeType = ...\n    META: NodeType = ...\n    LINK_TAG: NodeType = ...\n    STYLE: NodeType = ...\n    SCRIPT: NodeType = ...\n    BASE: NodeType = ...\n    CUSTOM: NodeType = ...\n    def __init__(self, value: int | str) -> None: ...\n\nclass VisitResultContinueVariant(TypedDict):\n    type: Literal[\"Continue\"]\n\nclass VisitResultCustomVariant(TypedDict):\n    type: Literal[\"Custom\"]\n    _0: str\n\nclass VisitResultSkipVariant(TypedDict):\n    type: Literal[\"Skip\"]\n\nclass VisitResultPreserveHtmlVariant(TypedDict):\n    type: Literal[\"PreserveHtml\"]\n\nclass VisitResultErrorVariant(TypedDict):\n    type: Literal[\"Error\"]\n    _0: str\n\nVisitResult: TypeAlias = (\n    VisitResultContinueVariant\n    | VisitResultCustomVariant\n    | VisitResultSkipVariant\n    | VisitResultPreserveHtmlVariant\n    | VisitResultErrorVariant\n)\n\ndef convert(html: str, options: ConversionOptions | None = None) -> ConversionResult: ...\n"
  },
  {
    "path": "packages/python/html_to_markdown/api.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:31497f7c3a0af66cea8ae5aaf0f59ded0bfa440183e942347c495e4760fde985\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Public API for conversion.\"\"\"\n\nimport html_to_markdown._html_to_markdown as _rust\n\nfrom ._html_to_markdown import ConversionOptions, ConversionResult, PreprocessingOptions\n\n\ndef _to_rust_preprocessing_options(\n    value: PreprocessingOptions | None,\n) -> _rust.PreprocessingOptions | None:\n    \"\"\"Convert Python PreprocessingOptions to Rust binding type.\"\"\"\n    if value is None:\n        return None\n    return _rust.PreprocessingOptions(\n        enabled=value.enabled,\n        preset=value.preset,\n        remove_navigation=value.remove_navigation,\n        remove_forms=value.remove_forms,\n    )\n\n\ndef _to_rust_conversion_options(value: ConversionOptions | None) -> _rust.ConversionOptions | None:\n    \"\"\"Convert Python ConversionOptions to Rust binding type.\"\"\"\n    if value is None:\n        return None\n    return _rust.ConversionOptions(\n        heading_style=value.heading_style,\n        list_indent_type=value.list_indent_type,\n        list_indent_width=value.list_indent_width,\n        bullets=value.bullets,\n        strong_em_symbol=value.strong_em_symbol,\n        escape_asterisks=value.escape_asterisks,\n        escape_underscores=value.escape_underscores,\n        escape_misc=value.escape_misc,\n        escape_ascii=value.escape_ascii,\n        code_language=value.code_language,\n        autolinks=value.autolinks,\n        default_title=value.default_title,\n        br_in_tables=value.br_in_tables,\n        highlight_style=value.highlight_style,\n        extract_metadata=value.extract_metadata,\n        whitespace_mode=value.whitespace_mode,\n        strip_newlines=value.strip_newlines,\n        wrap=value.wrap,\n        wrap_width=value.wrap_width,\n        convert_as_inline=value.convert_as_inline,\n        sub_symbol=value.sub_symbol,\n        sup_symbol=value.sup_symbol,\n        newline_style=value.newline_style,\n        code_block_style=value.code_block_style,\n        keep_inline_images_in=value.keep_inline_images_in,\n        preprocessing=_to_rust_preprocessing_options(value.preprocessing),\n        encoding=value.encoding,\n        debug=value.debug,\n        strip_tags=value.strip_tags,\n        preserve_tags=value.preserve_tags,\n        skip_images=value.skip_images,\n        link_style=value.link_style,\n        output_format=value.output_format,\n        include_document_structure=value.include_document_structure,\n        extract_images=value.extract_images,\n        max_image_size=value.max_image_size,\n        capture_svg=value.capture_svg,\n        infer_dimensions=value.infer_dimensions,\n        max_depth=value.max_depth,\n        exclude_selectors=value.exclude_selectors,\n        visitor=value.visitor,\n    )\n\n\ndef convert(html: str, options: ConversionOptions | None = None) -> ConversionResult:\n    \"\"\"Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images.\"\"\"\n    _rust_options = _to_rust_conversion_options(options) if options is not None else None\n    return _rust.convert(html=html, options=_rust_options)\n"
  },
  {
    "path": "packages/python/html_to_markdown/exceptions.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:05a399678a46f30b64df851fb64ab5bcca6b1802f66c2dcb9ed587050b77774d\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Exception hierarchy.\"\"\"\n\n\nclass ConversionError(Exception):\n    \"\"\"Errors that can occur during HTML to Markdown conversion.\"\"\"\n\n\nclass ParseError(ConversionError):\n    \"\"\"HTML parsing error.\"\"\"\n\n\nclass SanitizationError(ConversionError):\n    \"\"\"HTML sanitization error.\"\"\"\n\n\nclass ConfigError(ConversionError):\n    \"\"\"Invalid configuration.\"\"\"\n\n\nclass IoError(ConversionError):\n    \"\"\"I/O error.\"\"\"\n\n\nclass PanicError(ConversionError):\n    \"\"\"Panic caught during conversion to prevent unwinding across FFI boundaries.\"\"\"\n\n\nclass InvalidInputError(ConversionError):\n    \"\"\"Invalid input data.\"\"\"\n\n\nclass OtherError(ConversionError):\n    \"\"\"Generic conversion error.\"\"\"\n"
  },
  {
    "path": "packages/python/html_to_markdown/options.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:1ed047d62758b0ade8624cc8456ad11808d6de5c98a99c243434d7043fecd98a\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Configuration options for the conversion API.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom typing import TYPE_CHECKING\n\nfrom ._html_to_markdown import (\n    CodeBlockStyle,\n    HeadingStyle,\n    HighlightStyle,\n    LinkStyle,\n    ListIndentType,\n    NewlineStyle,\n    OutputFormat,\n    PreprocessingPreset,\n    TextDirection,\n    WhitespaceMode,\n)\n\nif TYPE_CHECKING:\n    from ._html_to_markdown import (\n        GridCell,\n        HeaderMetadata,\n        ImageMetadata,\n        LinkMetadata,\n        StructuredData,\n        VisitorHandle,\n    )\n\n\nCodeBlockStyle.INDENTED = CodeBlockStyle.Indented\nCodeBlockStyle.BACKTICKS = CodeBlockStyle.Backticks\nCodeBlockStyle.TILDES = CodeBlockStyle.Tildes\n\nHeadingStyle.UNDERLINED = HeadingStyle.Underlined\nHeadingStyle.ATX = HeadingStyle.Atx\nHeadingStyle.ATX_CLOSED = HeadingStyle.AtxClosed\n\nHighlightStyle.DOUBLE_EQUAL = HighlightStyle.DoubleEqual\nHighlightStyle.HTML = HighlightStyle.Html\nHighlightStyle.BOLD = HighlightStyle.Bold\nHighlightStyle.NONE = HighlightStyle.None_\n\nLinkStyle.INLINE = LinkStyle.Inline\nLinkStyle.REFERENCE = LinkStyle.Reference\n\nListIndentType.SPACES = ListIndentType.Spaces\nListIndentType.TABS = ListIndentType.Tabs\n\nNewlineStyle.SPACES = NewlineStyle.Spaces\nNewlineStyle.BACKSLASH = NewlineStyle.Backslash\n\nOutputFormat.MARKDOWN = OutputFormat.Markdown\nOutputFormat.DJOT = OutputFormat.Djot\nOutputFormat.PLAIN = OutputFormat.Plain\n\nPreprocessingPreset.MINIMAL = PreprocessingPreset.Minimal\nPreprocessingPreset.STANDARD = PreprocessingPreset.Standard\nPreprocessingPreset.AGGRESSIVE = PreprocessingPreset.Aggressive\n\nTextDirection.LEFT_TO_RIGHT = TextDirection.LeftToRight\nTextDirection.RIGHT_TO_LEFT = TextDirection.RightToLeft\nTextDirection.AUTO = TextDirection.Auto\n\nWhitespaceMode.NORMALIZED = WhitespaceMode.Normalized\nWhitespaceMode.STRICT = WhitespaceMode.Strict\n\n\nclass LinkType(str, Enum):\n    \"\"\"Link classification based on href value and document context.\"\"\"\n\n    ANCHOR = \"anchor\"\n    INTERNAL = \"internal\"\n    EXTERNAL = \"external\"\n    EMAIL = \"email\"\n    PHONE = \"phone\"\n    OTHER = \"other\"\n\n\nclass ImageType(str, Enum):\n    \"\"\"Image source classification for proper handling and processing.\"\"\"\n\n    DATA_URI = \"data_uri\"\n    INLINE_SVG = \"inline_svg\"\n    EXTERNAL = \"external\"\n    RELATIVE = \"relative\"\n\n\nclass StructuredDataType(str, Enum):\n    \"\"\"Structured data format type.\"\"\"\n\n    JSON_LD = \"json_ld\"\n    MICRODATA = \"microdata\"\n    RD_FA = \"rd_fa\"\n\n\nclass WarningKind(str, Enum):\n    \"\"\"Categories of processing warnings.\"\"\"\n\n    IMAGE_EXTRACTION_FAILED = \"image_extraction_failed\"\n    ENCODING_FALLBACK = \"encoding_fallback\"\n    TRUNCATED_INPUT = \"truncated_input\"\n    MALFORMED_HTML = \"malformed_html\"\n    SANITIZATION_APPLIED = \"sanitization_applied\"\n    DEPTH_LIMIT_EXCEEDED = \"depth_limit_exceeded\"\n\n\nclass NodeType(str, Enum):\n    \"\"\"Node type enumeration covering all HTML element types.\"\"\"\n\n    TEXT = \"text\"\n    ELEMENT = \"element\"\n    HEADING = \"heading\"\n    PARAGRAPH = \"paragraph\"\n    DIV = \"div\"\n    BLOCKQUOTE = \"blockquote\"\n    PRE = \"pre\"\n    HR = \"hr\"\n    LIST = \"list\"\n    LIST_ITEM = \"list_item\"\n    DEFINITION_LIST = \"definition_list\"\n    DEFINITION_TERM = \"definition_term\"\n    DEFINITION_DESCRIPTION = \"definition_description\"\n    TABLE = \"table\"\n    TABLE_ROW = \"table_row\"\n    TABLE_CELL = \"table_cell\"\n    TABLE_HEADER = \"table_header\"\n    TABLE_BODY = \"table_body\"\n    TABLE_HEAD = \"table_head\"\n    TABLE_FOOT = \"table_foot\"\n    LINK = \"link\"\n    IMAGE = \"image\"\n    STRONG = \"strong\"\n    EM = \"em\"\n    CODE = \"code\"\n    STRIKETHROUGH = \"strikethrough\"\n    UNDERLINE = \"underline\"\n    SUBSCRIPT = \"subscript\"\n    SUPERSCRIPT = \"superscript\"\n    MARK = \"mark\"\n    SMALL = \"small\"\n    BR = \"br\"\n    SPAN = \"span\"\n    ARTICLE = \"article\"\n    SECTION = \"section\"\n    NAV = \"nav\"\n    ASIDE = \"aside\"\n    HEADER = \"header\"\n    FOOTER = \"footer\"\n    MAIN = \"main\"\n    FIGURE = \"figure\"\n    FIGCAPTION = \"figcaption\"\n    TIME = \"time\"\n    DETAILS = \"details\"\n    SUMMARY = \"summary\"\n    FORM = \"form\"\n    INPUT = \"input\"\n    SELECT = \"select\"\n    OPTION = \"option\"\n    BUTTON = \"button\"\n    TEXTAREA = \"textarea\"\n    LABEL = \"label\"\n    FIELDSET = \"fieldset\"\n    LEGEND = \"legend\"\n    AUDIO = \"audio\"\n    VIDEO = \"video\"\n    PICTURE = \"picture\"\n    SOURCE = \"source\"\n    IFRAME = \"iframe\"\n    SVG = \"svg\"\n    CANVAS = \"canvas\"\n    RUBY = \"ruby\"\n    RT = \"rt\"\n    RP = \"rp\"\n    ABBR = \"abbr\"\n    KBD = \"kbd\"\n    SAMP = \"samp\"\n    VAR = \"var\"\n    CITE = \"cite\"\n    Q = \"q\"\n    DEL = \"del\"\n    INS = \"ins\"\n    DATA = \"data\"\n    METER = \"meter\"\n    PROGRESS = \"progress\"\n    OUTPUT = \"output\"\n    TEMPLATE = \"template\"\n    SLOT = \"slot\"\n    HTML = \"html\"\n    HEAD = \"head\"\n    BODY = \"body\"\n    TITLE = \"title\"\n    META = \"meta\"\n    LINK_TAG = \"link_tag\"\n    STYLE = \"style\"\n    SCRIPT = \"script\"\n    BASE = \"base\"\n    CUSTOM = \"custom\"\n\n\n@dataclass\nclass DocumentMetadata:\n    \"\"\"Document-level metadata extracted from `<head>` and top-level elements.\"\"\"\n\n    title: str | None = None\n    \"\"\"Document title from `<title>` tag\"\"\"\n\n    description: str | None = None\n    \"\"\"Document description from `<meta name=\"description\">` tag\"\"\"\n\n    keywords: list[str] = field(default_factory=list)\n    \"\"\"Document keywords from `<meta name=\"keywords\">` tag, split on commas\"\"\"\n\n    author: str | None = None\n    \"\"\"Document author from `<meta name=\"author\">` tag\"\"\"\n\n    canonical_url: str | None = None\n    \"\"\"Canonical URL from `<link rel=\"canonical\">` tag\"\"\"\n\n    base_href: str | None = None\n    \"\"\"Base URL from `<base href=\"\">` tag for resolving relative URLs\"\"\"\n\n    language: str | None = None\n    \"\"\"Document language from `lang` attribute\"\"\"\n\n    text_direction: TextDirection | str | None = None\n    \"\"\"Document text direction from `dir` attribute\"\"\"\n\n    open_graph: dict[str, str] = field(default_factory=dict)\n    \"\"\"Open Graph metadata (og:* properties) for social media\"\"\"\n\n    twitter_card: dict[str, str] = field(default_factory=dict)\n    \"\"\"Twitter Card metadata (twitter:* properties)\"\"\"\n\n    meta_tags: dict[str, str] = field(default_factory=dict)\n    \"\"\"Additional meta tags not covered by specific fields\"\"\"\n\n\n@dataclass\nclass HtmlMetadata:\n    \"\"\"Comprehensive metadata extraction result from HTML document.\"\"\"\n\n    document: DocumentMetadata | None = None\n    \"\"\"Document-level metadata (title, description, canonical, etc.)\"\"\"\n\n    headers: list[HeaderMetadata] = field(default_factory=list)\n    \"\"\"Extracted header elements with hierarchy\"\"\"\n\n    links: list[LinkMetadata] = field(default_factory=list)\n    \"\"\"Extracted hyperlinks with type classification\"\"\"\n\n    images: list[ImageMetadata] = field(default_factory=list)\n    \"\"\"Extracted images with source and dimensions\"\"\"\n\n    structured_data: list[StructuredData] = field(default_factory=list)\n    \"\"\"Extracted structured data blocks\"\"\"\n\n\n@dataclass\nclass ConversionOptions:\n    \"\"\"Main conversion options for HTML to Markdown conversion.\"\"\"\n\n    heading_style: HeadingStyle | str = \"atx\"\n    \"\"\"Heading style to use in Markdown output (ATX `#` or Setext underline).\"\"\"\n\n    list_indent_type: ListIndentType | str = \"spaces\"\n    \"\"\"How to indent nested list items (spaces or tab).\"\"\"\n\n    list_indent_width: int = 2\n    \"\"\"Number of spaces (or tabs) to use for each level of list indentation.\"\"\"\n\n    bullets: str = \"-*+\"\n    \"\"\"Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\"\"\"\n\n    strong_em_symbol: str = \"*\"\n    \"\"\"Character used for bold/italic emphasis markers (`*` or `_`).\"\"\"\n\n    escape_asterisks: bool = False\n    \"\"\"Escape `*` characters in plain text to avoid unintended bold/italic.\"\"\"\n\n    escape_underscores: bool = False\n    \"\"\"Escape `_` characters in plain text to avoid unintended bold/italic.\"\"\"\n\n    escape_misc: bool = False\n    \"\"\"Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\"\"\"\n\n    escape_ascii: bool = False\n    \"\"\"Escape ASCII characters that have special meaning in certain Markdown dialects.\"\"\"\n\n    code_language: str = \"\"\n    \"\"\"Default language annotation for fenced code blocks that have no language hint.\"\"\"\n\n    autolinks: bool = True\n    \"\"\"Automatically convert bare URLs into Markdown autolinks.\"\"\"\n\n    default_title: bool = False\n    \"\"\"Emit a default title when no `<title>` tag is present.\"\"\"\n\n    br_in_tables: bool = False\n    \"\"\"Render `<br>` elements inside table cells as literal line breaks.\"\"\"\n\n    highlight_style: HighlightStyle | str = \"double_equal\"\n    \"\"\"Style used for `<mark>` / highlighted text (e.g. `==text==`).\"\"\"\n\n    extract_metadata: bool = True\n    \"\"\"Extract `<meta>` and `<head>` information into the result metadata.\"\"\"\n\n    whitespace_mode: WhitespaceMode | str = \"normalized\"\n    \"\"\"Controls how whitespace is normalised during conversion.\"\"\"\n\n    strip_newlines: bool = False\n    \"\"\"Strip all newlines from the output, producing a single-line result.\"\"\"\n\n    wrap: bool = False\n    \"\"\"Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\"\"\"\n\n    wrap_width: int = 80\n    \"\"\"Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\"\"\"\n\n    convert_as_inline: bool = False\n    \"\"\"Treat the entire document as inline content (no block-level wrappers).\"\"\"\n\n    sub_symbol: str = \"\"\n    \"\"\"Markdown notation for subscript text (e.g. `\"~\"`).\"\"\"\n\n    sup_symbol: str = \"\"\n    \"\"\"Markdown notation for superscript text (e.g. `\"^\"`).\"\"\"\n\n    newline_style: NewlineStyle | str = \"spaces\"\n    \"\"\"How to encode hard line breaks (`<br>`) in Markdown.\"\"\"\n\n    code_block_style: CodeBlockStyle | str = \"backticks\"\n    \"\"\"Style used for fenced code blocks (backticks or tilde).\"\"\"\n\n    keep_inline_images_in: list[str] = field(default_factory=list)\n    \"\"\"HTML tag names whose `<img>` children are kept inline instead of block.\"\"\"\n\n    preprocessing: PreprocessingOptions | None = None\n    \"\"\"Pre-processing options applied to the HTML before conversion.\"\"\"\n\n    encoding: str = \"utf-8\"\n    \"\"\"Expected character encoding of the input HTML (default `\"utf-8\"`).\"\"\"\n\n    debug: bool = False\n    \"\"\"Emit debug information during conversion.\"\"\"\n\n    strip_tags: list[str] = field(default_factory=list)\n    \"\"\"HTML tag names whose content is stripped from the output entirely.\"\"\"\n\n    preserve_tags: list[str] = field(default_factory=list)\n    \"\"\"HTML tag names that are preserved verbatim in the output.\"\"\"\n\n    skip_images: bool = False\n    \"\"\"Skip conversion of `<img>` elements (omit images from output).\"\"\"\n\n    link_style: LinkStyle | str = \"inline\"\n    \"\"\"Link rendering style (inline or reference).\"\"\"\n\n    output_format: OutputFormat | str = \"markdown\"\n    \"\"\"Target output format (Markdown, plain text, etc.).\"\"\"\n\n    include_document_structure: bool = False\n    \"\"\"Include structured document tree in result.\"\"\"\n\n    extract_images: bool = False\n    \"\"\"Extract inline images from data URIs and SVGs.\"\"\"\n\n    max_image_size: int = 5242880\n    \"\"\"Maximum decoded image size in bytes (default 5MB).\"\"\"\n\n    capture_svg: bool = False\n    \"\"\"Capture SVG elements as images.\"\"\"\n\n    infer_dimensions: bool = True\n    \"\"\"Infer image dimensions from data.\"\"\"\n\n    max_depth: int | None = None\n    \"\"\"Maximum DOM traversal depth. `None` means unlimited.\"\"\"\n\n    exclude_selectors: list[str] = field(default_factory=list)\n    \"\"\"CSS selectors for elements to exclude entirely (element + all content).\"\"\"\n\n    visitor: VisitorHandle | None = None\n    \"\"\"Optional visitor for custom traversal logic.\"\"\"\n\n\n@dataclass\nclass PreprocessingOptions:\n    \"\"\"HTML preprocessing options for document cleanup before conversion.\"\"\"\n\n    enabled: bool = True\n    \"\"\"Enable HTML preprocessing globally\"\"\"\n\n    preset: PreprocessingPreset | str = \"standard\"\n    \"\"\"Preprocessing preset level (Minimal, Standard, Aggressive)\"\"\"\n\n    remove_navigation: bool = True\n    \"\"\"Remove navigation elements (nav, breadcrumbs, menus, sidebars)\"\"\"\n\n    remove_forms: bool = True\n    \"\"\"Remove form elements (forms, inputs, buttons, etc.)\"\"\"\n\n\n@dataclass\nclass TableGrid:\n    \"\"\"A structured table grid with cell-level data including spans.\"\"\"\n\n    rows: int = 0\n    \"\"\"Number of rows.\"\"\"\n\n    cols: int = 0\n    \"\"\"Number of columns.\"\"\"\n\n    cells: list[GridCell] = field(default_factory=list)\n    \"\"\"All cells in the table (may be fewer than rows*cols due to spans).\"\"\"\n"
  },
  {
    "path": "packages/python/html_to_markdown/py.typed",
    "content": ""
  },
  {
    "path": "packages/python/pyproject.toml",
    "content": "[build-system]\nbuild-backend = \"maturin\"\nrequires = [ \"maturin>=1,<2\" ]\n\n[project]\nname = \"html-to-markdown\"\nversion = \"3.4.0rc25\"\ndescription = \"High-performance HTML to Markdown converter\"\nkeywords = [ \"converter\", \"html\", \"markdown\" ]\nlicense = \"MIT\"\nlicense-files = [ \"LICENSE\" ]\nauthors = [ { name = \"Kreuzberg Team\" } ]\nrequires-python = \">=3.10\"\nclassifiers = [\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n]\nurls.repository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\n\n[dependency-groups]\ndev = [ \"mypy>=1.19\", \"pytest>=9.0.2\", \"ruff>=0.14.8\" ]\n\n[tool.maturin]\nmodule-name = \"html_to_markdown._html_to_markdown\"\nmanifest-path = \"../../crates/html-to-markdown-py/Cargo.toml\"\nfeatures = [ \"pyo3/extension-module\" ]\npython-packages = [ \"html_to_markdown\" ]\n\n[tool.ruff]\ntarget-version = \"py310\"\nline-length = 120\nformat.docstring-code-line-length = 120\nformat.docstring-code-format = true\nlint.select = [ \"ALL\" ]\nlint.ignore = [\n  \"ANN401\",\n  \"ASYNC109\",\n  \"ASYNC110\",\n  \"BLE001\",\n  \"COM812\",\n  \"D100\",\n  \"D104\",\n  \"D107\",\n  \"D203\",\n  \"D213\",\n  \"ERA001\",\n  \"FBT\",\n  \"FIX002\",\n  \"ISC001\",\n  \"N818\",\n  \"PLR0911\",\n  \"PLR0912\",\n  \"PLR0913\",\n  \"PLW0603\",\n  \"S104\",\n  \"S110\",\n  \"S603\",\n  \"TD\",\n  \"TRY\",\n]\nlint.per-file-ignores.\"tests/**\" = [ \"ANN\", \"D103\", \"PLR2004\", \"S101\" ]\nlint.mccabe.max-complexity = 15\nlint.pydocstyle.convention = \"google\"\nlint.pylint.max-args = 10\nlint.pylint.max-branches = 15\nlint.pylint.max-returns = 10\n\n[tool.mypy]\npython_version = \"3.10\"\nstrict = true\nshow_error_codes = true\n"
  },
  {
    "path": "packages/python/tests/commonmark_spec.json",
    "content": "[\n  {\n    \"markdown\": \"\\tfoo\\tbaz\\t\\tbim\\n\",\n    \"html\": \"<pre><code>foo\\tbaz\\t\\tbim\\n</code></pre>\\n\",\n    \"example\": 1,\n    \"start_line\": 355,\n    \"end_line\": 360,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"  \\tfoo\\tbaz\\t\\tbim\\n\",\n    \"html\": \"<pre><code>foo\\tbaz\\t\\tbim\\n</code></pre>\\n\",\n    \"example\": 2,\n    \"start_line\": 362,\n    \"end_line\": 367,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"    a\\ta\\n    ὐ\\ta\\n\",\n    \"html\": \"<pre><code>a\\ta\\nὐ\\ta\\n</code></pre>\\n\",\n    \"example\": 3,\n    \"start_line\": 369,\n    \"end_line\": 376,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"  - foo\\n\\n\\tbar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 4,\n    \"start_line\": 382,\n    \"end_line\": 393,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n\\t\\tbar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<pre><code>  bar\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 5,\n    \"start_line\": 395,\n    \"end_line\": 407,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \">\\t\\tfoo\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>  foo\\n</code></pre>\\n</blockquote>\\n\",\n    \"example\": 6,\n    \"start_line\": 418,\n    \"end_line\": 425,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"-\\t\\tfoo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<pre><code>  foo\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 7,\n    \"start_line\": 427,\n    \"end_line\": 436,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"    foo\\n\\tbar\\n\",\n    \"html\": \"<pre><code>foo\\nbar\\n</code></pre>\\n\",\n    \"example\": 8,\n    \"start_line\": 439,\n    \"end_line\": 446,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \" - foo\\n   - bar\\n\\t - baz\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>baz</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 9,\n    \"start_line\": 448,\n    \"end_line\": 464,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"#\\tFoo\\n\",\n    \"html\": \"<h1>Foo</h1>\\n\",\n    \"example\": 10,\n    \"start_line\": 466,\n    \"end_line\": 470,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"*\\t*\\t*\\t\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 11,\n    \"start_line\": 472,\n    \"end_line\": 476,\n    \"section\": \"Tabs\"\n  },\n  {\n    \"markdown\": \"\\\\!\\\\\\\"\\\\#\\\\$\\\\%\\\\&\\\\'\\\\(\\\\)\\\\*\\\\+\\\\,\\\\-\\\\.\\\\/\\\\:\\\\;\\\\<\\\\=\\\\>\\\\?\\\\@\\\\[\\\\\\\\\\\\]\\\\^\\\\_\\\\`\\\\{\\\\|\\\\}\\\\~\\n\",\n    \"html\": \"<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\\\\]^_`{|}~</p>\\n\",\n    \"example\": 12,\n    \"start_line\": 489,\n    \"end_line\": 493,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\\\t\\\\A\\\\a\\\\ \\\\3\\\\φ\\\\«\\n\",\n    \"html\": \"<p>\\\\\\t\\\\A\\\\a\\\\ \\\\3\\\\φ\\\\«</p>\\n\",\n    \"example\": 13,\n    \"start_line\": 499,\n    \"end_line\": 503,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\*not emphasized*\\n\\\\<br/> not a tag\\n\\\\[not a link](/foo)\\n\\\\`not code`\\n1\\\\. not a list\\n\\\\* not a list\\n\\\\# not a heading\\n\\\\[foo]: /url \\\"not a reference\\\"\\n\\\\&ouml; not a character entity\\n\",\n    \"html\": \"<p>*not emphasized*\\n&lt;br/&gt; not a tag\\n[not a link](/foo)\\n`not code`\\n1. not a list\\n* not a list\\n# not a heading\\n[foo]: /url &quot;not a reference&quot;\\n&amp;ouml; not a character entity</p>\\n\",\n    \"example\": 14,\n    \"start_line\": 509,\n    \"end_line\": 529,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"\\\\\\\\*emphasis*\\n\",\n    \"html\": \"<p>\\\\<em>emphasis</em></p>\\n\",\n    \"example\": 15,\n    \"start_line\": 534,\n    \"end_line\": 538,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\nbar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 16,\n    \"start_line\": 543,\n    \"end_line\": 549,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"`` \\\\[\\\\` ``\\n\",\n    \"html\": \"<p><code>\\\\[\\\\`</code></p>\\n\",\n    \"example\": 17,\n    \"start_line\": 555,\n    \"end_line\": 559,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"    \\\\[\\\\]\\n\",\n    \"html\": \"<pre><code>\\\\[\\\\]\\n</code></pre>\\n\",\n    \"example\": 18,\n    \"start_line\": 562,\n    \"end_line\": 567,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"~~~\\n\\\\[\\\\]\\n~~~\\n\",\n    \"html\": \"<pre><code>\\\\[\\\\]\\n</code></pre>\\n\",\n    \"example\": 19,\n    \"start_line\": 570,\n    \"end_line\": 577,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"<https://example.com?find=\\\\*>\\n\",\n    \"html\": \"<p><a href=\\\"https://example.com?find=%5C*\\\">https://example.com?find=\\\\*</a></p>\\n\",\n    \"example\": 20,\n    \"start_line\": 580,\n    \"end_line\": 584,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"/bar\\\\/)\\\">\\n\",\n    \"html\": \"<a href=\\\"/bar\\\\/)\\\">\\n\",\n    \"example\": 21,\n    \"start_line\": 587,\n    \"end_line\": 591,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"[foo](/bar\\\\* \\\"ti\\\\*tle\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/bar*\\\" title=\\\"ti*tle\\\">foo</a></p>\\n\",\n    \"example\": 22,\n    \"start_line\": 597,\n    \"end_line\": 601,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /bar\\\\* \\\"ti\\\\*tle\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/bar*\\\" title=\\\"ti*tle\\\">foo</a></p>\\n\",\n    \"example\": 23,\n    \"start_line\": 604,\n    \"end_line\": 610,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"``` foo\\\\+bar\\nfoo\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-foo+bar\\\">foo\\n</code></pre>\\n\",\n    \"example\": 24,\n    \"start_line\": 613,\n    \"end_line\": 620,\n    \"section\": \"Backslash escapes\"\n  },\n  {\n    \"markdown\": \"&nbsp; &amp; &copy; &AElig; &Dcaron;\\n&frac34; &HilbertSpace; &DifferentialD;\\n&ClockwiseContourIntegral; &ngE;\\n\",\n    \"html\": \"<p>  &amp; © Æ Ď\\n¾ ℋ ⅆ\\n∲ ≧̸</p>\\n\",\n    \"example\": 25,\n    \"start_line\": 649,\n    \"end_line\": 657,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#35; &#1234; &#992; &#0;\\n\",\n    \"html\": \"<p># Ӓ Ϡ �</p>\\n\",\n    \"example\": 26,\n    \"start_line\": 668,\n    \"end_line\": 672,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#X22; &#XD06; &#xcab;\\n\",\n    \"html\": \"<p>&quot; ആ ಫ</p>\\n\",\n    \"example\": 27,\n    \"start_line\": 681,\n    \"end_line\": 685,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&nbsp &x; &#; &#x;\\n&#87654321;\\n&#abcdef0;\\n&ThisIsNotDefined; &hi?;\\n\",\n    \"html\": \"<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;\\n&amp;#87654321;\\n&amp;#abcdef0;\\n&amp;ThisIsNotDefined; &amp;hi?;</p>\\n\",\n    \"example\": 28,\n    \"start_line\": 690,\n    \"end_line\": 700,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&copy\\n\",\n    \"html\": \"<p>&amp;copy</p>\\n\",\n    \"example\": 29,\n    \"start_line\": 707,\n    \"end_line\": 711,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&MadeUpEntity;\\n\",\n    \"html\": \"<p>&amp;MadeUpEntity;</p>\\n\",\n    \"example\": 30,\n    \"start_line\": 717,\n    \"end_line\": 721,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"&ouml;&ouml;.html\\\">\\n\",\n    \"html\": \"<a href=\\\"&ouml;&ouml;.html\\\">\\n\",\n    \"example\": 31,\n    \"start_line\": 728,\n    \"end_line\": 732,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[foo](/f&ouml;&ouml; \\\"f&ouml;&ouml;\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/f%C3%B6%C3%B6\\\" title=\\\"föö\\\">foo</a></p>\\n\",\n    \"example\": 32,\n    \"start_line\": 735,\n    \"end_line\": 739,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /f&ouml;&ouml; \\\"f&ouml;&ouml;\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/f%C3%B6%C3%B6\\\" title=\\\"föö\\\">foo</a></p>\\n\",\n    \"example\": 33,\n    \"start_line\": 742,\n    \"end_line\": 748,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"``` f&ouml;&ouml;\\nfoo\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-föö\\\">foo\\n</code></pre>\\n\",\n    \"example\": 34,\n    \"start_line\": 751,\n    \"end_line\": 758,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"`f&ouml;&ouml;`\\n\",\n    \"html\": \"<p><code>f&amp;ouml;&amp;ouml;</code></p>\\n\",\n    \"example\": 35,\n    \"start_line\": 764,\n    \"end_line\": 768,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"    f&ouml;f&ouml;\\n\",\n    \"html\": \"<pre><code>f&amp;ouml;f&amp;ouml;\\n</code></pre>\\n\",\n    \"example\": 36,\n    \"start_line\": 771,\n    \"end_line\": 776,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#42;foo&#42;\\n*foo*\\n\",\n    \"html\": \"<p>*foo*\\n<em>foo</em></p>\\n\",\n    \"example\": 37,\n    \"start_line\": 783,\n    \"end_line\": 789,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#42; foo\\n\\n* foo\\n\",\n    \"html\": \"<p>* foo</p>\\n<ul>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 38,\n    \"start_line\": 791,\n    \"end_line\": 800,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"foo&#10;&#10;bar\\n\",\n    \"html\": \"<p>foo\\n\\nbar</p>\\n\",\n    \"example\": 39,\n    \"start_line\": 802,\n    \"end_line\": 808,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"&#9;foo\\n\",\n    \"html\": \"<p>\\tfoo</p>\\n\",\n    \"example\": 40,\n    \"start_line\": 810,\n    \"end_line\": 814,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"[a](url &quot;tit&quot;)\\n\",\n    \"html\": \"<p>[a](url &quot;tit&quot;)</p>\\n\",\n    \"example\": 41,\n    \"start_line\": 817,\n    \"end_line\": 821,\n    \"section\": \"Entity and numeric character references\"\n  },\n  {\n    \"markdown\": \"- `one\\n- two`\\n\",\n    \"html\": \"<ul>\\n<li>`one</li>\\n<li>two`</li>\\n</ul>\\n\",\n    \"example\": 42,\n    \"start_line\": 840,\n    \"end_line\": 848,\n    \"section\": \"Precedence\"\n  },\n  {\n    \"markdown\": \"***\\n---\\n___\\n\",\n    \"html\": \"<hr />\\n<hr />\\n<hr />\\n\",\n    \"example\": 43,\n    \"start_line\": 879,\n    \"end_line\": 887,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"+++\\n\",\n    \"html\": \"<p>+++</p>\\n\",\n    \"example\": 44,\n    \"start_line\": 892,\n    \"end_line\": 896,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"===\\n\",\n    \"html\": \"<p>===</p>\\n\",\n    \"example\": 45,\n    \"start_line\": 899,\n    \"end_line\": 903,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"--\\n**\\n__\\n\",\n    \"html\": \"<p>--\\n**\\n__</p>\\n\",\n    \"example\": 46,\n    \"start_line\": 908,\n    \"end_line\": 916,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" ***\\n  ***\\n   ***\\n\",\n    \"html\": \"<hr />\\n<hr />\\n<hr />\\n\",\n    \"example\": 47,\n    \"start_line\": 921,\n    \"end_line\": 929,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"    ***\\n\",\n    \"html\": \"<pre><code>***\\n</code></pre>\\n\",\n    \"example\": 48,\n    \"start_line\": 934,\n    \"end_line\": 939,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n    ***\\n\",\n    \"html\": \"<p>Foo\\n***</p>\\n\",\n    \"example\": 49,\n    \"start_line\": 942,\n    \"end_line\": 948,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"_____________________________________\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 50,\n    \"start_line\": 953,\n    \"end_line\": 957,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" - - -\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 51,\n    \"start_line\": 962,\n    \"end_line\": 966,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" **  * ** * ** * **\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 52,\n    \"start_line\": 969,\n    \"end_line\": 973,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"-     -      -      -\\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 53,\n    \"start_line\": 976,\n    \"end_line\": 980,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- - - -    \\n\",\n    \"html\": \"<hr />\\n\",\n    \"example\": 54,\n    \"start_line\": 985,\n    \"end_line\": 989,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"_ _ _ _ a\\n\\na------\\n\\n---a---\\n\",\n    \"html\": \"<p>_ _ _ _ a</p>\\n<p>a------</p>\\n<p>---a---</p>\\n\",\n    \"example\": 55,\n    \"start_line\": 994,\n    \"end_line\": 1004,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \" *-*\\n\",\n    \"html\": \"<p><em>-</em></p>\\n\",\n    \"example\": 56,\n    \"start_line\": 1010,\n    \"end_line\": 1014,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- foo\\n***\\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<hr />\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 57,\n    \"start_line\": 1019,\n    \"end_line\": 1031,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n***\\nbar\\n\",\n    \"html\": \"<p>Foo</p>\\n<hr />\\n<p>bar</p>\\n\",\n    \"example\": 58,\n    \"start_line\": 1036,\n    \"end_line\": 1044,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"Foo\\n---\\nbar\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<p>bar</p>\\n\",\n    \"example\": 59,\n    \"start_line\": 1053,\n    \"end_line\": 1060,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"* Foo\\n* * *\\n* Bar\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n</ul>\\n<hr />\\n<ul>\\n<li>Bar</li>\\n</ul>\\n\",\n    \"example\": 60,\n    \"start_line\": 1066,\n    \"end_line\": 1078,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"- Foo\\n- * * *\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n<li>\\n<hr />\\n</li>\\n</ul>\\n\",\n    \"example\": 61,\n    \"start_line\": 1083,\n    \"end_line\": 1093,\n    \"section\": \"Thematic breaks\"\n  },\n  {\n    \"markdown\": \"# foo\\n## foo\\n### foo\\n#### foo\\n##### foo\\n###### foo\\n\",\n    \"html\": \"<h1>foo</h1>\\n<h2>foo</h2>\\n<h3>foo</h3>\\n<h4>foo</h4>\\n<h5>foo</h5>\\n<h6>foo</h6>\\n\",\n    \"example\": 62,\n    \"start_line\": 1112,\n    \"end_line\": 1126,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"####### foo\\n\",\n    \"html\": \"<p>####### foo</p>\\n\",\n    \"example\": 63,\n    \"start_line\": 1131,\n    \"end_line\": 1135,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"#5 bolt\\n\\n#hashtag\\n\",\n    \"html\": \"<p>#5 bolt</p>\\n<p>#hashtag</p>\\n\",\n    \"example\": 64,\n    \"start_line\": 1146,\n    \"end_line\": 1153,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"\\\\## foo\\n\",\n    \"html\": \"<p>## foo</p>\\n\",\n    \"example\": 65,\n    \"start_line\": 1158,\n    \"end_line\": 1162,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo *bar* \\\\*baz\\\\*\\n\",\n    \"html\": \"<h1>foo <em>bar</em> *baz*</h1>\\n\",\n    \"example\": 66,\n    \"start_line\": 1167,\n    \"end_line\": 1171,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"#                  foo                     \\n\",\n    \"html\": \"<h1>foo</h1>\\n\",\n    \"example\": 67,\n    \"start_line\": 1176,\n    \"end_line\": 1180,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \" ### foo\\n  ## foo\\n   # foo\\n\",\n    \"html\": \"<h3>foo</h3>\\n<h2>foo</h2>\\n<h1>foo</h1>\\n\",\n    \"example\": 68,\n    \"start_line\": 1185,\n    \"end_line\": 1193,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"    # foo\\n\",\n    \"html\": \"<pre><code># foo\\n</code></pre>\\n\",\n    \"example\": 69,\n    \"start_line\": 1198,\n    \"end_line\": 1203,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"foo\\n    # bar\\n\",\n    \"html\": \"<p>foo\\n# bar</p>\\n\",\n    \"example\": 70,\n    \"start_line\": 1206,\n    \"end_line\": 1212,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"## foo ##\\n  ###   bar    ###\\n\",\n    \"html\": \"<h2>foo</h2>\\n<h3>bar</h3>\\n\",\n    \"example\": 71,\n    \"start_line\": 1217,\n    \"end_line\": 1223,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo ##################################\\n##### foo ##\\n\",\n    \"html\": \"<h1>foo</h1>\\n<h5>foo</h5>\\n\",\n    \"example\": 72,\n    \"start_line\": 1228,\n    \"end_line\": 1234,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo ###     \\n\",\n    \"html\": \"<h3>foo</h3>\\n\",\n    \"example\": 73,\n    \"start_line\": 1239,\n    \"end_line\": 1243,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo ### b\\n\",\n    \"html\": \"<h3>foo ### b</h3>\\n\",\n    \"example\": 74,\n    \"start_line\": 1250,\n    \"end_line\": 1254,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"# foo#\\n\",\n    \"html\": \"<h1>foo#</h1>\\n\",\n    \"example\": 75,\n    \"start_line\": 1259,\n    \"end_line\": 1263,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"### foo \\\\###\\n## foo #\\\\##\\n# foo \\\\#\\n\",\n    \"html\": \"<h3>foo ###</h3>\\n<h2>foo ###</h2>\\n<h1>foo #</h1>\\n\",\n    \"example\": 76,\n    \"start_line\": 1269,\n    \"end_line\": 1277,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"****\\n## foo\\n****\\n\",\n    \"html\": \"<hr />\\n<h2>foo</h2>\\n<hr />\\n\",\n    \"example\": 77,\n    \"start_line\": 1283,\n    \"end_line\": 1291,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"Foo bar\\n# baz\\nBar foo\\n\",\n    \"html\": \"<p>Foo bar</p>\\n<h1>baz</h1>\\n<p>Bar foo</p>\\n\",\n    \"example\": 78,\n    \"start_line\": 1294,\n    \"end_line\": 1302,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"## \\n#\\n### ###\\n\",\n    \"html\": \"<h2></h2>\\n<h1></h1>\\n<h3></h3>\\n\",\n    \"example\": 79,\n    \"start_line\": 1307,\n    \"end_line\": 1315,\n    \"section\": \"ATX headings\"\n  },\n  {\n    \"markdown\": \"Foo *bar*\\n=========\\n\\nFoo *bar*\\n---------\\n\",\n    \"html\": \"<h1>Foo <em>bar</em></h1>\\n<h2>Foo <em>bar</em></h2>\\n\",\n    \"example\": 80,\n    \"start_line\": 1347,\n    \"end_line\": 1356,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo *bar\\nbaz*\\n====\\n\",\n    \"html\": \"<h1>Foo <em>bar\\nbaz</em></h1>\\n\",\n    \"example\": 81,\n    \"start_line\": 1361,\n    \"end_line\": 1368,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"  Foo *bar\\nbaz*\\t\\n====\\n\",\n    \"html\": \"<h1>Foo <em>bar\\nbaz</em></h1>\\n\",\n    \"example\": 82,\n    \"start_line\": 1375,\n    \"end_line\": 1382,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n-------------------------\\n\\nFoo\\n=\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<h1>Foo</h1>\\n\",\n    \"example\": 83,\n    \"start_line\": 1387,\n    \"end_line\": 1396,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"   Foo\\n---\\n\\n  Foo\\n-----\\n\\n  Foo\\n  ===\\n\",\n    \"html\": \"<h2>Foo</h2>\\n<h2>Foo</h2>\\n<h1>Foo</h1>\\n\",\n    \"example\": 84,\n    \"start_line\": 1402,\n    \"end_line\": 1415,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    Foo\\n    ---\\n\\n    Foo\\n---\\n\",\n    \"html\": \"<pre><code>Foo\\n---\\n\\nFoo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 85,\n    \"start_line\": 1420,\n    \"end_line\": 1433,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n   ----      \\n\",\n    \"html\": \"<h2>Foo</h2>\\n\",\n    \"example\": 86,\n    \"start_line\": 1439,\n    \"end_line\": 1444,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n    ---\\n\",\n    \"html\": \"<p>Foo\\n---</p>\\n\",\n    \"example\": 87,\n    \"start_line\": 1449,\n    \"end_line\": 1455,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n= =\\n\\nFoo\\n--- -\\n\",\n    \"html\": \"<p>Foo\\n= =</p>\\n<p>Foo</p>\\n<hr />\\n\",\n    \"example\": 88,\n    \"start_line\": 1460,\n    \"end_line\": 1471,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo  \\n-----\\n\",\n    \"html\": \"<h2>Foo</h2>\\n\",\n    \"example\": 89,\n    \"start_line\": 1476,\n    \"end_line\": 1481,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\\\\\n----\\n\",\n    \"html\": \"<h2>Foo\\\\</h2>\\n\",\n    \"example\": 90,\n    \"start_line\": 1486,\n    \"end_line\": 1491,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"`Foo\\n----\\n`\\n\\n<a title=\\\"a lot\\n---\\nof dashes\\\"/>\\n\",\n    \"html\": \"<h2>`Foo</h2>\\n<p>`</p>\\n<h2>&lt;a title=&quot;a lot</h2>\\n<p>of dashes&quot;/&gt;</p>\\n\",\n    \"example\": 91,\n    \"start_line\": 1497,\n    \"end_line\": 1510,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> Foo\\n---\\n\",\n    \"html\": \"<blockquote>\\n<p>Foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 92,\n    \"start_line\": 1516,\n    \"end_line\": 1524,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> foo\\nbar\\n===\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\nbar\\n===</p>\\n</blockquote>\\n\",\n    \"example\": 93,\n    \"start_line\": 1527,\n    \"end_line\": 1537,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"- Foo\\n---\\n\",\n    \"html\": \"<ul>\\n<li>Foo</li>\\n</ul>\\n<hr />\\n\",\n    \"example\": 94,\n    \"start_line\": 1540,\n    \"end_line\": 1548,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nBar\\n---\\n\",\n    \"html\": \"<h2>Foo\\nBar</h2>\\n\",\n    \"example\": 95,\n    \"start_line\": 1555,\n    \"end_line\": 1562,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"---\\nFoo\\n---\\nBar\\n---\\nBaz\\n\",\n    \"html\": \"<hr />\\n<h2>Foo</h2>\\n<h2>Bar</h2>\\n<p>Baz</p>\\n\",\n    \"example\": 96,\n    \"start_line\": 1568,\n    \"end_line\": 1580,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"\\n====\\n\",\n    \"html\": \"<p>====</p>\\n\",\n    \"example\": 97,\n    \"start_line\": 1585,\n    \"end_line\": 1590,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"---\\n---\\n\",\n    \"html\": \"<hr />\\n<hr />\\n\",\n    \"example\": 98,\n    \"start_line\": 1597,\n    \"end_line\": 1603,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"- foo\\n-----\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<hr />\\n\",\n    \"example\": 99,\n    \"start_line\": 1606,\n    \"end_line\": 1614,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    foo\\n---\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 100,\n    \"start_line\": 1617,\n    \"end_line\": 1624,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"> foo\\n-----\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 101,\n    \"start_line\": 1627,\n    \"end_line\": 1635,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"\\\\> foo\\n------\\n\",\n    \"html\": \"<h2>&gt; foo</h2>\\n\",\n    \"example\": 102,\n    \"start_line\": 1641,\n    \"end_line\": 1646,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\n\\nbar\\n---\\nbaz\\n\",\n    \"html\": \"<p>Foo</p>\\n<h2>bar</h2>\\n<p>baz</p>\\n\",\n    \"example\": 103,\n    \"start_line\": 1672,\n    \"end_line\": 1682,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n\\n---\\n\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n<hr />\\n<p>baz</p>\\n\",\n    \"example\": 104,\n    \"start_line\": 1688,\n    \"end_line\": 1700,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n* * *\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n<hr />\\n<p>baz</p>\\n\",\n    \"example\": 105,\n    \"start_line\": 1706,\n    \"end_line\": 1716,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"Foo\\nbar\\n\\\\---\\nbaz\\n\",\n    \"html\": \"<p>Foo\\nbar\\n---\\nbaz</p>\\n\",\n    \"example\": 106,\n    \"start_line\": 1721,\n    \"end_line\": 1731,\n    \"section\": \"Setext headings\"\n  },\n  {\n    \"markdown\": \"    a simple\\n      indented code block\\n\",\n    \"html\": \"<pre><code>a simple\\n  indented code block\\n</code></pre>\\n\",\n    \"example\": 107,\n    \"start_line\": 1749,\n    \"end_line\": 1756,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"  - foo\\n\\n    bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 108,\n    \"start_line\": 1763,\n    \"end_line\": 1774,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"1.  foo\\n\\n    - bar\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>foo</p>\\n<ul>\\n<li>bar</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 109,\n    \"start_line\": 1777,\n    \"end_line\": 1790,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    <a/>\\n    *hi*\\n\\n    - one\\n\",\n    \"html\": \"<pre><code>&lt;a/&gt;\\n*hi*\\n\\n- one\\n</code></pre>\\n\",\n    \"example\": 110,\n    \"start_line\": 1797,\n    \"end_line\": 1808,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    chunk1\\n\\n    chunk2\\n  \\n \\n \\n    chunk3\\n\",\n    \"html\": \"<pre><code>chunk1\\n\\nchunk2\\n\\n\\n\\nchunk3\\n</code></pre>\\n\",\n    \"example\": 111,\n    \"start_line\": 1813,\n    \"end_line\": 1830,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    chunk1\\n      \\n      chunk2\\n\",\n    \"html\": \"<pre><code>chunk1\\n  \\n  chunk2\\n</code></pre>\\n\",\n    \"example\": 112,\n    \"start_line\": 1836,\n    \"end_line\": 1845,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n    bar\\n\\n\",\n    \"html\": \"<p>Foo\\nbar</p>\\n\",\n    \"example\": 113,\n    \"start_line\": 1851,\n    \"end_line\": 1858,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    foo\\nbar\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n<p>bar</p>\\n\",\n    \"example\": 114,\n    \"start_line\": 1865,\n    \"end_line\": 1872,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"# Heading\\n    foo\\nHeading\\n------\\n    foo\\n----\\n\",\n    \"html\": \"<h1>Heading</h1>\\n<pre><code>foo\\n</code></pre>\\n<h2>Heading</h2>\\n<pre><code>foo\\n</code></pre>\\n<hr />\\n\",\n    \"example\": 115,\n    \"start_line\": 1878,\n    \"end_line\": 1893,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"        foo\\n    bar\\n\",\n    \"html\": \"<pre><code>    foo\\nbar\\n</code></pre>\\n\",\n    \"example\": 116,\n    \"start_line\": 1898,\n    \"end_line\": 1905,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"\\n    \\n    foo\\n    \\n\\n\",\n    \"html\": \"<pre><code>foo\\n</code></pre>\\n\",\n    \"example\": 117,\n    \"start_line\": 1911,\n    \"end_line\": 1920,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"    foo  \\n\",\n    \"html\": \"<pre><code>foo  \\n</code></pre>\\n\",\n    \"example\": 118,\n    \"start_line\": 1925,\n    \"end_line\": 1930,\n    \"section\": \"Indented code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n<\\n >\\n```\\n\",\n    \"html\": \"<pre><code>&lt;\\n &gt;\\n</code></pre>\\n\",\n    \"example\": 119,\n    \"start_line\": 1980,\n    \"end_line\": 1989,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~\\n<\\n >\\n~~~\\n\",\n    \"html\": \"<pre><code>&lt;\\n &gt;\\n</code></pre>\\n\",\n    \"example\": 120,\n    \"start_line\": 1994,\n    \"end_line\": 2003,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``\\nfoo\\n``\\n\",\n    \"html\": \"<p><code>foo</code></p>\\n\",\n    \"example\": 121,\n    \"start_line\": 2007,\n    \"end_line\": 2013,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n~~~\\n```\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~\\n</code></pre>\\n\",\n    \"example\": 122,\n    \"start_line\": 2018,\n    \"end_line\": 2027,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~\\naaa\\n```\\n~~~\\n\",\n    \"html\": \"<pre><code>aaa\\n```\\n</code></pre>\\n\",\n    \"example\": 123,\n    \"start_line\": 2030,\n    \"end_line\": 2039,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"````\\naaa\\n```\\n``````\\n\",\n    \"html\": \"<pre><code>aaa\\n```\\n</code></pre>\\n\",\n    \"example\": 124,\n    \"start_line\": 2044,\n    \"end_line\": 2053,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~\\naaa\\n~~~\\n~~~~\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~\\n</code></pre>\\n\",\n    \"example\": 125,\n    \"start_line\": 2056,\n    \"end_line\": 2065,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n\",\n    \"html\": \"<pre><code></code></pre>\\n\",\n    \"example\": 126,\n    \"start_line\": 2071,\n    \"end_line\": 2075,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"`````\\n\\n```\\naaa\\n\",\n    \"html\": \"<pre><code>\\n```\\naaa\\n</code></pre>\\n\",\n    \"example\": 127,\n    \"start_line\": 2078,\n    \"end_line\": 2088,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"> ```\\n> aaa\\n\\nbbb\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>aaa\\n</code></pre>\\n</blockquote>\\n<p>bbb</p>\\n\",\n    \"example\": 128,\n    \"start_line\": 2091,\n    \"end_line\": 2102,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n\\n  \\n```\\n\",\n    \"html\": \"<pre><code>\\n  \\n</code></pre>\\n\",\n    \"example\": 129,\n    \"start_line\": 2107,\n    \"end_line\": 2116,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n```\\n\",\n    \"html\": \"<pre><code></code></pre>\\n\",\n    \"example\": 130,\n    \"start_line\": 2121,\n    \"end_line\": 2126,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \" ```\\n aaa\\naaa\\n```\\n\",\n    \"html\": \"<pre><code>aaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 131,\n    \"start_line\": 2133,\n    \"end_line\": 2142,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"  ```\\naaa\\n  aaa\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\naaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 132,\n    \"start_line\": 2145,\n    \"end_line\": 2156,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"   ```\\n   aaa\\n    aaa\\n  aaa\\n   ```\\n\",\n    \"html\": \"<pre><code>aaa\\n aaa\\naaa\\n</code></pre>\\n\",\n    \"example\": 133,\n    \"start_line\": 2159,\n    \"end_line\": 2170,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"    ```\\n    aaa\\n    ```\\n\",\n    \"html\": \"<pre><code>```\\naaa\\n```\\n</code></pre>\\n\",\n    \"example\": 134,\n    \"start_line\": 2175,\n    \"end_line\": 2184,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n\",\n    \"example\": 135,\n    \"start_line\": 2190,\n    \"end_line\": 2197,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"   ```\\naaa\\n  ```\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n\",\n    \"example\": 136,\n    \"start_line\": 2200,\n    \"end_line\": 2207,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\naaa\\n    ```\\n\",\n    \"html\": \"<pre><code>aaa\\n    ```\\n</code></pre>\\n\",\n    \"example\": 137,\n    \"start_line\": 2212,\n    \"end_line\": 2220,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``` ```\\naaa\\n\",\n    \"html\": \"<p><code> </code>\\naaa</p>\\n\",\n    \"example\": 138,\n    \"start_line\": 2226,\n    \"end_line\": 2232,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~~~\\naaa\\n~~~ ~~\\n\",\n    \"html\": \"<pre><code>aaa\\n~~~ ~~\\n</code></pre>\\n\",\n    \"example\": 139,\n    \"start_line\": 2235,\n    \"end_line\": 2243,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"foo\\n```\\nbar\\n```\\nbaz\\n\",\n    \"html\": \"<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n<p>baz</p>\\n\",\n    \"example\": 140,\n    \"start_line\": 2249,\n    \"end_line\": 2260,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"foo\\n---\\n~~~\\nbar\\n~~~\\n# baz\\n\",\n    \"html\": \"<h2>foo</h2>\\n<pre><code>bar\\n</code></pre>\\n<h1>baz</h1>\\n\",\n    \"example\": 141,\n    \"start_line\": 2266,\n    \"end_line\": 2278,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```ruby\\ndef foo(x)\\n  return 3\\nend\\n```\\n\",\n    \"html\": \"<pre><code class=\\\"language-ruby\\\">def foo(x)\\n  return 3\\nend\\n</code></pre>\\n\",\n    \"example\": 142,\n    \"start_line\": 2288,\n    \"end_line\": 2299,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~~    ruby startline=3 $%@#$\\ndef foo(x)\\n  return 3\\nend\\n~~~~~~~\\n\",\n    \"html\": \"<pre><code class=\\\"language-ruby\\\">def foo(x)\\n  return 3\\nend\\n</code></pre>\\n\",\n    \"example\": 143,\n    \"start_line\": 2302,\n    \"end_line\": 2313,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"````;\\n````\\n\",\n    \"html\": \"<pre><code class=\\\"language-;\\\"></code></pre>\\n\",\n    \"example\": 144,\n    \"start_line\": 2316,\n    \"end_line\": 2321,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"``` aa ```\\nfoo\\n\",\n    \"html\": \"<p><code>aa</code>\\nfoo</p>\\n\",\n    \"example\": 145,\n    \"start_line\": 2326,\n    \"end_line\": 2332,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"~~~ aa ``` ~~~\\nfoo\\n~~~\\n\",\n    \"html\": \"<pre><code class=\\\"language-aa\\\">foo\\n</code></pre>\\n\",\n    \"example\": 146,\n    \"start_line\": 2337,\n    \"end_line\": 2344,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"```\\n``` aaa\\n```\\n\",\n    \"html\": \"<pre><code>``` aaa\\n</code></pre>\\n\",\n    \"example\": 147,\n    \"start_line\": 2349,\n    \"end_line\": 2356,\n    \"section\": \"Fenced code blocks\"\n  },\n  {\n    \"markdown\": \"<table><tr><td>\\n<pre>\\n**Hello**,\\n\\n_world_.\\n</pre>\\n</td></tr></table>\\n\",\n    \"html\": \"<table><tr><td>\\n<pre>\\n**Hello**,\\n<p><em>world</em>.\\n</pre></p>\\n</td></tr></table>\\n\",\n    \"example\": 148,\n    \"start_line\": 2428,\n    \"end_line\": 2443,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n  <tr>\\n    <td>\\n           hi\\n    </td>\\n  </tr>\\n</table>\\n\\nokay.\\n\",\n    \"html\": \"<table>\\n  <tr>\\n    <td>\\n           hi\\n    </td>\\n  </tr>\\n</table>\\n<p>okay.</p>\\n\",\n    \"example\": 149,\n    \"start_line\": 2457,\n    \"end_line\": 2476,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \" <div>\\n  *hello*\\n         <foo><a>\\n\",\n    \"html\": \" <div>\\n  *hello*\\n         <foo><a>\\n\",\n    \"example\": 150,\n    \"start_line\": 2479,\n    \"end_line\": 2487,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"</div>\\n*foo*\\n\",\n    \"html\": \"</div>\\n*foo*\\n\",\n    \"example\": 151,\n    \"start_line\": 2492,\n    \"end_line\": 2498,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<DIV CLASS=\\\"foo\\\">\\n\\n*Markdown*\\n\\n</DIV>\\n\",\n    \"html\": \"<DIV CLASS=\\\"foo\\\">\\n<p><em>Markdown</em></p>\\n</DIV>\\n\",\n    \"example\": 152,\n    \"start_line\": 2503,\n    \"end_line\": 2513,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\"\\n  class=\\\"bar\\\">\\n</div>\\n\",\n    \"html\": \"<div id=\\\"foo\\\"\\n  class=\\\"bar\\\">\\n</div>\\n\",\n    \"example\": 153,\n    \"start_line\": 2519,\n    \"end_line\": 2527,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\" class=\\\"bar\\n  baz\\\">\\n</div>\\n\",\n    \"html\": \"<div id=\\\"foo\\\" class=\\\"bar\\n  baz\\\">\\n</div>\\n\",\n    \"example\": 154,\n    \"start_line\": 2530,\n    \"end_line\": 2538,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n*foo*\\n\\n*bar*\\n\",\n    \"html\": \"<div>\\n*foo*\\n<p><em>bar</em></p>\\n\",\n    \"example\": 155,\n    \"start_line\": 2542,\n    \"end_line\": 2551,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div id=\\\"foo\\\"\\n*hi*\\n\",\n    \"html\": \"<div id=\\\"foo\\\"\\n*hi*\\n\",\n    \"example\": 156,\n    \"start_line\": 2558,\n    \"end_line\": 2564,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div class\\nfoo\\n\",\n    \"html\": \"<div class\\nfoo\\n\",\n    \"example\": 157,\n    \"start_line\": 2567,\n    \"end_line\": 2573,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div *???-&&&-<---\\n*foo*\\n\",\n    \"html\": \"<div *???-&&&-<---\\n*foo*\\n\",\n    \"example\": 158,\n    \"start_line\": 2579,\n    \"end_line\": 2585,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div><a href=\\\"bar\\\">*foo*</a></div>\\n\",\n    \"html\": \"<div><a href=\\\"bar\\\">*foo*</a></div>\\n\",\n    \"example\": 159,\n    \"start_line\": 2591,\n    \"end_line\": 2595,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table><tr><td>\\nfoo\\n</td></tr></table>\\n\",\n    \"html\": \"<table><tr><td>\\nfoo\\n</td></tr></table>\\n\",\n    \"example\": 160,\n    \"start_line\": 2598,\n    \"end_line\": 2606,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div></div>\\n``` c\\nint x = 33;\\n```\\n\",\n    \"html\": \"<div></div>\\n``` c\\nint x = 33;\\n```\\n\",\n    \"example\": 161,\n    \"start_line\": 2615,\n    \"end_line\": 2625,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo\\\">\\n*bar*\\n</a>\\n\",\n    \"html\": \"<a href=\\\"foo\\\">\\n*bar*\\n</a>\\n\",\n    \"example\": 162,\n    \"start_line\": 2632,\n    \"end_line\": 2640,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<Warning>\\n*bar*\\n</Warning>\\n\",\n    \"html\": \"<Warning>\\n*bar*\\n</Warning>\\n\",\n    \"example\": 163,\n    \"start_line\": 2645,\n    \"end_line\": 2653,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<i class=\\\"foo\\\">\\n*bar*\\n</i>\\n\",\n    \"html\": \"<i class=\\\"foo\\\">\\n*bar*\\n</i>\\n\",\n    \"example\": 164,\n    \"start_line\": 2656,\n    \"end_line\": 2664,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"</ins>\\n*bar*\\n\",\n    \"html\": \"</ins>\\n*bar*\\n\",\n    \"example\": 165,\n    \"start_line\": 2667,\n    \"end_line\": 2673,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>\\n*foo*\\n</del>\\n\",\n    \"html\": \"<del>\\n*foo*\\n</del>\\n\",\n    \"example\": 166,\n    \"start_line\": 2682,\n    \"end_line\": 2690,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>\\n\\n*foo*\\n\\n</del>\\n\",\n    \"html\": \"<del>\\n<p><em>foo</em></p>\\n</del>\\n\",\n    \"example\": 167,\n    \"start_line\": 2697,\n    \"end_line\": 2707,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<del>*foo*</del>\\n\",\n    \"html\": \"<p><del><em>foo</em></del></p>\\n\",\n    \"example\": 168,\n    \"start_line\": 2715,\n    \"end_line\": 2719,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<pre language=\\\"haskell\\\"><code>\\nimport Text.HTML.TagSoup\\n\\nmain :: IO ()\\nmain = print $ parseTags tags\\n</code></pre>\\nokay\\n\",\n    \"html\": \"<pre language=\\\"haskell\\\"><code>\\nimport Text.HTML.TagSoup\\n\\nmain :: IO ()\\nmain = print $ parseTags tags\\n</code></pre>\\n<p>okay</p>\\n\",\n    \"example\": 169,\n    \"start_line\": 2731,\n    \"end_line\": 2747,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<script type=\\\"text/javascript\\\">\\n// JavaScript example\\n\\ndocument.getElementById(\\\"demo\\\").innerHTML = \\\"Hello JavaScript!\\\";\\n</script>\\nokay\\n\",\n    \"html\": \"<script type=\\\"text/javascript\\\">\\n// JavaScript example\\n\\ndocument.getElementById(\\\"demo\\\").innerHTML = \\\"Hello JavaScript!\\\";\\n</script>\\n<p>okay</p>\\n\",\n    \"example\": 170,\n    \"start_line\": 2752,\n    \"end_line\": 2766,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<textarea>\\n\\n*foo*\\n\\n_bar_\\n\\n</textarea>\\n\",\n    \"html\": \"<textarea>\\n\\n*foo*\\n\\n_bar_\\n\\n</textarea>\\n\",\n    \"example\": 171,\n    \"start_line\": 2771,\n    \"end_line\": 2787,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style\\n  type=\\\"text/css\\\">\\nh1 {color:red;}\\n\\np {color:blue;}\\n</style>\\nokay\\n\",\n    \"html\": \"<style\\n  type=\\\"text/css\\\">\\nh1 {color:red;}\\n\\np {color:blue;}\\n</style>\\n<p>okay</p>\\n\",\n    \"example\": 172,\n    \"start_line\": 2791,\n    \"end_line\": 2807,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style\\n  type=\\\"text/css\\\">\\n\\nfoo\\n\",\n    \"html\": \"<style\\n  type=\\\"text/css\\\">\\n\\nfoo\\n\",\n    \"example\": 173,\n    \"start_line\": 2814,\n    \"end_line\": 2824,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"> <div>\\n> foo\\n\\nbar\\n\",\n    \"html\": \"<blockquote>\\n<div>\\nfoo\\n</blockquote>\\n<p>bar</p>\\n\",\n    \"example\": 174,\n    \"start_line\": 2827,\n    \"end_line\": 2838,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"- <div>\\n- foo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<div>\\n</li>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 175,\n    \"start_line\": 2841,\n    \"end_line\": 2851,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<style>p{color:red;}</style>\\n*foo*\\n\",\n    \"html\": \"<style>p{color:red;}</style>\\n<p><em>foo</em></p>\\n\",\n    \"example\": 176,\n    \"start_line\": 2856,\n    \"end_line\": 2862,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!-- foo -->*bar*\\n*baz*\\n\",\n    \"html\": \"<!-- foo -->*bar*\\n<p><em>baz</em></p>\\n\",\n    \"example\": 177,\n    \"start_line\": 2865,\n    \"end_line\": 2871,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<script>\\nfoo\\n</script>1. *bar*\\n\",\n    \"html\": \"<script>\\nfoo\\n</script>1. *bar*\\n\",\n    \"example\": 178,\n    \"start_line\": 2877,\n    \"end_line\": 2885,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!-- Foo\\n\\nbar\\n   baz -->\\nokay\\n\",\n    \"html\": \"<!-- Foo\\n\\nbar\\n   baz -->\\n<p>okay</p>\\n\",\n    \"example\": 179,\n    \"start_line\": 2890,\n    \"end_line\": 2902,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<?php\\n\\n  echo '>';\\n\\n?>\\nokay\\n\",\n    \"html\": \"<?php\\n\\n  echo '>';\\n\\n?>\\n<p>okay</p>\\n\",\n    \"example\": 180,\n    \"start_line\": 2908,\n    \"end_line\": 2922,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<!DOCTYPE html>\\n\",\n    \"html\": \"<!DOCTYPE html>\\n\",\n    \"example\": 181,\n    \"start_line\": 2927,\n    \"end_line\": 2931,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<![CDATA[\\nfunction matchwo(a,b)\\n{\\n  if (a < b && a < 0) then {\\n    return 1;\\n\\n  } else {\\n\\n    return 0;\\n  }\\n}\\n]]>\\nokay\\n\",\n    \"html\": \"<![CDATA[\\nfunction matchwo(a,b)\\n{\\n  if (a < b && a < 0) then {\\n    return 1;\\n\\n  } else {\\n\\n    return 0;\\n  }\\n}\\n]]>\\n<p>okay</p>\\n\",\n    \"example\": 182,\n    \"start_line\": 2936,\n    \"end_line\": 2964,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"  <!-- foo -->\\n\\n    <!-- foo -->\\n\",\n    \"html\": \"  <!-- foo -->\\n<pre><code>&lt;!-- foo --&gt;\\n</code></pre>\\n\",\n    \"example\": 183,\n    \"start_line\": 2970,\n    \"end_line\": 2978,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"  <div>\\n\\n    <div>\\n\",\n    \"html\": \"  <div>\\n<pre><code>&lt;div&gt;\\n</code></pre>\\n\",\n    \"example\": 184,\n    \"start_line\": 2981,\n    \"end_line\": 2989,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n<div>\\nbar\\n</div>\\n\",\n    \"html\": \"<p>Foo</p>\\n<div>\\nbar\\n</div>\\n\",\n    \"example\": 185,\n    \"start_line\": 2995,\n    \"end_line\": 3005,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\nbar\\n</div>\\n*foo*\\n\",\n    \"html\": \"<div>\\nbar\\n</div>\\n*foo*\\n\",\n    \"example\": 186,\n    \"start_line\": 3012,\n    \"end_line\": 3022,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"Foo\\n<a href=\\\"bar\\\">\\nbaz\\n\",\n    \"html\": \"<p>Foo\\n<a href=\\\"bar\\\">\\nbaz</p>\\n\",\n    \"example\": 187,\n    \"start_line\": 3027,\n    \"end_line\": 3035,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n\\n*Emphasized* text.\\n\\n</div>\\n\",\n    \"html\": \"<div>\\n<p><em>Emphasized</em> text.</p>\\n</div>\\n\",\n    \"example\": 188,\n    \"start_line\": 3068,\n    \"end_line\": 3078,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<div>\\n*Emphasized* text.\\n</div>\\n\",\n    \"html\": \"<div>\\n*Emphasized* text.\\n</div>\\n\",\n    \"example\": 189,\n    \"start_line\": 3081,\n    \"end_line\": 3089,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n\\n<tr>\\n\\n<td>\\nHi\\n</td>\\n\\n</tr>\\n\\n</table>\\n\",\n    \"html\": \"<table>\\n<tr>\\n<td>\\nHi\\n</td>\\n</tr>\\n</table>\\n\",\n    \"example\": 190,\n    \"start_line\": 3103,\n    \"end_line\": 3123,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"<table>\\n\\n  <tr>\\n\\n    <td>\\n      Hi\\n    </td>\\n\\n  </tr>\\n\\n</table>\\n\",\n    \"html\": \"<table>\\n  <tr>\\n<pre><code>&lt;td&gt;\\n  Hi\\n&lt;/td&gt;\\n</code></pre>\\n  </tr>\\n</table>\\n\",\n    \"example\": 191,\n    \"start_line\": 3130,\n    \"end_line\": 3151,\n    \"section\": \"HTML blocks\"\n  },\n  {\n    \"markdown\": \"[foo]: /url \\\"title\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 192,\n    \"start_line\": 3179,\n    \"end_line\": 3185,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"   [foo]: \\n      /url  \\n           'the title'  \\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"the title\\\">foo</a></p>\\n\",\n    \"example\": 193,\n    \"start_line\": 3188,\n    \"end_line\": 3196,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[Foo*bar\\\\]]:my_(url) 'title (with parens)'\\n\\n[Foo*bar\\\\]]\\n\",\n    \"html\": \"<p><a href=\\\"my_(url)\\\" title=\\\"title (with parens)\\\">Foo*bar]</a></p>\\n\",\n    \"example\": 194,\n    \"start_line\": 3199,\n    \"end_line\": 3205,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[Foo bar]:\\n<my url>\\n'title'\\n\\n[Foo bar]\\n\",\n    \"html\": \"<p><a href=\\\"my%20url\\\" title=\\\"title\\\">Foo bar</a></p>\\n\",\n    \"example\": 195,\n    \"start_line\": 3208,\n    \"end_line\": 3216,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url '\\ntitle\\nline1\\nline2\\n'\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"\\ntitle\\nline1\\nline2\\n\\\">foo</a></p>\\n\",\n    \"example\": 196,\n    \"start_line\": 3221,\n    \"end_line\": 3235,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url 'title\\n\\nwith blank line'\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]: /url 'title</p>\\n<p>with blank line'</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 197,\n    \"start_line\": 3240,\n    \"end_line\": 3250,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]:\\n/url\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 198,\n    \"start_line\": 3255,\n    \"end_line\": 3262,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]:\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]:</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 199,\n    \"start_line\": 3267,\n    \"end_line\": 3274,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: <>\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"\\\">foo</a></p>\\n\",\n    \"example\": 200,\n    \"start_line\": 3279,\n    \"end_line\": 3285,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: <bar>(baz)\\n\\n[foo]\\n\",\n    \"html\": \"<p>[foo]: <bar>(baz)</p>\\n<p>[foo]</p>\\n\",\n    \"example\": 201,\n    \"start_line\": 3290,\n    \"end_line\": 3297,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\\\bar\\\\*baz \\\"foo\\\\\\\"bar\\\\baz\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url%5Cbar*baz\\\" title=\\\"foo&quot;bar\\\\baz\\\">foo</a></p>\\n\",\n    \"example\": 202,\n    \"start_line\": 3303,\n    \"end_line\": 3309,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: url\\n\",\n    \"html\": \"<p><a href=\\\"url\\\">foo</a></p>\\n\",\n    \"example\": 203,\n    \"start_line\": 3314,\n    \"end_line\": 3320,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: first\\n[foo]: second\\n\",\n    \"html\": \"<p><a href=\\\"first\\\">foo</a></p>\\n\",\n    \"example\": 204,\n    \"start_line\": 3326,\n    \"end_line\": 3333,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[FOO]: /url\\n\\n[Foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">Foo</a></p>\\n\",\n    \"example\": 205,\n    \"start_line\": 3339,\n    \"end_line\": 3345,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[ΑΓΩ]: /φου\\n\\n[αγω]\\n\",\n    \"html\": \"<p><a href=\\\"/%CF%86%CE%BF%CF%85\\\">αγω</a></p>\\n\",\n    \"example\": 206,\n    \"start_line\": 3348,\n    \"end_line\": 3354,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n\",\n    \"html\": \"\",\n    \"example\": 207,\n    \"start_line\": 3363,\n    \"end_line\": 3366,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[\\nfoo\\n]: /url\\nbar\\n\",\n    \"html\": \"<p>bar</p>\\n\",\n    \"example\": 208,\n    \"start_line\": 3371,\n    \"end_line\": 3378,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url \\\"title\\\" ok\\n\",\n    \"html\": \"<p>[foo]: /url &quot;title&quot; ok</p>\\n\",\n    \"example\": 209,\n    \"start_line\": 3384,\n    \"end_line\": 3388,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n\\\"title\\\" ok\\n\",\n    \"html\": \"<p>&quot;title&quot; ok</p>\\n\",\n    \"example\": 210,\n    \"start_line\": 3393,\n    \"end_line\": 3398,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"    [foo]: /url \\\"title\\\"\\n\\n[foo]\\n\",\n    \"html\": \"<pre><code>[foo]: /url &quot;title&quot;\\n</code></pre>\\n<p>[foo]</p>\\n\",\n    \"example\": 211,\n    \"start_line\": 3404,\n    \"end_line\": 3412,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"```\\n[foo]: /url\\n```\\n\\n[foo]\\n\",\n    \"html\": \"<pre><code>[foo]: /url\\n</code></pre>\\n<p>[foo]</p>\\n\",\n    \"example\": 212,\n    \"start_line\": 3418,\n    \"end_line\": 3428,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"Foo\\n[bar]: /baz\\n\\n[bar]\\n\",\n    \"html\": \"<p>Foo\\n[bar]: /baz</p>\\n<p>[bar]</p>\\n\",\n    \"example\": 213,\n    \"start_line\": 3433,\n    \"end_line\": 3442,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"# [Foo]\\n[foo]: /url\\n> bar\\n\",\n    \"html\": \"<h1><a href=\\\"/url\\\">Foo</a></h1>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 214,\n    \"start_line\": 3448,\n    \"end_line\": 3457,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\nbar\\n===\\n[foo]\\n\",\n    \"html\": \"<h1>bar</h1>\\n<p><a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 215,\n    \"start_line\": 3459,\n    \"end_line\": 3467,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /url\\n===\\n[foo]\\n\",\n    \"html\": \"<p>===\\n<a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 216,\n    \"start_line\": 3469,\n    \"end_line\": 3476,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]: /foo-url \\\"foo\\\"\\n[bar]: /bar-url\\n  \\\"bar\\\"\\n[baz]: /baz-url\\n\\n[foo],\\n[bar],\\n[baz]\\n\",\n    \"html\": \"<p><a href=\\\"/foo-url\\\" title=\\\"foo\\\">foo</a>,\\n<a href=\\\"/bar-url\\\" title=\\\"bar\\\">bar</a>,\\n<a href=\\\"/baz-url\\\">baz</a></p>\\n\",\n    \"example\": 217,\n    \"start_line\": 3482,\n    \"end_line\": 3495,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n> [foo]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a></p>\\n<blockquote>\\n</blockquote>\\n\",\n    \"example\": 218,\n    \"start_line\": 3503,\n    \"end_line\": 3511,\n    \"section\": \"Link reference definitions\"\n  },\n  {\n    \"markdown\": \"aaa\\n\\nbbb\\n\",\n    \"html\": \"<p>aaa</p>\\n<p>bbb</p>\\n\",\n    \"example\": 219,\n    \"start_line\": 3525,\n    \"end_line\": 3532,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\nbbb\\n\\nccc\\nddd\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n<p>ccc\\nddd</p>\\n\",\n    \"example\": 220,\n    \"start_line\": 3537,\n    \"end_line\": 3548,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\n\\n\\nbbb\\n\",\n    \"html\": \"<p>aaa</p>\\n<p>bbb</p>\\n\",\n    \"example\": 221,\n    \"start_line\": 3553,\n    \"end_line\": 3561,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"  aaa\\n bbb\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n\",\n    \"example\": 222,\n    \"start_line\": 3566,\n    \"end_line\": 3572,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa\\n             bbb\\n                                       ccc\\n\",\n    \"html\": \"<p>aaa\\nbbb\\nccc</p>\\n\",\n    \"example\": 223,\n    \"start_line\": 3578,\n    \"end_line\": 3586,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"   aaa\\nbbb\\n\",\n    \"html\": \"<p>aaa\\nbbb</p>\\n\",\n    \"example\": 224,\n    \"start_line\": 3592,\n    \"end_line\": 3598,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"    aaa\\nbbb\\n\",\n    \"html\": \"<pre><code>aaa\\n</code></pre>\\n<p>bbb</p>\\n\",\n    \"example\": 225,\n    \"start_line\": 3601,\n    \"end_line\": 3608,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"aaa     \\nbbb     \\n\",\n    \"html\": \"<p>aaa<br />\\nbbb</p>\\n\",\n    \"example\": 226,\n    \"start_line\": 3615,\n    \"end_line\": 3621,\n    \"section\": \"Paragraphs\"\n  },\n  {\n    \"markdown\": \"  \\n\\naaa\\n  \\n\\n# aaa\\n\\n  \\n\",\n    \"html\": \"<p>aaa</p>\\n<h1>aaa</h1>\\n\",\n    \"example\": 227,\n    \"start_line\": 3632,\n    \"end_line\": 3644,\n    \"section\": \"Blank lines\"\n  },\n  {\n    \"markdown\": \"> # Foo\\n> bar\\n> baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 228,\n    \"start_line\": 3700,\n    \"end_line\": 3710,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"># Foo\\n>bar\\n> baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 229,\n    \"start_line\": 3715,\n    \"end_line\": 3725,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"   > # Foo\\n   > bar\\n > baz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 230,\n    \"start_line\": 3730,\n    \"end_line\": 3740,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"    > # Foo\\n    > bar\\n    > baz\\n\",\n    \"html\": \"<pre><code>&gt; # Foo\\n&gt; bar\\n&gt; baz\\n</code></pre>\\n\",\n    \"example\": 231,\n    \"start_line\": 3745,\n    \"end_line\": 3754,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> # Foo\\n> bar\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<h1>Foo</h1>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 232,\n    \"start_line\": 3760,\n    \"end_line\": 3770,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\nbaz\\n> foo\\n\",\n    \"html\": \"<blockquote>\\n<p>bar\\nbaz\\nfoo</p>\\n</blockquote>\\n\",\n    \"example\": 233,\n    \"start_line\": 3776,\n    \"end_line\": 3786,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n---\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<hr />\\n\",\n    \"example\": 234,\n    \"start_line\": 3800,\n    \"end_line\": 3808,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> - foo\\n- bar\\n\",\n    \"html\": \"<blockquote>\\n<ul>\\n<li>foo</li>\\n</ul>\\n</blockquote>\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 235,\n    \"start_line\": 3820,\n    \"end_line\": 3832,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">     foo\\n    bar\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>foo\\n</code></pre>\\n</blockquote>\\n<pre><code>bar\\n</code></pre>\\n\",\n    \"example\": 236,\n    \"start_line\": 3838,\n    \"end_line\": 3848,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> ```\\nfoo\\n```\\n\",\n    \"html\": \"<blockquote>\\n<pre><code></code></pre>\\n</blockquote>\\n<p>foo</p>\\n<pre><code></code></pre>\\n\",\n    \"example\": 237,\n    \"start_line\": 3851,\n    \"end_line\": 3861,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n    - bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\n- bar</p>\\n</blockquote>\\n\",\n    \"example\": 238,\n    \"start_line\": 3867,\n    \"end_line\": 3875,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n\",\n    \"html\": \"<blockquote>\\n</blockquote>\\n\",\n    \"example\": 239,\n    \"start_line\": 3891,\n    \"end_line\": 3896,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n>  \\n> \\n\",\n    \"html\": \"<blockquote>\\n</blockquote>\\n\",\n    \"example\": 240,\n    \"start_line\": 3899,\n    \"end_line\": 3906,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">\\n> foo\\n>  \\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n\",\n    \"example\": 241,\n    \"start_line\": 3911,\n    \"end_line\": 3919,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n</blockquote>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 242,\n    \"start_line\": 3924,\n    \"end_line\": 3935,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo\\nbar</p>\\n</blockquote>\\n\",\n    \"example\": 243,\n    \"start_line\": 3946,\n    \"end_line\": 3954,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> foo\\n>\\n> bar\\n\",\n    \"html\": \"<blockquote>\\n<p>foo</p>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 244,\n    \"start_line\": 3959,\n    \"end_line\": 3968,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"foo\\n> bar\\n\",\n    \"html\": \"<p>foo</p>\\n<blockquote>\\n<p>bar</p>\\n</blockquote>\\n\",\n    \"example\": 245,\n    \"start_line\": 3973,\n    \"end_line\": 3981,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> aaa\\n***\\n> bbb\\n\",\n    \"html\": \"<blockquote>\\n<p>aaa</p>\\n</blockquote>\\n<hr />\\n<blockquote>\\n<p>bbb</p>\\n</blockquote>\\n\",\n    \"example\": 246,\n    \"start_line\": 3987,\n    \"end_line\": 3999,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar\\nbaz</p>\\n</blockquote>\\n\",\n    \"example\": 247,\n    \"start_line\": 4005,\n    \"end_line\": 4013,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\n\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar</p>\\n</blockquote>\\n<p>baz</p>\\n\",\n    \"example\": 248,\n    \"start_line\": 4016,\n    \"end_line\": 4025,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> bar\\n>\\nbaz\\n\",\n    \"html\": \"<blockquote>\\n<p>bar</p>\\n</blockquote>\\n<p>baz</p>\\n\",\n    \"example\": 249,\n    \"start_line\": 4028,\n    \"end_line\": 4037,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"> > > foo\\nbar\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<blockquote>\\n<p>foo\\nbar</p>\\n</blockquote>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 250,\n    \"start_line\": 4044,\n    \"end_line\": 4056,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">>> foo\\n> bar\\n>>baz\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<blockquote>\\n<p>foo\\nbar\\nbaz</p>\\n</blockquote>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 251,\n    \"start_line\": 4059,\n    \"end_line\": 4073,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \">     code\\n\\n>    not code\\n\",\n    \"html\": \"<blockquote>\\n<pre><code>code\\n</code></pre>\\n</blockquote>\\n<blockquote>\\n<p>not code</p>\\n</blockquote>\\n\",\n    \"example\": 252,\n    \"start_line\": 4081,\n    \"end_line\": 4093,\n    \"section\": \"Block quotes\"\n  },\n  {\n    \"markdown\": \"A paragraph\\nwith two lines.\\n\\n    indented code\\n\\n> A block quote.\\n\",\n    \"html\": \"<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n\",\n    \"example\": 253,\n    \"start_line\": 4135,\n    \"end_line\": 4150,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.  A paragraph\\n    with two lines.\\n\\n        indented code\\n\\n    > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 254,\n    \"start_line\": 4157,\n    \"end_line\": 4176,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- one\\n\\n two\\n\",\n    \"html\": \"<ul>\\n<li>one</li>\\n</ul>\\n<p>two</p>\\n\",\n    \"example\": 255,\n    \"start_line\": 4190,\n    \"end_line\": 4199,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- one\\n\\n  two\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 256,\n    \"start_line\": 4202,\n    \"end_line\": 4213,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" -    one\\n\\n     two\\n\",\n    \"html\": \"<ul>\\n<li>one</li>\\n</ul>\\n<pre><code> two\\n</code></pre>\\n\",\n    \"example\": 257,\n    \"start_line\": 4216,\n    \"end_line\": 4226,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" -    one\\n\\n      two\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 258,\n    \"start_line\": 4229,\n    \"end_line\": 4240,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   > > 1.  one\\n>>\\n>>     two\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<ol>\\n<li>\\n<p>one</p>\\n<p>two</p>\\n</li>\\n</ol>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 259,\n    \"start_line\": 4251,\n    \"end_line\": 4266,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \">>- one\\n>>\\n  >  > two\\n\",\n    \"html\": \"<blockquote>\\n<blockquote>\\n<ul>\\n<li>one</li>\\n</ul>\\n<p>two</p>\\n</blockquote>\\n</blockquote>\\n\",\n    \"example\": 260,\n    \"start_line\": 4278,\n    \"end_line\": 4291,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-one\\n\\n2.two\\n\",\n    \"html\": \"<p>-one</p>\\n<p>2.two</p>\\n\",\n    \"example\": 261,\n    \"start_line\": 4297,\n    \"end_line\": 4304,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n\\n  bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 262,\n    \"start_line\": 4310,\n    \"end_line\": 4322,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.  foo\\n\\n    ```\\n    bar\\n    ```\\n\\n    baz\\n\\n    > bam\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n<p>baz</p>\\n<blockquote>\\n<p>bam</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 263,\n    \"start_line\": 4327,\n    \"end_line\": 4349,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- Foo\\n\\n      bar\\n\\n\\n      baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>Foo</p>\\n<pre><code>bar\\n\\n\\nbaz\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 264,\n    \"start_line\": 4355,\n    \"end_line\": 4373,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"123456789. ok\\n\",\n    \"html\": \"<ol start=\\\"123456789\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 265,\n    \"start_line\": 4377,\n    \"end_line\": 4383,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1234567890. not ok\\n\",\n    \"html\": \"<p>1234567890. not ok</p>\\n\",\n    \"example\": 266,\n    \"start_line\": 4386,\n    \"end_line\": 4390,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"0. ok\\n\",\n    \"html\": \"<ol start=\\\"0\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 267,\n    \"start_line\": 4395,\n    \"end_line\": 4401,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"003. ok\\n\",\n    \"html\": \"<ol start=\\\"3\\\">\\n<li>ok</li>\\n</ol>\\n\",\n    \"example\": 268,\n    \"start_line\": 4404,\n    \"end_line\": 4410,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-1. not ok\\n\",\n    \"html\": \"<p>-1. not ok</p>\\n\",\n    \"example\": 269,\n    \"start_line\": 4415,\n    \"end_line\": 4419,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n      bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 270,\n    \"start_line\": 4438,\n    \"end_line\": 4450,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  10.  foo\\n\\n           bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>\\n<p>foo</p>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 271,\n    \"start_line\": 4455,\n    \"end_line\": 4467,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"    indented code\\n\\nparagraph\\n\\n    more code\\n\",\n    \"html\": \"<pre><code>indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n\",\n    \"example\": 272,\n    \"start_line\": 4474,\n    \"end_line\": 4486,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.     indented code\\n\\n   paragraph\\n\\n       more code\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code>indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 273,\n    \"start_line\": 4489,\n    \"end_line\": 4505,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1.      indented code\\n\\n   paragraph\\n\\n       more code\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code> indented code\\n</code></pre>\\n<p>paragraph</p>\\n<pre><code>more code\\n</code></pre>\\n</li>\\n</ol>\\n\",\n    \"example\": 274,\n    \"start_line\": 4511,\n    \"end_line\": 4527,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   foo\\n\\nbar\\n\",\n    \"html\": \"<p>foo</p>\\n<p>bar</p>\\n\",\n    \"example\": 275,\n    \"start_line\": 4538,\n    \"end_line\": 4545,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-    foo\\n\\n  bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n<p>bar</p>\\n\",\n    \"example\": 276,\n    \"start_line\": 4548,\n    \"end_line\": 4557,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-  foo\\n\\n   bar\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>bar</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 277,\n    \"start_line\": 4565,\n    \"end_line\": 4576,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-\\n  foo\\n-\\n  ```\\n  bar\\n  ```\\n-\\n      baz\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>\\n<pre><code>bar\\n</code></pre>\\n</li>\\n<li>\\n<pre><code>baz\\n</code></pre>\\n</li>\\n</ul>\\n\",\n    \"example\": 278,\n    \"start_line\": 4592,\n    \"end_line\": 4613,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-   \\n  foo\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n</ul>\\n\",\n    \"example\": 279,\n    \"start_line\": 4618,\n    \"end_line\": 4625,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"-\\n\\n  foo\\n\",\n    \"html\": \"<ul>\\n<li></li>\\n</ul>\\n<p>foo</p>\\n\",\n    \"example\": 280,\n    \"start_line\": 4632,\n    \"end_line\": 4641,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n-\\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 281,\n    \"start_line\": 4646,\n    \"end_line\": 4656,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n-   \\n- bar\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 282,\n    \"start_line\": 4661,\n    \"end_line\": 4671,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1. foo\\n2.\\n3. bar\\n\",\n    \"html\": \"<ol>\\n<li>foo</li>\\n<li></li>\\n<li>bar</li>\\n</ol>\\n\",\n    \"example\": 283,\n    \"start_line\": 4676,\n    \"end_line\": 4686,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"*\\n\",\n    \"html\": \"<ul>\\n<li></li>\\n</ul>\\n\",\n    \"example\": 284,\n    \"start_line\": 4691,\n    \"end_line\": 4697,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"foo\\n*\\n\\nfoo\\n1.\\n\",\n    \"html\": \"<p>foo\\n*</p>\\n<p>foo\\n1.</p>\\n\",\n    \"example\": 285,\n    \"start_line\": 4701,\n    \"end_line\": 4712,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \" 1.  A paragraph\\n     with two lines.\\n\\n         indented code\\n\\n     > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 286,\n    \"start_line\": 4723,\n    \"end_line\": 4742,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\n      with two lines.\\n\\n          indented code\\n\\n      > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 287,\n    \"start_line\": 4747,\n    \"end_line\": 4766,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"   1.  A paragraph\\n       with two lines.\\n\\n           indented code\\n\\n       > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 288,\n    \"start_line\": 4771,\n    \"end_line\": 4790,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"    1.  A paragraph\\n        with two lines.\\n\\n            indented code\\n\\n        > A block quote.\\n\",\n    \"html\": \"<pre><code>1.  A paragraph\\n    with two lines.\\n\\n        indented code\\n\\n    &gt; A block quote.\\n</code></pre>\\n\",\n    \"example\": 289,\n    \"start_line\": 4795,\n    \"end_line\": 4810,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\nwith two lines.\\n\\n          indented code\\n\\n      > A block quote.\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>A paragraph\\nwith two lines.</p>\\n<pre><code>indented code\\n</code></pre>\\n<blockquote>\\n<p>A block quote.</p>\\n</blockquote>\\n</li>\\n</ol>\\n\",\n    \"example\": 290,\n    \"start_line\": 4825,\n    \"end_line\": 4844,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"  1.  A paragraph\\n    with two lines.\\n\",\n    \"html\": \"<ol>\\n<li>A paragraph\\nwith two lines.</li>\\n</ol>\\n\",\n    \"example\": 291,\n    \"start_line\": 4849,\n    \"end_line\": 4857,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"> 1. > Blockquote\\ncontinued here.\\n\",\n    \"html\": \"<blockquote>\\n<ol>\\n<li>\\n<blockquote>\\n<p>Blockquote\\ncontinued here.</p>\\n</blockquote>\\n</li>\\n</ol>\\n</blockquote>\\n\",\n    \"example\": 292,\n    \"start_line\": 4862,\n    \"end_line\": 4876,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"> 1. > Blockquote\\n> continued here.\\n\",\n    \"html\": \"<blockquote>\\n<ol>\\n<li>\\n<blockquote>\\n<p>Blockquote\\ncontinued here.</p>\\n</blockquote>\\n</li>\\n</ol>\\n</blockquote>\\n\",\n    \"example\": 293,\n    \"start_line\": 4879,\n    \"end_line\": 4893,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n  - bar\\n    - baz\\n      - boo\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>baz\\n<ul>\\n<li>boo</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 294,\n    \"start_line\": 4907,\n    \"end_line\": 4928,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n - bar\\n  - baz\\n   - boo\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n<li>baz</li>\\n<li>boo</li>\\n</ul>\\n\",\n    \"example\": 295,\n    \"start_line\": 4933,\n    \"end_line\": 4945,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"10) foo\\n    - bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>foo\\n<ul>\\n<li>bar</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 296,\n    \"start_line\": 4950,\n    \"end_line\": 4961,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"10) foo\\n   - bar\\n\",\n    \"html\": \"<ol start=\\\"10\\\">\\n<li>foo</li>\\n</ol>\\n<ul>\\n<li>bar</li>\\n</ul>\\n\",\n    \"example\": 297,\n    \"start_line\": 4966,\n    \"end_line\": 4976,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- - foo\\n\",\n    \"html\": \"<ul>\\n<li>\\n<ul>\\n<li>foo</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 298,\n    \"start_line\": 4981,\n    \"end_line\": 4991,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"1. - 2. foo\\n\",\n    \"html\": \"<ol>\\n<li>\\n<ul>\\n<li>\\n<ol start=\\\"2\\\">\\n<li>foo</li>\\n</ol>\\n</li>\\n</ul>\\n</li>\\n</ol>\\n\",\n    \"example\": 299,\n    \"start_line\": 4994,\n    \"end_line\": 5008,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- # Foo\\n- Bar\\n  ---\\n  baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<h1>Foo</h1>\\n</li>\\n<li>\\n<h2>Bar</h2>\\nbaz</li>\\n</ul>\\n\",\n    \"example\": 300,\n    \"start_line\": 5013,\n    \"end_line\": 5027,\n    \"section\": \"List items\"\n  },\n  {\n    \"markdown\": \"- foo\\n- bar\\n+ baz\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n</ul>\\n<ul>\\n<li>baz</li>\\n</ul>\\n\",\n    \"example\": 301,\n    \"start_line\": 5249,\n    \"end_line\": 5261,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. foo\\n2. bar\\n3) baz\\n\",\n    \"html\": \"<ol>\\n<li>foo</li>\\n<li>bar</li>\\n</ol>\\n<ol start=\\\"3\\\">\\n<li>baz</li>\\n</ol>\\n\",\n    \"example\": 302,\n    \"start_line\": 5264,\n    \"end_line\": 5276,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"Foo\\n- bar\\n- baz\\n\",\n    \"html\": \"<p>Foo</p>\\n<ul>\\n<li>bar</li>\\n<li>baz</li>\\n</ul>\\n\",\n    \"example\": 303,\n    \"start_line\": 5283,\n    \"end_line\": 5293,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"The number of windows in my house is\\n14.  The number of doors is 6.\\n\",\n    \"html\": \"<p>The number of windows in my house is\\n14.  The number of doors is 6.</p>\\n\",\n    \"example\": 304,\n    \"start_line\": 5360,\n    \"end_line\": 5366,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"The number of windows in my house is\\n1.  The number of doors is 6.\\n\",\n    \"html\": \"<p>The number of windows in my house is</p>\\n<ol>\\n<li>The number of doors is 6.</li>\\n</ol>\\n\",\n    \"example\": 305,\n    \"start_line\": 5370,\n    \"end_line\": 5378,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n\\n- bar\\n\\n\\n- baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n</li>\\n<li>\\n<p>bar</p>\\n</li>\\n<li>\\n<p>baz</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 306,\n    \"start_line\": 5384,\n    \"end_line\": 5403,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n  - bar\\n    - baz\\n\\n\\n      bim\\n\",\n    \"html\": \"<ul>\\n<li>foo\\n<ul>\\n<li>bar\\n<ul>\\n<li>\\n<p>baz</p>\\n<p>bim</p>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 307,\n    \"start_line\": 5405,\n    \"end_line\": 5427,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- foo\\n- bar\\n\\n<!-- -->\\n\\n- baz\\n- bim\\n\",\n    \"html\": \"<ul>\\n<li>foo</li>\\n<li>bar</li>\\n</ul>\\n<!-- -->\\n<ul>\\n<li>baz</li>\\n<li>bim</li>\\n</ul>\\n\",\n    \"example\": 308,\n    \"start_line\": 5435,\n    \"end_line\": 5453,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"-   foo\\n\\n    notcode\\n\\n-   foo\\n\\n<!-- -->\\n\\n    code\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<p>notcode</p>\\n</li>\\n<li>\\n<p>foo</p>\\n</li>\\n</ul>\\n<!-- -->\\n<pre><code>code\\n</code></pre>\\n\",\n    \"example\": 309,\n    \"start_line\": 5456,\n    \"end_line\": 5479,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n - b\\n  - c\\n   - d\\n  - e\\n - f\\n- g\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>b</li>\\n<li>c</li>\\n<li>d</li>\\n<li>e</li>\\n<li>f</li>\\n<li>g</li>\\n</ul>\\n\",\n    \"example\": 310,\n    \"start_line\": 5487,\n    \"end_line\": 5505,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. a\\n\\n  2. b\\n\\n   3. c\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>c</p>\\n</li>\\n</ol>\\n\",\n    \"example\": 311,\n    \"start_line\": 5508,\n    \"end_line\": 5526,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n - b\\n  - c\\n   - d\\n    - e\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>b</li>\\n<li>c</li>\\n<li>d\\n- e</li>\\n</ul>\\n\",\n    \"example\": 312,\n    \"start_line\": 5532,\n    \"end_line\": 5546,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. a\\n\\n  2. b\\n\\n    3. c\\n\",\n    \"html\": \"<ol>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n</ol>\\n<pre><code>3. c\\n</code></pre>\\n\",\n    \"example\": 313,\n    \"start_line\": 5552,\n    \"end_line\": 5569,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n- c\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>c</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 314,\n    \"start_line\": 5575,\n    \"end_line\": 5592,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* a\\n*\\n\\n* c\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li></li>\\n<li>\\n<p>c</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 315,\n    \"start_line\": 5597,\n    \"end_line\": 5612,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n  c\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n<p>c</p>\\n</li>\\n<li>\\n<p>d</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 316,\n    \"start_line\": 5619,\n    \"end_line\": 5638,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- b\\n\\n  [ref]: /url\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n</li>\\n<li>\\n<p>b</p>\\n</li>\\n<li>\\n<p>d</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 317,\n    \"start_line\": 5641,\n    \"end_line\": 5659,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n- ```\\n  b\\n\\n\\n  ```\\n- c\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n<li>\\n<pre><code>b\\n\\n\\n</code></pre>\\n</li>\\n<li>c</li>\\n</ul>\\n\",\n    \"example\": 318,\n    \"start_line\": 5664,\n    \"end_line\": 5683,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n\\n    c\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<ul>\\n<li>\\n<p>b</p>\\n<p>c</p>\\n</li>\\n</ul>\\n</li>\\n<li>d</li>\\n</ul>\\n\",\n    \"example\": 319,\n    \"start_line\": 5690,\n    \"end_line\": 5708,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* a\\n  > b\\n  >\\n* c\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<blockquote>\\n<p>b</p>\\n</blockquote>\\n</li>\\n<li>c</li>\\n</ul>\\n\",\n    \"example\": 320,\n    \"start_line\": 5714,\n    \"end_line\": 5728,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  > b\\n  ```\\n  c\\n  ```\\n- d\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<blockquote>\\n<p>b</p>\\n</blockquote>\\n<pre><code>c\\n</code></pre>\\n</li>\\n<li>d</li>\\n</ul>\\n\",\n    \"example\": 321,\n    \"start_line\": 5734,\n    \"end_line\": 5752,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n\",\n    \"html\": \"<ul>\\n<li>a</li>\\n</ul>\\n\",\n    \"example\": 322,\n    \"start_line\": 5757,\n    \"end_line\": 5763,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n\",\n    \"html\": \"<ul>\\n<li>a\\n<ul>\\n<li>b</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 323,\n    \"start_line\": 5766,\n    \"end_line\": 5777,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"1. ```\\n   foo\\n   ```\\n\\n   bar\\n\",\n    \"html\": \"<ol>\\n<li>\\n<pre><code>foo\\n</code></pre>\\n<p>bar</p>\\n</li>\\n</ol>\\n\",\n    \"example\": 324,\n    \"start_line\": 5783,\n    \"end_line\": 5797,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"* foo\\n  * bar\\n\\n  baz\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>foo</p>\\n<ul>\\n<li>bar</li>\\n</ul>\\n<p>baz</p>\\n</li>\\n</ul>\\n\",\n    \"example\": 325,\n    \"start_line\": 5802,\n    \"end_line\": 5817,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"- a\\n  - b\\n  - c\\n\\n- d\\n  - e\\n  - f\\n\",\n    \"html\": \"<ul>\\n<li>\\n<p>a</p>\\n<ul>\\n<li>b</li>\\n<li>c</li>\\n</ul>\\n</li>\\n<li>\\n<p>d</p>\\n<ul>\\n<li>e</li>\\n<li>f</li>\\n</ul>\\n</li>\\n</ul>\\n\",\n    \"example\": 326,\n    \"start_line\": 5820,\n    \"end_line\": 5845,\n    \"section\": \"Lists\"\n  },\n  {\n    \"markdown\": \"`hi`lo`\\n\",\n    \"html\": \"<p><code>hi</code>lo`</p>\\n\",\n    \"example\": 327,\n    \"start_line\": 5854,\n    \"end_line\": 5858,\n    \"section\": \"Inlines\"\n  },\n  {\n    \"markdown\": \"`foo`\\n\",\n    \"html\": \"<p><code>foo</code></p>\\n\",\n    \"example\": 328,\n    \"start_line\": 5886,\n    \"end_line\": 5890,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`` foo ` bar ``\\n\",\n    \"html\": \"<p><code>foo ` bar</code></p>\\n\",\n    \"example\": 329,\n    \"start_line\": 5897,\n    \"end_line\": 5901,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` `` `\\n\",\n    \"html\": \"<p><code>``</code></p>\\n\",\n    \"example\": 330,\n    \"start_line\": 5907,\n    \"end_line\": 5911,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`  ``  `\\n\",\n    \"html\": \"<p><code> `` </code></p>\\n\",\n    \"example\": 331,\n    \"start_line\": 5915,\n    \"end_line\": 5919,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` a`\\n\",\n    \"html\": \"<p><code> a</code></p>\\n\",\n    \"example\": 332,\n    \"start_line\": 5924,\n    \"end_line\": 5928,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` b `\\n\",\n    \"html\": \"<p><code> b </code></p>\\n\",\n    \"example\": 333,\n    \"start_line\": 5933,\n    \"end_line\": 5937,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` `\\n`  `\\n\",\n    \"html\": \"<p><code> </code>\\n<code>  </code></p>\\n\",\n    \"example\": 334,\n    \"start_line\": 5941,\n    \"end_line\": 5947,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``\\nfoo\\nbar  \\nbaz\\n``\\n\",\n    \"html\": \"<p><code>foo bar   baz</code></p>\\n\",\n    \"example\": 335,\n    \"start_line\": 5952,\n    \"end_line\": 5960,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``\\nfoo \\n``\\n\",\n    \"html\": \"<p><code>foo </code></p>\\n\",\n    \"example\": 336,\n    \"start_line\": 5962,\n    \"end_line\": 5968,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo   bar \\nbaz`\\n\",\n    \"html\": \"<p><code>foo   bar  baz</code></p>\\n\",\n    \"example\": 337,\n    \"start_line\": 5973,\n    \"end_line\": 5978,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo\\\\`bar`\\n\",\n    \"html\": \"<p><code>foo\\\\</code>bar`</p>\\n\",\n    \"example\": 338,\n    \"start_line\": 5990,\n    \"end_line\": 5994,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"``foo`bar``\\n\",\n    \"html\": \"<p><code>foo`bar</code></p>\\n\",\n    \"example\": 339,\n    \"start_line\": 6001,\n    \"end_line\": 6005,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"` foo `` bar `\\n\",\n    \"html\": \"<p><code>foo `` bar</code></p>\\n\",\n    \"example\": 340,\n    \"start_line\": 6007,\n    \"end_line\": 6011,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"*foo`*`\\n\",\n    \"html\": \"<p>*foo<code>*</code></p>\\n\",\n    \"example\": 341,\n    \"start_line\": 6019,\n    \"end_line\": 6023,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"[not a `link](/foo`)\\n\",\n    \"html\": \"<p>[not a <code>link](/foo</code>)</p>\\n\",\n    \"example\": 342,\n    \"start_line\": 6028,\n    \"end_line\": 6032,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`<a href=\\\"`\\\">`\\n\",\n    \"html\": \"<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>\\n\",\n    \"example\": 343,\n    \"start_line\": 6038,\n    \"end_line\": 6042,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"`\\\">`\\n\",\n    \"html\": \"<p><a href=\\\"`\\\">`</p>\\n\",\n    \"example\": 344,\n    \"start_line\": 6047,\n    \"end_line\": 6051,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`<https://foo.bar.`baz>`\\n\",\n    \"html\": \"<p><code>&lt;https://foo.bar.</code>baz&gt;`</p>\\n\",\n    \"example\": 345,\n    \"start_line\": 6056,\n    \"end_line\": 6060,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"<https://foo.bar.`baz>`\\n\",\n    \"html\": \"<p><a href=\\\"https://foo.bar.%60baz\\\">https://foo.bar.`baz</a>`</p>\\n\",\n    \"example\": 346,\n    \"start_line\": 6065,\n    \"end_line\": 6069,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"```foo``\\n\",\n    \"html\": \"<p>```foo``</p>\\n\",\n    \"example\": 347,\n    \"start_line\": 6075,\n    \"end_line\": 6079,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo\\n\",\n    \"html\": \"<p>`foo</p>\\n\",\n    \"example\": 348,\n    \"start_line\": 6082,\n    \"end_line\": 6086,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"`foo``bar``\\n\",\n    \"html\": \"<p>`foo<code>bar</code></p>\\n\",\n    \"example\": 349,\n    \"start_line\": 6091,\n    \"end_line\": 6095,\n    \"section\": \"Code spans\"\n  },\n  {\n    \"markdown\": \"*foo bar*\\n\",\n    \"html\": \"<p><em>foo bar</em></p>\\n\",\n    \"example\": 350,\n    \"start_line\": 6308,\n    \"end_line\": 6312,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a * foo bar*\\n\",\n    \"html\": \"<p>a * foo bar*</p>\\n\",\n    \"example\": 351,\n    \"start_line\": 6318,\n    \"end_line\": 6322,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a*\\\"foo\\\"*\\n\",\n    \"html\": \"<p>a*&quot;foo&quot;*</p>\\n\",\n    \"example\": 352,\n    \"start_line\": 6329,\n    \"end_line\": 6333,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"* a *\\n\",\n    \"html\": \"<p>* a *</p>\\n\",\n    \"example\": 353,\n    \"start_line\": 6338,\n    \"end_line\": 6342,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*$*alpha.\\n\\n*£*bravo.\\n\\n*€*charlie.\\n\",\n    \"html\": \"<p>*$*alpha.</p>\\n<p>*£*bravo.</p>\\n<p>*€*charlie.</p>\\n\",\n    \"example\": 354,\n    \"start_line\": 6347,\n    \"end_line\": 6357,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo*bar*\\n\",\n    \"html\": \"<p>foo<em>bar</em></p>\\n\",\n    \"example\": 355,\n    \"start_line\": 6362,\n    \"end_line\": 6366,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5*6*78\\n\",\n    \"html\": \"<p>5<em>6</em>78</p>\\n\",\n    \"example\": 356,\n    \"start_line\": 6369,\n    \"end_line\": 6373,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo bar_\\n\",\n    \"html\": \"<p><em>foo bar</em></p>\\n\",\n    \"example\": 357,\n    \"start_line\": 6378,\n    \"end_line\": 6382,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_ foo bar_\\n\",\n    \"html\": \"<p>_ foo bar_</p>\\n\",\n    \"example\": 358,\n    \"start_line\": 6388,\n    \"end_line\": 6392,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a_\\\"foo\\\"_\\n\",\n    \"html\": \"<p>a_&quot;foo&quot;_</p>\\n\",\n    \"example\": 359,\n    \"start_line\": 6398,\n    \"end_line\": 6402,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo_bar_\\n\",\n    \"html\": \"<p>foo_bar_</p>\\n\",\n    \"example\": 360,\n    \"start_line\": 6407,\n    \"end_line\": 6411,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5_6_78\\n\",\n    \"html\": \"<p>5_6_78</p>\\n\",\n    \"example\": 361,\n    \"start_line\": 6414,\n    \"end_line\": 6418,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"пристаням_стремятся_\\n\",\n    \"html\": \"<p>пристаням_стремятся_</p>\\n\",\n    \"example\": 362,\n    \"start_line\": 6421,\n    \"end_line\": 6425,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"aa_\\\"bb\\\"_cc\\n\",\n    \"html\": \"<p>aa_&quot;bb&quot;_cc</p>\\n\",\n    \"example\": 363,\n    \"start_line\": 6431,\n    \"end_line\": 6435,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo-_(bar)_\\n\",\n    \"html\": \"<p>foo-<em>(bar)</em></p>\\n\",\n    \"example\": 364,\n    \"start_line\": 6442,\n    \"end_line\": 6446,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo*\\n\",\n    \"html\": \"<p>_foo*</p>\\n\",\n    \"example\": 365,\n    \"start_line\": 6454,\n    \"end_line\": 6458,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo bar *\\n\",\n    \"html\": \"<p>*foo bar *</p>\\n\",\n    \"example\": 366,\n    \"start_line\": 6464,\n    \"end_line\": 6468,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo bar\\n*\\n\",\n    \"html\": \"<p>*foo bar\\n*</p>\\n\",\n    \"example\": 367,\n    \"start_line\": 6473,\n    \"end_line\": 6479,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(*foo)\\n\",\n    \"html\": \"<p>*(*foo)</p>\\n\",\n    \"example\": 368,\n    \"start_line\": 6486,\n    \"end_line\": 6490,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(*foo*)*\\n\",\n    \"html\": \"<p><em>(<em>foo</em>)</em></p>\\n\",\n    \"example\": 369,\n    \"start_line\": 6496,\n    \"end_line\": 6500,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo*bar\\n\",\n    \"html\": \"<p><em>foo</em>bar</p>\\n\",\n    \"example\": 370,\n    \"start_line\": 6505,\n    \"end_line\": 6509,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo bar _\\n\",\n    \"html\": \"<p>_foo bar _</p>\\n\",\n    \"example\": 371,\n    \"start_line\": 6518,\n    \"end_line\": 6522,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(_foo)\\n\",\n    \"html\": \"<p>_(_foo)</p>\\n\",\n    \"example\": 372,\n    \"start_line\": 6528,\n    \"end_line\": 6532,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(_foo_)_\\n\",\n    \"html\": \"<p><em>(<em>foo</em>)</em></p>\\n\",\n    \"example\": 373,\n    \"start_line\": 6537,\n    \"end_line\": 6541,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo_bar\\n\",\n    \"html\": \"<p>_foo_bar</p>\\n\",\n    \"example\": 374,\n    \"start_line\": 6546,\n    \"end_line\": 6550,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_пристаням_стремятся\\n\",\n    \"html\": \"<p>_пристаням_стремятся</p>\\n\",\n    \"example\": 375,\n    \"start_line\": 6553,\n    \"end_line\": 6557,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo_bar_baz_\\n\",\n    \"html\": \"<p><em>foo_bar_baz</em></p>\\n\",\n    \"example\": 376,\n    \"start_line\": 6560,\n    \"end_line\": 6564,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(bar)_.\\n\",\n    \"html\": \"<p><em>(bar)</em>.</p>\\n\",\n    \"example\": 377,\n    \"start_line\": 6571,\n    \"end_line\": 6575,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo bar**\\n\",\n    \"html\": \"<p><strong>foo bar</strong></p>\\n\",\n    \"example\": 378,\n    \"start_line\": 6580,\n    \"end_line\": 6584,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"** foo bar**\\n\",\n    \"html\": \"<p>** foo bar**</p>\\n\",\n    \"example\": 379,\n    \"start_line\": 6590,\n    \"end_line\": 6594,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a**\\\"foo\\\"**\\n\",\n    \"html\": \"<p>a**&quot;foo&quot;**</p>\\n\",\n    \"example\": 380,\n    \"start_line\": 6601,\n    \"end_line\": 6605,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo**bar**\\n\",\n    \"html\": \"<p>foo<strong>bar</strong></p>\\n\",\n    \"example\": 381,\n    \"start_line\": 6610,\n    \"end_line\": 6614,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo bar__\\n\",\n    \"html\": \"<p><strong>foo bar</strong></p>\\n\",\n    \"example\": 382,\n    \"start_line\": 6619,\n    \"end_line\": 6623,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__ foo bar__\\n\",\n    \"html\": \"<p>__ foo bar__</p>\\n\",\n    \"example\": 383,\n    \"start_line\": 6629,\n    \"end_line\": 6633,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__\\nfoo bar__\\n\",\n    \"html\": \"<p>__\\nfoo bar__</p>\\n\",\n    \"example\": 384,\n    \"start_line\": 6637,\n    \"end_line\": 6643,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"a__\\\"foo\\\"__\\n\",\n    \"html\": \"<p>a__&quot;foo&quot;__</p>\\n\",\n    \"example\": 385,\n    \"start_line\": 6649,\n    \"end_line\": 6653,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo__bar__\\n\",\n    \"html\": \"<p>foo__bar__</p>\\n\",\n    \"example\": 386,\n    \"start_line\": 6658,\n    \"end_line\": 6662,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"5__6__78\\n\",\n    \"html\": \"<p>5__6__78</p>\\n\",\n    \"example\": 387,\n    \"start_line\": 6665,\n    \"end_line\": 6669,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"пристаням__стремятся__\\n\",\n    \"html\": \"<p>пристаням__стремятся__</p>\\n\",\n    \"example\": 388,\n    \"start_line\": 6672,\n    \"end_line\": 6676,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo, __bar__, baz__\\n\",\n    \"html\": \"<p><strong>foo, <strong>bar</strong>, baz</strong></p>\\n\",\n    \"example\": 389,\n    \"start_line\": 6679,\n    \"end_line\": 6683,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo-__(bar)__\\n\",\n    \"html\": \"<p>foo-<strong>(bar)</strong></p>\\n\",\n    \"example\": 390,\n    \"start_line\": 6690,\n    \"end_line\": 6694,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo bar **\\n\",\n    \"html\": \"<p>**foo bar **</p>\\n\",\n    \"example\": 391,\n    \"start_line\": 6703,\n    \"end_line\": 6707,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**(**foo)\\n\",\n    \"html\": \"<p>**(**foo)</p>\\n\",\n    \"example\": 392,\n    \"start_line\": 6716,\n    \"end_line\": 6720,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*(**foo**)*\\n\",\n    \"html\": \"<p><em>(<strong>foo</strong>)</em></p>\\n\",\n    \"example\": 393,\n    \"start_line\": 6726,\n    \"end_line\": 6730,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\\n*Asclepias physocarpa*)**\\n\",\n    \"html\": \"<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\\n<em>Asclepias physocarpa</em>)</strong></p>\\n\",\n    \"example\": 394,\n    \"start_line\": 6733,\n    \"end_line\": 6739,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo \\\"*bar*\\\" foo**\\n\",\n    \"html\": \"<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>\\n\",\n    \"example\": 395,\n    \"start_line\": 6742,\n    \"end_line\": 6746,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo**bar\\n\",\n    \"html\": \"<p><strong>foo</strong>bar</p>\\n\",\n    \"example\": 396,\n    \"start_line\": 6751,\n    \"end_line\": 6755,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo bar __\\n\",\n    \"html\": \"<p>__foo bar __</p>\\n\",\n    \"example\": 397,\n    \"start_line\": 6763,\n    \"end_line\": 6767,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__(__foo)\\n\",\n    \"html\": \"<p>__(__foo)</p>\\n\",\n    \"example\": 398,\n    \"start_line\": 6773,\n    \"end_line\": 6777,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_(__foo__)_\\n\",\n    \"html\": \"<p><em>(<strong>foo</strong>)</em></p>\\n\",\n    \"example\": 399,\n    \"start_line\": 6783,\n    \"end_line\": 6787,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__bar\\n\",\n    \"html\": \"<p>__foo__bar</p>\\n\",\n    \"example\": 400,\n    \"start_line\": 6792,\n    \"end_line\": 6796,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__пристаням__стремятся\\n\",\n    \"html\": \"<p>__пристаням__стремятся</p>\\n\",\n    \"example\": 401,\n    \"start_line\": 6799,\n    \"end_line\": 6803,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__bar__baz__\\n\",\n    \"html\": \"<p><strong>foo__bar__baz</strong></p>\\n\",\n    \"example\": 402,\n    \"start_line\": 6806,\n    \"end_line\": 6810,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__(bar)__.\\n\",\n    \"html\": \"<p><strong>(bar)</strong>.</p>\\n\",\n    \"example\": 403,\n    \"start_line\": 6817,\n    \"end_line\": 6821,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo [bar](/url)*\\n\",\n    \"html\": \"<p><em>foo <a href=\\\"/url\\\">bar</a></em></p>\\n\",\n    \"example\": 404,\n    \"start_line\": 6829,\n    \"end_line\": 6833,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo\\nbar*\\n\",\n    \"html\": \"<p><em>foo\\nbar</em></p>\\n\",\n    \"example\": 405,\n    \"start_line\": 6836,\n    \"end_line\": 6842,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo __bar__ baz_\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong> baz</em></p>\\n\",\n    \"example\": 406,\n    \"start_line\": 6848,\n    \"end_line\": 6852,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo _bar_ baz_\\n\",\n    \"html\": \"<p><em>foo <em>bar</em> baz</em></p>\\n\",\n    \"example\": 407,\n    \"start_line\": 6855,\n    \"end_line\": 6859,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo_ bar_\\n\",\n    \"html\": \"<p><em><em>foo</em> bar</em></p>\\n\",\n    \"example\": 408,\n    \"start_line\": 6862,\n    \"end_line\": 6866,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo *bar**\\n\",\n    \"html\": \"<p><em>foo <em>bar</em></em></p>\\n\",\n    \"example\": 409,\n    \"start_line\": 6869,\n    \"end_line\": 6873,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar** baz*\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong> baz</em></p>\\n\",\n    \"example\": 410,\n    \"start_line\": 6876,\n    \"end_line\": 6880,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar**baz*\\n\",\n    \"html\": \"<p><em>foo<strong>bar</strong>baz</em></p>\\n\",\n    \"example\": 411,\n    \"start_line\": 6882,\n    \"end_line\": 6886,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar*\\n\",\n    \"html\": \"<p><em>foo**bar</em></p>\\n\",\n    \"example\": 412,\n    \"start_line\": 6906,\n    \"end_line\": 6910,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo** bar*\\n\",\n    \"html\": \"<p><em><strong>foo</strong> bar</em></p>\\n\",\n    \"example\": 413,\n    \"start_line\": 6919,\n    \"end_line\": 6923,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar***\\n\",\n    \"html\": \"<p><em>foo <strong>bar</strong></em></p>\\n\",\n    \"example\": 414,\n    \"start_line\": 6926,\n    \"end_line\": 6930,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**bar***\\n\",\n    \"html\": \"<p><em>foo<strong>bar</strong></em></p>\\n\",\n    \"example\": 415,\n    \"start_line\": 6933,\n    \"end_line\": 6937,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo***bar***baz\\n\",\n    \"html\": \"<p>foo<em><strong>bar</strong></em>baz</p>\\n\",\n    \"example\": 416,\n    \"start_line\": 6944,\n    \"end_line\": 6948,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo******bar*********baz\\n\",\n    \"html\": \"<p>foo<strong><strong><strong>bar</strong></strong></strong>***baz</p>\\n\",\n    \"example\": 417,\n    \"start_line\": 6950,\n    \"end_line\": 6954,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo **bar *baz* bim** bop*\\n\",\n    \"html\": \"<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\\n\",\n    \"example\": 418,\n    \"start_line\": 6959,\n    \"end_line\": 6963,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo [*bar*](/url)*\\n\",\n    \"html\": \"<p><em>foo <a href=\\\"/url\\\"><em>bar</em></a></em></p>\\n\",\n    \"example\": 419,\n    \"start_line\": 6966,\n    \"end_line\": 6970,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"** is not an empty emphasis\\n\",\n    \"html\": \"<p>** is not an empty emphasis</p>\\n\",\n    \"example\": 420,\n    \"start_line\": 6975,\n    \"end_line\": 6979,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**** is not an empty strong emphasis\\n\",\n    \"html\": \"<p>**** is not an empty strong emphasis</p>\\n\",\n    \"example\": 421,\n    \"start_line\": 6982,\n    \"end_line\": 6986,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo [bar](/url)**\\n\",\n    \"html\": \"<p><strong>foo <a href=\\\"/url\\\">bar</a></strong></p>\\n\",\n    \"example\": 422,\n    \"start_line\": 6995,\n    \"end_line\": 6999,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo\\nbar**\\n\",\n    \"html\": \"<p><strong>foo\\nbar</strong></p>\\n\",\n    \"example\": 423,\n    \"start_line\": 7002,\n    \"end_line\": 7008,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo _bar_ baz__\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em> baz</strong></p>\\n\",\n    \"example\": 424,\n    \"start_line\": 7014,\n    \"end_line\": 7018,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo __bar__ baz__\\n\",\n    \"html\": \"<p><strong>foo <strong>bar</strong> baz</strong></p>\\n\",\n    \"example\": 425,\n    \"start_line\": 7021,\n    \"end_line\": 7025,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo__ bar__\\n\",\n    \"html\": \"<p><strong><strong>foo</strong> bar</strong></p>\\n\",\n    \"example\": 426,\n    \"start_line\": 7028,\n    \"end_line\": 7032,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo **bar****\\n\",\n    \"html\": \"<p><strong>foo <strong>bar</strong></strong></p>\\n\",\n    \"example\": 427,\n    \"start_line\": 7035,\n    \"end_line\": 7039,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar* baz**\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em> baz</strong></p>\\n\",\n    \"example\": 428,\n    \"start_line\": 7042,\n    \"end_line\": 7046,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo*bar*baz**\\n\",\n    \"html\": \"<p><strong>foo<em>bar</em>baz</strong></p>\\n\",\n    \"example\": 429,\n    \"start_line\": 7049,\n    \"end_line\": 7053,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo* bar**\\n\",\n    \"html\": \"<p><strong><em>foo</em> bar</strong></p>\\n\",\n    \"example\": 430,\n    \"start_line\": 7056,\n    \"end_line\": 7060,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar***\\n\",\n    \"html\": \"<p><strong>foo <em>bar</em></strong></p>\\n\",\n    \"example\": 431,\n    \"start_line\": 7063,\n    \"end_line\": 7067,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo *bar **baz**\\nbim* bop**\\n\",\n    \"html\": \"<p><strong>foo <em>bar <strong>baz</strong>\\nbim</em> bop</strong></p>\\n\",\n    \"example\": 432,\n    \"start_line\": 7072,\n    \"end_line\": 7078,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo [*bar*](/url)**\\n\",\n    \"html\": \"<p><strong>foo <a href=\\\"/url\\\"><em>bar</em></a></strong></p>\\n\",\n    \"example\": 433,\n    \"start_line\": 7081,\n    \"end_line\": 7085,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__ is not an empty emphasis\\n\",\n    \"html\": \"<p>__ is not an empty emphasis</p>\\n\",\n    \"example\": 434,\n    \"start_line\": 7090,\n    \"end_line\": 7094,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____ is not an empty strong emphasis\\n\",\n    \"html\": \"<p>____ is not an empty strong emphasis</p>\\n\",\n    \"example\": 435,\n    \"start_line\": 7097,\n    \"end_line\": 7101,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo ***\\n\",\n    \"html\": \"<p>foo ***</p>\\n\",\n    \"example\": 436,\n    \"start_line\": 7107,\n    \"end_line\": 7111,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *\\\\**\\n\",\n    \"html\": \"<p>foo <em>*</em></p>\\n\",\n    \"example\": 437,\n    \"start_line\": 7114,\n    \"end_line\": 7118,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *_*\\n\",\n    \"html\": \"<p>foo <em>_</em></p>\\n\",\n    \"example\": 438,\n    \"start_line\": 7121,\n    \"end_line\": 7125,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo *****\\n\",\n    \"html\": \"<p>foo *****</p>\\n\",\n    \"example\": 439,\n    \"start_line\": 7128,\n    \"end_line\": 7132,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo **\\\\***\\n\",\n    \"html\": \"<p>foo <strong>*</strong></p>\\n\",\n    \"example\": 440,\n    \"start_line\": 7135,\n    \"end_line\": 7139,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo **_**\\n\",\n    \"html\": \"<p>foo <strong>_</strong></p>\\n\",\n    \"example\": 441,\n    \"start_line\": 7142,\n    \"end_line\": 7146,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo*\\n\",\n    \"html\": \"<p>*<em>foo</em></p>\\n\",\n    \"example\": 442,\n    \"start_line\": 7153,\n    \"end_line\": 7157,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo**\\n\",\n    \"html\": \"<p><em>foo</em>*</p>\\n\",\n    \"example\": 443,\n    \"start_line\": 7160,\n    \"end_line\": 7164,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo**\\n\",\n    \"html\": \"<p>*<strong>foo</strong></p>\\n\",\n    \"example\": 444,\n    \"start_line\": 7167,\n    \"end_line\": 7171,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"****foo*\\n\",\n    \"html\": \"<p>***<em>foo</em></p>\\n\",\n    \"example\": 445,\n    \"start_line\": 7174,\n    \"end_line\": 7178,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo***\\n\",\n    \"html\": \"<p><strong>foo</strong>*</p>\\n\",\n    \"example\": 446,\n    \"start_line\": 7181,\n    \"end_line\": 7185,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo****\\n\",\n    \"html\": \"<p><em>foo</em>***</p>\\n\",\n    \"example\": 447,\n    \"start_line\": 7188,\n    \"end_line\": 7192,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo ___\\n\",\n    \"html\": \"<p>foo ___</p>\\n\",\n    \"example\": 448,\n    \"start_line\": 7198,\n    \"end_line\": 7202,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _\\\\__\\n\",\n    \"html\": \"<p>foo <em>_</em></p>\\n\",\n    \"example\": 449,\n    \"start_line\": 7205,\n    \"end_line\": 7209,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _*_\\n\",\n    \"html\": \"<p>foo <em>*</em></p>\\n\",\n    \"example\": 450,\n    \"start_line\": 7212,\n    \"end_line\": 7216,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo _____\\n\",\n    \"html\": \"<p>foo _____</p>\\n\",\n    \"example\": 451,\n    \"start_line\": 7219,\n    \"end_line\": 7223,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo __\\\\___\\n\",\n    \"html\": \"<p>foo <strong>_</strong></p>\\n\",\n    \"example\": 452,\n    \"start_line\": 7226,\n    \"end_line\": 7230,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"foo __*__\\n\",\n    \"html\": \"<p>foo <strong>*</strong></p>\\n\",\n    \"example\": 453,\n    \"start_line\": 7233,\n    \"end_line\": 7237,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo_\\n\",\n    \"html\": \"<p>_<em>foo</em></p>\\n\",\n    \"example\": 454,\n    \"start_line\": 7240,\n    \"end_line\": 7244,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo__\\n\",\n    \"html\": \"<p><em>foo</em>_</p>\\n\",\n    \"example\": 455,\n    \"start_line\": 7251,\n    \"end_line\": 7255,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"___foo__\\n\",\n    \"html\": \"<p>_<strong>foo</strong></p>\\n\",\n    \"example\": 456,\n    \"start_line\": 7258,\n    \"end_line\": 7262,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo_\\n\",\n    \"html\": \"<p>___<em>foo</em></p>\\n\",\n    \"example\": 457,\n    \"start_line\": 7265,\n    \"end_line\": 7269,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo___\\n\",\n    \"html\": \"<p><strong>foo</strong>_</p>\\n\",\n    \"example\": 458,\n    \"start_line\": 7272,\n    \"end_line\": 7276,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo____\\n\",\n    \"html\": \"<p><em>foo</em>___</p>\\n\",\n    \"example\": 459,\n    \"start_line\": 7279,\n    \"end_line\": 7283,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo**\\n\",\n    \"html\": \"<p><strong>foo</strong></p>\\n\",\n    \"example\": 460,\n    \"start_line\": 7289,\n    \"end_line\": 7293,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*_foo_*\\n\",\n    \"html\": \"<p><em><em>foo</em></em></p>\\n\",\n    \"example\": 461,\n    \"start_line\": 7296,\n    \"end_line\": 7300,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__foo__\\n\",\n    \"html\": \"<p><strong>foo</strong></p>\\n\",\n    \"example\": 462,\n    \"start_line\": 7303,\n    \"end_line\": 7307,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_*foo*_\\n\",\n    \"html\": \"<p><em><em>foo</em></em></p>\\n\",\n    \"example\": 463,\n    \"start_line\": 7310,\n    \"end_line\": 7314,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"****foo****\\n\",\n    \"html\": \"<p><strong><strong>foo</strong></strong></p>\\n\",\n    \"example\": 464,\n    \"start_line\": 7320,\n    \"end_line\": 7324,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"____foo____\\n\",\n    \"html\": \"<p><strong><strong>foo</strong></strong></p>\\n\",\n    \"example\": 465,\n    \"start_line\": 7327,\n    \"end_line\": 7331,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"******foo******\\n\",\n    \"html\": \"<p><strong><strong><strong>foo</strong></strong></strong></p>\\n\",\n    \"example\": 466,\n    \"start_line\": 7338,\n    \"end_line\": 7342,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"***foo***\\n\",\n    \"html\": \"<p><em><strong>foo</strong></em></p>\\n\",\n    \"example\": 467,\n    \"start_line\": 7347,\n    \"end_line\": 7351,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_____foo_____\\n\",\n    \"html\": \"<p><em><strong><strong>foo</strong></strong></em></p>\\n\",\n    \"example\": 468,\n    \"start_line\": 7354,\n    \"end_line\": 7358,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo _bar* baz_\\n\",\n    \"html\": \"<p><em>foo _bar</em> baz_</p>\\n\",\n    \"example\": 469,\n    \"start_line\": 7363,\n    \"end_line\": 7367,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo __bar *baz bim__ bam*\\n\",\n    \"html\": \"<p><em>foo <strong>bar *baz bim</strong> bam</em></p>\\n\",\n    \"example\": 470,\n    \"start_line\": 7370,\n    \"end_line\": 7374,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**foo **bar baz**\\n\",\n    \"html\": \"<p>**foo <strong>bar baz</strong></p>\\n\",\n    \"example\": 471,\n    \"start_line\": 7379,\n    \"end_line\": 7383,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*foo *bar baz*\\n\",\n    \"html\": \"<p>*foo <em>bar baz</em></p>\\n\",\n    \"example\": 472,\n    \"start_line\": 7386,\n    \"end_line\": 7390,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*[bar*](/url)\\n\",\n    \"html\": \"<p>*<a href=\\\"/url\\\">bar*</a></p>\\n\",\n    \"example\": 473,\n    \"start_line\": 7395,\n    \"end_line\": 7399,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_foo [bar_](/url)\\n\",\n    \"html\": \"<p>_foo <a href=\\\"/url\\\">bar_</a></p>\\n\",\n    \"example\": 474,\n    \"start_line\": 7402,\n    \"end_line\": 7406,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*<img src=\\\"foo\\\" title=\\\"*\\\"/>\\n\",\n    \"html\": \"<p>*<img src=\\\"foo\\\" title=\\\"*\\\"/></p>\\n\",\n    \"example\": 475,\n    \"start_line\": 7409,\n    \"end_line\": 7413,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**<a href=\\\"**\\\">\\n\",\n    \"html\": \"<p>**<a href=\\\"**\\\"></p>\\n\",\n    \"example\": 476,\n    \"start_line\": 7416,\n    \"end_line\": 7420,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__<a href=\\\"__\\\">\\n\",\n    \"html\": \"<p>__<a href=\\\"__\\\"></p>\\n\",\n    \"example\": 477,\n    \"start_line\": 7423,\n    \"end_line\": 7427,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"*a `*`*\\n\",\n    \"html\": \"<p><em>a <code>*</code></em></p>\\n\",\n    \"example\": 478,\n    \"start_line\": 7430,\n    \"end_line\": 7434,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"_a `_`_\\n\",\n    \"html\": \"<p><em>a <code>_</code></em></p>\\n\",\n    \"example\": 479,\n    \"start_line\": 7437,\n    \"end_line\": 7441,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"**a<https://foo.bar/?q=**>\\n\",\n    \"html\": \"<p>**a<a href=\\\"https://foo.bar/?q=**\\\">https://foo.bar/?q=**</a></p>\\n\",\n    \"example\": 480,\n    \"start_line\": 7444,\n    \"end_line\": 7448,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"__a<https://foo.bar/?q=__>\\n\",\n    \"html\": \"<p>__a<a href=\\\"https://foo.bar/?q=__\\\">https://foo.bar/?q=__</a></p>\\n\",\n    \"example\": 481,\n    \"start_line\": 7451,\n    \"end_line\": 7455,\n    \"section\": \"Emphasis and strong emphasis\"\n  },\n  {\n    \"markdown\": \"[link](/uri \\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 482,\n    \"start_line\": 7539,\n    \"end_line\": 7543,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link</a></p>\\n\",\n    \"example\": 483,\n    \"start_line\": 7549,\n    \"end_line\": 7553,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[](./target.md)\\n\",\n    \"html\": \"<p><a href=\\\"./target.md\\\"></a></p>\\n\",\n    \"example\": 484,\n    \"start_line\": 7555,\n    \"end_line\": 7559,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link]()\\n\",\n    \"html\": \"<p><a href=\\\"\\\">link</a></p>\\n\",\n    \"example\": 485,\n    \"start_line\": 7562,\n    \"end_line\": 7566,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<>)\\n\",\n    \"html\": \"<p><a href=\\\"\\\">link</a></p>\\n\",\n    \"example\": 486,\n    \"start_line\": 7569,\n    \"end_line\": 7573,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[]()\\n\",\n    \"html\": \"<p><a href=\\\"\\\"></a></p>\\n\",\n    \"example\": 487,\n    \"start_line\": 7576,\n    \"end_line\": 7580,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/my uri)\\n\",\n    \"html\": \"<p>[link](/my uri)</p>\\n\",\n    \"example\": 488,\n    \"start_line\": 7585,\n    \"end_line\": 7589,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](</my uri>)\\n\",\n    \"html\": \"<p><a href=\\\"/my%20uri\\\">link</a></p>\\n\",\n    \"example\": 489,\n    \"start_line\": 7591,\n    \"end_line\": 7595,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\nbar)\\n\",\n    \"html\": \"<p>[link](foo\\nbar)</p>\\n\",\n    \"example\": 490,\n    \"start_line\": 7600,\n    \"end_line\": 7606,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo\\nbar>)\\n\",\n    \"html\": \"<p>[link](<foo\\nbar>)</p>\\n\",\n    \"example\": 491,\n    \"start_line\": 7608,\n    \"end_line\": 7614,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[a](<b)c>)\\n\",\n    \"html\": \"<p><a href=\\\"b)c\\\">a</a></p>\\n\",\n    \"example\": 492,\n    \"start_line\": 7619,\n    \"end_line\": 7623,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo\\\\>)\\n\",\n    \"html\": \"<p>[link](&lt;foo&gt;)</p>\\n\",\n    \"example\": 493,\n    \"start_line\": 7627,\n    \"end_line\": 7631,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[a](<b)c\\n[a](<b)c>\\n[a](<b>c)\\n\",\n    \"html\": \"<p>[a](&lt;b)c\\n[a](&lt;b)c&gt;\\n[a](<b>c)</p>\\n\",\n    \"example\": 494,\n    \"start_line\": 7636,\n    \"end_line\": 7644,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](\\\\(foo\\\\))\\n\",\n    \"html\": \"<p><a href=\\\"(foo)\\\">link</a></p>\\n\",\n    \"example\": 495,\n    \"start_line\": 7648,\n    \"end_line\": 7652,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo(and(bar)))\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar))\\\">link</a></p>\\n\",\n    \"example\": 496,\n    \"start_line\": 7657,\n    \"end_line\": 7661,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo(and(bar))\\n\",\n    \"html\": \"<p>[link](foo(and(bar))</p>\\n\",\n    \"example\": 497,\n    \"start_line\": 7666,\n    \"end_line\": 7670,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\(and\\\\(bar\\\\))\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar)\\\">link</a></p>\\n\",\n    \"example\": 498,\n    \"start_line\": 7673,\n    \"end_line\": 7677,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](<foo(and(bar)>)\\n\",\n    \"html\": \"<p><a href=\\\"foo(and(bar)\\\">link</a></p>\\n\",\n    \"example\": 499,\n    \"start_line\": 7680,\n    \"end_line\": 7684,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\)\\\\:)\\n\",\n    \"html\": \"<p><a href=\\\"foo):\\\">link</a></p>\\n\",\n    \"example\": 500,\n    \"start_line\": 7690,\n    \"end_line\": 7694,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](#fragment)\\n\\n[link](https://example.com#fragment)\\n\\n[link](https://example.com?foo=3#frag)\\n\",\n    \"html\": \"<p><a href=\\\"#fragment\\\">link</a></p>\\n<p><a href=\\\"https://example.com#fragment\\\">link</a></p>\\n<p><a href=\\\"https://example.com?foo=3#frag\\\">link</a></p>\\n\",\n    \"example\": 501,\n    \"start_line\": 7699,\n    \"end_line\": 7709,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo\\\\bar)\\n\",\n    \"html\": \"<p><a href=\\\"foo%5Cbar\\\">link</a></p>\\n\",\n    \"example\": 502,\n    \"start_line\": 7715,\n    \"end_line\": 7719,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](foo%20b&auml;)\\n\",\n    \"html\": \"<p><a href=\\\"foo%20b%C3%A4\\\">link</a></p>\\n\",\n    \"example\": 503,\n    \"start_line\": 7731,\n    \"end_line\": 7735,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](\\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"%22title%22\\\">link</a></p>\\n\",\n    \"example\": 504,\n    \"start_line\": 7742,\n    \"end_line\": 7746,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title\\\")\\n[link](/url 'title')\\n[link](/url (title))\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">link</a>\\n<a href=\\\"/url\\\" title=\\\"title\\\">link</a>\\n<a href=\\\"/url\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 505,\n    \"start_line\": 7751,\n    \"end_line\": 7759,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title \\\\\\\"&quot;\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title &quot;&quot;\\\">link</a></p>\\n\",\n    \"example\": 506,\n    \"start_line\": 7765,\n    \"end_line\": 7769,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title\\\")\\n\",\n    \"html\": \"<p><a href=\\\"/url%C2%A0%22title%22\\\">link</a></p>\\n\",\n    \"example\": 507,\n    \"start_line\": 7776,\n    \"end_line\": 7780,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url \\\"title \\\"and\\\" title\\\")\\n\",\n    \"html\": \"<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>\\n\",\n    \"example\": 508,\n    \"start_line\": 7785,\n    \"end_line\": 7789,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](/url 'title \\\"and\\\" title')\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title &quot;and&quot; title\\\">link</a></p>\\n\",\n    \"example\": 509,\n    \"start_line\": 7794,\n    \"end_line\": 7798,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link](   /uri\\n  \\\"title\\\"  )\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\" title=\\\"title\\\">link</a></p>\\n\",\n    \"example\": 510,\n    \"start_line\": 7819,\n    \"end_line\": 7824,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link] (/uri)\\n\",\n    \"html\": \"<p>[link] (/uri)</p>\\n\",\n    \"example\": 511,\n    \"start_line\": 7830,\n    \"end_line\": 7834,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [foo [bar]]](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [foo [bar]]</a></p>\\n\",\n    \"example\": 512,\n    \"start_line\": 7840,\n    \"end_line\": 7844,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link] bar](/uri)\\n\",\n    \"html\": \"<p>[link] bar](/uri)</p>\\n\",\n    \"example\": 513,\n    \"start_line\": 7847,\n    \"end_line\": 7851,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [bar](/uri)\\n\",\n    \"html\": \"<p>[link <a href=\\\"/uri\\\">bar</a></p>\\n\",\n    \"example\": 514,\n    \"start_line\": 7854,\n    \"end_line\": 7858,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link \\\\[bar](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [bar</a></p>\\n\",\n    \"example\": 515,\n    \"start_line\": 7861,\n    \"end_line\": 7865,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link *foo **bar** `#`*](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\\n\",\n    \"example\": 516,\n    \"start_line\": 7870,\n    \"end_line\": 7874,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[![moon](moon.jpg)](/uri)\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\"><img src=\\\"moon.jpg\\\" alt=\\\"moon\\\" /></a></p>\\n\",\n    \"example\": 517,\n    \"start_line\": 7877,\n    \"end_line\": 7881,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo [bar](/uri)](/uri)\\n\",\n    \"html\": \"<p>[foo <a href=\\\"/uri\\\">bar</a>](/uri)</p>\\n\",\n    \"example\": 518,\n    \"start_line\": 7886,\n    \"end_line\": 7890,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *[bar [baz](/uri)](/uri)*](/uri)\\n\",\n    \"html\": \"<p>[foo <em>[bar <a href=\\\"/uri\\\">baz</a>](/uri)</em>](/uri)</p>\\n\",\n    \"example\": 519,\n    \"start_line\": 7893,\n    \"end_line\": 7897,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"![[[foo](uri1)](uri2)](uri3)\\n\",\n    \"html\": \"<p><img src=\\\"uri3\\\" alt=\\\"[foo](uri2)\\\" /></p>\\n\",\n    \"example\": 520,\n    \"start_line\": 7900,\n    \"end_line\": 7904,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*[foo*](/uri)\\n\",\n    \"html\": \"<p>*<a href=\\\"/uri\\\">foo*</a></p>\\n\",\n    \"example\": 521,\n    \"start_line\": 7910,\n    \"end_line\": 7914,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar](baz*)\\n\",\n    \"html\": \"<p><a href=\\\"baz*\\\">foo *bar</a></p>\\n\",\n    \"example\": 522,\n    \"start_line\": 7917,\n    \"end_line\": 7921,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*foo [bar* baz]\\n\",\n    \"html\": \"<p><em>foo [bar</em> baz]</p>\\n\",\n    \"example\": 523,\n    \"start_line\": 7927,\n    \"end_line\": 7931,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo <bar attr=\\\"](baz)\\\">\\n\",\n    \"html\": \"<p>[foo <bar attr=\\\"](baz)\\\"></p>\\n\",\n    \"example\": 524,\n    \"start_line\": 7937,\n    \"end_line\": 7941,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo`](/uri)`\\n\",\n    \"html\": \"<p>[foo<code>](/uri)</code></p>\\n\",\n    \"example\": 525,\n    \"start_line\": 7944,\n    \"end_line\": 7948,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo<https://example.com/?search=](uri)>\\n\",\n    \"html\": \"<p>[foo<a href=\\\"https://example.com/?search=%5D(uri)\\\">https://example.com/?search=](uri)</a></p>\\n\",\n    \"example\": 526,\n    \"start_line\": 7951,\n    \"end_line\": 7955,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 527,\n    \"start_line\": 7989,\n    \"end_line\": 7995,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link [foo [bar]]][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [foo [bar]]</a></p>\\n\",\n    \"example\": 528,\n    \"start_line\": 8004,\n    \"end_line\": 8010,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link \\\\[bar][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link [bar</a></p>\\n\",\n    \"example\": 529,\n    \"start_line\": 8013,\n    \"end_line\": 8019,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[link *foo **bar** `#`*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\\n\",\n    \"example\": 530,\n    \"start_line\": 8024,\n    \"end_line\": 8030,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[![moon](moon.jpg)][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\"><img src=\\\"moon.jpg\\\" alt=\\\"moon\\\" /></a></p>\\n\",\n    \"example\": 531,\n    \"start_line\": 8033,\n    \"end_line\": 8039,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo [bar](/uri)][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <a href=\\\"/uri\\\">bar</a>]<a href=\\\"/uri\\\">ref</a></p>\\n\",\n    \"example\": 532,\n    \"start_line\": 8044,\n    \"end_line\": 8050,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar [baz][ref]*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <em>bar <a href=\\\"/uri\\\">baz</a></em>]<a href=\\\"/uri\\\">ref</a></p>\\n\",\n    \"example\": 533,\n    \"start_line\": 8053,\n    \"end_line\": 8059,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"*[foo*][ref]\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>*<a href=\\\"/uri\\\">foo*</a></p>\\n\",\n    \"example\": 534,\n    \"start_line\": 8068,\n    \"end_line\": 8074,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo *bar][ref]*\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">foo *bar</a>*</p>\\n\",\n    \"example\": 535,\n    \"start_line\": 8077,\n    \"end_line\": 8083,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo <bar attr=\\\"][ref]\\\">\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo <bar attr=\\\"][ref]\\\"></p>\\n\",\n    \"example\": 536,\n    \"start_line\": 8089,\n    \"end_line\": 8095,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo`][ref]`\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo<code>][ref]</code></p>\\n\",\n    \"example\": 537,\n    \"start_line\": 8098,\n    \"end_line\": 8104,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo<https://example.com/?search=][ref]>\\n\\n[ref]: /uri\\n\",\n    \"html\": \"<p>[foo<a href=\\\"https://example.com/?search=%5D%5Bref%5D\\\">https://example.com/?search=][ref]</a></p>\\n\",\n    \"example\": 538,\n    \"start_line\": 8107,\n    \"end_line\": 8113,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][BaR]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 539,\n    \"start_line\": 8118,\n    \"end_line\": 8124,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[ẞ]\\n\\n[SS]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">ẞ</a></p>\\n\",\n    \"example\": 540,\n    \"start_line\": 8129,\n    \"end_line\": 8135,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo\\n  bar]: /url\\n\\n[Baz][Foo bar]\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">Baz</a></p>\\n\",\n    \"example\": 541,\n    \"start_line\": 8141,\n    \"end_line\": 8148,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] [bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo] <a href=\\\"/url\\\" title=\\\"title\\\">bar</a></p>\\n\",\n    \"example\": 542,\n    \"start_line\": 8154,\n    \"end_line\": 8160,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]\\n[bar]\\n\\n[bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo]\\n<a href=\\\"/url\\\" title=\\\"title\\\">bar</a></p>\\n\",\n    \"example\": 543,\n    \"start_line\": 8163,\n    \"end_line\": 8171,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]: /url1\\n\\n[foo]: /url2\\n\\n[bar][foo]\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">bar</a></p>\\n\",\n    \"example\": 544,\n    \"start_line\": 8204,\n    \"end_line\": 8212,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[bar][foo\\\\!]\\n\\n[foo!]: /url\\n\",\n    \"html\": \"<p>[bar][foo!]</p>\\n\",\n    \"example\": 545,\n    \"start_line\": 8219,\n    \"end_line\": 8225,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref[]\\n\\n[ref[]: /uri\\n\",\n    \"html\": \"<p>[foo][ref[]</p>\\n<p>[ref[]: /uri</p>\\n\",\n    \"example\": 546,\n    \"start_line\": 8231,\n    \"end_line\": 8238,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref[bar]]\\n\\n[ref[bar]]: /uri\\n\",\n    \"html\": \"<p>[foo][ref[bar]]</p>\\n<p>[ref[bar]]: /uri</p>\\n\",\n    \"example\": 547,\n    \"start_line\": 8241,\n    \"end_line\": 8248,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[[foo]]]\\n\\n[[[foo]]]: /url\\n\",\n    \"html\": \"<p>[[[foo]]]</p>\\n<p>[[[foo]]]: /url</p>\\n\",\n    \"example\": 548,\n    \"start_line\": 8251,\n    \"end_line\": 8258,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][ref\\\\[]\\n\\n[ref\\\\[]: /uri\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">foo</a></p>\\n\",\n    \"example\": 549,\n    \"start_line\": 8261,\n    \"end_line\": 8267,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[bar\\\\\\\\]: /uri\\n\\n[bar\\\\\\\\]\\n\",\n    \"html\": \"<p><a href=\\\"/uri\\\">bar\\\\</a></p>\\n\",\n    \"example\": 550,\n    \"start_line\": 8272,\n    \"end_line\": 8278,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[]\\n\\n[]: /uri\\n\",\n    \"html\": \"<p>[]</p>\\n<p>[]: /uri</p>\\n\",\n    \"example\": 551,\n    \"start_line\": 8284,\n    \"end_line\": 8291,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[\\n ]\\n\\n[\\n ]: /uri\\n\",\n    \"html\": \"<p>[\\n]</p>\\n<p>[\\n]: /uri</p>\\n\",\n    \"example\": 552,\n    \"start_line\": 8294,\n    \"end_line\": 8305,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 553,\n    \"start_line\": 8317,\n    \"end_line\": 8323,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[*foo* bar][]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a></p>\\n\",\n    \"example\": 554,\n    \"start_line\": 8326,\n    \"end_line\": 8332,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">Foo</a></p>\\n\",\n    \"example\": 555,\n    \"start_line\": 8337,\n    \"end_line\": 8343,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] \\n[]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a>\\n[]</p>\\n\",\n    \"example\": 556,\n    \"start_line\": 8350,\n    \"end_line\": 8358,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 557,\n    \"start_line\": 8370,\n    \"end_line\": 8376,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[*foo* bar]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a></p>\\n\",\n    \"example\": 558,\n    \"start_line\": 8379,\n    \"end_line\": 8385,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[*foo* bar]]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[<a href=\\\"/url\\\" title=\\\"title\\\"><em>foo</em> bar</a>]</p>\\n\",\n    \"example\": 559,\n    \"start_line\": 8388,\n    \"end_line\": 8394,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[[bar [foo]\\n\\n[foo]: /url\\n\",\n    \"html\": \"<p>[[bar <a href=\\\"/url\\\">foo</a></p>\\n\",\n    \"example\": 560,\n    \"start_line\": 8397,\n    \"end_line\": 8403,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[Foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\" title=\\\"title\\\">Foo</a></p>\\n\",\n    \"example\": 561,\n    \"start_line\": 8408,\n    \"end_line\": 8414,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo] bar\\n\\n[foo]: /url\\n\",\n    \"html\": \"<p><a href=\\\"/url\\\">foo</a> bar</p>\\n\",\n    \"example\": 562,\n    \"start_line\": 8419,\n    \"end_line\": 8425,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"\\\\[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>[foo]</p>\\n\",\n    \"example\": 563,\n    \"start_line\": 8431,\n    \"end_line\": 8437,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo*]: /url\\n\\n*[foo*]\\n\",\n    \"html\": \"<p>*<a href=\\\"/url\\\">foo*</a></p>\\n\",\n    \"example\": 564,\n    \"start_line\": 8443,\n    \"end_line\": 8449,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar]\\n\\n[foo]: /url1\\n[bar]: /url2\\n\",\n    \"html\": \"<p><a href=\\\"/url2\\\">foo</a></p>\\n\",\n    \"example\": 565,\n    \"start_line\": 8455,\n    \"end_line\": 8462,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][]\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">foo</a></p>\\n\",\n    \"example\": 566,\n    \"start_line\": 8464,\n    \"end_line\": 8470,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo]()\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"\\\">foo</a></p>\\n\",\n    \"example\": 567,\n    \"start_line\": 8474,\n    \"end_line\": 8480,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo](not a link)\\n\\n[foo]: /url1\\n\",\n    \"html\": \"<p><a href=\\\"/url1\\\">foo</a>(not a link)</p>\\n\",\n    \"example\": 568,\n    \"start_line\": 8482,\n    \"end_line\": 8488,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url\\n\",\n    \"html\": \"<p>[foo]<a href=\\\"/url\\\">bar</a></p>\\n\",\n    \"example\": 569,\n    \"start_line\": 8493,\n    \"end_line\": 8499,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url1\\n[bar]: /url2\\n\",\n    \"html\": \"<p><a href=\\\"/url2\\\">foo</a><a href=\\\"/url1\\\">baz</a></p>\\n\",\n    \"example\": 570,\n    \"start_line\": 8505,\n    \"end_line\": 8512,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"[foo][bar][baz]\\n\\n[baz]: /url1\\n[foo]: /url2\\n\",\n    \"html\": \"<p>[foo]<a href=\\\"/url1\\\">bar</a></p>\\n\",\n    \"example\": 571,\n    \"start_line\": 8518,\n    \"end_line\": 8525,\n    \"section\": \"Links\"\n  },\n  {\n    \"markdown\": \"![foo](/url \\\"title\\\")\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 572,\n    \"start_line\": 8541,\n    \"end_line\": 8545,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*]\\n\\n[foo *bar*]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 573,\n    \"start_line\": 8548,\n    \"end_line\": 8554,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo ![bar](/url)](/url2)\\n\",\n    \"html\": \"<p><img src=\\\"/url2\\\" alt=\\\"foo bar\\\" /></p>\\n\",\n    \"example\": 574,\n    \"start_line\": 8557,\n    \"end_line\": 8561,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo [bar](/url)](/url2)\\n\",\n    \"html\": \"<p><img src=\\\"/url2\\\" alt=\\\"foo bar\\\" /></p>\\n\",\n    \"example\": 575,\n    \"start_line\": 8564,\n    \"end_line\": 8568,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*][]\\n\\n[foo *bar*]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 576,\n    \"start_line\": 8578,\n    \"end_line\": 8584,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo *bar*][foobar]\\n\\n[FOOBAR]: train.jpg \\\"train & tracks\\\"\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"train &amp; tracks\\\" /></p>\\n\",\n    \"example\": 577,\n    \"start_line\": 8587,\n    \"end_line\": 8593,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo](train.jpg)\\n\",\n    \"html\": \"<p><img src=\\\"train.jpg\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 578,\n    \"start_line\": 8596,\n    \"end_line\": 8600,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"My ![foo bar](/path/to/train.jpg  \\\"title\\\"   )\\n\",\n    \"html\": \"<p>My <img src=\\\"/path/to/train.jpg\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 579,\n    \"start_line\": 8603,\n    \"end_line\": 8607,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo](<url>)\\n\",\n    \"html\": \"<p><img src=\\\"url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 580,\n    \"start_line\": 8610,\n    \"end_line\": 8614,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![](/url)\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"\\\" /></p>\\n\",\n    \"example\": 581,\n    \"start_line\": 8617,\n    \"end_line\": 8621,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][bar]\\n\\n[bar]: /url\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 582,\n    \"start_line\": 8626,\n    \"end_line\": 8632,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][bar]\\n\\n[BAR]: /url\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" /></p>\\n\",\n    \"example\": 583,\n    \"start_line\": 8635,\n    \"end_line\": 8641,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 584,\n    \"start_line\": 8646,\n    \"end_line\": 8652,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![*foo* bar][]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 585,\n    \"start_line\": 8655,\n    \"end_line\": 8661,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![Foo][]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"Foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 586,\n    \"start_line\": 8666,\n    \"end_line\": 8672,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo] \\n[]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" />\\n[]</p>\\n\",\n    \"example\": 587,\n    \"start_line\": 8678,\n    \"end_line\": 8686,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 588,\n    \"start_line\": 8691,\n    \"end_line\": 8697,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![*foo* bar]\\n\\n[*foo* bar]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"foo bar\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 589,\n    \"start_line\": 8700,\n    \"end_line\": 8706,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![[foo]]\\n\\n[[foo]]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>![[foo]]</p>\\n<p>[[foo]]: /url &quot;title&quot;</p>\\n\",\n    \"example\": 590,\n    \"start_line\": 8711,\n    \"end_line\": 8718,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"![Foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p><img src=\\\"/url\\\" alt=\\\"Foo\\\" title=\\\"title\\\" /></p>\\n\",\n    \"example\": 591,\n    \"start_line\": 8723,\n    \"end_line\": 8729,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"!\\\\[foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>![foo]</p>\\n\",\n    \"example\": 592,\n    \"start_line\": 8735,\n    \"end_line\": 8741,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"\\\\![foo]\\n\\n[foo]: /url \\\"title\\\"\\n\",\n    \"html\": \"<p>!<a href=\\\"/url\\\" title=\\\"title\\\">foo</a></p>\\n\",\n    \"example\": 593,\n    \"start_line\": 8747,\n    \"end_line\": 8753,\n    \"section\": \"Images\"\n  },\n  {\n    \"markdown\": \"<http://foo.bar.baz>\\n\",\n    \"html\": \"<p><a href=\\\"http://foo.bar.baz\\\">http://foo.bar.baz</a></p>\\n\",\n    \"example\": 594,\n    \"start_line\": 8780,\n    \"end_line\": 8784,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<https://foo.bar.baz/test?q=hello&id=22&boolean>\\n\",\n    \"html\": \"<p><a href=\\\"https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean\\\">https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>\\n\",\n    \"example\": 595,\n    \"start_line\": 8787,\n    \"end_line\": 8791,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<irc://foo.bar:2233/baz>\\n\",\n    \"html\": \"<p><a href=\\\"irc://foo.bar:2233/baz\\\">irc://foo.bar:2233/baz</a></p>\\n\",\n    \"example\": 596,\n    \"start_line\": 8794,\n    \"end_line\": 8798,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<MAILTO:FOO@BAR.BAZ>\\n\",\n    \"html\": \"<p><a href=\\\"MAILTO:FOO@BAR.BAZ\\\">MAILTO:FOO@BAR.BAZ</a></p>\\n\",\n    \"example\": 597,\n    \"start_line\": 8803,\n    \"end_line\": 8807,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<a+b+c:d>\\n\",\n    \"html\": \"<p><a href=\\\"a+b+c:d\\\">a+b+c:d</a></p>\\n\",\n    \"example\": 598,\n    \"start_line\": 8815,\n    \"end_line\": 8819,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<made-up-scheme://foo,bar>\\n\",\n    \"html\": \"<p><a href=\\\"made-up-scheme://foo,bar\\\">made-up-scheme://foo,bar</a></p>\\n\",\n    \"example\": 599,\n    \"start_line\": 8822,\n    \"end_line\": 8826,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<https://../>\\n\",\n    \"html\": \"<p><a href=\\\"https://../\\\">https://../</a></p>\\n\",\n    \"example\": 600,\n    \"start_line\": 8829,\n    \"end_line\": 8833,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<localhost:5001/foo>\\n\",\n    \"html\": \"<p><a href=\\\"localhost:5001/foo\\\">localhost:5001/foo</a></p>\\n\",\n    \"example\": 601,\n    \"start_line\": 8836,\n    \"end_line\": 8840,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<https://foo.bar/baz bim>\\n\",\n    \"html\": \"<p>&lt;https://foo.bar/baz bim&gt;</p>\\n\",\n    \"example\": 602,\n    \"start_line\": 8845,\n    \"end_line\": 8849,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<https://example.com/\\\\[\\\\>\\n\",\n    \"html\": \"<p><a href=\\\"https://example.com/%5C%5B%5C\\\">https://example.com/\\\\[\\\\</a></p>\\n\",\n    \"example\": 603,\n    \"start_line\": 8854,\n    \"end_line\": 8858,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo@bar.example.com>\\n\",\n    \"html\": \"<p><a href=\\\"mailto:foo@bar.example.com\\\">foo@bar.example.com</a></p>\\n\",\n    \"example\": 604,\n    \"start_line\": 8876,\n    \"end_line\": 8880,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo+special@Bar.baz-bar0.com>\\n\",\n    \"html\": \"<p><a href=\\\"mailto:foo+special@Bar.baz-bar0.com\\\">foo+special@Bar.baz-bar0.com</a></p>\\n\",\n    \"example\": 605,\n    \"start_line\": 8883,\n    \"end_line\": 8887,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo\\\\+@bar.example.com>\\n\",\n    \"html\": \"<p>&lt;foo+@bar.example.com&gt;</p>\\n\",\n    \"example\": 606,\n    \"start_line\": 8892,\n    \"end_line\": 8896,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<>\\n\",\n    \"html\": \"<p>&lt;&gt;</p>\\n\",\n    \"example\": 607,\n    \"start_line\": 8901,\n    \"end_line\": 8905,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"< https://foo.bar >\\n\",\n    \"html\": \"<p>&lt; https://foo.bar &gt;</p>\\n\",\n    \"example\": 608,\n    \"start_line\": 8908,\n    \"end_line\": 8912,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<m:abc>\\n\",\n    \"html\": \"<p>&lt;m:abc&gt;</p>\\n\",\n    \"example\": 609,\n    \"start_line\": 8915,\n    \"end_line\": 8919,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<foo.bar.baz>\\n\",\n    \"html\": \"<p>&lt;foo.bar.baz&gt;</p>\\n\",\n    \"example\": 610,\n    \"start_line\": 8922,\n    \"end_line\": 8926,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"https://example.com\\n\",\n    \"html\": \"<p>https://example.com</p>\\n\",\n    \"example\": 611,\n    \"start_line\": 8929,\n    \"end_line\": 8933,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"foo@bar.example.com\\n\",\n    \"html\": \"<p>foo@bar.example.com</p>\\n\",\n    \"example\": 612,\n    \"start_line\": 8936,\n    \"end_line\": 8940,\n    \"section\": \"Autolinks\"\n  },\n  {\n    \"markdown\": \"<a><bab><c2c>\\n\",\n    \"html\": \"<p><a><bab><c2c></p>\\n\",\n    \"example\": 613,\n    \"start_line\": 9016,\n    \"end_line\": 9020,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a/><b2/>\\n\",\n    \"html\": \"<p><a/><b2/></p>\\n\",\n    \"example\": 614,\n    \"start_line\": 9025,\n    \"end_line\": 9029,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a  /><b2\\ndata=\\\"foo\\\" >\\n\",\n    \"html\": \"<p><a  /><b2\\ndata=\\\"foo\\\" ></p>\\n\",\n    \"example\": 615,\n    \"start_line\": 9034,\n    \"end_line\": 9040,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a foo=\\\"bar\\\" bam = 'baz <em>\\\"</em>'\\n_boolean zoop:33=zoop:33 />\\n\",\n    \"html\": \"<p><a foo=\\\"bar\\\" bam = 'baz <em>\\\"</em>'\\n_boolean zoop:33=zoop:33 /></p>\\n\",\n    \"example\": 616,\n    \"start_line\": 9045,\n    \"end_line\": 9051,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"Foo <responsive-image src=\\\"foo.jpg\\\" />\\n\",\n    \"html\": \"<p>Foo <responsive-image src=\\\"foo.jpg\\\" /></p>\\n\",\n    \"example\": 617,\n    \"start_line\": 9056,\n    \"end_line\": 9060,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<33> <__>\\n\",\n    \"html\": \"<p>&lt;33&gt; &lt;__&gt;</p>\\n\",\n    \"example\": 618,\n    \"start_line\": 9065,\n    \"end_line\": 9069,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a h*#ref=\\\"hi\\\">\\n\",\n    \"html\": \"<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>\\n\",\n    \"example\": 619,\n    \"start_line\": 9074,\n    \"end_line\": 9078,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"hi'> <a href=hi'>\\n\",\n    \"html\": \"<p>&lt;a href=&quot;hi'&gt; &lt;a href=hi'&gt;</p>\\n\",\n    \"example\": 620,\n    \"start_line\": 9083,\n    \"end_line\": 9087,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"< a><\\nfoo><bar/ >\\n<foo bar=baz\\nbim!bop />\\n\",\n    \"html\": \"<p>&lt; a&gt;&lt;\\nfoo&gt;&lt;bar/ &gt;\\n&lt;foo bar=baz\\nbim!bop /&gt;</p>\\n\",\n    \"example\": 621,\n    \"start_line\": 9092,\n    \"end_line\": 9102,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href='bar'title=title>\\n\",\n    \"html\": \"<p>&lt;a href='bar'title=title&gt;</p>\\n\",\n    \"example\": 622,\n    \"start_line\": 9107,\n    \"end_line\": 9111,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"</a></foo >\\n\",\n    \"html\": \"<p></a></foo ></p>\\n\",\n    \"example\": 623,\n    \"start_line\": 9116,\n    \"end_line\": 9120,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"</a href=\\\"foo\\\">\\n\",\n    \"html\": \"<p>&lt;/a href=&quot;foo&quot;&gt;</p>\\n\",\n    \"example\": 624,\n    \"start_line\": 9125,\n    \"end_line\": 9129,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!-- this is a --\\ncomment - with hyphens -->\\n\",\n    \"html\": \"<p>foo <!-- this is a --\\ncomment - with hyphens --></p>\\n\",\n    \"example\": 625,\n    \"start_line\": 9134,\n    \"end_line\": 9140,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!--> foo -->\\n\\nfoo <!---> foo -->\\n\",\n    \"html\": \"<p>foo <!--> foo --&gt;</p>\\n<p>foo <!---> foo --&gt;</p>\\n\",\n    \"example\": 626,\n    \"start_line\": 9142,\n    \"end_line\": 9149,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <?php echo $a; ?>\\n\",\n    \"html\": \"<p>foo <?php echo $a; ?></p>\\n\",\n    \"example\": 627,\n    \"start_line\": 9154,\n    \"end_line\": 9158,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <!ELEMENT br EMPTY>\\n\",\n    \"html\": \"<p>foo <!ELEMENT br EMPTY></p>\\n\",\n    \"example\": 628,\n    \"start_line\": 9163,\n    \"end_line\": 9167,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <![CDATA[>&<]]>\\n\",\n    \"html\": \"<p>foo <![CDATA[>&<]]></p>\\n\",\n    \"example\": 629,\n    \"start_line\": 9172,\n    \"end_line\": 9176,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <a href=\\\"&ouml;\\\">\\n\",\n    \"html\": \"<p>foo <a href=\\\"&ouml;\\\"></p>\\n\",\n    \"example\": 630,\n    \"start_line\": 9182,\n    \"end_line\": 9186,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo <a href=\\\"\\\\*\\\">\\n\",\n    \"html\": \"<p>foo <a href=\\\"\\\\*\\\"></p>\\n\",\n    \"example\": 631,\n    \"start_line\": 9191,\n    \"end_line\": 9195,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"\\\\\\\"\\\">\\n\",\n    \"html\": \"<p>&lt;a href=&quot;&quot;&quot;&gt;</p>\\n\",\n    \"example\": 632,\n    \"start_line\": 9198,\n    \"end_line\": 9202,\n    \"section\": \"Raw HTML\"\n  },\n  {\n    \"markdown\": \"foo  \\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 633,\n    \"start_line\": 9212,\n    \"end_line\": 9218,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 634,\n    \"start_line\": 9224,\n    \"end_line\": 9230,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo       \\nbaz\\n\",\n    \"html\": \"<p>foo<br />\\nbaz</p>\\n\",\n    \"example\": 635,\n    \"start_line\": 9235,\n    \"end_line\": 9241,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo  \\n     bar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 636,\n    \"start_line\": 9246,\n    \"end_line\": 9252,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\n     bar\\n\",\n    \"html\": \"<p>foo<br />\\nbar</p>\\n\",\n    \"example\": 637,\n    \"start_line\": 9255,\n    \"end_line\": 9261,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"*foo  \\nbar*\\n\",\n    \"html\": \"<p><em>foo<br />\\nbar</em></p>\\n\",\n    \"example\": 638,\n    \"start_line\": 9267,\n    \"end_line\": 9273,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"*foo\\\\\\nbar*\\n\",\n    \"html\": \"<p><em>foo<br />\\nbar</em></p>\\n\",\n    \"example\": 639,\n    \"start_line\": 9276,\n    \"end_line\": 9282,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"`code  \\nspan`\\n\",\n    \"html\": \"<p><code>code   span</code></p>\\n\",\n    \"example\": 640,\n    \"start_line\": 9287,\n    \"end_line\": 9292,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"`code\\\\\\nspan`\\n\",\n    \"html\": \"<p><code>code\\\\ span</code></p>\\n\",\n    \"example\": 641,\n    \"start_line\": 9295,\n    \"end_line\": 9300,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo  \\nbar\\\">\\n\",\n    \"html\": \"<p><a href=\\\"foo  \\nbar\\\"></p>\\n\",\n    \"example\": 642,\n    \"start_line\": 9305,\n    \"end_line\": 9311,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"<a href=\\\"foo\\\\\\nbar\\\">\\n\",\n    \"html\": \"<p><a href=\\\"foo\\\\\\nbar\\\"></p>\\n\",\n    \"example\": 643,\n    \"start_line\": 9314,\n    \"end_line\": 9320,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\\\\\n\",\n    \"html\": \"<p>foo\\\\</p>\\n\",\n    \"example\": 644,\n    \"start_line\": 9327,\n    \"end_line\": 9331,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo  \\n\",\n    \"html\": \"<p>foo</p>\\n\",\n    \"example\": 645,\n    \"start_line\": 9334,\n    \"end_line\": 9338,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"### foo\\\\\\n\",\n    \"html\": \"<h3>foo\\\\</h3>\\n\",\n    \"example\": 646,\n    \"start_line\": 9341,\n    \"end_line\": 9345,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"### foo  \\n\",\n    \"html\": \"<h3>foo</h3>\\n\",\n    \"example\": 647,\n    \"start_line\": 9348,\n    \"end_line\": 9352,\n    \"section\": \"Hard line breaks\"\n  },\n  {\n    \"markdown\": \"foo\\nbaz\\n\",\n    \"html\": \"<p>foo\\nbaz</p>\\n\",\n    \"example\": 648,\n    \"start_line\": 9363,\n    \"end_line\": 9369,\n    \"section\": \"Soft line breaks\"\n  },\n  {\n    \"markdown\": \"foo \\n baz\\n\",\n    \"html\": \"<p>foo\\nbaz</p>\\n\",\n    \"example\": 649,\n    \"start_line\": 9375,\n    \"end_line\": 9381,\n    \"section\": \"Soft line breaks\"\n  },\n  {\n    \"markdown\": \"hello $.;'there\\n\",\n    \"html\": \"<p>hello $.;'there</p>\\n\",\n    \"example\": 650,\n    \"start_line\": 9395,\n    \"end_line\": 9399,\n    \"section\": \"Textual content\"\n  },\n  {\n    \"markdown\": \"Foo χρῆν\\n\",\n    \"html\": \"<p>Foo χρῆν</p>\\n\",\n    \"example\": 651,\n    \"start_line\": 9402,\n    \"end_line\": 9406,\n    \"section\": \"Textual content\"\n  },\n  {\n    \"markdown\": \"Multiple     spaces\\n\",\n    \"html\": \"<p>Multiple     spaces</p>\\n\",\n    \"example\": 652,\n    \"start_line\": 9411,\n    \"end_line\": 9415,\n    \"section\": \"Textual content\"\n  }\n]\n"
  },
  {
    "path": "packages/r/.Rbuildignore",
    "content": "^.*\\.Rproj$\n^\\.Rproj\\.user$\n^\\.lintr$\n^\\.github$\n^README\\.Rmd$\n^LICENSE\\.md$\n^cran-comments\\.md$\n^CRAN-SUBMISSION$\n^src/\\.cargo$\n^src/rust/\\.cargo$\n^src/rust/vendor$\n^src/rust/target$\n^src/Makevars$\n^src/Makevars\\.win$\n"
  },
  {
    "path": "packages/r/.gitignore",
    "content": "*.o\n*.so\n*.dll\n*.dylib\n*.tar.gz\nsrc/.cargo/\nsrc/rust/.cargo/\nsrc/rust/target/\nsrc/rust/vendor/\nsrc/rust/vendor.tar.xz\nsrc/Makevars\nsrc/Makevars.win\n"
  },
  {
    "path": "packages/r/.lintr",
    "content": "linters: linters_with_defaults(\n    line_length_linter(120),\n    object_name_linter(styles = \"snake_case\"),\n    object_usage_linter = NULL\n  )\nexclusions: list(\n    \"R/extendr-wrappers.R\"\n  )\n"
  },
  {
    "path": "packages/r/DESCRIPTION",
    "content": "Package: htmltomarkdown\nTitle: High-Performance HTML to Markdown Converter\nVersion: 3.4.0.9025\nAuthors@R: c(\n    person(\"Na'aman\", \"Hirschfeld\", email = \"nhirschfeld@gmail.com\", role = c(\"aut\", \"cre\")),\n    person(\"The authors of the dependency Rust crates\", role = \"ctb\",\n           comment = \"see inst/AUTHORS file for details\"))\nDescription: High-performance HTML to Markdown converter powered by a Rust core\n    engine via 'extendr'. Supports full conversion options, metadata extraction,\n    inline image extraction, and visitor-based conversion. Provides bindings to\n    the 'html-to-markdown' Rust library for native performance.\nLicense: MIT + file LICENSE\nURL: https://github.com/kreuzberg-dev/html-to-markdown\nBugReports: https://github.com/kreuzberg-dev/html-to-markdown/issues\nDepends: R (>= 4.2)\nSystemRequirements: Cargo (Rust's package manager), rustc (>= 1.85)\nEncoding: UTF-8\nRoxygen: list(markdown = TRUE)\nRoxygenNote: 7.3.3\nSuggests:\n    testthat (>= 3.0.0),\n    devtools,\n    lintr\nConfig/testthat/edition: 3\nConfig/htmltomarkdown/MSRV: 1.85.0\n"
  },
  {
    "path": "packages/r/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2024-2025 Na'aman Hirschfeld\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/r/NAMESPACE",
    "content": "# Generated by roxygen2: do not edit by hand\n\nexport(conversion_options)\nexport(convert)\nuseDynLib(htmltomarkdown, .registration = TRUE)\n"
  },
  {
    "path": "packages/r/R/extendr-wrappers.R",
    "content": "# Generated by extendr: Do not edit by hand\n\n#' @useDynLib htmltomarkdown, .registration = TRUE\nNULL\n\n#' Convert HTML to Markdown, returning a named list with content, metadata, tables, and warnings.\n#' @param html A character string of HTML content.\n#' @param options A named list of conversion options, or NULL for defaults.\n#' @return A named list with content (character or NULL), metadata (list), tables (list), warnings (list).\n#' @export\nconvert <- function(html, options = NULL) {\n  .Call(wrap__convert, html, options)\n}\n"
  },
  {
    "path": "packages/r/R/htmltomarkdown-package.R",
    "content": "#' @keywords internal\n#' @useDynLib htmltomarkdown, .registration = TRUE\n\"_PACKAGE\"\n"
  },
  {
    "path": "packages/r/R/htmltomarkdown.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:a841ecc71d153fa0efe5f7aa6ba36d09488e4212ada2aa78bb9fcf75ff83807e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\n#' @useDynLib htmltomarkdown, .registration = TRUE\nNULL\n\n#' Convert HTML to Markdown, returning a [`ConversionResult`] with content, metadata, images,\n#' and warnings.\n#'\n#' # Arguments\n#'\n#' * `html` — the HTML string to convert.\n#' * `options` — optional conversion options. Defaults to [`ConversionOptions::default`].\n#'   When the `visitor` feature is enabled, a custom [`crate::visitor::HtmlVisitor`] can be\n#'   attached via the `visitor` field on `ConversionOptions`.\n#'\n#' # Example\n#'\n#' ```\n#' use html_to_markdown_rs::convert;\n#'\n#' let html = \"<h1>Hello World</h1>\";\n#' let result = convert(html, None).unwrap();\n#' assert!(result.content.as_deref().unwrap_or(\"\").contains(\"Hello World\"));\n#' ```\n#'\n#' # Errors\n#'\n#' Returns an error if HTML parsing fails or if the input contains invalid UTF-8.\n#' @export\nconvert <- function(html, options = NULL) {\n  .Call(\"htm_convert\", html, options)\n}\n"
  },
  {
    "path": "packages/r/R/options.R",
    "content": "#' Create conversion options for html-to-markdown.\n#'\n#' Returns a named list of conversion options to pass to conversion functions.\n#' All parameters are optional; NULL values are omitted.\n#'\n#' @param heading_style Style for headings: \"atx\", \"atx_closed\", or \"underlined\".\n#' @param list_indent_type Indent type for lists: \"spaces\" or \"tabs\".\n#' @param list_indent_width Number of spaces/tabs per indent level.\n#' @param bullets Characters to use for bullet points.\n#' @param strong_em_symbol Character for strong/emphasis: \"*\" or \"_\".\n#' @param escape_asterisks Whether to escape asterisks.\n#' @param escape_underscores Whether to escape underscores.\n#' @param escape_misc Whether to escape miscellaneous characters.\n#' @param escape_ascii Whether to escape ASCII characters.\n#' @param code_language Default language for code blocks.\n#' @param encoding Input encoding (e.g., \"utf-8\").\n#' @param autolinks Whether to use autolinks for URLs.\n#' @param default_title Whether to use default title attributes.\n#' @param keep_inline_images_in Tags to preserve inline images in.\n#' @param br_in_tables Whether to use br tags in table cells.\n#' @param highlight_style Highlight style: \"double_equal\", \"html\", \"bold\", \"none\".\n#' @param extract_metadata Whether to extract metadata.\n#' @param whitespace_mode Whitespace handling: \"normalized\" or \"strict\".\n#' @param strip_newlines Whether to strip newlines.\n#' @param wrap Whether to wrap text.\n#' @param wrap_width Maximum line width for wrapping.\n#' @param strip_tags Tags to strip from output.\n#' @param preserve_tags Tags to preserve in output.\n#' @param convert_as_inline Whether to convert as inline.\n#' @param sub_symbol Symbol for subscript.\n#' @param sup_symbol Symbol for superscript.\n#' @param newline_style Newline style: \"spaces\" or \"backslash\".\n#' @param code_block_style Code block style: \"indented\", \"backticks\", or \"tildes\".\n#' @param preprocessing Named list with preprocessing options.\n#' @param debug Whether to enable debug output.\n#' @return A named list of conversion options.\n#' @export\nconversion_options <- function(\n  heading_style = NULL,\n  list_indent_type = NULL,\n  list_indent_width = NULL,\n  bullets = NULL,\n  strong_em_symbol = NULL,\n  escape_asterisks = NULL,\n  escape_underscores = NULL,\n  escape_misc = NULL,\n  escape_ascii = NULL,\n  code_language = NULL,\n  encoding = NULL,\n  autolinks = NULL,\n  default_title = NULL,\n  keep_inline_images_in = NULL,\n  br_in_tables = NULL,\n  highlight_style = NULL,\n  extract_metadata = NULL,\n  whitespace_mode = NULL,\n  strip_newlines = NULL,\n  wrap = NULL,\n  wrap_width = NULL,\n  strip_tags = NULL,\n  preserve_tags = NULL,\n  convert_as_inline = NULL,\n  sub_symbol = NULL,\n  sup_symbol = NULL,\n  newline_style = NULL,\n  code_block_style = NULL,\n  preprocessing = NULL,\n  debug = NULL\n) {\n  opts <- list()\n\n  if (!is.null(heading_style)) opts$heading_style <- heading_style\n  if (!is.null(list_indent_type)) opts$list_indent_type <- list_indent_type\n  if (!is.null(list_indent_width)) opts$list_indent_width <- as.integer(list_indent_width)\n  if (!is.null(bullets)) opts$bullets <- bullets\n  if (!is.null(strong_em_symbol)) opts$strong_em_symbol <- strong_em_symbol\n  if (!is.null(escape_asterisks)) opts$escape_asterisks <- escape_asterisks\n  if (!is.null(escape_underscores)) opts$escape_underscores <- escape_underscores\n  if (!is.null(escape_misc)) opts$escape_misc <- escape_misc\n  if (!is.null(escape_ascii)) opts$escape_ascii <- escape_ascii\n  if (!is.null(code_language)) opts$code_language <- code_language\n  if (!is.null(encoding)) opts$encoding <- encoding\n  if (!is.null(autolinks)) opts$autolinks <- autolinks\n  if (!is.null(default_title)) opts$default_title <- default_title\n  if (!is.null(keep_inline_images_in)) opts$keep_inline_images_in <- keep_inline_images_in\n  if (!is.null(br_in_tables)) opts$br_in_tables <- br_in_tables\n  if (!is.null(highlight_style)) opts$highlight_style <- highlight_style\n  if (!is.null(extract_metadata)) opts$extract_metadata <- extract_metadata\n  if (!is.null(whitespace_mode)) opts$whitespace_mode <- whitespace_mode\n  if (!is.null(strip_newlines)) opts$strip_newlines <- strip_newlines\n  if (!is.null(wrap)) opts$wrap <- wrap\n  if (!is.null(wrap_width)) opts$wrap_width <- as.integer(wrap_width)\n  if (!is.null(strip_tags)) opts$strip_tags <- strip_tags\n  if (!is.null(preserve_tags)) opts$preserve_tags <- preserve_tags\n  if (!is.null(convert_as_inline)) opts$convert_as_inline <- convert_as_inline\n  if (!is.null(sub_symbol)) opts$sub_symbol <- sub_symbol\n  if (!is.null(sup_symbol)) opts$sup_symbol <- sup_symbol\n  if (!is.null(newline_style)) opts$newline_style <- newline_style\n  if (!is.null(code_block_style)) opts$code_block_style <- code_block_style\n  if (!is.null(preprocessing)) opts$preprocessing <- preprocessing\n  if (!is.null(debug)) opts$debug <- debug\n\n  opts\n}\n"
  },
  {
    "path": "packages/r/R/version.R",
    "content": "# version() is defined in extendr-wrappers.R\n"
  },
  {
    "path": "packages/r/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter with R bindings powered by a Rust core via extendr.\nShip identical Markdown across every runtime while enjoying native performance with extendr bindings.\n\n## Installation\n\n```bash\ninstall.packages(\"htmltomarkdown\")\n```\n\nRequires R 4.3+ and a Rust toolchain (cargo, rustc).\n\n```r\ninstall.packages(\"htmltomarkdown\")\n```\n\nOr install the development version from GitHub:\n\n```r\ndevtools::install_github(\"kreuzberg-dev/html-to-markdown\", subdir = \"packages/r\")\n```\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document            | Size  | Latency | Throughput |\n| ------------------- | ----- | ------- | ---------- |\n| Lists (Timeline)    | 129KB | 0.68ms  | 190 MB/s   |\n| Tables (Countries)  | 360KB | 2.10ms  | 171 MB/s   |\n| Mixed (Python wiki) | 656KB | 4.75ms  | 138 MB/s   |\n\n## Quick Start\n\nBasic conversion:\n\n```r\nlibrary(htmltomarkdown)\n\nhtml <- \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult <- convert(html)\nmarkdown <- result$content\ncat(markdown)\n```\n\nWith conversion options:\n\n```r\nlibrary(htmltomarkdown)\n\nopts <- conversion_options(\n  heading_style = \"atx\",\n  wrap = TRUE,\n  wrap_width = 80L\n)\n\nresult <- convert(\"<h1>Hello</h1><p>World</p>\", opts)\ncat(result$content)\n```\n\n## API Reference\n\n### Core Function\n\n**`convert(html, options = NULL)`**\n\nConverts HTML to Markdown. Returns a named list `ConversionResult` with all results in a single call.\n\n```r\nresult   <- convert(html)\nmarkdown <- result$content    # Converted Markdown string\nmetadata <- result$metadata   # Metadata (when extract_metadata = TRUE)\ntables   <- result$tables     # Table data (when extract_tables = TRUE)\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```r\nhtml <- \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nresult <- convert(html, options = list(output_format = \"plain\"))\nplain <- result$content\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nopts <- conversion_options(extract_metadata = TRUE)\nresult <- convert(html, opts)\n\ncat(result$content)                    # Converted Markdown\nresult$metadata$document$title        # Document title\nresult$metadata$headers                # All h1-h6 elements\nresult$metadata$links                  # All hyperlinks\nresult$metadata$images                 # All images with alt text\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nopts <- conversion_options(extract_metadata = FALSE)\nresult <- convert(html, opts)\ncat(result$content)\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/r/cleanup",
    "content": "#!/usr/bin/env sh\nrm -f src/Makevars\n"
  },
  {
    "path": "packages/r/cleanup.win",
    "content": "#!/usr/bin/env sh\nrm -f src/Makevars.win\n"
  },
  {
    "path": "packages/r/configure",
    "content": "#!/usr/bin/env sh\n\n# Vendor html-to-markdown-rs core crate if vendor directory is not populated.\n#\n# Two strategies:\n#   1. Monorepo: if we're inside the html-to-markdown monorepo, vendor directly\n#      from the local crates/ directory (fast, used by CI and local dev).\n#   2. Download: fetch the source archive from GitHub for the matching version\n#      tag and run the vendoring script against the extracted tree.\n#      This is the path taken by r-universe and anyone installing from a\n#      source tarball built outside the monorepo.\n\nif [ ! -f \"src/rust/vendor/html-to-markdown-rs/Cargo.toml\" ]; then\n  # Strategy 1: monorepo\n  REPO_ROOT=\"$(cd ../.. 2>/dev/null && pwd)\"\n  VENDOR_SCRIPT=\"${REPO_ROOT}/scripts/ci/r/vendor-core-crate.py\"\n\n  if [ -f \"${VENDOR_SCRIPT}\" ] && [ -d \"${REPO_ROOT}/crates/html-to-markdown\" ]; then\n    echo \"* vendoring html-to-markdown-rs core crate from monorepo...\"\n    REPO_ROOT=\"${REPO_ROOT}\" python3 \"${VENDOR_SCRIPT}\" || {\n      echo \"ERROR: vendoring from monorepo failed.\"\n      exit 1\n    }\n  else\n    # Strategy 2: download source from GitHub\n    VERSION=\"$(grep '^Version:' DESCRIPTION | sed 's/Version:[[:space:]]*//')\"\n    GITHUB_URL=\"https://github.com/kreuzberg-dev/html-to-markdown/archive/refs/tags/v${VERSION}.tar.gz\"\n\n    echo \"* monorepo not detected — downloading html-to-markdown v${VERSION} source...\"\n\n    WORK=\"$(mktemp -d)\"\n    cleanup() { rm -rf \"${WORK}\"; }\n    trap cleanup EXIT\n\n    TARBALL=\"${WORK}/html-to-markdown-${VERSION}.tar.gz\"\n\n    if command -v curl >/dev/null 2>&1; then\n      curl -fsSL \"${GITHUB_URL}\" -o \"${TARBALL}\"\n    elif command -v wget >/dev/null 2>&1; then\n      wget -q \"${GITHUB_URL}\" -O \"${TARBALL}\"\n    else\n      echo \"ERROR: neither curl nor wget found. Cannot download source.\"\n      exit 1\n    fi\n\n    if [ ! -s \"${TARBALL}\" ]; then\n      echo \"ERROR: failed to download source from ${GITHUB_URL}\"\n      exit 1\n    fi\n\n    tar xzf \"${TARBALL}\" -C \"${WORK}\"\n    EXTRACTED=\"${WORK}/html-to-markdown-${VERSION}\"\n\n    if [ ! -d \"${EXTRACTED}/crates/html-to-markdown\" ]; then\n      echo \"ERROR: downloaded source does not contain expected crate structure.\"\n      exit 1\n    fi\n\n    echo \"* running vendoring script against downloaded source...\"\n    REPO_ROOT=\"${EXTRACTED}\" python3 \"${EXTRACTED}/scripts/ci/r/vendor-core-crate.py\" || {\n      echo \"ERROR: vendoring from downloaded source failed.\"\n      exit 1\n    }\n\n    VENDOR_SRC=\"${EXTRACTED}/packages/r/src/rust/vendor/html-to-markdown-rs\"\n    if [ ! -d \"${VENDOR_SRC}\" ]; then\n      echo \"ERROR: vendor directory was not created by vendoring script.\"\n      exit 1\n    fi\n\n    mkdir -p src/rust/vendor\n    cp -R \"${VENDOR_SRC}\" src/rust/vendor/html-to-markdown-rs\n    echo \"* vendoring complete.\"\n  fi\nfi\n\n: \"${R_HOME=$(R RHOME)}\"\n\"${R_HOME}/bin/Rscript\" tools/config.R\n"
  },
  {
    "path": "packages/r/configure.win",
    "content": "#!/usr/bin/env sh\n\n# Vendor html-to-markdown-rs core crate if vendor directory is not populated.\n#\n# Two strategies:\n#   1. Monorepo: if we're inside the html-to-markdown monorepo, vendor directly\n#      from the local crates/ directory (fast, used by CI and local dev).\n#   2. Download: fetch the source archive from GitHub for the matching version\n#      tag and run the vendoring script against the extracted tree.\n#      This is the path taken by r-universe and anyone installing from a\n#      source tarball built outside the monorepo.\n\nif [ ! -f \"src/rust/vendor/html-to-markdown-rs/Cargo.toml\" ]; then\n  # Strategy 1: monorepo\n  REPO_ROOT=\"$(cd ../.. 2>/dev/null && pwd)\"\n  VENDOR_SCRIPT=\"${REPO_ROOT}/scripts/ci/r/vendor-core-crate.py\"\n\n  if [ -f \"${VENDOR_SCRIPT}\" ] && [ -d \"${REPO_ROOT}/crates/html-to-markdown\" ]; then\n    echo \"* vendoring html-to-markdown-rs core crate from monorepo...\"\n    REPO_ROOT=\"${REPO_ROOT}\" python3 \"${VENDOR_SCRIPT}\" || {\n      echo \"ERROR: vendoring from monorepo failed.\"\n      exit 1\n    }\n  else\n    # Strategy 2: download source from GitHub\n    VERSION=\"$(grep '^Version:' DESCRIPTION | sed 's/Version:[[:space:]]*//')\"\n    GITHUB_URL=\"https://github.com/kreuzberg-dev/html-to-markdown/archive/refs/tags/v${VERSION}.tar.gz\"\n\n    echo \"* monorepo not detected — downloading html-to-markdown v${VERSION} source...\"\n\n    WORK=\"$(mktemp -d)\"\n    cleanup() { rm -rf \"${WORK}\"; }\n    trap cleanup EXIT\n\n    TARBALL=\"${WORK}/html-to-markdown-${VERSION}.tar.gz\"\n\n    if command -v curl >/dev/null 2>&1; then\n      curl -fsSL \"${GITHUB_URL}\" -o \"${TARBALL}\"\n    elif command -v wget >/dev/null 2>&1; then\n      wget -q \"${GITHUB_URL}\" -O \"${TARBALL}\"\n    else\n      echo \"ERROR: neither curl nor wget found. Cannot download source.\"\n      exit 1\n    fi\n\n    if [ ! -s \"${TARBALL}\" ]; then\n      echo \"ERROR: failed to download source from ${GITHUB_URL}\"\n      exit 1\n    fi\n\n    tar xzf \"${TARBALL}\" -C \"${WORK}\"\n    EXTRACTED=\"${WORK}/html-to-markdown-${VERSION}\"\n\n    if [ ! -d \"${EXTRACTED}/crates/html-to-markdown\" ]; then\n      echo \"ERROR: downloaded source does not contain expected crate structure.\"\n      exit 1\n    fi\n\n    echo \"* running vendoring script against downloaded source...\"\n    REPO_ROOT=\"${EXTRACTED}\" python3 \"${EXTRACTED}/scripts/ci/r/vendor-core-crate.py\" || {\n      echo \"ERROR: vendoring from downloaded source failed.\"\n      exit 1\n    }\n\n    VENDOR_SRC=\"${EXTRACTED}/packages/r/src/rust/vendor/html-to-markdown-rs\"\n    if [ ! -d \"${VENDOR_SRC}\" ]; then\n      echo \"ERROR: vendor directory was not created by vendoring script.\"\n      exit 1\n    fi\n\n    mkdir -p src/rust/vendor\n    cp -R \"${VENDOR_SRC}\" src/rust/vendor/html-to-markdown-rs\n    echo \"* vendoring complete.\"\n  fi\nfi\n\n\"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe\" tools/config.R\n"
  },
  {
    "path": "packages/r/inst/AUTHORS",
    "content": "# Authors of vendored Rust crates\n\nThis file lists the authors of the Rust crates vendored in this package.\n\nThe htmltomarkdown R package includes Rust code from the following crates:\n\naddr2line 0.25.1 (Apache-2.0 OR MIT): (no authors listed)\nadler2 2.0.1 (0BSD OR MIT OR Apache-2.0): Jonas Schievink <jonasschievink@gmail.com>, oyvindln <oyvindln@users.noreply.github.com>\nahash 0.8.12 (MIT OR Apache-2.0): Tom Kaitchuck <Tom.Kaitchuck@gmail.com>\naho-corasick 1.1.4 (Unlicense OR MIT): Andrew Gallant <jamslam@gmail.com>\naligned-vec 0.6.4 (MIT): sarah <>\nallocator-api2 0.2.21 (MIT OR Apache-2.0): Zakarum <zaq.dev@icloud.com>\nanyhow 1.0.102 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\narrayvec 0.7.6 (MIT OR Apache-2.0): bluss\nastral-tl 0.7.11 (MIT): y21\nasync-trait 0.1.89 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nautocfg 1.5.0 (Apache-2.0 OR MIT): Josh Stone <cuviper@gmail.com>\nbacktrace 0.3.76 (MIT OR Apache-2.0): The Rust Project Developers\nbase64 0.22.1 (MIT OR Apache-2.0): Marshall Pierce <marshall@mpierce.org>\nbitflags 2.11.0 (MIT OR Apache-2.0): The Rust Project Developers\nbitflags 1.3.2 (MIT/Apache-2.0): The Rust Project Developers\nbumpalo 3.20.2 (MIT OR Apache-2.0): Nick Fitzgerald <fitzgen@gmail.com>\nbytemuck 1.25.0 (Zlib OR Apache-2.0 OR MIT): Lokathor <zefria@gmail.com>\nbyteorder-lite 0.1.0 (Unlicense OR MIT): (no authors listed)\ncc 1.2.56 (MIT OR Apache-2.0): Alex Crichton <alex@alexcrichton.com>\ncfg-if 1.0.4 (MIT OR Apache-2.0): Alex Crichton <alex@alexcrichton.com>\ncolor_quant 1.1.0 (MIT): nwin <nwin@users.noreply.github.com>\ncpp_demangle 0.5.1 (MIT OR Apache-2.0): Nick Fitzgerald <fitzgen@gmail.com>, Jim Blandy <jimb@red-bean.com>, Kyle Huey <khuey@kylehuey.com>\ncrc32fast 1.5.0 (MIT OR Apache-2.0): Sam Rijs <srijs@airpost.net>, Alex Crichton <alex@alexcrichton.com>\ndebugid 0.8.0 (Apache-2.0): Sentry <hello@sentry.io>\nequator 0.4.2 (MIT): sarah <>\nequator-macro 0.4.2 (MIT): sarah <>\nequivalent 1.0.2 (Apache-2.0 OR MIT): (no authors listed)\nerrno 0.3.14 (MIT OR Apache-2.0): Chris Wong <lambda.fairy@gmail.com>, Dan Gohman <dev@sunfishcode.online>\nextendr-api 0.8.1 (MIT): andy-thomason <andy@andythomason.com>, Thomas Down, Mossa Merhi Reimert <mossa@sund.ku.dk>, Josiah Parry <josiah.parry@gmail.com>, Claus O. Wilke <wilke@austin.utexas.edu>, Hiroaki Yutani, Ilia A. Kosenkov <ilia.kosenkov@outlook.com>, Michael Milton <michael.r.milton@gmail.com>\nextendr-ffi 0.8.1 (MIT): andy-thomason <andy@andythomason.com>, Thomas Down, Mossa Merhi Reimert <mossa@sund.ku.dk>, Josiah Parry <josiah.parry@gmail.com>, Claus O. Wilke <wilke@austin.utexas.edu>, Hiroaki Yutani, Ilia A. Kosenkov <ilia.kosenkov@outlook.com>, Michael Milton <michael.r.milton@gmail.com>\nextendr-macros 0.8.1 (MIT): andy-thomason <andy@andythomason.com>, Thomas Down, Mossa Merhi Reimert <mossa@sund.ku.dk>, Josiah Parry <josiah.parry@gmail.com>, Claus O. Wilke <wilke@austin.utexas.edu>, Hiroaki Yutani, Ilia A. Kosenkov <ilia.kosenkov@outlook.com>, Michael Milton <michael.r.milton@gmail.com>\nfastrand 2.3.0 (Apache-2.0 OR MIT): Stjepan Glavina <stjepang@gmail.com>\nfdeflate 0.3.7 (MIT OR Apache-2.0): The image-rs Developers\nfind-msvc-tools 0.1.9 (MIT OR Apache-2.0): (no authors listed)\nfindshlibs 0.10.2 (MIT OR Apache-2.0): (no authors listed)\nflate2 1.1.9 (MIT OR Apache-2.0): Alex Crichton <alex@alexcrichton.com>, Josh Triplett <josh@joshtriplett.org>\nfoldhash 0.2.0 (Zlib): Orson Peters <orsonpeters@gmail.com>\nfoldhash 0.1.5 (Zlib): Orson Peters <orsonpeters@gmail.com>\nfutf 0.1.5 (MIT / Apache-2.0): Keegan McAllister <kmcallister@mozilla.com>\nfutures 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-channel 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-core 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-executor 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-io 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-macro 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-sink 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-task 0.3.32 (MIT OR Apache-2.0): (no authors listed)\nfutures-util 0.3.32 (MIT OR Apache-2.0): (no authors listed)\ngetrandom 0.4.1 (MIT OR Apache-2.0): The Rand Project Developers\ngetrandom 0.3.4 (MIT OR Apache-2.0): The Rand Project Developers\ngif 0.14.1 (MIT OR Apache-2.0): The image-rs Developers\ngimli 0.32.3 (MIT OR Apache-2.0): (no authors listed)\nhashbrown 0.16.1 (MIT OR Apache-2.0): Amanieu d'Antras <amanieu@gmail.com>\nhashbrown 0.15.5 (MIT OR Apache-2.0): Amanieu d'Antras <amanieu@gmail.com>\nheck 0.5.0 (MIT OR Apache-2.0): (no authors listed)\nhermit-abi 0.5.2 (MIT OR Apache-2.0): Stefan Lankes\nhtml-escape 0.2.13 (MIT): Magic Len <len@magiclen.org>\nhtml-to-markdown-rs 2.25.1 (MIT): Na'aman Hirschfeld <nhirschfeld@gmail.com>\nhtml5ever 0.36.1 (MIT OR Apache-2.0): The html5ever Project Developers\nid-arena 2.3.0 (MIT/Apache-2.0): Nick Fitzgerald <fitzgen@gmail.com>, Aleksey Kladov <aleksey.kladov@gmail.com>\nimage 0.25.9 (MIT OR Apache-2.0): The image-rs Developers\nimage-webp 0.2.4 (MIT OR Apache-2.0): (no authors listed)\nindexmap 2.13.0 (Apache-2.0 OR MIT): (no authors listed)\ninferno 0.11.21 (CDDL-1.0): Jon Gjengset <jon@thesquareplanet.com>\nis-terminal 0.4.17 (MIT): softprops <d.tangren@gmail.com>, Dan Gohman <dev@sunfishcode.online>\nitoa 1.0.17 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\njs-sys 0.3.89 (MIT OR Apache-2.0): The wasm-bindgen Developers\nlazy_static 1.5.0 (MIT OR Apache-2.0): Marvin Löbel <loebel.marvin@gmail.com>\nleb128fmt 0.1.0 (MIT OR Apache-2.0): Bryant Luk <code@bryantluk.com>\nlibc 0.2.182 (MIT OR Apache-2.0): The Rust Project Developers\nlinux-raw-sys 0.12.1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Dan Gohman <dev@sunfishcode.online>\nlock_api 0.4.14 (MIT OR Apache-2.0): Amanieu d'Antras <amanieu@gmail.com>\nlog 0.4.29 (MIT OR Apache-2.0): The Rust Project Developers\nlru 0.16.3 (MIT): Jerome Froelich <jeromefroelic@hotmail.com>\nmac 0.1.1 (MIT/Apache-2.0): Jonathan Reem <jonathan.reem@gmail.com>\nmarkup5ever 0.36.1 (MIT OR Apache-2.0): The html5ever Project Developers\nmemchr 2.8.0 (Unlicense OR MIT): Andrew Gallant <jamslam@gmail.com>, bluss\nmemmap2 0.9.10 (MIT OR Apache-2.0): Dan Burkert <dan@danburkert.com>, Yevhenii Reizner <razrfalcon@gmail.com>, The Contributors\nminiz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0): Frommi <daniil.liferenko@gmail.com>, oyvindln <oyvindln@users.noreply.github.com>, Rich Geldreich richgel99@gmail.com\nmoxcms 0.7.11 (BSD-3-Clause OR Apache-2.0): Radzivon Bartoshyk\nnew_debug_unreachable 1.0.6 (MIT): Matt Brubeck <mbrubeck@limpet.net>, Jonathan Reem <jonathan.reem@gmail.com>\nnix 0.26.4 (MIT): The nix-rust Project Developers\nnum-format 0.4.4 (MIT/Apache-2.0): Brian Myers <brian.carl.myers@gmail.com>\nnum-traits 0.2.19 (MIT OR Apache-2.0): The Rust Project Developers\nobject 0.37.3 (Apache-2.0 OR MIT): (no authors listed)\nonce_cell 1.21.3 (MIT OR Apache-2.0): Aleksey Kladov <aleksey.kladov@gmail.com>\nparking_lot 0.12.5 (MIT OR Apache-2.0): Amanieu d'Antras <amanieu@gmail.com>\nparking_lot_core 0.9.12 (MIT OR Apache-2.0): Amanieu d'Antras <amanieu@gmail.com>\npaste 1.0.15 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nphf 0.13.1 (MIT): Steven Fackler <sfackler@gmail.com>\nphf_codegen 0.13.1 (MIT): Steven Fackler <sfackler@gmail.com>\nphf_generator 0.13.1 (MIT): Steven Fackler <sfackler@gmail.com>\nphf_shared 0.13.1 (MIT): Steven Fackler <sfackler@gmail.com>\npin-project-lite 0.2.16 (Apache-2.0 OR MIT): (no authors listed)\npng 0.18.1 (MIT OR Apache-2.0): The image-rs Developers\npprof 0.15.0 (Apache-2.0): Yang Keao <keao.yang@yahoo.com>\nprecomputed-hash 0.1.1 (MIT): Emilio Cobos Álvarez <emilio@crisal.io>\nprettyplease 0.2.37 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nproc-macro2 1.0.106 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>, Alex Crichton <alex@alexcrichton.com>\npxfm 0.1.27 (BSD-3-Clause OR Apache-2.0): Radzivon Bartoshyk\nquick-error 2.0.1 (MIT/Apache-2.0): Paul Colomiets <paul@colomiets.name>, Colin Kiegel <kiegel@gmx.de>\nquick-xml 0.26.0 (MIT): (no authors listed)\nquote 1.0.44 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nr-efi 5.3.0 (MIT OR Apache-2.0 OR LGPL-2.1-or-later): (no authors listed)\nredox_syscall 0.5.18 (MIT): Jeremy Soller <jackpot51@gmail.com>\nregex 1.12.3 (MIT OR Apache-2.0): The Rust Project Developers, Andrew Gallant <jamslam@gmail.com>\nregex-automata 0.4.14 (MIT OR Apache-2.0): The Rust Project Developers, Andrew Gallant <jamslam@gmail.com>\nregex-syntax 0.8.10 (MIT OR Apache-2.0): The Rust Project Developers, Andrew Gallant <jamslam@gmail.com>\nrgb 0.8.52 (MIT): Kornel Lesiński <kornel@geekhood.net>, James Forster <james.forsterer@gmail.com>\nrustc-demangle 0.1.27 (MIT/Apache-2.0): Alex Crichton <alex@alexcrichton.com>\nrustix 1.1.4 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Dan Gohman <dev@sunfishcode.online>, Jakub Konka <kubkon@jakubkonka.com>\nrustversion 1.0.22 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nscopeguard 1.2.0 (MIT OR Apache-2.0): bluss\nsemver 1.0.27 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nserde 1.0.228 (MIT OR Apache-2.0): Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>\nserde_core 1.0.228 (MIT OR Apache-2.0): Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>\nserde_derive 1.0.228 (MIT OR Apache-2.0): Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>\nserde_json 1.0.149 (MIT OR Apache-2.0): Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>\nshlex 1.3.0 (MIT OR Apache-2.0): comex <comexk@gmail.com>, Fenhl <fenhl@fenhl.net>, Adrian Taylor <adetaylor@chromium.org>, Alex Touchet <alextouchet@outlook.com>, Daniel Parks <dp+git@oxidized.org>, Garrett Berg <googberg@gmail.com>\nsimd-adler32 0.3.8 (MIT): Marvin Countryman <me@maar.vin>\nsiphasher 1.0.2 (MIT/Apache-2.0): Frank Denis <github@pureftpd.org>\nslab 0.4.12 (MIT): Carl Lerche <me@carllerche.com>\nsmallvec 1.15.1 (MIT OR Apache-2.0): The Servo Project Developers\nspin 0.10.0 (MIT): Mathijs van de Nes <git@mathijs.vd-nes.nl>, John Ericson <git@JohnEricson.me>, Joshua Barretto <joshua.s.barretto@gmail.com>\nstable_deref_trait 1.2.1 (MIT OR Apache-2.0): Robert Grosse <n210241048576@gmail.com>\nstr_stack 0.1.0 (MIT/Apache-2.0): Steven Allen <steven@stebalien.com>\nstring_cache 0.9.0 (MIT OR Apache-2.0): The Servo Project Developers\nstring_cache_codegen 0.6.1 (MIT OR Apache-2.0): The Servo Project Developers\nsymbolic-common 12.17.2 (MIT): Armin Ronacher <armin.ronacher@active-4.com>, Jan Michael Auer <mail@jauer.org>\nsymbolic-demangle 12.17.2 (MIT): Armin Ronacher <armin.ronacher@active-4.com>, Jan Michael Auer <mail@jauer.org>\nsyn 2.0.117 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\ntempfile 3.26.0 (MIT OR Apache-2.0): Steven Allen <steven@stebalien.com>, The Rust Project Developers, Ashley Mannix <ashleymannix@live.com.au>, Jason White <me@jasonwhite.io>\ntendril 0.4.3 (MIT/Apache-2.0): Keegan McAllister <mcallister.keegan@gmail.com>, Simon Sapin <simon.sapin@exyr.org>, Chris Morgan <me@chrismorgan.info>\nthiserror 2.0.18 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\nthiserror-impl 2.0.18 (MIT OR Apache-2.0): David Tolnay <dtolnay@gmail.com>\ntokio 1.49.0 (MIT): Tokio Contributors <team@tokio.rs>\nunicode-ident 1.0.24 ((MIT OR Apache-2.0) AND Unicode-3.0): David Tolnay <dtolnay@gmail.com>\nunicode-xid 0.2.6 (MIT OR Apache-2.0): erick.tryzelaar <erick.tryzelaar@gmail.com>, kwantam <kwantam@gmail.com>, Manish Goregaokar <manishsmail@gmail.com>\nutf-8 0.7.6 (MIT OR Apache-2.0): Simon Sapin <simon.sapin@exyr.org>\nutf8-width 0.1.8 (MIT): Magic Len <len@magiclen.org>\nuuid 1.21.0 (Apache-2.0 OR MIT): Ashley Mannix<ashleymannix@live.com.au>, Dylan DPC<dylan.dpc@gmail.com>, Hunar Roop Kahlon<hunar.roop@gmail.com>\nversion_check 0.9.5 (MIT/Apache-2.0): Sergio Benitez <sb@sergio.bz>\nwasip2 1.0.1+wasi-0.2.4 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): (no authors listed)\nwasip3 0.4.0+wasi-0.3.0-rc-2026-01-06 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): (no authors listed)\nwasm-bindgen 0.2.112 (MIT OR Apache-2.0): The wasm-bindgen Developers\nwasm-bindgen-macro 0.2.112 (MIT OR Apache-2.0): The wasm-bindgen Developers\nwasm-bindgen-macro-support 0.2.112 (MIT OR Apache-2.0): The wasm-bindgen Developers\nwasm-bindgen-shared 0.2.112 (MIT OR Apache-2.0): The wasm-bindgen Developers\nwasm-encoder 0.244.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Nick Fitzgerald <fitzgen@gmail.com>\nwasm-metadata 0.244.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): (no authors listed)\nwasmparser 0.244.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Yury Delendik <ydelendik@mozilla.com>\nweb_atoms 0.2.3 (MIT OR Apache-2.0): The html5ever Project Developers\nweezl 0.1.12 (MIT OR Apache-2.0): The image-rs Developers\nwinapi 0.3.9 (MIT/Apache-2.0): Peter Atashian <retep998@gmail.com>\nwinapi-i686-pc-windows-gnu 0.4.0 (MIT/Apache-2.0): Peter Atashian <retep998@gmail.com>\nwinapi-x86_64-pc-windows-gnu 0.4.0 (MIT/Apache-2.0): Peter Atashian <retep998@gmail.com>\nwindows-link 0.2.1 (MIT OR Apache-2.0): (no authors listed)\nwindows-sys 0.61.2 (MIT OR Apache-2.0): (no authors listed)\nwit-bindgen 0.51.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nwit-bindgen 0.46.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nwit-bindgen-core 0.51.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nwit-bindgen-rust 0.51.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nwit-bindgen-rust-macro 0.51.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nwit-component 0.244.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Peter Huene <peter@huene.dev>\nwit-parser 0.244.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT): Alex Crichton <alex@alexcrichton.com>\nzerocopy 0.8.39 (BSD-2-Clause OR Apache-2.0 OR MIT): Joshua Liebow-Feeser <joshlf@google.com>, Jack Wrenn <jswrenn@amazon.com>\nzerocopy-derive 0.8.39 (BSD-2-Clause OR Apache-2.0 OR MIT): Joshua Liebow-Feeser <joshlf@google.com>, Jack Wrenn <jswrenn@amazon.com>\nzmij 1.0.21 (MIT): David Tolnay <dtolnay@gmail.com>\nzune-core 0.5.1 (MIT OR Apache-2.0 OR Zlib): (no authors listed)\nzune-jpeg 0.5.12 (MIT OR Apache-2.0 OR Zlib): caleb <etemesicaleb@gmail.com>\n"
  },
  {
    "path": "packages/r/man/conversion_options.Rd",
    "content": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/options.R\n\\name{conversion_options}\n\\alias{conversion_options}\n\\title{Create conversion options for html-to-markdown.}\n\\usage{\nconversion_options(\n  heading_style = NULL,\n  list_indent_type = NULL,\n  list_indent_width = NULL,\n  bullets = NULL,\n  strong_em_symbol = NULL,\n  escape_asterisks = NULL,\n  escape_underscores = NULL,\n  escape_misc = NULL,\n  escape_ascii = NULL,\n  code_language = NULL,\n  encoding = NULL,\n  autolinks = NULL,\n  default_title = NULL,\n  keep_inline_images_in = NULL,\n  br_in_tables = NULL,\n  hocr_spatial_tables = NULL,\n  highlight_style = NULL,\n  extract_metadata = NULL,\n  whitespace_mode = NULL,\n  strip_newlines = NULL,\n  wrap = NULL,\n  wrap_width = NULL,\n  strip_tags = NULL,\n  preserve_tags = NULL,\n  convert_as_inline = NULL,\n  sub_symbol = NULL,\n  sup_symbol = NULL,\n  newline_style = NULL,\n  code_block_style = NULL,\n  preprocessing = NULL,\n  debug = NULL\n)\n}\n\\arguments{\n\\item{heading_style}{Style for headings: \"atx\", \"atx_closed\", or \"underlined\".}\n\n\\item{list_indent_type}{Indent type for lists: \"spaces\" or \"tabs\".}\n\n\\item{list_indent_width}{Number of spaces/tabs per indent level.}\n\n\\item{bullets}{Characters to use for bullet points.}\n\n\\item{strong_em_symbol}{Character for strong/emphasis: \"*\" or \"_\".}\n\n\\item{escape_asterisks}{Whether to escape asterisks.}\n\n\\item{escape_underscores}{Whether to escape underscores.}\n\n\\item{escape_misc}{Whether to escape miscellaneous characters.}\n\n\\item{escape_ascii}{Whether to escape ASCII characters.}\n\n\\item{code_language}{Default language for code blocks.}\n\n\\item{encoding}{Input encoding (e.g., \"utf-8\").}\n\n\\item{autolinks}{Whether to use autolinks for URLs.}\n\n\\item{default_title}{Whether to use default title attributes.}\n\n\\item{keep_inline_images_in}{Tags to preserve inline images in.}\n\n\\item{br_in_tables}{Whether to use br tags in table cells.}\n\n\\item{hocr_spatial_tables}{Whether to use HOCR spatial table layout.}\n\n\\item{highlight_style}{Highlight style: \"double_equal\", \"html\", \"bold\", \"none\".}\n\n\\item{extract_metadata}{Whether to extract metadata.}\n\n\\item{whitespace_mode}{Whitespace handling: \"normalized\" or \"strict\".}\n\n\\item{strip_newlines}{Whether to strip newlines.}\n\n\\item{wrap}{Whether to wrap text.}\n\n\\item{wrap_width}{Maximum line width for wrapping.}\n\n\\item{strip_tags}{Tags to strip from output.}\n\n\\item{preserve_tags}{Tags to preserve in output.}\n\n\\item{convert_as_inline}{Whether to convert as inline.}\n\n\\item{sub_symbol}{Symbol for subscript.}\n\n\\item{sup_symbol}{Symbol for superscript.}\n\n\\item{newline_style}{Newline style: \"spaces\" or \"backslash\".}\n\n\\item{code_block_style}{Code block style: \"indented\", \"backticks\", or \"tildes\".}\n\n\\item{preprocessing}{Named list with preprocessing options.}\n\n\\item{debug}{Whether to enable debug output.}\n}\n\\value{\nA named list of conversion options.\n}\n\\description{\nReturns a named list of conversion options to pass to conversion functions.\nAll parameters are optional; NULL values are omitted.\n}\n"
  },
  {
    "path": "packages/r/man/convert.Rd",
    "content": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/extendr-wrappers.R\n\\name{convert}\n\\alias{convert}\n\\title{Convert HTML to Markdown.}\n\\usage{\nconvert(html)\n}\n\\arguments{\n\\item{html}{A character string of HTML content.}\n}\n\\value{\nA character string of Markdown content.\n}\n\\description{\nConvert HTML to Markdown.\n}\n"
  },
  {
    "path": "packages/r/man/htmltomarkdown-package.Rd",
    "content": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/extendr-wrappers.R, R/htmltomarkdown-package.R\n\\docType{package}\n\\name{htmltomarkdown-package}\n\\alias{htmltomarkdown-package}\n\\title{htmltomarkdown: High-Performance HTML to Markdown Converter}\n\\description{\nHigh-performance HTML to Markdown converter powered by a Rust core engine via 'extendr'. Supports full conversion options, metadata extraction, inline image extraction, and visitor-based conversion. Provides bindings to the 'html-to-markdown' Rust library for native performance.\n\nHigh-performance HTML to Markdown converter powered by a Rust core engine via 'extendr'. Supports full conversion options, metadata extraction, inline image extraction, and visitor-based conversion. Provides bindings to the 'html-to-markdown' Rust library for native performance.\n}\n\\seealso{\nUseful links:\n\\itemize{\n  \\item \\url{https://github.com/kreuzberg-dev/html-to-markdown}\n  \\item Report bugs at \\url{https://github.com/kreuzberg-dev/html-to-markdown/issues}\n}\n\n\nUseful links:\n\\itemize{\n  \\item \\url{https://github.com/kreuzberg-dev/html-to-markdown}\n  \\item Report bugs at \\url{https://github.com/kreuzberg-dev/html-to-markdown/issues}\n}\n\n}\n\\author{\n\\strong{Maintainer}: Na'aman Hirschfeld \\email{nhirschfeld@gmail.com}\n\nOther contributors:\n\\itemize{\n  \\item The authors of the dependency Rust crates (see inst/AUTHORS file for details) [contributor]\n}\n\n}\n\\keyword{internal}\n"
  },
  {
    "path": "packages/r/man/version.Rd",
    "content": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/extendr-wrappers.R\n\\name{version}\n\\alias{version}\n\\title{Get the version of the html-to-markdown Rust core.}\n\\usage{\nversion()\n}\n\\value{\nA character string with the version.\n}\n\\description{\nGet the version of the html-to-markdown Rust core.\n}\n"
  },
  {
    "path": "packages/r/src/Makevars.in",
    "content": "VENDORING = @VENDORING@\n\nTARGET_DIR = ./rust/target\nLIBDIR = $(TARGET_DIR)/@LIBDIR@\nSTATLIB = $(LIBDIR)/libhtml_to_markdown_r.a\nPKG_LIBS = -L$(LIBDIR) -lhtml_to_markdown_r\n\nall: $(SHLIB) rust_clean\n\n.PHONY: $(STATLIB)\n\n$(SHLIB): $(STATLIB)\n\nCARGOTMP = $(CURDIR)/rust/.cargo\n\n$(STATLIB):\n\trm -Rf $(CARGOTMP)\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then \\\n\t\ttar xf ./rust/vendor.tar.xz -C ./rust && \\\n\t\tmkdir -p $(CARGOTMP) && \\\n\t\tcp ./rust/vendor-config.toml $(CARGOTMP)/config.toml; \\\n\tfi\n\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then \\\n\t\texport CARGO_HOME=$(CARGOTMP) && \\\n\t\texport PATH=\"$(PATH):$(HOME)/.cargo/bin\" && \\\n\t\tcargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@; \\\n\telse \\\n\t\texport PATH=\"$(PATH):$(HOME)/.cargo/bin\" && \\\n\t\tcargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@; \\\n\tfi\n\n\trm -Rf $(CARGOTMP);\n\nrust_clean: $(SHLIB)\n\trm -Rf $(CARGOTMP) @CLEAN_TARGET@\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then rm -Rf ./rust/vendor; fi\n\nclean:\n\trm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR)\n"
  },
  {
    "path": "packages/r/src/Makevars.win.in",
    "content": "VENDORING = @VENDORING@\n\nTARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu\n\nTARGET_DIR = ./rust/target\nLIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@\nSTATLIB = $(LIBDIR)/libhtml_to_markdown_r.a\nPKG_LIBS = -L$(LIBDIR) -lhtml_to_markdown_r -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll\n\nall: $(SHLIB) rust_clean\n\n.PHONY: $(STATLIB)\n\n$(SHLIB): $(STATLIB)\n\nCARGOTMP = $(CURDIR)/rust/.cargo\n\n$(STATLIB):\n\tmkdir -p $(TARGET_DIR)/libgcc_mock\n\ttouch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a\n\n\trm -Rf $(CARGOTMP)\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then \\\n\t\ttar xf ./rust/vendor.tar.xz -C ./rust && \\\n\t\tmkdir -p $(CARGOTMP) && \\\n\t\tcp ./rust/vendor-config.toml $(CARGOTMP)/config.toml; \\\n\tfi\n\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then \\\n\t\texport CARGO_HOME=$(CARGOTMP) && \\\n\t\texport LIBRARY_PATH=\"$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock\" && \\\n\t\tcargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR); \\\n\telse \\\n\t\texport LIBRARY_PATH=\"$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock\" && \\\n\t\tcargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR); \\\n\tfi\n\n\trm -Rf $(CARGOTMP);\n\nrust_clean: $(SHLIB)\n\trm -Rf $(CARGOTMP) @CLEAN_TARGET@\n\tif [ \"$(VENDORING)\" = \"yes\" ]; then rm -Rf ./rust/vendor; fi\n\nclean:\n\trm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) ./rust/vendor\n"
  },
  {
    "path": "packages/r/src/entrypoint.c",
    "content": "// We need to forward routine registration from C to Rust\n// to avoid the linker removing the static library.\n\nvoid R_init_htmltomarkdown_extendr(void *dll);\n\nvoid R_init_htmltomarkdown(void *dll) {\n  R_init_htmltomarkdown_extendr(dll);\n}\n"
  },
  {
    "path": "packages/r/src/rust/Cargo.toml",
    "content": "[workspace]\n\n[package]\nname = \"html_to_markdown_r\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"extendr bindings for html-to-markdown\"\nrepository = \"https://github.com/kreuzberg-dev/html-to-markdown\"\n\n[lib]\nname = \"html_to_markdown_r\"\ncrate-type = [\"staticlib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"vendor/html-to-markdown-rs\", features = [\n  \"inline-images\",\n  \"metadata\",\n  \"visitor\",\n] }\nextendr-api = \"0.9\"\nserde_json = \"1.0\"\n\n[features]\ndefault = [\"metadata\"]\nmetadata = []\n\n[package.metadata.cargo-machete]\nignored = [\"extendr-api\", \"html-to-markdown-rs\"]\n"
  },
  {
    "path": "packages/r/src/rust/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:921733d1f98d9de16a42a4b1098acc2610eabecbf8ddfcfb716d27774ba8c9b3\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unused_unit,\n    clippy::unnecessary_cast,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions\n)]\n\nuse extendr_api::prelude::*;\nuse std::sync::Arc;\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct DocumentMetadata {\n    /// Document title from `<title>` tag\n    pub title: Option<String>,\n    /// Document description from `<meta name=\"description\">` tag\n    pub description: Option<String>,\n    /// Document keywords from `<meta name=\"keywords\">` tag, split on commas\n    pub keywords: Vec<String>,\n    /// Document author from `<meta name=\"author\">` tag\n    pub author: Option<String>,\n    /// Canonical URL from `<link rel=\"canonical\">` tag\n    pub canonical_url: Option<String>,\n    /// Base URL from `<base href=\"\">` tag for resolving relative URLs\n    pub base_href: Option<String>,\n    /// Document language from `lang` attribute\n    pub language: Option<String>,\n    /// Document text direction from `dir` attribute\n    pub text_direction: Option<TextDirection>,\n    /// Open Graph metadata (og:* properties) for social media\n    /// Keys like \"title\", \"description\", \"image\", \"url\", etc.\n    pub open_graph: HashMap<String, String>,\n    /// Twitter Card metadata (twitter:* properties)\n    /// Keys like \"card\", \"site\", \"creator\", \"title\", \"description\", \"image\", etc.\n    pub twitter_card: HashMap<String, String>,\n    /// Additional meta tags not covered by specific fields\n    /// Keys are meta name/property attributes, values are content\n    pub meta_tags: HashMap<String, String>,\n}\n\nimpl DocumentMetadata {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n\n    pub fn new(\n        keywords: Option<Vec<String>>,\n        open_graph: Option<HashMap<String, String>>,\n        twitter_card: Option<HashMap<String, String>>,\n        meta_tags: Option<HashMap<String, String>>,\n        title: Option<String>,\n        description: Option<String>,\n        author: Option<String>,\n        canonical_url: Option<String>,\n        base_href: Option<String>,\n        language: Option<String>,\n        text_direction: Option<TextDirection>,\n    ) -> Self {\n        Self {\n            title,\n            description,\n            keywords: keywords.unwrap_or_default(),\n            author,\n            canonical_url,\n            base_href,\n            language,\n            text_direction,\n            open_graph: open_graph.unwrap_or_default(),\n            twitter_card: twitter_card.unwrap_or_default(),\n            meta_tags: meta_tags.unwrap_or_default(),\n        }\n    }\n}\n\n#[extendr]\npub fn new_documentmetadata(\n    title: Option<String>,\n    description: Option<String>,\n    keywords: Option<Vec<String>>,\n    author: Option<String>,\n    canonical_url: Option<String>,\n    base_href: Option<String>,\n    language: Option<String>,\n    text_direction: Option<TextDirection>,\n    open_graph: Option<HashMap<String, String>>,\n    twitter_card: Option<HashMap<String, String>>,\n    meta_tags: Option<HashMap<String, String>>,\n) -> DocumentMetadata {\n    let mut __out = <DocumentMetadata>::default();\n    if let Some(v) = title {\n        __out.title = v;\n    }\n    if let Some(v) = description {\n        __out.description = v;\n    }\n    if let Some(v) = keywords {\n        __out.keywords = v;\n    }\n    if let Some(v) = author {\n        __out.author = v;\n    }\n    if let Some(v) = canonical_url {\n        __out.canonical_url = v;\n    }\n    if let Some(v) = base_href {\n        __out.base_href = v;\n    }\n    if let Some(v) = language {\n        __out.language = v;\n    }\n    if let Some(v) = text_direction {\n        __out.text_direction = v;\n    }\n    if let Some(v) = open_graph {\n        __out.open_graph = v;\n    }\n    if let Some(v) = twitter_card {\n        __out.twitter_card = v;\n    }\n    if let Some(v) = meta_tags {\n        __out.meta_tags = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct HeaderMetadata {\n    /// Header level: 1 (h1) through 6 (h6)\n    pub level: i32,\n    /// Normalized text content of the header\n    pub text: String,\n    /// HTML id attribute if present\n    pub id: Option<String>,\n    /// Document tree depth at the header element\n    pub depth: f64,\n    /// Byte offset in original HTML document\n    pub html_offset: f64,\n}\n\nimpl HeaderMetadata {\n    #[must_use]\n\n    pub fn new(level: i32, text: String, depth: f64, html_offset: f64, id: Option<String>) -> Self {\n        Self {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        }\n    }\n\n    pub fn is_valid(&self) -> bool {\n        let core_self = html_to_markdown_rs::metadata::HeaderMetadata {\n            level: self.level,\n            text: self.text.clone(),\n            id: self.id.clone(),\n            depth: self.depth,\n            html_offset: self.html_offset,\n        };\n        core_self.is_valid()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct LinkMetadata {\n    /// The href URL value\n    pub href: String,\n    /// Link text content (normalized, concatenated if mixed with elements)\n    pub text: String,\n    /// Optional title attribute (often shown as tooltip)\n    pub title: Option<String>,\n    /// Link type classification\n    pub link_type: LinkType,\n    /// Rel attribute values (e.g., \"nofollow\", \"stylesheet\", \"canonical\")\n    pub rel: Vec<String>,\n    /// Additional HTML attributes\n    pub attributes: HashMap<String, String>,\n}\n\nimpl LinkMetadata {\n    #[must_use]\n\n    pub fn new(\n        href: String,\n        text: String,\n        link_type: LinkType,\n        rel: Vec<String>,\n        attributes: HashMap<String, String>,\n        title: Option<String>,\n    ) -> Self {\n        Self {\n            href,\n            text,\n            title,\n            link_type,\n            rel,\n            attributes,\n        }\n    }\n\n    pub fn classify_link(href: String) -> LinkType {\n        html_to_markdown_rs::metadata::LinkMetadata::classify_link(&href).into()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ImageMetadata {\n    /// Image source (URL, data URI, or SVG content identifier)\n    pub src: String,\n    /// Alternative text from alt attribute (for accessibility)\n    pub alt: Option<String>,\n    /// Title attribute (often shown as tooltip)\n    pub title: Option<String>,\n    /// Image dimensions as (width, height) if available\n    pub dimensions: Option<Vec<i32>>,\n    /// Image type classification\n    pub image_type: ImageType,\n    /// Additional HTML attributes\n    pub attributes: HashMap<String, String>,\n}\n\nimpl ImageMetadata {\n    #[must_use]\n\n    pub fn new(\n        src: String,\n        image_type: ImageType,\n        attributes: HashMap<String, String>,\n        alt: Option<String>,\n        title: Option<String>,\n        dimensions: Option<Vec<i32>>,\n    ) -> Self {\n        Self {\n            src,\n            alt,\n            title,\n            dimensions,\n            image_type,\n            attributes,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct StructuredData {\n    /// Type of structured data (JSON-LD, Microdata, RDFa)\n    pub data_type: StructuredDataType,\n    /// Raw JSON string (for JSON-LD) or serialized representation\n    pub raw_json: String,\n    /// Schema type if detectable (e.g., \"Article\", \"Event\", \"Product\")\n    pub schema_type: Option<String>,\n}\n\nimpl StructuredData {\n    #[must_use]\n\n    pub fn new(data_type: StructuredDataType, raw_json: String, schema_type: Option<String>) -> Self {\n        Self {\n            data_type,\n            raw_json,\n            schema_type,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct HtmlMetadata {\n    /// Document-level metadata (title, description, canonical, etc.)\n    pub document: DocumentMetadata,\n    /// Extracted header elements with hierarchy\n    pub headers: Vec<HeaderMetadata>,\n    /// Extracted hyperlinks with type classification\n    pub links: Vec<LinkMetadata>,\n    /// Extracted images with source and dimensions\n    pub images: Vec<ImageMetadata>,\n    /// Extracted structured data blocks\n    pub structured_data: Vec<StructuredData>,\n}\n\nimpl HtmlMetadata {\n    #[must_use]\n\n    pub fn new(\n        document: Option<DocumentMetadata>,\n        headers: Option<Vec<HeaderMetadata>>,\n        links: Option<Vec<LinkMetadata>>,\n        images: Option<Vec<ImageMetadata>>,\n        structured_data: Option<Vec<StructuredData>>,\n    ) -> Self {\n        Self {\n            document: document.unwrap_or_default(),\n            headers: headers.unwrap_or_default(),\n            links: links.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            structured_data: structured_data.unwrap_or_default(),\n        }\n    }\n}\n\n#[extendr]\npub fn new_htmlmetadata(\n    document: Option<DocumentMetadata>,\n    headers: Option<Vec<HeaderMetadata>>,\n    links: Option<Vec<LinkMetadata>>,\n    images: Option<Vec<ImageMetadata>>,\n    structured_data: Option<Vec<StructuredData>>,\n) -> HtmlMetadata {\n    let mut __out = <HtmlMetadata>::default();\n    if let Some(v) = document {\n        __out.document = v;\n    }\n    if let Some(v) = headers {\n        __out.headers = v;\n    }\n    if let Some(v) = links {\n        __out.links = v;\n    }\n    if let Some(v) = images {\n        __out.images = v;\n    }\n    if let Some(v) = structured_data {\n        __out.structured_data = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[serde(default)]\npub struct ConversionOptions {\n    /// Heading style to use in Markdown output (ATX `#` or Setext underline).\n    pub heading_style: HeadingStyle,\n    /// How to indent nested list items (spaces or tab).\n    pub list_indent_type: ListIndentType,\n    /// Number of spaces (or tabs) to use for each level of list indentation.\n    pub list_indent_width: f64,\n    /// Bullet character(s) to use for unordered list items (e.g. `\"-\"`, `\"*\"`).\n    pub bullets: String,\n    /// Character used for bold/italic emphasis markers (`*` or `_`).\n    pub strong_em_symbol: String,\n    /// Escape `*` characters in plain text to avoid unintended bold/italic.\n    pub escape_asterisks: bool,\n    /// Escape `_` characters in plain text to avoid unintended bold/italic.\n    pub escape_underscores: bool,\n    /// Escape miscellaneous Markdown metacharacters (`[]()#` etc.) in plain text.\n    pub escape_misc: bool,\n    /// Escape ASCII characters that have special meaning in certain Markdown dialects.\n    pub escape_ascii: bool,\n    /// Default language annotation for fenced code blocks that have no language hint.\n    pub code_language: String,\n    /// Automatically convert bare URLs into Markdown autolinks.\n    pub autolinks: bool,\n    /// Emit a default title when no `<title>` tag is present.\n    pub default_title: bool,\n    /// Render `<br>` elements inside table cells as literal line breaks.\n    pub br_in_tables: bool,\n    /// Style used for `<mark>` / highlighted text (e.g. `==text==`).\n    pub highlight_style: HighlightStyle,\n    /// Extract `<meta>` and `<head>` information into the result metadata.\n    pub extract_metadata: bool,\n    /// Controls how whitespace is normalised during conversion.\n    pub whitespace_mode: WhitespaceMode,\n    /// Strip all newlines from the output, producing a single-line result.\n    pub strip_newlines: bool,\n    /// Wrap long lines at [`wrap_width`](Self::wrap_width) characters.\n    pub wrap: bool,\n    /// Maximum line width when [`wrap`](Self::wrap) is enabled (default `80`).\n    pub wrap_width: f64,\n    /// Treat the entire document as inline content (no block-level wrappers).\n    pub convert_as_inline: bool,\n    /// Markdown notation for subscript text (e.g. `\"~\"`).\n    pub sub_symbol: String,\n    /// Markdown notation for superscript text (e.g. `\"^\"`).\n    pub sup_symbol: String,\n    /// How to encode hard line breaks (`<br>`) in Markdown.\n    pub newline_style: NewlineStyle,\n    /// Style used for fenced code blocks (backticks or tilde).\n    pub code_block_style: CodeBlockStyle,\n    /// HTML tag names whose `<img>` children are kept inline instead of block.\n    pub keep_inline_images_in: Vec<String>,\n    /// Pre-processing options applied to the HTML before conversion.\n    pub preprocessing: PreprocessingOptions,\n    /// Expected character encoding of the input HTML (default `\"utf-8\"`).\n    pub encoding: String,\n    /// Emit debug information during conversion.\n    pub debug: bool,\n    /// HTML tag names whose content is stripped from the output entirely.\n    pub strip_tags: Vec<String>,\n    /// HTML tag names that are preserved verbatim in the output.\n    pub preserve_tags: Vec<String>,\n    /// Skip conversion of `<img>` elements (omit images from output).\n    pub skip_images: bool,\n    /// Link rendering style (inline or reference).\n    pub link_style: LinkStyle,\n    /// Target output format (Markdown, plain text, etc.).\n    pub output_format: OutputFormat,\n    /// Include structured document tree in result.\n    pub include_document_structure: bool,\n    /// Extract inline images from data URIs and SVGs.\n    pub extract_images: bool,\n    /// Maximum decoded image size in bytes (default 5MB).\n    pub max_image_size: f64,\n    /// Capture SVG elements as images.\n    pub capture_svg: bool,\n    /// Infer image dimensions from data.\n    pub infer_dimensions: bool,\n    /// Maximum DOM traversal depth. `None` means unlimited.\n    /// When set, subtrees beyond this depth are silently truncated.\n    pub max_depth: Option<f64>,\n    /// CSS selectors for elements to exclude entirely (element + all content).\n    ///\n    /// Unlike `strip_tags` (which removes the tag wrapper but keeps children),\n    /// excluded elements and all their descendants are dropped from the output.\n    /// Supports any CSS selector that `tl` supports: tag names, `.class`,\n    /// `#id`, `[attribute]`, etc.\n    ///\n    /// Invalid selectors are silently skipped at conversion time.\n    ///\n    /// Example: `vec![\".cookie-banner\".into(), \"#ad-container\".into(), \"[role='complementary']\".into()]`\n    pub exclude_selectors: Vec<String>,\n    /// Optional visitor for custom traversal logic.\n    ///\n    /// When set, the visitor's callbacks are invoked for matching HTML elements\n    /// during conversion, allowing custom output, skipping, or HTML preservation.\n    /// See [`crate::visitor::HtmlVisitor`].\n    #[serde(skip)]\n    pub visitor: Option<extendr_api::Robj>,\n}\n\nimpl ConversionOptions {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n\n    pub fn new(\n        heading_style: Option<HeadingStyle>,\n        list_indent_type: Option<ListIndentType>,\n        list_indent_width: Option<f64>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<HighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<f64>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<NewlineStyle>,\n        code_block_style: Option<CodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<PreprocessingOptions>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<LinkStyle>,\n        output_format: Option<OutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<f64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        exclude_selectors: Option<Vec<String>>,\n        max_depth: Option<f64>,\n        visitor: Option<VisitorHandle>,\n    ) -> Self {\n        Self {\n            heading_style: heading_style.unwrap_or_default(),\n            list_indent_type: list_indent_type.unwrap_or_default(),\n            list_indent_width: list_indent_width.unwrap_or(2),\n            bullets: bullets.unwrap_or_else(|| \"-*+\".to_string()),\n            strong_em_symbol: strong_em_symbol.unwrap_or_else(|| \"*\".to_string()),\n            escape_asterisks: escape_asterisks.unwrap_or(false),\n            escape_underscores: escape_underscores.unwrap_or(false),\n            escape_misc: escape_misc.unwrap_or(false),\n            escape_ascii: escape_ascii.unwrap_or(false),\n            code_language: code_language.unwrap_or_else(|| \"\".to_string()),\n            autolinks: autolinks.unwrap_or(true),\n            default_title: default_title.unwrap_or(false),\n            br_in_tables: br_in_tables.unwrap_or(false),\n            highlight_style: highlight_style.unwrap_or_default(),\n            extract_metadata: extract_metadata.unwrap_or(true),\n            whitespace_mode: whitespace_mode.unwrap_or_default(),\n            strip_newlines: strip_newlines.unwrap_or(false),\n            wrap: wrap.unwrap_or(false),\n            wrap_width: wrap_width.unwrap_or(80),\n            convert_as_inline: convert_as_inline.unwrap_or(false),\n            sub_symbol: sub_symbol.unwrap_or_else(|| \"\".to_string()),\n            sup_symbol: sup_symbol.unwrap_or_else(|| \"\".to_string()),\n            newline_style: newline_style.unwrap_or_default(),\n            code_block_style: code_block_style.unwrap_or_default(),\n            keep_inline_images_in: keep_inline_images_in.unwrap_or_default(),\n            preprocessing: preprocessing.unwrap_or_default(),\n            encoding: encoding.unwrap_or_else(|| \"utf-8\".to_string()),\n            debug: debug.unwrap_or(false),\n            strip_tags: strip_tags.unwrap_or_default(),\n            preserve_tags: preserve_tags.unwrap_or_default(),\n            skip_images: skip_images.unwrap_or(false),\n            link_style: link_style.unwrap_or_default(),\n            output_format: output_format.unwrap_or_default(),\n            include_document_structure: include_document_structure.unwrap_or(false),\n            extract_images: extract_images.unwrap_or(false),\n            max_image_size: max_image_size.unwrap_or(5242880),\n            capture_svg: capture_svg.unwrap_or(false),\n            infer_dimensions: infer_dimensions.unwrap_or(true),\n            max_depth,\n            exclude_selectors: exclude_selectors.unwrap_or_default(),\n            visitor,\n        }\n    }\n\n    pub fn apply_update(&self, update: ConversionOptionsUpdate) -> Self {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        #[allow(clippy::needless_update)]\n        let mut core_self = html_to_markdown_rs::options::ConversionOptions {\n            heading_style: self.heading_style.clone().into(),\n            list_indent_type: self.list_indent_type.clone().into(),\n            list_indent_width: self.list_indent_width,\n            bullets: self.bullets.clone(),\n            strong_em_symbol: self.strong_em_symbol.chars().next().unwrap_or('*'),\n            escape_asterisks: self.escape_asterisks,\n            escape_underscores: self.escape_underscores,\n            escape_misc: self.escape_misc,\n            escape_ascii: self.escape_ascii,\n            code_language: self.code_language.clone(),\n            autolinks: self.autolinks,\n            default_title: self.default_title,\n            br_in_tables: self.br_in_tables,\n            highlight_style: self.highlight_style.clone().into(),\n            extract_metadata: self.extract_metadata,\n            whitespace_mode: self.whitespace_mode.clone().into(),\n            strip_newlines: self.strip_newlines,\n            wrap: self.wrap,\n            wrap_width: self.wrap_width,\n            convert_as_inline: self.convert_as_inline,\n            sub_symbol: self.sub_symbol.clone(),\n            sup_symbol: self.sup_symbol.clone(),\n            newline_style: self.newline_style.clone().into(),\n            code_block_style: self.code_block_style.clone().into(),\n            keep_inline_images_in: self.keep_inline_images_in.clone(),\n            preprocessing: self.preprocessing.clone().into(),\n            encoding: self.encoding.clone(),\n            debug: self.debug,\n            strip_tags: self.strip_tags.clone(),\n            preserve_tags: self.preserve_tags.clone(),\n            skip_images: self.skip_images,\n            link_style: self.link_style.clone().into(),\n            output_format: self.output_format.clone().into(),\n            include_document_structure: self.include_document_structure,\n            extract_images: self.extract_images,\n            max_image_size: self.max_image_size,\n            capture_svg: self.capture_svg,\n            infer_dimensions: self.infer_dimensions,\n            max_depth: self.max_depth,\n            exclude_selectors: self.exclude_selectors.clone(),\n            visitor: self.visitor.clone().map(Into::into),\n            ..Default::default()\n        };\n        core_self.apply_update(update_core);\n        core_self.into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn default() -> ConversionOptions {\n        html_to_markdown_rs::options::ConversionOptions::default().into()\n    }\n\n    pub fn builder() -> ConversionOptionsBuilder {\n        ConversionOptionsBuilder {\n            inner: Arc::new(html_to_markdown_rs::options::ConversionOptions::builder()),\n        }\n    }\n\n    pub fn from_update(update: ConversionOptionsUpdate) -> ConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::options::ConversionOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn from(update: ConversionOptionsUpdate) -> ConversionOptions {\n        let update_core: html_to_markdown_rs::ConversionOptionsUpdate = update.into();\n        html_to_markdown_rs::options::ConversionOptions::from(update_core).into()\n    }\n}\n\n#[extendr]\npub fn new_conversionoptions(\n    heading_style: Option<HeadingStyle>,\n    list_indent_type: Option<ListIndentType>,\n    list_indent_width: Option<f64>,\n    bullets: Option<String>,\n    strong_em_symbol: Option<String>,\n    escape_asterisks: Option<bool>,\n    escape_underscores: Option<bool>,\n    escape_misc: Option<bool>,\n    escape_ascii: Option<bool>,\n    code_language: Option<String>,\n    autolinks: Option<bool>,\n    default_title: Option<bool>,\n    br_in_tables: Option<bool>,\n    highlight_style: Option<HighlightStyle>,\n    extract_metadata: Option<bool>,\n    whitespace_mode: Option<WhitespaceMode>,\n    strip_newlines: Option<bool>,\n    wrap: Option<bool>,\n    wrap_width: Option<f64>,\n    convert_as_inline: Option<bool>,\n    sub_symbol: Option<String>,\n    sup_symbol: Option<String>,\n    newline_style: Option<NewlineStyle>,\n    code_block_style: Option<CodeBlockStyle>,\n    keep_inline_images_in: Option<Vec<String>>,\n    preprocessing: Option<PreprocessingOptions>,\n    encoding: Option<String>,\n    debug: Option<bool>,\n    strip_tags: Option<Vec<String>>,\n    preserve_tags: Option<Vec<String>>,\n    skip_images: Option<bool>,\n    link_style: Option<LinkStyle>,\n    output_format: Option<OutputFormat>,\n    include_document_structure: Option<bool>,\n    extract_images: Option<bool>,\n    max_image_size: Option<f64>,\n    capture_svg: Option<bool>,\n    infer_dimensions: Option<bool>,\n    max_depth: Option<f64>,\n    exclude_selectors: Option<Vec<String>>,\n    visitor: Option<extendr_api::Robj>,\n) -> ConversionOptions {\n    let mut __out = <ConversionOptions>::default();\n    if let Some(v) = heading_style {\n        __out.heading_style = v;\n    }\n    if let Some(v) = list_indent_type {\n        __out.list_indent_type = v;\n    }\n    if let Some(v) = list_indent_width {\n        __out.list_indent_width = v;\n    }\n    if let Some(v) = bullets {\n        __out.bullets = v;\n    }\n    if let Some(v) = strong_em_symbol {\n        __out.strong_em_symbol = v;\n    }\n    if let Some(v) = escape_asterisks {\n        __out.escape_asterisks = v;\n    }\n    if let Some(v) = escape_underscores {\n        __out.escape_underscores = v;\n    }\n    if let Some(v) = escape_misc {\n        __out.escape_misc = v;\n    }\n    if let Some(v) = escape_ascii {\n        __out.escape_ascii = v;\n    }\n    if let Some(v) = code_language {\n        __out.code_language = v;\n    }\n    if let Some(v) = autolinks {\n        __out.autolinks = v;\n    }\n    if let Some(v) = default_title {\n        __out.default_title = v;\n    }\n    if let Some(v) = br_in_tables {\n        __out.br_in_tables = v;\n    }\n    if let Some(v) = highlight_style {\n        __out.highlight_style = v;\n    }\n    if let Some(v) = extract_metadata {\n        __out.extract_metadata = v;\n    }\n    if let Some(v) = whitespace_mode {\n        __out.whitespace_mode = v;\n    }\n    if let Some(v) = strip_newlines {\n        __out.strip_newlines = v;\n    }\n    if let Some(v) = wrap {\n        __out.wrap = v;\n    }\n    if let Some(v) = wrap_width {\n        __out.wrap_width = v;\n    }\n    if let Some(v) = convert_as_inline {\n        __out.convert_as_inline = v;\n    }\n    if let Some(v) = sub_symbol {\n        __out.sub_symbol = v;\n    }\n    if let Some(v) = sup_symbol {\n        __out.sup_symbol = v;\n    }\n    if let Some(v) = newline_style {\n        __out.newline_style = v;\n    }\n    if let Some(v) = code_block_style {\n        __out.code_block_style = v;\n    }\n    if let Some(v) = keep_inline_images_in {\n        __out.keep_inline_images_in = v;\n    }\n    if let Some(v) = preprocessing {\n        __out.preprocessing = v;\n    }\n    if let Some(v) = encoding {\n        __out.encoding = v;\n    }\n    if let Some(v) = debug {\n        __out.debug = v;\n    }\n    if let Some(v) = strip_tags {\n        __out.strip_tags = v;\n    }\n    if let Some(v) = preserve_tags {\n        __out.preserve_tags = v;\n    }\n    if let Some(v) = skip_images {\n        __out.skip_images = v;\n    }\n    if let Some(v) = link_style {\n        __out.link_style = v;\n    }\n    if let Some(v) = output_format {\n        __out.output_format = v;\n    }\n    if let Some(v) = include_document_structure {\n        __out.include_document_structure = v;\n    }\n    if let Some(v) = extract_images {\n        __out.extract_images = v;\n    }\n    if let Some(v) = max_image_size {\n        __out.max_image_size = v;\n    }\n    if let Some(v) = capture_svg {\n        __out.capture_svg = v;\n    }\n    if let Some(v) = infer_dimensions {\n        __out.infer_dimensions = v;\n    }\n    if let Some(v) = max_depth {\n        __out.max_depth = v;\n    }\n    if let Some(v) = exclude_selectors {\n        __out.exclude_selectors = v;\n    }\n    if let Some(v) = visitor {\n        __out.visitor = Some(v);\n    }\n    __out\n}\n\n#[derive(Clone)]\npub struct ConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\nimpl ConversionOptionsBuilder {\n    pub fn strip_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().strip_tags(tags)),\n        }\n    }\n\n    pub fn preserve_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().preserve_tags(tags)),\n        }\n    }\n\n    pub fn keep_inline_images_in(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    pub fn exclude_selectors(&self, selectors: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().exclude_selectors(selectors)),\n        }\n    }\n\n    pub fn visitor(&self, visitor: Option<VisitorHandle>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new((*self.inner).clone().visitor(visitor.as_ref().map(|v| &v.inner))),\n        }\n    }\n\n    pub fn preprocessing(&self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder {\n        let preprocessing_core: html_to_markdown_rs::PreprocessingOptions = preprocessing.into();\n        Self {\n            inner: Arc::new((*self.inner).clone().preprocessing(preprocessing_core)),\n        }\n    }\n\n    pub fn build(&self) -> ConversionOptions {\n        (*self.inner).clone().build().into()\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::similar_names)]\npub struct ConversionOptionsUpdate {\n    /// Optional override for [`ConversionOptions::heading_style`].\n    pub heading_style: Option<HeadingStyle>,\n    /// Optional override for [`ConversionOptions::list_indent_type`].\n    pub list_indent_type: Option<ListIndentType>,\n    /// Optional override for [`ConversionOptions::list_indent_width`].\n    pub list_indent_width: Option<f64>,\n    /// Optional override for [`ConversionOptions::bullets`].\n    pub bullets: Option<String>,\n    /// Optional override for [`ConversionOptions::strong_em_symbol`].\n    pub strong_em_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::escape_asterisks`].\n    pub escape_asterisks: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_underscores`].\n    pub escape_underscores: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_misc`].\n    pub escape_misc: Option<bool>,\n    /// Optional override for [`ConversionOptions::escape_ascii`].\n    pub escape_ascii: Option<bool>,\n    /// Optional override for [`ConversionOptions::code_language`].\n    pub code_language: Option<String>,\n    /// Optional override for [`ConversionOptions::autolinks`].\n    pub autolinks: Option<bool>,\n    /// Optional override for [`ConversionOptions::default_title`].\n    pub default_title: Option<bool>,\n    /// Optional override for [`ConversionOptions::br_in_tables`].\n    pub br_in_tables: Option<bool>,\n    /// Optional override for [`ConversionOptions::highlight_style`].\n    pub highlight_style: Option<HighlightStyle>,\n    /// Optional override for [`ConversionOptions::extract_metadata`].\n    pub extract_metadata: Option<bool>,\n    /// Optional override for [`ConversionOptions::whitespace_mode`].\n    pub whitespace_mode: Option<WhitespaceMode>,\n    /// Optional override for [`ConversionOptions::strip_newlines`].\n    pub strip_newlines: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap`].\n    pub wrap: Option<bool>,\n    /// Optional override for [`ConversionOptions::wrap_width`].\n    pub wrap_width: Option<f64>,\n    /// Optional override for [`ConversionOptions::convert_as_inline`].\n    pub convert_as_inline: Option<bool>,\n    /// Optional override for [`ConversionOptions::sub_symbol`].\n    pub sub_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::sup_symbol`].\n    pub sup_symbol: Option<String>,\n    /// Optional override for [`ConversionOptions::newline_style`].\n    pub newline_style: Option<NewlineStyle>,\n    /// Optional override for [`ConversionOptions::code_block_style`].\n    pub code_block_style: Option<CodeBlockStyle>,\n    /// Optional override for [`ConversionOptions::keep_inline_images_in`].\n    pub keep_inline_images_in: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preprocessing`].\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    /// Optional override for [`ConversionOptions::encoding`].\n    pub encoding: Option<String>,\n    /// Optional override for [`ConversionOptions::debug`].\n    pub debug: Option<bool>,\n    /// Optional override for [`ConversionOptions::strip_tags`].\n    pub strip_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::preserve_tags`].\n    pub preserve_tags: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::skip_images`].\n    pub skip_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::link_style`].\n    pub link_style: Option<LinkStyle>,\n    /// Optional override for [`ConversionOptions::output_format`].\n    pub output_format: Option<OutputFormat>,\n    /// Optional override for [`ConversionOptions::include_document_structure`].\n    pub include_document_structure: Option<bool>,\n    /// Optional override for [`ConversionOptions::extract_images`].\n    pub extract_images: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_image_size`].\n    pub max_image_size: Option<f64>,\n    /// Optional override for [`ConversionOptions::capture_svg`].\n    pub capture_svg: Option<bool>,\n    /// Optional override for [`ConversionOptions::infer_dimensions`].\n    pub infer_dimensions: Option<bool>,\n    /// Optional override for [`ConversionOptions::max_depth`].\n    pub max_depth: Option<f64>,\n    /// Optional override for [`ConversionOptions::exclude_selectors`].\n    pub exclude_selectors: Option<Vec<String>>,\n    /// Optional override for [`ConversionOptions::visitor`].\n    pub visitor: Option<VisitorHandle>,\n}\n\nimpl ConversionOptionsUpdate {\n    #[allow(clippy::too_many_arguments)]\n    #[must_use]\n\n    pub fn new(\n        heading_style: Option<HeadingStyle>,\n        list_indent_type: Option<ListIndentType>,\n        list_indent_width: Option<f64>,\n        bullets: Option<String>,\n        strong_em_symbol: Option<String>,\n        escape_asterisks: Option<bool>,\n        escape_underscores: Option<bool>,\n        escape_misc: Option<bool>,\n        escape_ascii: Option<bool>,\n        code_language: Option<String>,\n        autolinks: Option<bool>,\n        default_title: Option<bool>,\n        br_in_tables: Option<bool>,\n        highlight_style: Option<HighlightStyle>,\n        extract_metadata: Option<bool>,\n        whitespace_mode: Option<WhitespaceMode>,\n        strip_newlines: Option<bool>,\n        wrap: Option<bool>,\n        wrap_width: Option<f64>,\n        convert_as_inline: Option<bool>,\n        sub_symbol: Option<String>,\n        sup_symbol: Option<String>,\n        newline_style: Option<NewlineStyle>,\n        code_block_style: Option<CodeBlockStyle>,\n        keep_inline_images_in: Option<Vec<String>>,\n        preprocessing: Option<PreprocessingOptionsUpdate>,\n        encoding: Option<String>,\n        debug: Option<bool>,\n        strip_tags: Option<Vec<String>>,\n        preserve_tags: Option<Vec<String>>,\n        skip_images: Option<bool>,\n        link_style: Option<LinkStyle>,\n        output_format: Option<OutputFormat>,\n        include_document_structure: Option<bool>,\n        extract_images: Option<bool>,\n        max_image_size: Option<f64>,\n        capture_svg: Option<bool>,\n        infer_dimensions: Option<bool>,\n        max_depth: Option<f64>,\n        exclude_selectors: Option<Vec<String>>,\n        visitor: Option<VisitorHandle>,\n    ) -> Self {\n        Self {\n            heading_style,\n            list_indent_type,\n            list_indent_width,\n            bullets,\n            strong_em_symbol,\n            escape_asterisks,\n            escape_underscores,\n            escape_misc,\n            escape_ascii,\n            code_language,\n            autolinks,\n            default_title,\n            br_in_tables,\n            highlight_style,\n            extract_metadata,\n            whitespace_mode,\n            strip_newlines,\n            wrap,\n            wrap_width,\n            convert_as_inline,\n            sub_symbol,\n            sup_symbol,\n            newline_style,\n            code_block_style,\n            keep_inline_images_in,\n            preprocessing,\n            encoding,\n            debug,\n            strip_tags,\n            preserve_tags,\n            skip_images,\n            link_style,\n            output_format,\n            include_document_structure,\n            extract_images,\n            max_image_size,\n            capture_svg,\n            infer_dimensions,\n            max_depth,\n            exclude_selectors,\n            visitor,\n        }\n    }\n}\n\n#[extendr]\npub fn new_conversionoptionsupdate(\n    heading_style: Option<HeadingStyle>,\n    list_indent_type: Option<ListIndentType>,\n    list_indent_width: Option<f64>,\n    bullets: Option<String>,\n    strong_em_symbol: Option<String>,\n    escape_asterisks: Option<bool>,\n    escape_underscores: Option<bool>,\n    escape_misc: Option<bool>,\n    escape_ascii: Option<bool>,\n    code_language: Option<String>,\n    autolinks: Option<bool>,\n    default_title: Option<bool>,\n    br_in_tables: Option<bool>,\n    highlight_style: Option<HighlightStyle>,\n    extract_metadata: Option<bool>,\n    whitespace_mode: Option<WhitespaceMode>,\n    strip_newlines: Option<bool>,\n    wrap: Option<bool>,\n    wrap_width: Option<f64>,\n    convert_as_inline: Option<bool>,\n    sub_symbol: Option<String>,\n    sup_symbol: Option<String>,\n    newline_style: Option<NewlineStyle>,\n    code_block_style: Option<CodeBlockStyle>,\n    keep_inline_images_in: Option<Vec<String>>,\n    preprocessing: Option<PreprocessingOptionsUpdate>,\n    encoding: Option<String>,\n    debug: Option<bool>,\n    strip_tags: Option<Vec<String>>,\n    preserve_tags: Option<Vec<String>>,\n    skip_images: Option<bool>,\n    link_style: Option<LinkStyle>,\n    output_format: Option<OutputFormat>,\n    include_document_structure: Option<bool>,\n    extract_images: Option<bool>,\n    max_image_size: Option<f64>,\n    capture_svg: Option<bool>,\n    infer_dimensions: Option<bool>,\n    max_depth: Option<Option<f64>>,\n    exclude_selectors: Option<Vec<String>>,\n    visitor: Option<VisitorHandle>,\n) -> ConversionOptionsUpdate {\n    let mut __out = <ConversionOptionsUpdate>::default();\n    if let Some(v) = heading_style {\n        __out.heading_style = v;\n    }\n    if let Some(v) = list_indent_type {\n        __out.list_indent_type = v;\n    }\n    if let Some(v) = list_indent_width {\n        __out.list_indent_width = v;\n    }\n    if let Some(v) = bullets {\n        __out.bullets = v;\n    }\n    if let Some(v) = strong_em_symbol {\n        __out.strong_em_symbol = v;\n    }\n    if let Some(v) = escape_asterisks {\n        __out.escape_asterisks = v;\n    }\n    if let Some(v) = escape_underscores {\n        __out.escape_underscores = v;\n    }\n    if let Some(v) = escape_misc {\n        __out.escape_misc = v;\n    }\n    if let Some(v) = escape_ascii {\n        __out.escape_ascii = v;\n    }\n    if let Some(v) = code_language {\n        __out.code_language = v;\n    }\n    if let Some(v) = autolinks {\n        __out.autolinks = v;\n    }\n    if let Some(v) = default_title {\n        __out.default_title = v;\n    }\n    if let Some(v) = br_in_tables {\n        __out.br_in_tables = v;\n    }\n    if let Some(v) = highlight_style {\n        __out.highlight_style = v;\n    }\n    if let Some(v) = extract_metadata {\n        __out.extract_metadata = v;\n    }\n    if let Some(v) = whitespace_mode {\n        __out.whitespace_mode = v;\n    }\n    if let Some(v) = strip_newlines {\n        __out.strip_newlines = v;\n    }\n    if let Some(v) = wrap {\n        __out.wrap = v;\n    }\n    if let Some(v) = wrap_width {\n        __out.wrap_width = v;\n    }\n    if let Some(v) = convert_as_inline {\n        __out.convert_as_inline = v;\n    }\n    if let Some(v) = sub_symbol {\n        __out.sub_symbol = v;\n    }\n    if let Some(v) = sup_symbol {\n        __out.sup_symbol = v;\n    }\n    if let Some(v) = newline_style {\n        __out.newline_style = v;\n    }\n    if let Some(v) = code_block_style {\n        __out.code_block_style = v;\n    }\n    if let Some(v) = keep_inline_images_in {\n        __out.keep_inline_images_in = v;\n    }\n    if let Some(v) = preprocessing {\n        __out.preprocessing = v;\n    }\n    if let Some(v) = encoding {\n        __out.encoding = v;\n    }\n    if let Some(v) = debug {\n        __out.debug = v;\n    }\n    if let Some(v) = strip_tags {\n        __out.strip_tags = v;\n    }\n    if let Some(v) = preserve_tags {\n        __out.preserve_tags = v;\n    }\n    if let Some(v) = skip_images {\n        __out.skip_images = v;\n    }\n    if let Some(v) = link_style {\n        __out.link_style = v;\n    }\n    if let Some(v) = output_format {\n        __out.output_format = v;\n    }\n    if let Some(v) = include_document_structure {\n        __out.include_document_structure = v;\n    }\n    if let Some(v) = extract_images {\n        __out.extract_images = v;\n    }\n    if let Some(v) = max_image_size {\n        __out.max_image_size = v;\n    }\n    if let Some(v) = capture_svg {\n        __out.capture_svg = v;\n    }\n    if let Some(v) = infer_dimensions {\n        __out.infer_dimensions = v;\n    }\n    if let Some(v) = max_depth {\n        __out.max_depth = v;\n    }\n    if let Some(v) = exclude_selectors {\n        __out.exclude_selectors = v;\n    }\n    if let Some(v) = visitor {\n        __out.visitor = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct PreprocessingOptions {\n    /// Enable HTML preprocessing globally\n    pub enabled: bool,\n    /// Preprocessing preset level (Minimal, Standard, Aggressive)\n    pub preset: PreprocessingPreset,\n    /// Remove navigation elements (nav, breadcrumbs, menus, sidebars)\n    pub remove_navigation: bool,\n    /// Remove form elements (forms, inputs, buttons, etc.)\n    pub remove_forms: bool,\n}\n\nimpl PreprocessingOptions {\n    #[must_use]\n\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled: enabled.unwrap_or(true),\n            preset: preset.unwrap_or_default(),\n            remove_navigation: remove_navigation.unwrap_or(true),\n            remove_forms: remove_forms.unwrap_or(true),\n        }\n    }\n\n    pub fn apply_update(&self, update: PreprocessingOptionsUpdate) -> Self {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        let mut core_self = html_to_markdown_rs::options::PreprocessingOptions {\n            enabled: self.enabled,\n            preset: self.preset.clone().into(),\n            remove_navigation: self.remove_navigation,\n            remove_forms: self.remove_forms,\n        };\n        core_self.apply_update(update_core);\n        core_self.into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn default() -> PreprocessingOptions {\n        html_to_markdown_rs::options::PreprocessingOptions::default().into()\n    }\n\n    pub fn from_update(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::options::PreprocessingOptions::from_update(update_core).into()\n    }\n\n    #[allow(clippy::should_implement_trait)]\n    pub fn from(update: PreprocessingOptionsUpdate) -> PreprocessingOptions {\n        let update_core: html_to_markdown_rs::PreprocessingOptionsUpdate = update.into();\n        html_to_markdown_rs::options::PreprocessingOptions::from(update_core).into()\n    }\n}\n\n#[extendr]\npub fn new_preprocessingoptions(\n    enabled: Option<bool>,\n    preset: Option<PreprocessingPreset>,\n    remove_navigation: Option<bool>,\n    remove_forms: Option<bool>,\n) -> PreprocessingOptions {\n    let mut __out = <PreprocessingOptions>::default();\n    if let Some(v) = enabled {\n        __out.enabled = v;\n    }\n    if let Some(v) = preset {\n        __out.preset = v;\n    }\n    if let Some(v) = remove_navigation {\n        __out.remove_navigation = v;\n    }\n    if let Some(v) = remove_forms {\n        __out.remove_forms = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct PreprocessingOptionsUpdate {\n    /// Optional global preprocessing enablement override\n    pub enabled: Option<bool>,\n    /// Optional preprocessing preset level override (Minimal, Standard, Aggressive)\n    pub preset: Option<PreprocessingPreset>,\n    /// Optional navigation element removal override (nav, breadcrumbs, menus, sidebars)\n    pub remove_navigation: Option<bool>,\n    /// Optional form element removal override (forms, inputs, buttons, etc.)\n    pub remove_forms: Option<bool>,\n}\n\nimpl PreprocessingOptionsUpdate {\n    #[must_use]\n\n    pub fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled,\n            preset,\n            remove_navigation,\n            remove_forms,\n        }\n    }\n}\n\n#[extendr]\npub fn new_preprocessingoptionsupdate(\n    enabled: Option<bool>,\n    preset: Option<PreprocessingPreset>,\n    remove_navigation: Option<bool>,\n    remove_forms: Option<bool>,\n) -> PreprocessingOptionsUpdate {\n    let mut __out = <PreprocessingOptionsUpdate>::default();\n    if let Some(v) = enabled {\n        __out.enabled = v;\n    }\n    if let Some(v) = preset {\n        __out.preset = v;\n    }\n    if let Some(v) = remove_navigation {\n        __out.remove_navigation = v;\n    }\n    if let Some(v) = remove_forms {\n        __out.remove_forms = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct DocumentStructure {\n    /// All nodes in document reading order.\n    pub nodes: Vec<DocumentNode>,\n    /// The source format (always \"html\" for this crate).\n    pub source_format: Option<String>,\n}\n\nimpl DocumentStructure {\n    #[must_use]\n\n    pub fn new(nodes: Vec<DocumentNode>, source_format: Option<String>) -> Self {\n        Self { nodes, source_format }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct DocumentNode {\n    /// Deterministic node identifier.\n    pub id: String,\n    /// The semantic content of this node.\n    pub content: NodeContent,\n    /// Index of the parent node (None for root nodes).\n    pub parent: Option<i32>,\n    /// Indices of child nodes in reading order.\n    pub children: Vec<i32>,\n    /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.\n    pub annotations: Vec<TextAnnotation>,\n    /// Format-specific attributes (e.g. class, id, data-* attributes).\n    pub attributes: Option<HashMap<String, String>>,\n}\n\nimpl DocumentNode {\n    #[must_use]\n\n    pub fn new(\n        id: String,\n        content: NodeContent,\n        children: Vec<i32>,\n        annotations: Vec<TextAnnotation>,\n        parent: Option<i32>,\n        attributes: Option<HashMap<String, String>>,\n    ) -> Self {\n        Self {\n            id,\n            content,\n            parent,\n            children,\n            annotations,\n            attributes,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct TextAnnotation {\n    /// Start byte offset (inclusive) into the parent node's text.\n    pub start: i32,\n    /// End byte offset (exclusive) into the parent node's text.\n    pub end: i32,\n    /// The type of annotation.\n    pub kind: AnnotationKind,\n}\n\nimpl TextAnnotation {\n    #[must_use]\n\n    pub fn new(start: i32, end: i32, kind: AnnotationKind) -> Self {\n        Self { start, end, kind }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ConversionResult {\n    /// Converted text output (markdown, djot, or plain text).\n    ///\n    /// `None` when `output_format` is set to `OutputFormat::None`,\n    /// indicating extraction-only mode.\n    pub content: Option<String>,\n    /// Structured document tree with semantic elements.\n    ///\n    /// Populated when `include_document_structure` is `true` in options.\n    pub document: Option<DocumentStructure>,\n    /// Extracted HTML metadata (title, OG, links, images, structured data).\n    pub metadata: HtmlMetadata,\n    /// Extracted tables with structured cell data and markdown representation.\n    pub tables: Vec<TableData>,\n    /// Extracted inline images (data URIs and SVGs).\n    ///\n    /// Populated when `extract_images` is `true` in options.\n    pub images: Vec<String>,\n    /// Non-fatal processing warnings.\n    pub warnings: Vec<ProcessingWarning>,\n}\n\nimpl ConversionResult {\n    #[must_use]\n\n    pub fn new(\n        metadata: Option<HtmlMetadata>,\n        tables: Option<Vec<TableData>>,\n        images: Option<Vec<String>>,\n        warnings: Option<Vec<ProcessingWarning>>,\n        content: Option<String>,\n        document: Option<DocumentStructure>,\n    ) -> Self {\n        Self {\n            content,\n            document,\n            metadata: metadata.unwrap_or_default(),\n            tables: tables.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            warnings: warnings.unwrap_or_default(),\n        }\n    }\n}\n\n#[extendr]\npub fn new_conversionresult(\n    content: Option<String>,\n    document: Option<DocumentStructure>,\n    metadata: Option<HtmlMetadata>,\n    tables: Option<Vec<TableData>>,\n    images: Option<Vec<String>>,\n    warnings: Option<Vec<ProcessingWarning>>,\n) -> ConversionResult {\n    let mut __out = <ConversionResult>::default();\n    if let Some(v) = content {\n        __out.content = v;\n    }\n    if let Some(v) = document {\n        __out.document = v;\n    }\n    if let Some(v) = metadata {\n        __out.metadata = v;\n    }\n    if let Some(v) = tables {\n        __out.tables = v;\n    }\n    if let Some(v) = images {\n        __out.images = v;\n    }\n    if let Some(v) = warnings {\n        __out.warnings = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::similar_names)]\npub struct TableGrid {\n    /// Number of rows.\n    pub rows: i32,\n    /// Number of columns.\n    pub cols: i32,\n    /// All cells in the table (may be fewer than rows*cols due to spans).\n    pub cells: Vec<GridCell>,\n}\n\nimpl TableGrid {\n    #[must_use]\n\n    pub fn new(rows: Option<i32>, cols: Option<i32>, cells: Option<Vec<GridCell>>) -> Self {\n        Self {\n            rows: rows.unwrap_or_default(),\n            cols: cols.unwrap_or_default(),\n            cells: cells.unwrap_or_default(),\n        }\n    }\n}\n\n#[extendr]\npub fn new_tablegrid(rows: Option<i32>, cols: Option<i32>, cells: Option<Vec<GridCell>>) -> TableGrid {\n    let mut __out = <TableGrid>::default();\n    if let Some(v) = rows {\n        __out.rows = v;\n    }\n    if let Some(v) = cols {\n        __out.cols = v;\n    }\n    if let Some(v) = cells {\n        __out.cells = v;\n    }\n    __out\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::similar_names)]\npub struct GridCell {\n    /// The text content of the cell.\n    pub content: String,\n    /// 0-indexed row position.\n    pub row: i32,\n    /// 0-indexed column position.\n    pub col: i32,\n    /// Number of rows this cell spans (default 1).\n    pub row_span: i32,\n    /// Number of columns this cell spans (default 1).\n    pub col_span: i32,\n    /// Whether this is a header cell (`<th>`).\n    pub is_header: bool,\n}\n\nimpl GridCell {\n    #[must_use]\n\n    pub fn new(content: String, row: i32, col: i32, row_span: i32, col_span: i32, is_header: bool) -> Self {\n        Self {\n            content,\n            row,\n            col,\n            row_span,\n            col_span,\n            is_header,\n        }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct TableData {\n    /// The structured table grid.\n    pub grid: TableGrid,\n    /// The markdown rendering of this table.\n    pub markdown: String,\n}\n\nimpl TableData {\n    #[must_use]\n\n    pub fn new(grid: TableGrid, markdown: String) -> Self {\n        Self { grid, markdown }\n    }\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct ProcessingWarning {\n    /// Human-readable warning message.\n    pub message: String,\n    /// The category of warning.\n    pub kind: WarningKind,\n}\n\nimpl ProcessingWarning {\n    #[must_use]\n\n    pub fn new(message: String, kind: WarningKind) -> Self {\n        Self { message, kind }\n    }\n}\n\n#[derive(Clone)]\npub struct VisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\n#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]\npub struct NodeContext {\n    /// Coarse-grained node type classification\n    pub node_type: NodeType,\n    /// Raw HTML tag name (e.g., \"div\", \"h1\", \"custom-element\")\n    pub tag_name: String,\n    /// All HTML attributes as key-value pairs\n    pub attributes: HashMap<String, String>,\n    /// Depth in the DOM tree (0 = root)\n    pub depth: f64,\n    /// Index among siblings (0-based)\n    pub index_in_parent: f64,\n    /// Parent element's tag name (None if root)\n    pub parent_tag: Option<String>,\n    /// Whether this element is treated as inline vs block\n    pub is_inline: bool,\n}\n\nimpl NodeContext {\n    #[must_use]\n\n    pub fn new(\n        node_type: NodeType,\n        tag_name: String,\n        attributes: HashMap<String, String>,\n        depth: f64,\n        index_in_parent: f64,\n        is_inline: bool,\n        parent_tag: Option<String>,\n    ) -> Self {\n        Self {\n            node_type,\n            tag_name,\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline,\n        }\n    }\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum TextDirection {\n    #[default]\n    LeftToRight = 0,\n    RightToLeft = 1,\n    Auto = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum LinkType {\n    #[default]\n    Anchor = 0,\n    Internal = 1,\n    External = 2,\n    Email = 3,\n    Phone = 4,\n    Other = 5,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum ImageType {\n    #[default]\n    DataUri = 0,\n    InlineSvg = 1,\n    External = 2,\n    Relative = 3,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum StructuredDataType {\n    #[default]\n    JsonLd = 0,\n    Microdata = 1,\n    RDFa = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum PreprocessingPreset {\n    #[default]\n    Minimal = 0,\n    Standard = 1,\n    Aggressive = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum HeadingStyle {\n    #[default]\n    Underlined = 0,\n    Atx = 1,\n    AtxClosed = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum ListIndentType {\n    #[default]\n    Spaces = 0,\n    Tabs = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum WhitespaceMode {\n    #[default]\n    Normalized = 0,\n    Strict = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum NewlineStyle {\n    #[default]\n    Spaces = 0,\n    Backslash = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum CodeBlockStyle {\n    #[default]\n    Indented = 0,\n    Backticks = 1,\n    Tildes = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum HighlightStyle {\n    #[default]\n    DoubleEqual = 0,\n    Html = 1,\n    Bold = 2,\n    None = 3,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum LinkStyle {\n    #[default]\n    Inline = 0,\n    Reference = 1,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum OutputFormat {\n    #[default]\n    Markdown = 0,\n    Djot = 1,\n    Plain = 2,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum NodeContent {\n    #[default]\n    Heading = 0,\n    Paragraph = 1,\n    List = 2,\n    ListItem = 3,\n    Table = 4,\n    Image = 5,\n    Code = 6,\n    Quote = 7,\n    DefinitionList = 8,\n    DefinitionItem = 9,\n    RawBlock = 10,\n    MetadataBlock = 11,\n    Group = 12,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum AnnotationKind {\n    #[default]\n    Bold = 0,\n    Italic = 1,\n    Underline = 2,\n    Strikethrough = 3,\n    Code = 4,\n    Subscript = 5,\n    Superscript = 6,\n    Highlight = 7,\n    Link = 8,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum WarningKind {\n    #[default]\n    ImageExtractionFailed = 0,\n    EncodingFallback = 1,\n    TruncatedInput = 2,\n    MalformedHtml = 3,\n    SanitizationApplied = 4,\n    DepthLimitExceeded = 5,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum NodeType {\n    #[default]\n    Text = 0,\n    Element = 1,\n    Heading = 2,\n    Paragraph = 3,\n    Div = 4,\n    Blockquote = 5,\n    Pre = 6,\n    Hr = 7,\n    List = 8,\n    ListItem = 9,\n    DefinitionList = 10,\n    DefinitionTerm = 11,\n    DefinitionDescription = 12,\n    Table = 13,\n    TableRow = 14,\n    TableCell = 15,\n    TableHeader = 16,\n    TableBody = 17,\n    TableHead = 18,\n    TableFoot = 19,\n    Link = 20,\n    Image = 21,\n    Strong = 22,\n    Em = 23,\n    Code = 24,\n    Strikethrough = 25,\n    Underline = 26,\n    Subscript = 27,\n    Superscript = 28,\n    Mark = 29,\n    Small = 30,\n    Br = 31,\n    Span = 32,\n    Article = 33,\n    Section = 34,\n    Nav = 35,\n    Aside = 36,\n    Header = 37,\n    Footer = 38,\n    Main = 39,\n    Figure = 40,\n    Figcaption = 41,\n    Time = 42,\n    Details = 43,\n    Summary = 44,\n    Form = 45,\n    Input = 46,\n    Select = 47,\n    Option = 48,\n    Button = 49,\n    Textarea = 50,\n    Label = 51,\n    Fieldset = 52,\n    Legend = 53,\n    Audio = 54,\n    Video = 55,\n    Picture = 56,\n    Source = 57,\n    Iframe = 58,\n    Svg = 59,\n    Canvas = 60,\n    Ruby = 61,\n    Rt = 62,\n    Rp = 63,\n    Abbr = 64,\n    Kbd = 65,\n    Samp = 66,\n    Var = 67,\n    Cite = 68,\n    Q = 69,\n    Del = 70,\n    Ins = 71,\n    Data = 72,\n    Meter = 73,\n    Progress = 74,\n    Output = 75,\n    Template = 76,\n    Slot = 77,\n    Html = 78,\n    Head = 79,\n    Body = 80,\n    Title = 81,\n    Meta = 82,\n    LinkTag = 83,\n    Style = 84,\n    Script = 85,\n    Base = 86,\n    Custom = 87,\n}\n\n#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum VisitResult {\n    #[default]\n    Continue = 0,\n    Custom = 1,\n    Skip = 2,\n    PreserveHtml = 3,\n    Error = 4,\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for DocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for HeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<LinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for LinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for ImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<StructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for StructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for HtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: ConversionOptions) -> Self {\n        let mut __result = html_to_markdown_rs::options::ConversionOptions::default();\n        __result.heading_style = val.heading_style.into();\n        __result.list_indent_type = val.list_indent_type.into();\n        __result.list_indent_width = val.list_indent_width;\n        __result.bullets = val.bullets;\n        __result.strong_em_symbol = val.strong_em_symbol;\n        __result.escape_asterisks = val.escape_asterisks;\n        __result.escape_underscores = val.escape_underscores;\n        __result.escape_misc = val.escape_misc;\n        __result.escape_ascii = val.escape_ascii;\n        __result.code_language = val.code_language;\n        __result.autolinks = val.autolinks;\n        __result.default_title = val.default_title;\n        __result.br_in_tables = val.br_in_tables;\n        __result.highlight_style = val.highlight_style.into();\n        __result.extract_metadata = val.extract_metadata;\n        __result.whitespace_mode = val.whitespace_mode.into();\n        __result.strip_newlines = val.strip_newlines;\n        __result.wrap = val.wrap;\n        __result.wrap_width = val.wrap_width;\n        __result.convert_as_inline = val.convert_as_inline;\n        __result.sub_symbol = val.sub_symbol;\n        __result.sup_symbol = val.sup_symbol;\n        __result.newline_style = val.newline_style.into();\n        __result.code_block_style = val.code_block_style.into();\n        __result.keep_inline_images_in = val.keep_inline_images_in;\n        __result.preprocessing = val.preprocessing.into();\n        __result.encoding = val.encoding;\n        __result.debug = val.debug;\n        __result.strip_tags = val.strip_tags;\n        __result.preserve_tags = val.preserve_tags;\n        __result.skip_images = val.skip_images;\n        __result.link_style = val.link_style.into();\n        __result.output_format = val.output_format.into();\n        __result.include_document_structure = val.include_document_structure;\n        __result.extract_images = val.extract_images;\n        __result.max_image_size = val.max_image_size;\n        __result.capture_svg = val.capture_svg;\n        __result.infer_dimensions = val.infer_dimensions;\n        __result.max_depth = val.max_depth;\n        __result.exclude_selectors = val.exclude_selectors;\n        __result\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for ConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(Into::into),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for ConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten(),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for PreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for PreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for DocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for DocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for TextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: Default::default(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for ConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for TableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<GridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for GridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableData> for html_to_markdown_rs::TableData {\n    fn from(val: TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for TableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for ProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for NodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: val.attributes.into_iter().collect(),\n            depth: val.depth,\n            index_in_parent: val.index_in_parent,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<TextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: TextDirection) -> Self {\n        match val {\n            TextDirection::LeftToRight => Self::LeftToRight,\n            TextDirection::RightToLeft => Self::RightToLeft,\n            TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for TextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<LinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: LinkType) -> Self {\n        match val {\n            LinkType::Anchor => Self::Anchor,\n            LinkType::Internal => Self::Internal,\n            LinkType::External => Self::External,\n            LinkType::Email => Self::Email,\n            LinkType::Phone => Self::Phone,\n            LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for LinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<ImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: ImageType) -> Self {\n        match val {\n            ImageType::DataUri => Self::DataUri,\n            ImageType::InlineSvg => Self::InlineSvg,\n            ImageType::External => Self::External,\n            ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for ImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<StructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: StructuredDataType) -> Self {\n        match val {\n            StructuredDataType::JsonLd => Self::JsonLd,\n            StructuredDataType::Microdata => Self::Microdata,\n            StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for StructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<PreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: PreprocessingPreset) -> Self {\n        match val {\n            PreprocessingPreset::Minimal => Self::Minimal,\n            PreprocessingPreset::Standard => Self::Standard,\n            PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for PreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<HeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: HeadingStyle) -> Self {\n        match val {\n            HeadingStyle::Underlined => Self::Underlined,\n            HeadingStyle::Atx => Self::Atx,\n            HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for HeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<ListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: ListIndentType) -> Self {\n        match val {\n            ListIndentType::Spaces => Self::Spaces,\n            ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for ListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<WhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: WhitespaceMode) -> Self {\n        match val {\n            WhitespaceMode::Normalized => Self::Normalized,\n            WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for WhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<NewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: NewlineStyle) -> Self {\n        match val {\n            NewlineStyle::Spaces => Self::Spaces,\n            NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for NewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<CodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: CodeBlockStyle) -> Self {\n        match val {\n            CodeBlockStyle::Indented => Self::Indented,\n            CodeBlockStyle::Backticks => Self::Backticks,\n            CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for CodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<HighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: HighlightStyle) -> Self {\n        match val {\n            HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            HighlightStyle::Html => Self::Html,\n            HighlightStyle::Bold => Self::Bold,\n            HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for HighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<LinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: LinkStyle) -> Self {\n        match val {\n            LinkStyle::Inline => Self::Inline,\n            LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for LinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<OutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: OutputFormat) -> Self {\n        match val {\n            OutputFormat::Markdown => Self::Markdown,\n            OutputFormat::Djot => Self::Djot,\n            OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for OutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<NodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: NodeContent) -> Self {\n        match val {\n            NodeContent::Heading => Self::Heading {\n                level: Default::default(),\n                text: Default::default(),\n            },\n            NodeContent::Paragraph => Self::Paragraph {\n                text: Default::default(),\n            },\n            NodeContent::List => Self::List {\n                ordered: Default::default(),\n            },\n            NodeContent::ListItem => Self::ListItem {\n                text: Default::default(),\n            },\n            NodeContent::Table => Self::Table {\n                grid: Default::default(),\n            },\n            NodeContent::Image => Self::Image {\n                description: Default::default(),\n                src: Default::default(),\n                image_index: Default::default(),\n            },\n            NodeContent::Code => Self::Code {\n                text: Default::default(),\n                language: Default::default(),\n            },\n            NodeContent::Quote => Self::Quote,\n            NodeContent::DefinitionList => Self::DefinitionList,\n            NodeContent::DefinitionItem => Self::DefinitionItem {\n                term: Default::default(),\n                definition: Default::default(),\n            },\n            NodeContent::RawBlock => Self::RawBlock {\n                format: Default::default(),\n                content: Default::default(),\n            },\n            NodeContent::MetadataBlock => Self::MetadataBlock {\n                entries: Default::default(),\n            },\n            NodeContent::Group => Self::Group {\n                label: Default::default(),\n                heading_level: Default::default(),\n                heading_text: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for NodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { .. } => Self::Heading,\n            html_to_markdown_rs::NodeContent::Paragraph { .. } => Self::Paragraph,\n            html_to_markdown_rs::NodeContent::List { .. } => Self::List,\n            html_to_markdown_rs::NodeContent::ListItem { .. } => Self::ListItem,\n            html_to_markdown_rs::NodeContent::Table { .. } => Self::Table,\n            html_to_markdown_rs::NodeContent::Image { .. } => Self::Image,\n            html_to_markdown_rs::NodeContent::Code { .. } => Self::Code,\n            html_to_markdown_rs::NodeContent::Quote => Self::Quote,\n            html_to_markdown_rs::NodeContent::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeContent::DefinitionItem { .. } => Self::DefinitionItem,\n            html_to_markdown_rs::NodeContent::RawBlock { .. } => Self::RawBlock,\n            html_to_markdown_rs::NodeContent::MetadataBlock { .. } => Self::MetadataBlock,\n            html_to_markdown_rs::NodeContent::Group { .. } => Self::Group,\n        }\n    }\n}\n\nimpl From<AnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: AnnotationKind) -> Self {\n        match val {\n            AnnotationKind::Bold => Self::Bold,\n            AnnotationKind::Italic => Self::Italic,\n            AnnotationKind::Underline => Self::Underline,\n            AnnotationKind::Strikethrough => Self::Strikethrough,\n            AnnotationKind::Code => Self::Code,\n            AnnotationKind::Subscript => Self::Subscript,\n            AnnotationKind::Superscript => Self::Superscript,\n            AnnotationKind::Highlight => Self::Highlight,\n            AnnotationKind::Link => Self::Link {\n                url: Default::default(),\n                title: Default::default(),\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for AnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self::Bold,\n            html_to_markdown_rs::AnnotationKind::Italic => Self::Italic,\n            html_to_markdown_rs::AnnotationKind::Underline => Self::Underline,\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::AnnotationKind::Code => Self::Code,\n            html_to_markdown_rs::AnnotationKind::Subscript => Self::Subscript,\n            html_to_markdown_rs::AnnotationKind::Superscript => Self::Superscript,\n            html_to_markdown_rs::AnnotationKind::Highlight => Self::Highlight,\n            html_to_markdown_rs::AnnotationKind::Link { .. } => Self::Link,\n        }\n    }\n}\n\nimpl From<WarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: WarningKind) -> Self {\n        match val {\n            WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            WarningKind::EncodingFallback => Self::EncodingFallback,\n            WarningKind::TruncatedInput => Self::TruncatedInput,\n            WarningKind::MalformedHtml => Self::MalformedHtml,\n            WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for WarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for NodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for VisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        match val {\n            html_to_markdown_rs::VisitResult::Continue => Self::Continue,\n            html_to_markdown_rs::VisitResult::Custom(..) => Self::Custom,\n            html_to_markdown_rs::VisitResult::Skip => Self::Skip,\n            html_to_markdown_rs::VisitResult::PreserveHtml => Self::PreserveHtml,\n            html_to_markdown_rs::VisitResult::Error(..) => Self::Error,\n        }\n    }\n}\n\n#[allow(clippy::missing_errors_doc)]\n#[extendr]\n#[allow(unused_variables)]\npub fn convert(html: String, options: Option<ConversionOptions>) -> Result<ConversionResult> {\n    let mut options_binding: ConversionOptions = options.unwrap_or_default();\n    let _visitor_robj: Option<extendr_api::Robj> = options_binding.visitor.take();\n    let mut options_core: html_to_markdown_rs::ConversionOptions = options_binding.into();\n    if let Some(_v) = _visitor_robj {\n        if !_v.is_null() && !_v.is_na() {\n            let _bridge = RHtmlVisitorBridge::new(_v);\n            options_core.visitor =\n                Some(std::rc::Rc::new(std::cell::RefCell::new(_bridge)) as html_to_markdown_rs::visitor::VisitorHandle);\n        }\n    }\n    html_to_markdown_rs::convert(&html, options_core)\n        .map(|val| val.into())\n        .map_err(|e| extendr_api::Error::Other(e.to_string()))\n}\n\nfn nodecontext_to_robj(ctx: &html_to_markdown_rs::visitor::NodeContext) -> extendr_api::Robj {\n    use extendr_api::prelude::*;\n    let attrs: extendr_api::Robj = ctx\n        .attributes\n        .iter()\n        .map(|(k, v)| (k.as_str(), extendr_api::Robj::from(v.as_str())))\n        .collect::<List>()\n        .into();\n    list!(\n        node_type = format!(\"{:?}\", ctx.node_type),\n        tag_name = ctx.tag_name.as_str(),\n        depth = ctx.depth as i32,\n        index_in_parent = ctx.index_in_parent as i32,\n        is_inline = ctx.is_inline,\n        parent_tag = ctx.parent_tag.as_deref().unwrap_or(\"\"),\n        attributes = attrs,\n    )\n    .into()\n}\n\npub struct RHtmlVisitorBridge {\n    r_obj: extendr_api::Robj,\n}\n\nimpl std::fmt::Debug for RHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"RHtmlVisitorBridge\")\n    }\n}\n\nimpl RHtmlVisitorBridge {\n    pub fn new(r_obj: extendr_api::Robj) -> Self {\n        Self { r_obj }\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for RHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_element_start\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_element_end\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_output\", extendr_api::Robj::from(_output)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_text\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_link\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_href\", extendr_api::Robj::from(_href)),\n            (\"_text\", extendr_api::Robj::from(_text)),\n            (\n                \"_title\",\n                match _title {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_image\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_src\", extendr_api::Robj::from(_src)),\n            (\"_alt\", extendr_api::Robj::from(_alt)),\n            (\n                \"_title\",\n                match _title {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_heading\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_level\", extendr_api::Robj::from(_level as i32)),\n            (\"_text\", extendr_api::Robj::from(_text)),\n            (\n                \"_id\",\n                match _id {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_code_block\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\n                \"_lang\",\n                match _lang {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n            (\"_code\", extendr_api::Robj::from(_code)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_code_inline\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_code\", extendr_api::Robj::from(_code)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_list_item\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_ordered\", extendr_api::Robj::from(_ordered)),\n            (\"_marker\", extendr_api::Robj::from(_marker)),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_list_start\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_ordered\", extendr_api::Robj::from(_ordered)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_list_end\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_ordered\", extendr_api::Robj::from(_ordered)),\n            (\"_output\", extendr_api::Robj::from(_output)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_table_start\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_table_row\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_cells\", extendr_api::Robj::from(_cells)),\n            (\"_is_header\", extendr_api::Robj::from(_is_header)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_table_end\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_output\", extendr_api::Robj::from(_output)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_blockquote\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_content\", extendr_api::Robj::from(_content)),\n            (\"_depth\", extendr_api::Robj::from(_depth as f64)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_strong\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_emphasis\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_strikethrough\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_underline\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_subscript\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_superscript\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_mark\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_line_break\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_horizontal_rule\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_custom_element\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_tag_name\", extendr_api::Robj::from(_tag_name)),\n            (\"_html\", extendr_api::Robj::from(_html)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_definition_list_start\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_definition_term\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_definition_description\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_definition_list_end\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_output\", extendr_api::Robj::from(_output)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_form\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\n                \"_action\",\n                match _action {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n            (\n                \"_method\",\n                match _method {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_input\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_input_type\", extendr_api::Robj::from(_input_type)),\n            (\n                \"_name\",\n                match _name {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n            (\n                \"_value\",\n                match _value {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_button\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_audio\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\n                \"_src\",\n                match _src {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_video\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\n                \"_src\",\n                match _src {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_iframe\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\n                \"_src\",\n                match _src {\n                    Some(s) => extendr_api::Robj::from(s),\n                    None => extendr_api::Robj::from(extendr_api::NULL),\n                },\n            ),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_details\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_open\", extendr_api::Robj::from(_open)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_summary\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_figure_start\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[(\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx)))]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_figcaption\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_text\", extendr_api::Robj::from(_text)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        use extendr_api::prelude::*;\n        let maybe_fn = self.r_obj.dollar(\"visit_figure_end\");\n        let fn_robj = match maybe_fn {\n            Ok(v) if !v.is_null() && !v.is_na() => v,\n            _ => return html_to_markdown_rs::VisitResult::Continue,\n        };\n        let args = extendr_api::Pairlist::from_pairs(&[\n            (\"_ctx\", extendr_api::Robj::from(nodecontext_to_robj(_ctx))),\n            (\"_output\", extendr_api::Robj::from(_output)),\n        ]);\n        let result = fn_robj.call(args);\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                if let Some(s) = val.as_str() {\n                    match s.to_lowercase().as_str() {\n                        \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                        \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                        \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                        other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                    }\n                } else if val.is_null() || val.is_na() {\n                    html_to_markdown_rs::VisitResult::Continue\n                } else {\n                    if let Ok(custom_val) = val.dollar(\"custom\") {\n                        if let Some(s) = custom_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Custom(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else if let Ok(error_val) = val.dollar(\"error\") {\n                        if let Some(s) = error_val.as_str() {\n                            html_to_markdown_rs::VisitResult::Error(s.to_string())\n                        } else {\n                            html_to_markdown_rs::VisitResult::Continue\n                        }\n                    } else {\n                        html_to_markdown_rs::VisitResult::Continue\n                    }\n                }\n            }\n        }\n    }\n}\n\nextendr_module! {\n    mod htmltomarkdown;\n    impl DocumentMetadata;\n    impl HeaderMetadata;\n    impl LinkMetadata;\n    impl ImageMetadata;\n    impl StructuredData;\n    impl HtmlMetadata;\n    impl ConversionOptions;\n    impl ConversionOptionsBuilder;\n    impl ConversionOptionsUpdate;\n    impl PreprocessingOptions;\n    impl PreprocessingOptionsUpdate;\n    impl DocumentStructure;\n    impl DocumentNode;\n    impl TextAnnotation;\n    impl ConversionResult;\n    impl TableGrid;\n    impl GridCell;\n    impl TableData;\n    impl ProcessingWarning;\n    impl VisitorHandle;\n    impl HtmlVisitor;\n    impl NodeContext;\n    fn convert;\n}\n"
  },
  {
    "path": "packages/r/src/rust/src/options.rs",
    "content": "//! Option parsing for R bindings.\n\nuse extendr_api::prelude::*;\nuse html_to_markdown_rs::{\n    CodeBlockStyle, ConversionOptions, ConversionOptionsUpdate, HeadingStyle, HighlightStyle,\n    LinkStyle, ListIndentType, NewlineStyle, OutputFormat, PreprocessingOptionsUpdate,\n    PreprocessingPreset, WhitespaceMode,\n};\n\n/// Decode an R list into ConversionOptions.\npub fn decode_options(options: Robj) -> std::result::Result<ConversionOptions, String> {\n    if options.is_null() {\n        return Ok(ConversionOptions::default());\n    }\n\n    let list = options\n        .as_list()\n        .ok_or_else(|| \"options must be a named list\".to_string())?;\n\n    apply_options(&list)\n}\n\nfn apply_options(list: &List) -> std::result::Result<ConversionOptions, String> {\n    let mut update = ConversionOptionsUpdate::default();\n\n    for (key, value) in list.iter() {\n        // Accept both snake_case and camelCase option keys for compatibility with\n        // auto-generated test suites and user convenience.\n        match key {\n            \"heading_style\" | \"headingStyle\" => {\n                update.heading_style = Some(parse_heading_style(&value)?)\n            }\n            \"list_indent_type\" | \"listIndentType\" => {\n                update.list_indent_type = Some(parse_list_indent_type(&value)?)\n            }\n            \"list_indent_width\" | \"listIndentWidth\" => {\n                update.list_indent_width =\n                    Some(decode_positive_integer(&value, \"list_indent_width\")?)\n            }\n            \"bullets\" => update.bullets = Some(decode_string(&value, \"bullets\")?),\n            \"strong_em_symbol\" | \"strongEmSymbol\" => {\n                let symbol = decode_string(&value, \"strong_em_symbol\")?;\n                let ch = symbol\n                    .chars()\n                    .next()\n                    .ok_or_else(|| \"strong_em_symbol: must not be empty\".to_string())?;\n                update.strong_em_symbol = Some(ch);\n            }\n            \"escape_asterisks\" | \"escapeAsterisks\" => {\n                update.escape_asterisks = Some(decode_bool(&value, \"escape_asterisks\")?)\n            }\n            \"escape_underscores\" | \"escapeUnderscores\" => {\n                update.escape_underscores = Some(decode_bool(&value, \"escape_underscores\")?)\n            }\n            \"escape_misc\" | \"escapeMisc\" => {\n                update.escape_misc = Some(decode_bool(&value, \"escape_misc\")?)\n            }\n            \"escape_ascii\" | \"escapeAscii\" => {\n                update.escape_ascii = Some(decode_bool(&value, \"escape_ascii\")?)\n            }\n            \"code_language\" | \"codeLanguage\" => {\n                update.code_language = Some(decode_string(&value, \"code_language\")?)\n            }\n            \"encoding\" => update.encoding = Some(decode_string(&value, \"encoding\")?),\n            \"autolinks\" => update.autolinks = Some(decode_bool(&value, \"autolinks\")?),\n            \"default_title\" | \"defaultTitle\" => {\n                update.default_title = Some(decode_bool(&value, \"default_title\")?)\n            }\n            \"keep_inline_images_in\" | \"keepInlineImagesIn\" => {\n                update.keep_inline_images_in =\n                    Some(decode_string_list(&value, \"keep_inline_images_in\")?)\n            }\n            \"br_in_tables\" | \"brInTables\" => {\n                update.br_in_tables = Some(decode_bool(&value, \"br_in_tables\")?)\n            }\n            \"highlight_style\" | \"highlightStyle\" => {\n                update.highlight_style = Some(parse_highlight_style(&value)?)\n            }\n            \"extract_metadata\" | \"extractMetadata\" => {\n                update.extract_metadata = Some(decode_bool(&value, \"extract_metadata\")?)\n            }\n            \"whitespace_mode\" | \"whitespaceMode\" => {\n                update.whitespace_mode = Some(parse_whitespace_mode(&value)?)\n            }\n            \"strip_newlines\" | \"stripNewlines\" => {\n                update.strip_newlines = Some(decode_bool(&value, \"strip_newlines\")?)\n            }\n            \"wrap\" => update.wrap = Some(decode_bool(&value, \"wrap\")?),\n            \"wrap_width\" | \"wrapWidth\" => {\n                update.wrap_width = Some(decode_positive_integer(&value, \"wrap_width\")?)\n            }\n            \"strip_tags\" | \"stripTags\" => {\n                update.strip_tags = Some(decode_string_list(&value, \"strip_tags\")?)\n            }\n            \"preserve_tags\" | \"preserveTags\" => {\n                update.preserve_tags = Some(decode_string_list(&value, \"preserve_tags\")?)\n            }\n            \"convert_as_inline\" | \"convertAsInline\" => {\n                update.convert_as_inline = Some(decode_bool(&value, \"convert_as_inline\")?)\n            }\n            \"sub_symbol\" | \"subSymbol\" => {\n                update.sub_symbol = Some(decode_string(&value, \"sub_symbol\")?)\n            }\n            \"sup_symbol\" | \"supSymbol\" => {\n                update.sup_symbol = Some(decode_string(&value, \"sup_symbol\")?)\n            }\n            \"newline_style\" | \"newlineStyle\" => {\n                update.newline_style = Some(parse_newline_style(&value)?)\n            }\n            \"code_block_style\" | \"codeBlockStyle\" => {\n                update.code_block_style = Some(parse_code_block_style(&value)?)\n            }\n            \"output_format\" | \"outputFormat\" => {\n                update.output_format = Some(parse_output_format(&value)?)\n            }\n            \"link_style\" | \"linkStyle\" => update.link_style = Some(parse_link_style(&value)?),\n            \"skip_images\" | \"skipImages\" => {\n                update.skip_images = Some(decode_bool(&value, \"skip_images\")?)\n            }\n            \"include_document_structure\" | \"includeDocumentStructure\" => {\n                update.include_document_structure =\n                    Some(decode_bool(&value, \"include_document_structure\")?)\n            }\n            \"extract_images\" | \"extractImages\" => {\n                update.extract_images = Some(decode_bool(&value, \"extract_images\")?)\n            }\n            \"max_image_size\" | \"maxImageSize\" => {\n                update.max_image_size =\n                    Some(decode_positive_integer(&value, \"max_image_size\")? as u64)\n            }\n            \"capture_svg\" | \"captureSvg\" => {\n                update.capture_svg = Some(decode_bool(&value, \"capture_svg\")?)\n            }\n            \"infer_dimensions\" | \"inferDimensions\" => {\n                update.infer_dimensions = Some(decode_bool(&value, \"infer_dimensions\")?)\n            }\n            \"preprocessing\" => update.preprocessing = Some(decode_preprocessing(&value)?),\n            \"debug\" => update.debug = Some(decode_bool(&value, \"debug\")?),\n            _ => {}\n        }\n    }\n\n    Ok(ConversionOptions::from(update))\n}\n\nfn decode_preprocessing(value: &Robj) -> std::result::Result<PreprocessingOptionsUpdate, String> {\n    let list = value\n        .as_list()\n        .ok_or_else(|| \"preprocessing: must be a named list\".to_string())?;\n\n    let mut update = PreprocessingOptionsUpdate::default();\n\n    for (key, val) in list.iter() {\n        match key {\n            \"enabled\" => update.enabled = Some(decode_bool(&val, \"preprocessing.enabled\")?),\n            \"preset\" => update.preset = Some(parse_preset(&val)?),\n            \"remove_navigation\" => {\n                update.remove_navigation =\n                    Some(decode_bool(&val, \"preprocessing.remove_navigation\")?)\n            }\n            \"remove_forms\" => {\n                update.remove_forms = Some(decode_bool(&val, \"preprocessing.remove_forms\")?)\n            }\n            _ => {}\n        }\n    }\n\n    Ok(update)\n}\n\nfn decode_bool(value: &Robj, field: &str) -> std::result::Result<bool, String> {\n    value\n        .as_bool()\n        .ok_or_else(|| format!(\"{field}: must be a logical (TRUE/FALSE)\"))\n}\n\nfn decode_string(value: &Robj, field: &str) -> std::result::Result<String, String> {\n    value\n        .as_str()\n        .map(|s| s.to_string())\n        .ok_or_else(|| format!(\"{field}: must be a character string\"))\n}\n\nfn decode_string_list(value: &Robj, field: &str) -> std::result::Result<Vec<String>, String> {\n    let strs = value\n        .as_str_vector()\n        .ok_or_else(|| format!(\"{field}: must be a character vector\"))?;\n    Ok(strs.into_iter().map(|s| s.to_string()).collect())\n}\n\nfn decode_positive_integer(value: &Robj, field: &str) -> std::result::Result<usize, String> {\n    let v = value\n        .as_integer()\n        .or_else(|| value.as_real().map(|r| r as i32))\n        .ok_or_else(|| format!(\"{field}: must be a positive integer\"))?;\n    if v <= 0 {\n        return Err(format!(\"{field}: must be greater than zero\"));\n    }\n    Ok(v as usize)\n}\n\nfn parse_heading_style(value: &Robj) -> std::result::Result<HeadingStyle, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"heading_style: must be a character string\".to_string())?;\n    match s {\n        \"atx\" => Ok(HeadingStyle::Atx),\n        // Accept both snake_case and camelCase variants for ATX closed style.\n        \"atx_closed\" | \"atxclosed\" | \"atxClosed\" => Ok(HeadingStyle::AtxClosed),\n        \"underlined\" => Ok(HeadingStyle::Underlined),\n        _ => Err(format!(\"heading_style: invalid value: {s}\")),\n    }\n}\n\nfn parse_list_indent_type(value: &Robj) -> std::result::Result<ListIndentType, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"list_indent_type: must be a character string\".to_string())?;\n    match s {\n        \"spaces\" => Ok(ListIndentType::Spaces),\n        \"tabs\" => Ok(ListIndentType::Tabs),\n        _ => Err(format!(\"list_indent_type: invalid value: {s}\")),\n    }\n}\n\nfn parse_highlight_style(value: &Robj) -> std::result::Result<HighlightStyle, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"highlight_style: must be a character string\".to_string())?;\n    let normalized = s.replace('-', \"_\");\n    match normalized.as_str() {\n        \"double_equal\" => Ok(HighlightStyle::DoubleEqual),\n        \"html\" => Ok(HighlightStyle::Html),\n        \"bold\" => Ok(HighlightStyle::Bold),\n        \"none\" => Ok(HighlightStyle::None),\n        _ => Err(format!(\"highlight_style: invalid value: {s}\")),\n    }\n}\n\nfn parse_whitespace_mode(value: &Robj) -> std::result::Result<WhitespaceMode, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"whitespace_mode: must be a character string\".to_string())?;\n    match s {\n        \"normalized\" => Ok(WhitespaceMode::Normalized),\n        \"strict\" => Ok(WhitespaceMode::Strict),\n        _ => Err(format!(\"whitespace_mode: invalid value: {s}\")),\n    }\n}\n\nfn parse_newline_style(value: &Robj) -> std::result::Result<NewlineStyle, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"newline_style: must be a character string\".to_string())?;\n    match s {\n        \"spaces\" => Ok(NewlineStyle::Spaces),\n        \"backslash\" => Ok(NewlineStyle::Backslash),\n        _ => Err(format!(\"newline_style: invalid value: {s}\")),\n    }\n}\n\nfn parse_code_block_style(value: &Robj) -> std::result::Result<CodeBlockStyle, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"code_block_style: must be a character string\".to_string())?;\n    match s {\n        \"indented\" => Ok(CodeBlockStyle::Indented),\n        \"backticks\" => Ok(CodeBlockStyle::Backticks),\n        \"tildes\" => Ok(CodeBlockStyle::Tildes),\n        _ => Err(format!(\"code_block_style: invalid value: {s}\")),\n    }\n}\n\nfn parse_output_format(value: &Robj) -> std::result::Result<OutputFormat, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"output_format: must be a character string\".to_string())?;\n    match s {\n        \"markdown\" => Ok(OutputFormat::Markdown),\n        \"djot\" => Ok(OutputFormat::Djot),\n        \"plain\" | \"plaintext\" | \"text\" => Ok(OutputFormat::Plain),\n        _ => Err(format!(\"output_format: invalid value: {s}\")),\n    }\n}\n\nfn parse_link_style(value: &Robj) -> std::result::Result<LinkStyle, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"link_style: must be a character string\".to_string())?;\n    match s {\n        \"inline\" => Ok(LinkStyle::Inline),\n        \"reference\" => Ok(LinkStyle::Reference),\n        _ => Err(format!(\"link_style: invalid value: {s}\")),\n    }\n}\n\nfn parse_preset(value: &Robj) -> std::result::Result<PreprocessingPreset, String> {\n    let s = value\n        .as_str()\n        .ok_or_else(|| \"preprocessing.preset: must be a character string\".to_string())?;\n    let normalized = s.replace('-', \"_\");\n    match normalized.as_str() {\n        \"minimal\" => Ok(PreprocessingPreset::Minimal),\n        \"aggressive\" => Ok(PreprocessingPreset::Aggressive),\n        \"standard\" => Ok(PreprocessingPreset::Standard),\n        _ => Err(format!(\"preprocessing.preset: invalid value: {s}\")),\n    }\n}\n"
  },
  {
    "path": "packages/r/src/rust/src/types.rs",
    "content": "//! R list type conversions for binding results.\n\nuse extendr_api::prelude::*;\nuse html_to_markdown_rs::{ConversionResult, WarningKind};\n\n/// Recursively convert a `serde_json::Value` into an `Robj`.\n///\n/// Objects become named lists, arrays become unnamed lists, scalars become\n/// their R equivalents, and `null` becomes R `NULL`.\nfn json_to_robj(value: &serde_json::Value) -> Robj {\n    match value {\n        serde_json::Value::Null => ().into(),\n        serde_json::Value::Bool(b) => (*b).into(),\n        serde_json::Value::Number(n) => {\n            if let Some(i) = n.as_i64() {\n                (i as i32).into()\n            } else if let Some(f) = n.as_f64() {\n                f.into()\n            } else {\n                ().into()\n            }\n        }\n        serde_json::Value::String(s) => s.as_str().into(),\n        serde_json::Value::Array(arr) => {\n            let items: Vec<Robj> = arr.iter().map(json_to_robj).collect();\n            List::from_values(items).into()\n        }\n        serde_json::Value::Object(map) => {\n            let names: Vec<&str> = map.keys().map(|k| k.as_str()).collect();\n            let values: Vec<Robj> = map.values().map(json_to_robj).collect();\n            let mut list = List::from_values(values);\n            let _ = list.set_names(names);\n            list.into()\n        }\n    }\n}\n\n#[cfg(feature = \"metadata\")]\nuse html_to_markdown_rs::metadata::{\n    DocumentMetadata, HeaderMetadata, HtmlMetadata, ImageMetadata, LinkMetadata, StructuredData,\n};\n#[cfg(feature = \"metadata\")]\nuse std::collections::HashMap;\n\n#[cfg(feature = \"metadata\")]\n/// Convert HtmlMetadata into an R list.\npub fn metadata_to_robj(metadata: HtmlMetadata) -> Robj {\n    list!(\n        document = document_metadata_to_robj(metadata.document),\n        headers = List::from_values(metadata.headers.into_iter().map(header_metadata_to_robj)),\n        links = List::from_values(metadata.links.into_iter().map(link_metadata_to_robj)),\n        images = List::from_values(metadata.images.into_iter().map(image_metadata_to_robj)),\n        structured_data =\n            List::from_values(metadata.structured_data.into_iter().map(structured_data_to_robj))\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn document_metadata_to_robj(doc: DocumentMetadata) -> Robj {\n    list!(\n        title = option_to_robj(doc.title),\n        description = option_to_robj(doc.description),\n        keywords = doc.keywords,\n        author = option_to_robj(doc.author),\n        canonical_url = option_to_robj(doc.canonical_url),\n        base_href = option_to_robj(doc.base_href),\n        language = option_to_robj(doc.language),\n        text_direction = option_to_robj(doc.text_direction.map(|td| td.to_string())),\n        open_graph = hashmap_to_robj(doc.open_graph.into_iter().collect()),\n        twitter_card = hashmap_to_robj(doc.twitter_card.into_iter().collect()),\n        meta_tags = hashmap_to_robj(doc.meta_tags.into_iter().collect())\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn header_metadata_to_robj(header: HeaderMetadata) -> Robj {\n    list!(\n        level = header.level as i32,\n        text = header.text,\n        id = option_to_robj(header.id),\n        depth = header.depth as i32,\n        html_offset = header.html_offset as i32\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn link_metadata_to_robj(link: LinkMetadata) -> Robj {\n    list!(\n        href = link.href,\n        text = link.text,\n        title = option_to_robj(link.title),\n        link_type = link.link_type.to_string(),\n        rel = link.rel,\n        attributes = hashmap_to_robj(link.attributes.into_iter().collect())\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn image_metadata_to_robj(image: ImageMetadata) -> Robj {\n    list!(\n        src = image.src,\n        alt = option_to_robj(image.alt),\n        title = option_to_robj(image.title),\n        dimensions = match image.dimensions {\n            Some((w, h)) => Robj::from(list!(width = w as i32, height = h as i32)),\n            None => ().into(),\n        },\n        image_type = image.image_type.to_string(),\n        attributes = hashmap_to_robj(image.attributes.into_iter().collect())\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn structured_data_to_robj(data: StructuredData) -> Robj {\n    list!(\n        data_type = data.data_type.to_string(),\n        raw_json = data.raw_json,\n        schema_type = option_to_robj(data.schema_type)\n    )\n    .into()\n}\n\n#[cfg(feature = \"metadata\")]\nfn option_to_robj(opt: Option<String>) -> Robj {\n    match opt {\n        Some(s) => s.into(),\n        None => ().into(),\n    }\n}\n\n#[cfg(feature = \"metadata\")]\nfn hashmap_to_robj(map: HashMap<String, String>) -> Robj {\n    let names: Vec<&str> = map.keys().map(|k| k.as_str()).collect();\n    let values: Vec<Robj> = map.values().map(|v| v.into_robj()).collect();\n    let mut list = List::from_values(values);\n    let _ = list.set_names(names);\n    list.into()\n}\n\n/// Convert a ConversionResult into an R list.\npub fn conversion_result_to_robj(result: ConversionResult) -> Robj {\n    let content_robj: Robj = match result.content {\n        Some(s) => s.into(),\n        None => ().into(),\n    };\n\n    #[cfg(feature = \"metadata\")]\n    let metadata_robj = metadata_to_robj(result.metadata);\n    #[cfg(not(feature = \"metadata\"))]\n    let metadata_robj: Robj = ().into();\n\n    let tables: Vec<Robj> = result\n        .tables\n        .into_iter()\n        .map(|t| {\n            let cells: Vec<Robj> = t\n                .grid\n                .cells\n                .into_iter()\n                .map(|c| {\n                    list!(\n                        content = c.content,\n                        row = c.row as i32,\n                        col = c.col as i32,\n                        row_span = c.row_span as i32,\n                        col_span = c.col_span as i32,\n                        is_header = c.is_header\n                    )\n                    .into()\n                })\n                .collect();\n            list!(\n                grid = list!(\n                    rows = t.grid.rows as i32,\n                    cols = t.grid.cols as i32,\n                    cells = List::from_values(cells)\n                ),\n                markdown = t.markdown\n            )\n            .into()\n        })\n        .collect();\n\n    let warnings: Vec<Robj> = result\n        .warnings\n        .into_iter()\n        .map(|w| {\n            let kind = match w.kind {\n                WarningKind::ImageExtractionFailed => \"image_extraction_failed\",\n                WarningKind::EncodingFallback => \"encoding_fallback\",\n                WarningKind::TruncatedInput => \"truncated_input\",\n                WarningKind::MalformedHtml => \"malformed_html\",\n                WarningKind::SanitizationApplied => \"sanitization_applied\",\n            };\n            list!(message = w.message, kind = kind).into()\n        })\n        .collect();\n\n    let document_robj: Robj = match result.document {\n        Some(doc) => {\n            // Serialize to JSON then parse into an R list so callers can\n            // access fields via `result$document$nodes` etc.\n            match serde_json::to_value(&doc) {\n                Ok(v) => json_to_robj(&v),\n                Err(_) => ().into(),\n            }\n        }\n        None => ().into(),\n    };\n\n    let images_robj: Robj = {\n        let image_list: Vec<Robj> = result\n            .images\n            .into_iter()\n            .map(|img| {\n                let dimensions_robj: Robj = match img.dimensions {\n                    Some((w, h)) => Robj::from(list!(width = w as i32, height = h as i32)),\n                    None => ().into(),\n                };\n                let attr_names: Vec<&str> = img.attributes.keys().map(|k: &String| k.as_str()).collect();\n                let attr_values: Vec<Robj> =\n                    img.attributes.values().map(|v: &String| v.into_robj()).collect();\n                let mut attr_list = List::from_values(attr_values);\n                let _ = attr_list.set_names(attr_names);\n                list!(\n                    data = img.data,\n                    format = img.format.to_string(),\n                    filename = match img.filename {\n                        Some(s) => Robj::from(s),\n                        None => ().into(),\n                    },\n                    description = match img.description {\n                        Some(s) => Robj::from(s),\n                        None => ().into(),\n                    },\n                    dimensions = dimensions_robj,\n                    source = img.source.to_string(),\n                    attributes = attr_list\n                )\n                .into()\n            })\n            .collect();\n        List::from_values(image_list).into()\n    };\n\n    list!(\n        content = content_robj,\n        document = document_robj,\n        metadata = metadata_robj,\n        tables = List::from_values(tables),\n        images = images_robj,\n        warnings = List::from_values(warnings)\n    )\n    .into()\n}\n"
  },
  {
    "path": "packages/r/src/rust/vendor-config.toml",
    "content": "[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.vendored-sources]\ndirectory = \"vendor\"\n"
  },
  {
    "path": "packages/r/tests/testthat.R",
    "content": "library(testthat)\nlibrary(htmltomarkdown)\n\ntest_check(\"htmltomarkdown\")\n"
  },
  {
    "path": "packages/r/tools/config.R",
    "content": "# Configuration script for htmltomarkdown package.\n# Generates Makevars from Makevars.in templates with CRAN-compliant settings.\n\n# Check MSRV first\nsource(\"tools/msrv.R\")\n\n# Check environment variables\nenv_debug <- Sys.getenv(\"DEBUG\")\nenv_not_cran <- Sys.getenv(\"NOT_CRAN\")\n\n# Check if vendored archive exists\nvendor_exists <- file.exists(\"src/rust/vendor.tar.xz\")\n\nis_not_cran <- nzchar(env_not_cran)\nis_debug <- nzchar(env_debug)\n\nif (is_debug) {\n  is_not_cran <- TRUE\n  message(\"Creating DEBUG build.\")\n}\n\nif (!is_not_cran) {\n  message(\"Building for CRAN.\")\n}\n\n# CRAN flags: limit parallelism and require offline builds\n.cran_flags <- ifelse(\n  !is_not_cran && vendor_exists,\n  \"-j 2 --offline\",\n  \"\"\n)\n\n# Enable vendoring only for CRAN builds with vendor archive\n.vendoring <- ifelse(!is_not_cran && vendor_exists, \"yes\", \"no\")\n\n.profile <- ifelse(is_debug, \"\", \"--release\")\n.clean_targets <- ifelse(is_debug, \"\", \"$(TARGET_DIR)\")\n\n# WebR / wasm support\nwebr_target <- \"wasm32-unknown-emscripten\"\nis_wasm <- identical(R.version$platform, webr_target)\n\ntarget_libpath <- if (is_wasm) \"wasm32-unknown-emscripten\" else NULL\ncfg <- if (is_debug) \"debug\" else \"release\"\n.libdir <- paste(c(target_libpath, cfg), collapse = \"/\")\n.target <- ifelse(is_wasm, paste0(\"--target=\", webr_target), \"\")\n\n# Select platform-specific template\nis_windows <- .Platform[[\"OS.type\"]] == \"windows\"\nmv_fp <- ifelse(is_windows, \"src/Makevars.win.in\", \"src/Makevars.in\")\nmv_ofp <- ifelse(is_windows, \"src/Makevars.win\", \"src/Makevars\")\n\n# Remove existing generated Makevars\nif (file.exists(mv_ofp)) {\n  invisible(file.remove(mv_ofp))\n}\n\n# Read template and substitute placeholders\nmv_txt <- readLines(mv_fp)\n\nnew_txt <- gsub(\"@CRAN_FLAGS@\", .cran_flags, mv_txt) |>\n  gsub(\"@VENDORING@\", .vendoring, x = _) |>\n  gsub(\"@PROFILE@\", .profile, x = _) |>\n  gsub(\"@CLEAN_TARGET@\", .clean_targets, x = _) |>\n  gsub(\"@LIBDIR@\", .libdir, x = _) |>\n  gsub(\"@TARGET@\", .target, x = _)\n\ncon <- file(mv_ofp, open = \"wb\")\nwriteLines(new_txt, con, sep = \"\\n\")\nclose(con)\n\nmessage(sprintf(\"Generated %s (vendoring=%s)\", mv_ofp, .vendoring))\n"
  },
  {
    "path": "packages/r/tools/msrv.R",
    "content": "# Check Minimum Supported Rust Version (MSRV)\n# Validates that installed rustc meets the package's minimum version requirement.\n\nextract_semver <- function(version_string) {\n  m <- regmatches(version_string, regexec(\"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\", version_string))\n  if (length(m[[1]]) >= 2) m[[1]][2] else NA_character_\n}\n\ndesc <- read.dcf(\"DESCRIPTION\")\n\nif (!\"SystemRequirements\" %in% colnames(desc)) {\n  stop(\"SystemRequirements not found in DESCRIPTION.\")\n}\n\nsysreqs <- desc[, \"SystemRequirements\"]\n\nif (!grepl(\"cargo\", sysreqs, ignore.case = TRUE)) {\n  stop(\"SystemRequirements must mention Cargo.\")\n}\nif (!grepl(\"rustc\", sysreqs, ignore.case = TRUE)) {\n  stop(\"SystemRequirements must mention rustc.\")\n}\n\n# Add ~/.cargo/bin to PATH so we can find cargo/rustc\nnew_path <- paste0(\n  Sys.getenv(\"PATH\"), \":\",\n  paste0(Sys.getenv(\"HOME\"), \"/.cargo/bin\")\n)\nSys.setenv(\"PATH\" = new_path)\n\n# Check cargo exists\ncargo_version <- tryCatch(\n  system(\"cargo --version\", intern = TRUE),\n  error = function(e) {\n    stop(\n      \"cargo not found. Please install Rust: https://www.rust-lang.org/tools/install\",\n      call. = FALSE\n    )\n  }\n)\n\n# Check rustc exists\nrustc_version <- tryCatch(\n  system(\"rustc --version\", intern = TRUE),\n  error = function(e) {\n    stop(\n      \"rustc not found. Please install Rust: https://www.rust-lang.org/tools/install\",\n      call. = FALSE\n    )\n  }\n)\n\n# Extract and check MSRV from SystemRequirements\nparts <- strsplit(sysreqs, \",\\\\s*\")[[1]]\nrustc_part <- parts[grepl(\"rustc\", parts)]\nmsrv <- extract_semver(rustc_part)\ncurrent_version <- extract_semver(rustc_version)\n\nif (!is.na(msrv) && !is.na(current_version)) {\n  if (utils::compareVersion(msrv, current_version) == 1) {\n    stop(\n      sprintf(\n        \"Minimum supported Rust version is %s but installed rustc is %s. Please update Rust.\",\n        msrv, current_version\n      ),\n      call. = FALSE\n    )\n  }\n}\n\nmessage(sprintf(\"Using %s\", cargo_version))\nmessage(sprintf(\"Using %s\", rustc_version))\n"
  },
  {
    "path": "packages/ruby/.gitignore",
    "content": "vendor/\n.cargo/\nrust-vendor/\n"
  },
  {
    "path": "packages/ruby/.rubocop.yml",
    "content": "plugins:\n  - rubocop-performance\n  - rubocop-rspec\n\nAllCops:\n  TargetRubyVersion: 3.2\n  NewCops: enable\n  SuggestExtensions: false\n  Exclude:\n    - \"vendor/**/*\"\n    - \"tmp/**/*\"\n    - \"lib/**/*.bundle\"\n    - \"ext/**/*\"\n\nStyle/FrozenStringLiteralComment:\n  Enabled: true\n  EnforcedStyle: always\n\nStyle/StringLiterals:\n  Enabled: true\n  EnforcedStyle: single_quotes\n\nStyle/StringLiteralsInInterpolation:\n  Enabled: true\n  EnforcedStyle: single_quotes\n\nStyle/Documentation:\n  Enabled: false\n\nLayout/LineLength:\n  Max: 120\n  AllowedPatterns:\n    - '\\A\\s*#'\n  Exclude:\n    - \"spec/**/*\"\n\nMetrics/MethodLength:\n  Max: 20\n  Exclude:\n    - \"spec/**/*\"\n\nMetrics/BlockLength:\n  Enabled: true\n  Max: 350\n  CountComments: false\n\nMetrics/AbcSize:\n  Max: 20\n  Exclude:\n    - \"spec/**/*\"\n\nRSpec/ExampleLength:\n  Max: 50\n\nRSpec/MultipleExpectations:\n  Max: 25\n\nRSpec/NestedGroups:\n  Max: 6\n"
  },
  {
    "path": "packages/ruby/Gemfile",
    "content": "# frozen_string_literal: true\n\nsource 'https://rubygems.org'\n\nruby '>= 3.2'\n\ngemspec\n\ngroup :development, :test do\n  gem 'rake-compiler'\n  gem 'rbs', require: false\n  gem 'rb_sys' # provides build tooling when developing locally\n  gem 'rspec'\n  gem 'rubocop', require: false\n  gem 'rubocop-performance', require: false\n  gem 'rubocop-rspec', require: false\n  gem 'steep', require: false\nend\n"
  },
  {
    "path": "packages/ruby/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nBlazing-fast HTML to Markdown conversion for Ruby, powered by the same Rust engine used by our Python, Node.js, WebAssembly, and PHP packages.\nShip identical Markdown across every runtime while enjoying native extension performance with Magnus bindings.\n\n## Installation\n\n```bash\ngem install html-to-markdown\n```\n\nRequires Ruby 3.2+ with Magnus native extension bindings. Published for Linux, macOS.\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document            | Size  | Latency | Throughput |\n| ------------------- | ----- | ------- | ---------- |\n| Lists (Timeline)    | 129KB | 0.71ms  | 182 MB/s   |\n| Tables (Countries)  | 360KB | 2.15ms  | 167 MB/s   |\n| Mixed (Python wiki) | 656KB | 4.89ms  | 134 MB/s   |\n\n## Quick Start\n\nBasic conversion:\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]\n```\n\nWith conversion options:\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\"\nresult = HtmlToMarkdown.convert(html, heading_style: :atx, code_block_style: :fenced)\nmarkdown = result[:content]\n```\n\n## API Reference\n\n### Core Function\n\n**`convert(html, options: nil, visitor: nil) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` hash with all results in a single call.\n\n```ruby\nrequire 'html_to_markdown'\n\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]       # Converted Markdown string\nmetadata = result[:metadata]      # Metadata (when extract_metadata: true)\ntables   = result[:tables]        # Structured table data (when extract_tables: true)\ndocument = result[:document]      # Document-level info\nimages   = result[:images]        # Extracted images\nwarnings = result[:warnings]      # Any conversion warnings\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = HtmlToMarkdown.convert(html, output_format: 'djot')\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = HtmlToMarkdown.convert(html, output_format: 'plain')\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = HtmlToMarkdown.convert(html, extract_metadata: true)\n\nputs result[:content]                             # Converted Markdown\nputs result[:metadata][:document][:title]         # Document title\nputs result[:metadata][:headers]                  # All h1-h6 elements\nputs result[:metadata][:links]                    # All hyperlinks\nputs result[:metadata][:images]                   # All images with alt text\nputs result[:metadata][:structured_data]          # JSON-LD, Microdata, RDFa\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```ruby\nrequire 'html_to_markdown'\n\nclass MyVisitor\n  def visit_link(ctx, href, text, title = nil)\n    # Rewrite CDN URLs\n    if href.start_with?('https://old-cdn.com')\n      href = href.sub('https://old-cdn.com', 'https://new-cdn.com')\n    end\n    { type: :custom, output: \"[#{text}](#{href})\" }\n  end\n\n  def visit_image(ctx, src, alt = nil, title = nil)\n    # Skip tracking pixels\n    src.include?('tracking') ? { type: :skip } : { type: :continue }\n  end\nend\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = HtmlToMarkdown.convert(html, visitor: MyVisitor.new)\nmarkdown = result[:content]\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **RubyGems:** [rubygems.org/gems/html-to-markdown](https://rubygems.org/gems/html-to-markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/ruby/Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler'\nBundler::GemHelper.install_tasks name: 'html_to_markdown'\nrequire 'rake/extensiontask'\nrequire 'rspec/core/rake_task'\n\nGEMSPEC = Gem::Specification.load(File.expand_path('html_to_markdown.gemspec', __dir__))\n\nRake::ExtensionTask.new('html_to_markdown_rb', GEMSPEC) do |ext|\n  ext.lib_dir = 'lib'\n  ext.ext_dir = 'ext/html_to_markdown_rb'\n  ext.cross_compile = true\n  ext.cross_platform = %w[\n    x86_64-linux\n    aarch64-linux\n    x86_64-darwin\n    arm64-darwin\n    x64-mingw32\n    x64-mingw-ucrt\n  ]\nend\n\nRSpec::Core::RakeTask.new(:spec)\n\ntask spec: :compile\ntask default: :spec\n"
  },
  {
    "path": "packages/ruby/Steepfile",
    "content": "# frozen_string_literal: true\n\n# Steepfile for type checking html-to-markdown Ruby gem\n\ntarget :lib do\n  signature 'sig'\n\n  check 'lib'\n\n  configure_code_diagnostics do |hash|\n    hash[Steep::Diagnostic::Ruby::UnannotatedEmptyCollection] = :hint\n    hash[Steep::Diagnostic::Ruby::UnknownConstant] = :hint\n    hash[Steep::Diagnostic::Ruby::NoMethod] = :hint\n  end\n\n  # Configure libraries\n  library 'pathname'\n  library 'open3'\n\n  # Ignore vendor directory\n  ignore 'vendor'\n\n  # Ignore spec directory\n  ignore 'spec'\n\n  # Ignore bin directory\n  ignore 'bin'\n\n  # Ignore internal implementation modules (not public API)\n  ignore 'lib/html_to_markdown/cli.rb'\n  ignore 'lib/html_to_markdown/cli_proxy.rb'\nend\n"
  },
  {
    "path": "packages/ruby/exe/html-to-markdown",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire 'html_to_markdown/cli'\n\nexit HtmlToMarkdown::CLI.run(ARGV)\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/Cargo.toml",
    "content": "[workspace]\n\n[package]\nname = \"html-to-markdown-rb\"\nversion = \"3.4.0-rc.25\"\nedition = \"2024\"\nlicense = \"MIT\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../../../../crates/html-to-markdown\", features = [\n  \"full\",\n  \"metadata\",\n  \"visitor\",\n  \"serde\",\n  \"inline-images\",\n] }\nmagnus = \"0.8\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/Makefile",
    "content": "RB_SYS_BUILD_DIR ?= /Users/naamanhirschfeld/workspace/kreuzberg-dev/html-to-markdown/packages/ruby/ext/html_to_markdown_rb/.rb-sys\nCARGO ?= cargo\nCARGO_BUILD_TARGET ?=\nSOEXT ?= dylib\n\n\n# Determine the prefix Cargo uses for the lib.\nifneq ($(SOEXT),dll)\n\tSOEXT_PREFIX ?= lib\nendif\n\nRB_SYS_CARGO_PROFILE ?= release\nRB_SYS_CARGO_FEATURES ?=\nRB_SYS_GLOBAL_RUSTFLAGS ?=\nRB_SYS_EXTRA_RUSTFLAGS ?=\nRB_SYS_EXTRA_CARGO_ARGS ?=\nRB_SYS_CARGO_MANIFEST_DIR ?= .\n\n# Set dirname for the profile, since the profiles do not directly map to target dir (i.e. dev -> debug)\nifeq ($(RB_SYS_CARGO_PROFILE),dev)\n\tRB_SYS_CARGO_PROFILE_DIR ?= debug\nelse\n\tRB_SYS_CARGO_PROFILE_DIR ?= $(RB_SYS_CARGO_PROFILE)\nendif\n\n# Set the build profile (dev, release, etc.).\n\tRB_SYS_CARGO_PROFILE_FLAG = --profile $(RB_SYS_CARGO_PROFILE)\n\n# Account for sub-directories when using `--target` argument with Cargo\nRB_SYS_CARGO_TARGET_DIR ?= target\nifneq ($(CARGO_BUILD_TARGET),)\n\tRB_SYS_FULL_TARGET_DIR = $(RB_SYS_CARGO_TARGET_DIR)/$(CARGO_BUILD_TARGET)\nelse\n\tRB_SYS_FULL_TARGET_DIR = $(RB_SYS_CARGO_TARGET_DIR)\nendif\n\ntarget_prefix =\nTARGET_NAME = html_to_markdown_rb\nTARGET_ENTRY = Init_$(TARGET_NAME)\nRUBYARCHDIR = $(sitearchdir)$(target_prefix)\nTARGET = html_to_markdown_rb\nDLLIB = $(TARGET).bundle\nRUSTLIBDIR = $(RB_SYS_FULL_TARGET_DIR)/$(RB_SYS_CARGO_PROFILE_DIR)\nRUSTLIB = $(RUSTLIBDIR)/$(SOEXT_PREFIX)$(TARGET_NAME).$(SOEXT)\nTIMESTAMP_DIR = .\nPOSTLINK = dsymutil $@ 2>/dev/null; { test -z '$(RUBY_CODESIGN)' || codesign -s '$(RUBY_CODESIGN)' $@; }\n\nCLEANOBJS = $(RUSTLIBDIR) $(RB_SYS_BUILD_DIR)\nCLEANLIBS = $(DLLIB) $(RUSTLIB)\nRUBYGEMS_CLEAN_DIRS = $(CLEANOBJS) $(CLEANFILES) ./cargo-vendor\n\n\nSHELL = /bin/sh\n\n# V=0 quiet, V=1 verbose.  other values don't work.\nV = 0\nV0 = $(V:0=)\nQ1 = $(V:1=)\nQ = $(Q1:0=@)\nECHO1 = $(V:1=@ :)\nECHO = $(ECHO1:0=@ echo)\nNULLCMD = :\n\n#### Start of system configuration section. ####\n\nsrcdir = .\ntopdir = /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0\nhdrdir = $(topdir)\narch_hdrdir = /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/arm64-darwin25\nPATH_SEPARATOR = :\nVPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby\n\nprefix = $(DESTDIR)/Users/naamanhirschfeld/.rbenv/versions/3.4.8\n\nrubysitearchprefix = $(rubylibprefix)/$(sitearch)\n\nrubyarchprefix = $(rubylibprefix)/$(arch)\n\nrubylibprefix = $(libdir)/$(RUBY_BASE_NAME)\n\nexec_prefix = $(prefix)\n\nvendorarchhdrdir = $(vendorhdrdir)/$(sitearch)\n\nsitearchhdrdir = $(sitehdrdir)/$(sitearch)\n\nrubyarchhdrdir = $(rubyhdrdir)/$(arch)\n\nvendorhdrdir = $(rubyhdrdir)/vendor_ruby\n\nsitehdrdir = $(rubyhdrdir)/site_ruby\n\nrubyhdrdir = $(includedir)/$(RUBY_VERSION_NAME)\n\nvendorarchdir = $(vendorlibdir)/$(sitearch)\n\nvendorlibdir = $(vendordir)/$(ruby_version)\n\nvendordir = $(rubylibprefix)/vendor_ruby\n\nsitearchdir = $(sitelibdir)/$(sitearch)\n\nsitelibdir = $(sitedir)/$(ruby_version)\n\nsitedir = $(rubylibprefix)/site_ruby\n\nrubyarchdir = $(rubylibdir)/$(arch)\n\nrubylibdir = $(rubylibprefix)/$(ruby_version)\n\nsitearchincludedir = $(includedir)/$(sitearch)\n\narchincludedir = $(includedir)/$(arch)\n\nsitearchlibdir = $(libdir)/$(sitearch)\n\narchlibdir = $(libdir)/$(arch)\n\nridir = $(datarootdir)/$(RI_BASE_NAME)\n\nmodular_gc_dir = $(DESTDIR)\n\nmandir = $(datarootdir)/man\n\nlocaledir = $(datarootdir)/locale\n\nlibdir = $(exec_prefix)/lib\n\npsdir = $(docdir)\n\npdfdir = $(docdir)\n\ndvidir = $(docdir)\n\nhtmldir = $(docdir)\n\ninfodir = $(datarootdir)/info\n\ndocdir = $(datarootdir)/doc/$(PACKAGE)\n\noldincludedir = $(DESTDIR)/usr/include\n\nincludedir = $(SDKROOT)$(prefix)/include\n\nrunstatedir = $(localstatedir)/run\n\nlocalstatedir = $(prefix)/var\n\nsharedstatedir = $(prefix)/com\n\nsysconfdir = $(prefix)/etc\n\ndatadir = $(datarootdir)\n\ndatarootdir = $(prefix)/share\n\nlibexecdir = $(exec_prefix)/libexec\n\nsbindir = $(exec_prefix)/sbin\n\nbindir = $(exec_prefix)/bin\n\narchdir = $(rubyarchdir)\n\n\n\nCC_WRAPPER =\nCC = clang\nCXX = clang++\nLIBRUBY = $(LIBRUBY_SO)\nLIBRUBY_A = lib$(RUBY_SO_NAME)-static.a\nLIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)\nLIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static -framework CoreFoundation $(MAINLIBS)\nempty =\nOUTFLAG = -o $(empty)\nCOUTFLAG = -o $(empty)\nCSRCFLAG = $(empty)\n\nRUBY_EXTCONF_H =\ncflags   = $(hardenflags) -fdeclspec  $(optflags) $(debugflags) $(warnflags)\ncxxflags =\noptflags = -O3 -fno-fast-math\ndebugflags = -ggdb3\nwarnflags = -Wall -Wextra -Wextra-tokens -Wdeprecated-declarations -Wdivision-by-zero -Wdiv-by-zero -Wimplicit-function-declaration -Wimplicit-int -Wpointer-arith -Wshorten-64-to-32 -Wwrite-strings -Wold-style-definition -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wunused-variable -Wmisleading-indentation -Wundef\ncppflags =\nCCDLFLAGS = -fno-common\nCFLAGS   = $(CCDLFLAGS) $(cflags) -fno-common -pipe $(ARCH_FLAG)\nINCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)\nDEFS     =\nCPPFLAGS =  -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT $(DEFS) $(cppflags)\nCXXFLAGS = $(CCDLFLAGS) -fdeclspec $(ARCH_FLAG)\nldflags  = -L. -fstack-protector-strong -L/opt/homebrew/Cellar/gmp/6.3.0/lib\ndldflags = -L/opt/homebrew/Cellar/gmp/6.3.0/lib -Wl,-undefined,dynamic_lookup\nARCH_FLAG = -arch arm64\nDLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)\nLDSHARED = $(CC) -dynamic -bundle\nLDSHAREDXX = $(CXX) -dynamic -bundle\nPOSTLINK = dsymutil $@ 2>/dev/null; { test -z '$(RUBY_CODESIGN)' || codesign -s '$(RUBY_CODESIGN)' $@; }\nAR = ar\nLD = ld\nEXEEXT =\n\n\nRUBY_INSTALL_NAME = $(RUBY_BASE_NAME)\n\nRUBY_SO_NAME = ruby.3.4\n\nRUBYW_INSTALL_NAME =\n\nRUBY_VERSION_NAME = $(RUBY_BASE_NAME)-$(ruby_version)\n\nRUBYW_BASE_NAME = rubyw\n\nRUBY_BASE_NAME = ruby\n\n\narch = arm64-darwin25\nsitearch = $(arch)\nruby_version = 3.4.0\nruby = $(bindir)/$(RUBY_BASE_NAME)\nRUBY = $(ruby)\nBUILTRUBY = $(bindir)/$(RUBY_BASE_NAME)\nruby_headers = $(hdrdir)/ruby.h $(hdrdir)/ruby/backward.h $(hdrdir)/ruby/ruby.h $(hdrdir)/ruby/defines.h $(hdrdir)/ruby/missing.h $(hdrdir)/ruby/intern.h $(hdrdir)/ruby/st.h $(hdrdir)/ruby/subst.h $(arch_hdrdir)/ruby/config.h\n\nRM = rm -f\nRM_RF = rm -fr\nRMDIRS = rmdir -p\nMAKEDIRS = /opt/homebrew/bin/gmkdir -p\nINSTALL = /opt/homebrew/bin/ginstall -c\nINSTALL_PROG = $(INSTALL) -m 0755\nINSTALL_DATA = $(INSTALL) -m 644\nCOPY = cp\nTOUCH = exit >\n\n#### End of system configuration section. ####\n\npreload =\n\nCLEANFILES = mkmf.log\nDISTCLEANFILES =\n\nall static install-rb: Makefile\n\t@$(NULLCMD)\n.PHONY: all static install-rb\n.PHONY: clean clean-so clean-static clean-rb\n\n\n\nclean-static::\nclean-rb-default::\nclean-rb::\nclean-so::\nclean: clean-so clean-static clean-rb-default clean-rb\n\t\t-$(Q)$(RM_RF) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES) .*.time\n\ndistclean-rb-default::\ndistclean-rb::\ndistclean-so::\ndistclean-static::\ndistclean: clean distclean-so distclean-static distclean-rb-default distclean-rb\n\t\t-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log\n\t\t-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)\n\t\t-$(Q)$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true\n\nrealclean: distclean\n\n\n.PHONY: gemclean\n\nifneq ($(RB_SYS_VERBOSE),)\n\tQ = $(0=@)\nendif\n\nCC = clang\nCXX = clang++\nAR = ar\nexport RBCONFIG_DESTDIR :=\nexport RBCONFIG_MAJOR := 3\nexport RBCONFIG_MINOR := 4\nexport RBCONFIG_TEENY := 8\nexport RBCONFIG_PATCHLEVEL := 72\nexport RBCONFIG_INSTALL := /opt/homebrew/bin/ginstall -c\nexport RBCONFIG_EXEEXT :=\nexport RBCONFIG_prefix := /Users/naamanhirschfeld/.rbenv/versions/3.4.8\nexport RBCONFIG_ruby_install_name := ruby\nexport RBCONFIG_RUBY_INSTALL_NAME := ruby\nexport RBCONFIG_RUBY_SO_NAME := ruby.3.4\nexport RBCONFIG_exec := exec\nexport RBCONFIG_ruby_pc := ruby-3.4.pc\nexport RBCONFIG_CC_WRAPPER :=\nexport RBCONFIG_PACKAGE := ruby\nexport RBCONFIG_BUILTIN_TRANSSRCS := enc/trans/newline.c\nexport RBCONFIG_MKMF_VERBOSE := 0\nexport RBCONFIG_MANTYPE := doc\nexport RBCONFIG_vendorarchhdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/vendor_ruby/arm64-darwin25\nexport RBCONFIG_sitearchhdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/site_ruby/arm64-darwin25\nexport RBCONFIG_rubyarchhdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/arm64-darwin25\nexport RBCONFIG_vendorhdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/vendor_ruby\nexport RBCONFIG_sitehdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0/site_ruby\nexport RBCONFIG_rubyhdrdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/ruby-3.4.0\nexport RBCONFIG_RUBY_SEARCH_PATH :=\nexport RBCONFIG_UNIVERSAL_INTS :=\nexport RBCONFIG_UNIVERSAL_ARCHNAMES :=\nexport RBCONFIG_configure_args := '--prefix=/Users/naamanhirschfeld/.rbenv/versions/3.4.8' '--with-openssl-dir=/opt/homebrew/opt/openssl@3' '--enable-shared' '--with-libyaml-dir=/opt/homebrew/opt/libyaml' '--with-gmp-dir=/opt/homebrew/opt/gmp' '--with-ext=openssl,psych,+' 'CC=clang'\nexport RBCONFIG_CONFIGURE := configure\nexport RBCONFIG_vendorarchdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/vendor_ruby/3.4.0/arm64-darwin25\nexport RBCONFIG_vendorlibdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/vendor_ruby/3.4.0\nexport RBCONFIG_vendordir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/vendor_ruby\nexport RBCONFIG_sitearchdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/site_ruby/3.4.0/arm64-darwin25\nexport RBCONFIG_sitelibdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/site_ruby/3.4.0\nexport RBCONFIG_sitedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/site_ruby\nexport RBCONFIG_rubyarchdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/3.4.0/arm64-darwin25\nexport RBCONFIG_rubylibdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/3.4.0\nexport RBCONFIG_ruby_version := 3.4.0\nexport RBCONFIG_sitearch := arm64-darwin25\nexport RBCONFIG_arch := arm64-darwin25\nexport RBCONFIG_sitearchincludedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/arm64-darwin25\nexport RBCONFIG_archincludedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include/arm64-darwin25\nexport RBCONFIG_sitearchlibdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/arm64-darwin25\nexport RBCONFIG_archlibdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/arm64-darwin25\nexport RBCONFIG_libdirname := libdir\nexport RBCONFIG_RUBY_EXEC_PREFIX := /Users/naamanhirschfeld/.rbenv/versions/3.4.8\nexport RBCONFIG_RUBY_LIB_VERSION :=\nexport RBCONFIG_RUBY_LIB_VERSION_STYLE := 3\t/* full */\nexport RBCONFIG_RI_BASE_NAME := ri\nexport RBCONFIG_ridir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/ri\nexport RBCONFIG_rubysitearchprefix := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/arm64-darwin25\nexport RBCONFIG_rubyarchprefix := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/arm64-darwin25\nexport RBCONFIG_MAKEFILES := Makefile GNUmakefile\nexport RBCONFIG_USE_LLVM_WINDRES :=\nexport RBCONFIG_PLATFORM_DIR :=\nexport RBCONFIG_COROUTINE_TYPE := arm64\nexport RBCONFIG_THREAD_MODEL := pthread\nexport RBCONFIG_SYMBOL_PREFIX := _\nexport RBCONFIG_EXPORT_PREFIX :=\nexport RBCONFIG_COMMON_HEADERS :=\nexport RBCONFIG_COMMON_MACROS :=\nexport RBCONFIG_COMMON_LIBS :=\nexport RBCONFIG_MAINLIBS := -lgmp -ldl -lobjc -lpthread\nexport RBCONFIG_ENABLE_SHARED := yes\nexport RBCONFIG_DLDSHARED := clang -dynamiclib\nexport RBCONFIG_DLDLIBS :=\nexport RBCONFIG_SOLIBS := -lgmp -ldl -lobjc -lpthread\nexport RBCONFIG_LIBRUBYARG_SHARED := -lruby.3.4\nexport RBCONFIG_LIBRUBYARG_STATIC := -lruby.3.4-static -framework CoreFoundation -lgmp -ldl -lobjc -lpthread\nexport RBCONFIG_LIBRUBYARG := -lruby.3.4\nexport RBCONFIG_LIBRUBY := libruby.3.4.dylib\nexport RBCONFIG_LIBRUBY_ALIASES := libruby.dylib\nexport RBCONFIG_LIBRUBY_SONAME := libruby.3.4.dylib\nexport RBCONFIG_LIBRUBY_SO := libruby.3.4.dylib\nexport RBCONFIG_LIBRUBY_A := libruby.3.4-static.a\nexport RBCONFIG_RUBYW_INSTALL_NAME :=\nexport RBCONFIG_rubyw_install_name :=\nexport RBCONFIG_EXTDLDFLAGS :=\nexport RBCONFIG_EXTLDFLAGS :=\nexport RBCONFIG_hardenflags := -fstack-protector-strong -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2\nexport RBCONFIG_strict_warnflags :=\nexport RBCONFIG_warnflags := -Wall -Wextra -Wextra-tokens -Wdeprecated-declarations -Wdivision-by-zero -Wdiv-by-zero -Wimplicit-function-declaration -Wimplicit-int -Wpointer-arith -Wshorten-64-to-32 -Wwrite-strings -Wold-style-definition -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wunused-variable -Wmisleading-indentation -Wundef\nexport RBCONFIG_debugflags := -ggdb3\nexport RBCONFIG_optflags := -O3 -fno-fast-math\nexport RBCONFIG_NULLCMD := :\nexport RBCONFIG_ENABLE_DEBUG_ENV :=\nexport RBCONFIG_DLNOBJ := dln.o\nexport RBCONFIG_RJIT_SUPPORT := yes\nexport RBCONFIG_YJIT_OBJ := yjit.o\nexport RBCONFIG_YJIT_LIBS := yjit/target/release/libyjit.a\nexport RBCONFIG_CARGO_BUILD_ARGS :=\nexport RBCONFIG_YJIT_SUPPORT := yes\nexport RBCONFIG_CARGO :=\nexport RBCONFIG_RUSTC := rustc\nexport RBCONFIG_INSTALL_STATIC_LIBRARY := no\nexport RBCONFIG_modular_gc_dir :=\nexport RBCONFIG_EXECUTABLE_EXTS :=\nexport RBCONFIG_ARCHFILE :=\nexport RBCONFIG_LIBRUBY_RELATIVE := no\nexport RBCONFIG_EXTOUT := .ext\nexport RBCONFIG_PREP := miniruby\nexport RBCONFIG_CROSS_COMPILING := no\nexport RBCONFIG_TEST_RUNNABLE := yes\nexport RBCONFIG_rubylibprefix := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby\nexport RBCONFIG_setup := Setup\nexport RBCONFIG_SOEXT := dylib\nexport RBCONFIG_TRY_LINK :=\nexport RBCONFIG_PRELOADENV := DYLD_INSERT_LIBRARIES\nexport RBCONFIG_LIBPATHENV := DYLD_LIBRARY_PATH\nexport RBCONFIG_RPATHFLAG :=\nexport RBCONFIG_LIBPATHFLAG := -L%s\nexport RBCONFIG_LINK_SO := \\ndsymutil $@ 2>/dev/null; { test -z '$(RUBY_CODESIGN)' || codesign -s '$(RUBY_CODESIGN)' $@; }\nexport RBCONFIG_ADDITIONAL_DLDFLAGS :=\nexport RBCONFIG_ENCSTATIC :=\nexport RBCONFIG_EXTSTATIC :=\nexport RBCONFIG_ASMEXT := S\nexport RBCONFIG_LIBEXT := a\nexport RBCONFIG_DLEXT := bundle\nexport RBCONFIG_LDSHAREDXX := clang++ -dynamic -bundle\nexport RBCONFIG_LDSHARED := clang -dynamic -bundle\nexport RBCONFIG_CCDLFLAGS := -fno-common\nexport RBCONFIG_STATIC :=\nexport RBCONFIG_ARCH_FLAG := -arch arm64\nexport RBCONFIG_DLDFLAGS := -L/opt/homebrew/Cellar/gmp/6.3.0/lib -Wl,-undefined,dynamic_lookup\nexport RBCONFIG_ALLOCA :=\nexport RBCONFIG_EGREP := /usr/bin/grep -E\nexport RBCONFIG_GREP := /usr/bin/grep\nexport RBCONFIG_dsymutil := dsymutil\nexport RBCONFIG_codesign := codesign\nexport RBCONFIG_cleanlibs := $(TARGET_SO:=.dSYM)\nexport RBCONFIG_POSTLINK := dsymutil $@ 2>/dev/null; { test -z '$(RUBY_CODESIGN)' || codesign -s '$(RUBY_CODESIGN)' $@; }\nexport RBCONFIG_incflags := -I/opt/homebrew/Cellar/gmp/6.3.0/include\nexport RBCONFIG_WERRORFLAG := -Werror\nexport RBCONFIG_RUBY_DEVEL :=\nexport RBCONFIG_CHDIR := cd -P\nexport RBCONFIG_RMALL := rm -fr\nexport RBCONFIG_RMDIRS := rmdir -p\nexport RBCONFIG_RMDIR := rmdir\nexport RBCONFIG_CP := cp\nexport RBCONFIG_RM := rm -f\nexport RBCONFIG_PKG_CONFIG := pkg-config\nexport RBCONFIG_DOXYGEN :=\nexport RBCONFIG_DOT :=\nexport RBCONFIG_MKDIR_P := /opt/homebrew/bin/gmkdir -p\nexport RBCONFIG_INSTALL_DATA := /opt/homebrew/bin/ginstall -c -m 644\nexport RBCONFIG_INSTALL_SCRIPT := /opt/homebrew/bin/ginstall -c\nexport RBCONFIG_INSTALL_PROGRAM := /opt/homebrew/bin/ginstall -c\nexport RBCONFIG_SET_MAKE :=\nexport RBCONFIG_LN_S := ln -s\nexport RBCONFIG_DLLWRAP :=\nexport RBCONFIG_WINDRES :=\nexport RBCONFIG_ARFLAGS := rcu\nexport RBCONFIG_try_header :=\nexport RBCONFIG_CC_VERSION_MESSAGE := Apple clang version 17.0.0 (clang-1700.6.3.2)\\nTarget: arm64-apple-darwin25.3.0\\nThread model: posix\\nInstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin\nexport RBCONFIG_CC_VERSION := clang --version\nexport RBCONFIG_CSRCFLAG :=\nexport RBCONFIG_COUTFLAG := -o\nexport RBCONFIG_OUTFLAG := -o\nexport RBCONFIG_CPPOUTFILE := -o conftest.i\nexport RBCONFIG_GNU_LD := no\nexport RBCONFIG_GCC := yes\nexport RBCONFIG_CPP := clang -E\nexport RBCONFIG_CXXFLAGS := -fdeclspec\nexport RBCONFIG_OBJEXT := o\nexport RBCONFIG_CPPFLAGS := -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT\nexport RBCONFIG_LDFLAGS := -L. -fstack-protector-strong -L/opt/homebrew/Cellar/gmp/6.3.0/lib\nexport RBCONFIG_CFLAGS := -fstack-protector-strong -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -fdeclspec  -O3 -fno-fast-math -ggdb3 -Wall -Wextra -Wextra-tokens -Wdeprecated-declarations -Wdivision-by-zero -Wdiv-by-zero -Wimplicit-function-declaration -Wimplicit-int -Wpointer-arith -Wshorten-64-to-32 -Wwrite-strings -Wold-style-definition -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wunused-variable -Wmisleading-indentation -Wundef -fno-common -pipe\nexport RBCONFIG_STRIP := strip -A -n\nexport RBCONFIG_RANLIB := ranlib\nexport RBCONFIG_OBJDUMP := objdump\nexport RBCONFIG_OBJCOPY := :\nexport RBCONFIG_NM := nm --no-llvm-bc\nexport RBCONFIG_LD := ld\nexport RBCONFIG_CXX := clang++\nexport RBCONFIG_AS := as\nexport RBCONFIG_AR := ar\nexport RBCONFIG_CC := clang\nexport RBCONFIG_wasmoptflags :=\nexport RBCONFIG_WASMOPT :=\nexport RBCONFIG_config_target := aarch64-apple-darwin25.3.0\nexport RBCONFIG_target_os := darwin25\nexport RBCONFIG_target_vendor := apple\nexport RBCONFIG_target_cpu := arm64\nexport RBCONFIG_target := arm64-apple-darwin25\nexport RBCONFIG_host_os := darwin25\nexport RBCONFIG_host_vendor := apple\nexport RBCONFIG_host_cpu := arm64\nexport RBCONFIG_host := arm64-apple-darwin25\nexport RBCONFIG_build_os := darwin25.3.0\nexport RBCONFIG_build_vendor := apple\nexport RBCONFIG_build_cpu := aarch64\nexport RBCONFIG_build := aarch64-apple-darwin25.3.0\nexport RBCONFIG_RUBY_VERSION_NAME := ruby-3.4.0\nexport RBCONFIG_RUBYW_BASE_NAME := rubyw\nexport RBCONFIG_RUBY_BASE_NAME := ruby\nexport RBCONFIG_RUBY_PROGRAM_VERSION := 3.4.8\nexport RBCONFIG_RUBY_API_VERSION := 3.4\nexport RBCONFIG_HAVE_GIT := yes\nexport RBCONFIG_GIT := git\nexport RBCONFIG_cxxflags :=\nexport RBCONFIG_cppflags :=\nexport RBCONFIG_cflags := -fstack-protector-strong -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -fdeclspec  -O3 -fno-fast-math -ggdb3 -Wall -Wextra -Wextra-tokens -Wdeprecated-declarations -Wdivision-by-zero -Wdiv-by-zero -Wimplicit-function-declaration -Wimplicit-int -Wpointer-arith -Wshorten-64-to-32 -Wwrite-strings -Wold-style-definition -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wunused-variable -Wmisleading-indentation -Wundef\nexport RBCONFIG_MAKEDIRS := /opt/homebrew/bin/gmkdir -p\nexport RBCONFIG_target_alias :=\nexport RBCONFIG_host_alias :=\nexport RBCONFIG_build_alias :=\nexport RBCONFIG_LIBS := -lpthread\nexport RBCONFIG_ECHO_T :=\nexport RBCONFIG_ECHO_N :=\nexport RBCONFIG_ECHO_C := \\\\c\nexport RBCONFIG_DEFS :=\nexport RBCONFIG_mandir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/man\nexport RBCONFIG_localedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/locale\nexport RBCONFIG_libdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib\nexport RBCONFIG_psdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/doc/ruby\nexport RBCONFIG_pdfdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/doc/ruby\nexport RBCONFIG_dvidir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/doc/ruby\nexport RBCONFIG_htmldir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/doc/ruby\nexport RBCONFIG_infodir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/info\nexport RBCONFIG_docdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share/doc/ruby\nexport RBCONFIG_oldincludedir := /usr/include\nexport RBCONFIG_includedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/include\nexport RBCONFIG_runstatedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/var/run\nexport RBCONFIG_localstatedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/var\nexport RBCONFIG_sharedstatedir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/com\nexport RBCONFIG_sysconfdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/etc\nexport RBCONFIG_datadir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share\nexport RBCONFIG_datarootdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/share\nexport RBCONFIG_libexecdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/libexec\nexport RBCONFIG_sbindir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/sbin\nexport RBCONFIG_bindir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/bin\nexport RBCONFIG_exec_prefix := /Users/naamanhirschfeld/.rbenv/versions/3.4.8\nexport RBCONFIG_PACKAGE_URL :=\nexport RBCONFIG_PACKAGE_BUGREPORT :=\nexport RBCONFIG_PACKAGE_STRING :=\nexport RBCONFIG_PACKAGE_VERSION :=\nexport RBCONFIG_PACKAGE_TARNAME :=\nexport RBCONFIG_PACKAGE_NAME :=\nexport RBCONFIG_PATH_SEPARATOR := :\nexport RBCONFIG_SHELL := /bin/sh\nexport RBCONFIG_UNICODE_VERSION := 15.0.0\nexport RBCONFIG_UNICODE_EMOJI_VERSION := 15.0\nexport RBCONFIG_SDKROOT :=\nexport RBCONFIG_platform := arm64-darwin25\nexport RBCONFIG_archdir := /Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib/ruby/3.4.0/arm64-darwin25\nexport RBCONFIG_topdir := .\nexport RBCONFIG_srcdir := .\nexport RUSTFLAGS := $(RB_SYS_GLOBAL_RUSTFLAGS) $(RB_SYS_EXTRA_RUSTFLAGS) $(RUSTFLAGS)\n\nFORCE: ;\n\nRB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN ?= false\n\n# Only run if the we are told to explicitly install the Rust toolchain\nifneq ($(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN),false)\nRB_SYS_RUSTUP_PROFILE ?= minimal\n\n# If the user passed true, we assume stable Rust. Otherwise, use what\n# was specified (i.e. RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN=beta)\nifeq ($(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN),true)\n  RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN = stable\nendif\n\n# If a $RUST_TARGET is specified (i.e. for rake-compiler-dock), append\n# that to the profile.\nifeq ($(RUST_TARGET),)\n  RB_SYS_DEFAULT_TOOLCHAIN = $(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)\nelse\n  RB_SYS_DEFAULT_TOOLCHAIN = $(RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN)-$(RUST_TARGET)\nendif\n\n# Since we are forcing the installation of the Rust toolchain, we need\n# to set these env vars unconditionally for the build.\nexport CARGO_HOME := $(RB_SYS_BUILD_DIR)/$(RB_SYS_DEFAULT_TOOLCHAIN)/cargo\nexport RUSTUP_HOME := $(RB_SYS_BUILD_DIR)/$(RB_SYS_DEFAULT_TOOLCHAIN)/rustup\nexport PATH := $(CARGO_HOME)/bin:$(RUSTUP_HOME)/bin:$(PATH)\nexport RUSTUP_TOOLCHAIN := $(RB_SYS_DEFAULT_TOOLCHAIN)\nexport CARGO := $(CARGO_HOME)/bin/cargo\n\n\n$(CARGO):\n\t$(Q) $(MAKEDIRS) $(CARGO_HOME) $(RUSTUP_HOME)\n\t$(Q) curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused -fsSL \"https://sh.rustup.rs\" | sh -s -- --no-modify-path --profile $(RB_SYS_RUSTUP_PROFILE) --default-toolchain none -y\n\t$(Q) $(CARGO_HOME)/bin/rustup toolchain install $(RB_SYS_DEFAULT_TOOLCHAIN) --profile $(RB_SYS_RUSTUP_PROFILE) || (sleep 5; $(Q) $(CARGO_HOME)/bin/rustup toolchain install $(RB_SYS_DEFAULT_TOOLCHAIN) --profile $(RB_SYS_RUSTUP_PROFILE)) || (sleep 5; $(Q) $(CARGO_HOME)/bin/rustup toolchain install $(RB_SYS_DEFAULT_TOOLCHAIN) --profile $(RB_SYS_RUSTUP_PROFILE))\n\t$(Q) $(CARGO_HOME)/bin/rustup default $(RB_SYS_DEFAULT_TOOLCHAIN) || (sleep 5; $(Q) $(CARGO_HOME)/bin/rustup default $(RB_SYS_DEFAULT_TOOLCHAIN)) || (sleep 5; $(Q) $(CARGO_HOME)/bin/rustup default $(RB_SYS_DEFAULT_TOOLCHAIN))\n\n\n$(RUSTLIB): $(CARGO)\nendif\n\n\n$(TIMESTAMP_DIR)/.sitearchdir.time:\n\t$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)\n\t$(Q) $(TOUCH) $@\n\n$(RUSTLIB): FORCE\n\t$(ECHO) generating $(@) \\(\"$(RB_SYS_CARGO_PROFILE)\"\\)\n\t$(CARGO) rustc $(RB_SYS_EXTRA_CARGO_ARGS) --manifest-path $(RB_SYS_CARGO_MANIFEST_DIR)/Cargo.toml --target-dir $(RB_SYS_CARGO_TARGET_DIR) --lib $(RB_SYS_CARGO_PROFILE_FLAG) -- -C linker=clang -L native=/Users/naamanhirschfeld/.rbenv/versions/3.4.8/lib -L native=/opt/homebrew/Cellar/gmp/6.3.0/lib -C link-arg=-Wl,-undefined,dynamic_lookup -l pthread\n\n$(DLLIB): $(RUSTLIB)\n\t$(Q) $(COPY) \"$(RUSTLIB)\" $@\n\t$(Q) $(POSTLINK)\n\ninstall-so: $(DLLIB) $(TIMESTAMP_DIR)/.sitearchdir.time\n\t$(ECHO) installing $(DLLIB) to $(RUBYARCHDIR)\n\t$(Q) install_name_tool -id \"\" $(DLLIB)\n\t$(Q) $(MAKEDIRS) $(RUBYARCHDIR)\n\t$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)\n\ngemclean: install-so\n\t$(ECHO) Cleaning gem artifacts\n\t-$(Q)$(RM_RF) $(RUBYGEMS_CLEAN_DIRS) 2> /dev/null || true\n\ninstall: install-so\n\nall: $(DLLIB)\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/extconf.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'mkmf'\nrequire 'rb_sys/mkmf'\n\ndefault_profile = ENV.fetch('CARGO_PROFILE', 'release')\n\ncreate_rust_makefile('html_to_markdown_rb') do |config|\n  config.profile = default_profile.to_sym\nend\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/native/Cargo.toml",
    "content": "[package]\nname = \"html-to-markdown-rb\"\nversion = \"3.4.0-rc.3\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"High-performance HTML to Markdown converter\"\nreadme = false\nkeywords = [\"html\", \"markdown\", \"converter\"]\ncategories = [\"text-processing\"]\n\n[package.metadata.cargo-machete]\nignored = [\"async-trait\", \"tokio\"]\n\n[lib]\nname = \"html_to_markdown_rb\"\npath = \"../src/lib.rs\"\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhtml-to-markdown-rs = { path = \"../../../../../crates/html-to-markdown\", features = [\n  \"full\",\n  \"metadata\",\n  \"visitor\",\n  \"serde\",\n  \"inline-images\",\n] }\nmagnus = \"0.8\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\ntokio = { version = \"1\", features = [\"rt-multi-thread\"] }\nasync-trait = \"0.1\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/src/html-to-markdown/version.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:f8348a772ac4111741cfb31c1e551af74cfd686707dbea8edc5c43b65ef2e96f\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nmodule HtmlToMarkdown\n  VERSION = \"3.4.0.pre.rc.25\"\nend\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/src/html-to-markdown.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:124fb5b94b7ac1422663dc05d74f1e392979bf0c58d17388c55cf44b57f3cfd8\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire_relative 'html-to-markdown/version'\nrequire_relative 'html-to-markdown/native'\n\nmodule HtmlToMarkdown\n  # Re-export all types and functions from native extension\nend\n"
  },
  {
    "path": "packages/ruby/ext/html_to_markdown_rb/src/lib.rs",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:fe575457381d032f690e727733dba64d02e662c9e9940938b8246fc6fc8cf04c\n// Re-generate with: alef generate\n#![allow(dead_code, unused_imports, unused_variables)]\n#![allow(\n    clippy::too_many_arguments,\n    clippy::let_unit_value,\n    clippy::needless_borrow,\n    clippy::map_identity,\n    clippy::just_underscores_and_digits,\n    clippy::unnecessary_cast,\n    clippy::unused_unit,\n    clippy::unwrap_or_default,\n    clippy::derivable_impls,\n    clippy::needless_borrows_for_generic_args,\n    clippy::unnecessary_fallible_conversions\n)]\n\nuse magnus::{Error, IntoValueFromNative, Ruby, function, method, prelude::*, try_convert::TryConvertOwned};\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\nfn json_to_ruby(handle: &Ruby, val: serde_json::Value) -> magnus::Value {\n    use magnus::IntoValue;\n    match val {\n        serde_json::Value::Null => handle.qnil().into_value_with(handle),\n        serde_json::Value::Bool(b) => b.into_value_with(handle),\n        serde_json::Value::Number(n) => {\n            if let Some(i) = n.as_i64() {\n                i.into_value_with(handle)\n            } else if let Some(f) = n.as_f64() {\n                f.into_value_with(handle)\n            } else {\n                handle.qnil().into_value_with(handle)\n            }\n        }\n        serde_json::Value::String(s) => s.into_value_with(handle),\n        serde_json::Value::Array(arr) => {\n            let ruby_arr = handle.ary_new_capa(arr.len());\n            for item in arr {\n                let _ = ruby_arr.push(json_to_ruby(handle, item));\n            }\n            ruby_arr.into_value_with(handle)\n        }\n        serde_json::Value::Object(map) => {\n            let hash = handle.hash_new();\n            for (k, v) in map {\n                let key = handle.to_symbol(&k);\n                let val = json_to_ruby(handle, v);\n                let _ = hash.aset(key, val);\n            }\n            hash.into_value_with(handle)\n        }\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::DocumentMetadata\")]\n#[serde(default)]\npub struct DocumentMetadata {\n    pub title: Option<String>,\n    pub description: Option<String>,\n    pub keywords: Vec<String>,\n    pub author: Option<String>,\n    pub canonical_url: Option<String>,\n    pub base_href: Option<String>,\n    pub language: Option<String>,\n    pub text_direction: Option<TextDirection>,\n    pub open_graph: HashMap<String, String>,\n    pub twitter_card: HashMap<String, String>,\n    pub meta_tags: HashMap<String, String>,\n}\n\nunsafe impl IntoValueFromNative for DocumentMetadata {}\n\nimpl magnus::TryConvert for DocumentMetadata {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &DocumentMetadata = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for DocumentMetadata {}\n\nimpl DocumentMetadata {\n    fn new(\n        title: Option<String>,\n        description: Option<String>,\n        keywords: Option<Vec<String>>,\n        author: Option<String>,\n        canonical_url: Option<String>,\n        base_href: Option<String>,\n        language: Option<String>,\n        text_direction: Option<TextDirection>,\n        open_graph: Option<HashMap<String, String>>,\n        twitter_card: Option<HashMap<String, String>>,\n        meta_tags: Option<HashMap<String, String>>,\n    ) -> Self {\n        Self {\n            title,\n            description,\n            keywords: keywords.unwrap_or_default(),\n            author,\n            canonical_url,\n            base_href,\n            language,\n            text_direction,\n            open_graph: open_graph.unwrap_or_default(),\n            twitter_card: twitter_card.unwrap_or_default(),\n            meta_tags: meta_tags.unwrap_or_default(),\n        }\n    }\n\n    fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    fn description(&self) -> Option<String> {\n        self.description.clone()\n    }\n\n    fn keywords(&self) -> Vec<String> {\n        self.keywords.clone()\n    }\n\n    fn author(&self) -> Option<String> {\n        self.author.clone()\n    }\n\n    fn canonical_url(&self) -> Option<String> {\n        self.canonical_url.clone()\n    }\n\n    fn base_href(&self) -> Option<String> {\n        self.base_href.clone()\n    }\n\n    fn language(&self) -> Option<String> {\n        self.language.clone()\n    }\n\n    fn text_direction(&self) -> Option<TextDirection> {\n        self.text_direction.clone()\n    }\n\n    fn open_graph(&self) -> HashMap<String, String> {\n        self.open_graph.clone()\n    }\n\n    fn twitter_card(&self) -> HashMap<String, String> {\n        self.twitter_card.clone()\n    }\n\n    fn meta_tags(&self) -> HashMap<String, String> {\n        self.meta_tags.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::HeaderMetadata\")]\npub struct HeaderMetadata {\n    pub level: u8,\n    pub text: String,\n    pub id: Option<String>,\n    pub depth: usize,\n    pub html_offset: usize,\n}\n\nunsafe impl IntoValueFromNative for HeaderMetadata {}\n\nimpl magnus::TryConvert for HeaderMetadata {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &HeaderMetadata = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for HeaderMetadata {}\n\nimpl HeaderMetadata {\n    fn new(level: u8, text: String, depth: usize, html_offset: usize, id: Option<String>) -> Self {\n        Self {\n            level,\n            text,\n            id,\n            depth,\n            html_offset,\n        }\n    }\n\n    fn level(&self) -> u8 {\n        self.level\n    }\n\n    fn text(&self) -> String {\n        self.text.clone()\n    }\n\n    fn id(&self) -> Option<String> {\n        self.id.clone()\n    }\n\n    fn depth(&self) -> usize {\n        self.depth\n    }\n\n    fn html_offset(&self) -> usize {\n        self.html_offset\n    }\n\n    fn is_valid(&self) -> bool {\n        let core_self = html_to_markdown_rs::metadata::HeaderMetadata {\n            level: self.level,\n            text: self.text.clone(),\n            id: self.id.clone(),\n            depth: self.depth,\n            html_offset: self.html_offset,\n        };\n        core_self.is_valid()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::LinkMetadata\")]\npub struct LinkMetadata {\n    pub href: String,\n    pub text: String,\n    pub title: Option<String>,\n    pub link_type: LinkType,\n    pub rel: Vec<String>,\n    pub attributes: HashMap<String, String>,\n}\n\nunsafe impl IntoValueFromNative for LinkMetadata {}\n\nimpl magnus::TryConvert for LinkMetadata {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &LinkMetadata = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for LinkMetadata {}\n\nimpl LinkMetadata {\n    fn new(\n        href: String,\n        text: String,\n        link_type: LinkType,\n        rel: Vec<String>,\n        attributes: HashMap<String, String>,\n        title: Option<String>,\n    ) -> Self {\n        Self {\n            href,\n            text,\n            title,\n            link_type,\n            rel,\n            attributes,\n        }\n    }\n\n    fn href(&self) -> String {\n        self.href.clone()\n    }\n\n    fn text(&self) -> String {\n        self.text.clone()\n    }\n\n    fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    fn link_type(&self) -> LinkType {\n        self.link_type.clone()\n    }\n\n    fn rel(&self) -> Vec<String> {\n        self.rel.clone()\n    }\n\n    fn attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ImageMetadata\")]\npub struct ImageMetadata {\n    pub src: String,\n    pub alt: Option<String>,\n    pub title: Option<String>,\n    pub dimensions: Option<Vec<u32>>,\n    pub image_type: ImageType,\n    pub attributes: HashMap<String, String>,\n}\n\nunsafe impl IntoValueFromNative for ImageMetadata {}\n\nimpl magnus::TryConvert for ImageMetadata {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ImageMetadata = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ImageMetadata {}\n\nimpl ImageMetadata {\n    fn new(\n        src: String,\n        image_type: ImageType,\n        attributes: HashMap<String, String>,\n        alt: Option<String>,\n        title: Option<String>,\n        dimensions: Option<Vec<u32>>,\n    ) -> Self {\n        Self {\n            src,\n            alt,\n            title,\n            dimensions,\n            image_type,\n            attributes,\n        }\n    }\n\n    fn src(&self) -> String {\n        self.src.clone()\n    }\n\n    fn alt(&self) -> Option<String> {\n        self.alt.clone()\n    }\n\n    fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    fn dimensions(&self) -> Option<Vec<u32>> {\n        self.dimensions.clone()\n    }\n\n    fn image_type(&self) -> ImageType {\n        self.image_type.clone()\n    }\n\n    fn attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::StructuredData\")]\npub struct StructuredData {\n    pub data_type: StructuredDataType,\n    pub raw_json: String,\n    pub schema_type: Option<String>,\n}\n\nunsafe impl IntoValueFromNative for StructuredData {}\n\nimpl magnus::TryConvert for StructuredData {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &StructuredData = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for StructuredData {}\n\nimpl StructuredData {\n    fn new(data_type: StructuredDataType, raw_json: String, schema_type: Option<String>) -> Self {\n        Self {\n            data_type,\n            raw_json,\n            schema_type,\n        }\n    }\n\n    fn data_type(&self) -> StructuredDataType {\n        self.data_type.clone()\n    }\n\n    fn raw_json(&self) -> String {\n        self.raw_json.clone()\n    }\n\n    fn schema_type(&self) -> Option<String> {\n        self.schema_type.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::HtmlMetadata\")]\n#[serde(default)]\npub struct HtmlMetadata {\n    pub document: DocumentMetadata,\n    pub headers: Vec<HeaderMetadata>,\n    pub links: Vec<LinkMetadata>,\n    pub images: Vec<ImageMetadata>,\n    pub structured_data: Vec<StructuredData>,\n}\n\nunsafe impl IntoValueFromNative for HtmlMetadata {}\n\nimpl magnus::TryConvert for HtmlMetadata {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &HtmlMetadata = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for HtmlMetadata {}\n\nimpl HtmlMetadata {\n    fn new(\n        document: Option<DocumentMetadata>,\n        headers: Option<Vec<HeaderMetadata>>,\n        links: Option<Vec<LinkMetadata>>,\n        images: Option<Vec<ImageMetadata>>,\n        structured_data: Option<Vec<StructuredData>>,\n    ) -> Self {\n        Self {\n            document: document.unwrap_or_default(),\n            headers: headers.unwrap_or_default(),\n            links: links.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            structured_data: structured_data.unwrap_or_default(),\n        }\n    }\n\n    fn document(&self) -> DocumentMetadata {\n        self.document.clone()\n    }\n\n    fn headers(&self) -> Vec<HeaderMetadata> {\n        self.headers.clone()\n    }\n\n    fn links(&self) -> Vec<LinkMetadata> {\n        self.links.clone()\n    }\n\n    fn images(&self) -> Vec<ImageMetadata> {\n        self.images.clone()\n    }\n\n    fn structured_data(&self) -> Vec<StructuredData> {\n        self.structured_data.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ConversionOptions\")]\n#[serde(default)]\npub struct ConversionOptions {\n    pub heading_style: HeadingStyle,\n    pub list_indent_type: ListIndentType,\n    pub list_indent_width: usize,\n    pub bullets: String,\n    pub strong_em_symbol: String,\n    pub escape_asterisks: bool,\n    pub escape_underscores: bool,\n    pub escape_misc: bool,\n    pub escape_ascii: bool,\n    pub code_language: String,\n    pub autolinks: bool,\n    pub default_title: bool,\n    pub br_in_tables: bool,\n    pub highlight_style: HighlightStyle,\n    pub extract_metadata: bool,\n    pub whitespace_mode: WhitespaceMode,\n    pub strip_newlines: bool,\n    pub wrap: bool,\n    pub wrap_width: usize,\n    pub convert_as_inline: bool,\n    pub sub_symbol: String,\n    pub sup_symbol: String,\n    pub newline_style: NewlineStyle,\n    pub code_block_style: CodeBlockStyle,\n    pub keep_inline_images_in: Vec<String>,\n    pub preprocessing: PreprocessingOptions,\n    pub encoding: String,\n    pub debug: bool,\n    pub strip_tags: Vec<String>,\n    pub preserve_tags: Vec<String>,\n    pub skip_images: bool,\n    pub link_style: LinkStyle,\n    pub output_format: OutputFormat,\n    pub include_document_structure: bool,\n    pub extract_images: bool,\n    pub max_image_size: u64,\n    pub capture_svg: bool,\n    pub infer_dimensions: bool,\n    pub max_depth: Option<usize>,\n    pub exclude_selectors: Vec<String>,\n    #[serde(skip)]\n    pub visitor: Option<magnus::Value>,\n}\n\nunsafe impl IntoValueFromNative for ConversionOptions {}\n\nimpl magnus::TryConvert for ConversionOptions {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ConversionOptions = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ConversionOptions {}\n\nimpl ConversionOptions {\n    fn new(kwargs: magnus::RHash) -> Result<Self, magnus::Error> {\n        let ruby = unsafe { magnus::Ruby::get_unchecked() };\n        Ok(Self {\n            heading_style: kwargs\n                .get(ruby.to_symbol(\"heading_style\"))\n                .and_then(|v| HeadingStyle::try_convert(v).ok())\n                .unwrap_or_default(),\n            list_indent_type: kwargs\n                .get(ruby.to_symbol(\"list_indent_type\"))\n                .and_then(|v| ListIndentType::try_convert(v).ok())\n                .unwrap_or_default(),\n            list_indent_width: kwargs\n                .get(ruby.to_symbol(\"list_indent_width\"))\n                .and_then(|v| usize::try_convert(v).ok())\n                .unwrap_or(2),\n            bullets: kwargs\n                .get(ruby.to_symbol(\"bullets\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"-*+\".to_string()),\n            strong_em_symbol: kwargs\n                .get(ruby.to_symbol(\"strong_em_symbol\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"*\".to_string()),\n            escape_asterisks: kwargs\n                .get(ruby.to_symbol(\"escape_asterisks\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            escape_underscores: kwargs\n                .get(ruby.to_symbol(\"escape_underscores\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            escape_misc: kwargs\n                .get(ruby.to_symbol(\"escape_misc\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            escape_ascii: kwargs\n                .get(ruby.to_symbol(\"escape_ascii\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            code_language: kwargs\n                .get(ruby.to_symbol(\"code_language\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"\".to_string()),\n            autolinks: kwargs\n                .get(ruby.to_symbol(\"autolinks\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(true),\n            default_title: kwargs\n                .get(ruby.to_symbol(\"default_title\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            br_in_tables: kwargs\n                .get(ruby.to_symbol(\"br_in_tables\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            highlight_style: kwargs\n                .get(ruby.to_symbol(\"highlight_style\"))\n                .and_then(|v| HighlightStyle::try_convert(v).ok())\n                .unwrap_or_default(),\n            extract_metadata: kwargs\n                .get(ruby.to_symbol(\"extract_metadata\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(true),\n            whitespace_mode: kwargs\n                .get(ruby.to_symbol(\"whitespace_mode\"))\n                .and_then(|v| WhitespaceMode::try_convert(v).ok())\n                .unwrap_or_default(),\n            strip_newlines: kwargs\n                .get(ruby.to_symbol(\"strip_newlines\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            wrap: kwargs\n                .get(ruby.to_symbol(\"wrap\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            wrap_width: kwargs\n                .get(ruby.to_symbol(\"wrap_width\"))\n                .and_then(|v| usize::try_convert(v).ok())\n                .unwrap_or(80),\n            convert_as_inline: kwargs\n                .get(ruby.to_symbol(\"convert_as_inline\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            sub_symbol: kwargs\n                .get(ruby.to_symbol(\"sub_symbol\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"\".to_string()),\n            sup_symbol: kwargs\n                .get(ruby.to_symbol(\"sup_symbol\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"\".to_string()),\n            newline_style: kwargs\n                .get(ruby.to_symbol(\"newline_style\"))\n                .and_then(|v| NewlineStyle::try_convert(v).ok())\n                .unwrap_or(NewlineStyle::Spaces),\n            code_block_style: kwargs\n                .get(ruby.to_symbol(\"code_block_style\"))\n                .and_then(|v| CodeBlockStyle::try_convert(v).ok())\n                .unwrap_or_default(),\n            keep_inline_images_in: kwargs\n                .get(ruby.to_symbol(\"keep_inline_images_in\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok())\n                .unwrap_or_default(),\n            preprocessing: kwargs\n                .get(ruby.to_symbol(\"preprocessing\"))\n                .and_then(|v| PreprocessingOptions::try_convert(v).ok())\n                .unwrap_or_default(),\n            encoding: kwargs\n                .get(ruby.to_symbol(\"encoding\"))\n                .and_then(|v| String::try_convert(v).ok())\n                .unwrap_or(\"utf-8\".to_string()),\n            debug: kwargs\n                .get(ruby.to_symbol(\"debug\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            strip_tags: kwargs\n                .get(ruby.to_symbol(\"strip_tags\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok())\n                .unwrap_or_default(),\n            preserve_tags: kwargs\n                .get(ruby.to_symbol(\"preserve_tags\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok())\n                .unwrap_or_default(),\n            skip_images: kwargs\n                .get(ruby.to_symbol(\"skip_images\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            link_style: kwargs\n                .get(ruby.to_symbol(\"link_style\"))\n                .and_then(|v| LinkStyle::try_convert(v).ok())\n                .unwrap_or_default(),\n            output_format: kwargs\n                .get(ruby.to_symbol(\"output_format\"))\n                .and_then(|v| OutputFormat::try_convert(v).ok())\n                .unwrap_or_default(),\n            include_document_structure: kwargs\n                .get(ruby.to_symbol(\"include_document_structure\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            extract_images: kwargs\n                .get(ruby.to_symbol(\"extract_images\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            max_image_size: kwargs\n                .get(ruby.to_symbol(\"max_image_size\"))\n                .and_then(|v| u64::try_convert(v).ok())\n                .unwrap_or(5242880),\n            capture_svg: kwargs\n                .get(ruby.to_symbol(\"capture_svg\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(false),\n            infer_dimensions: kwargs\n                .get(ruby.to_symbol(\"infer_dimensions\"))\n                .and_then(|v| bool::try_convert(v).ok())\n                .unwrap_or(true),\n            max_depth: kwargs\n                .get(ruby.to_symbol(\"max_depth\"))\n                .and_then(|v| usize::try_convert(v).ok()),\n            exclude_selectors: kwargs\n                .get(ruby.to_symbol(\"exclude_selectors\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok())\n                .unwrap_or_default(),\n            visitor: kwargs\n                .get(ruby.to_symbol(\"visitor\"))\n                .and_then(|v| VisitorHandle::try_convert(v).ok()),\n        })\n    }\n\n    fn heading_style(&self) -> HeadingStyle {\n        self.heading_style.clone()\n    }\n\n    fn list_indent_type(&self) -> ListIndentType {\n        self.list_indent_type.clone()\n    }\n\n    fn list_indent_width(&self) -> usize {\n        self.list_indent_width\n    }\n\n    fn bullets(&self) -> String {\n        self.bullets.clone()\n    }\n\n    fn strong_em_symbol(&self) -> String {\n        self.strong_em_symbol.clone()\n    }\n\n    fn escape_asterisks(&self) -> bool {\n        self.escape_asterisks\n    }\n\n    fn escape_underscores(&self) -> bool {\n        self.escape_underscores\n    }\n\n    fn escape_misc(&self) -> bool {\n        self.escape_misc\n    }\n\n    fn escape_ascii(&self) -> bool {\n        self.escape_ascii\n    }\n\n    fn code_language(&self) -> String {\n        self.code_language.clone()\n    }\n\n    fn autolinks(&self) -> bool {\n        self.autolinks\n    }\n\n    fn default_title(&self) -> bool {\n        self.default_title\n    }\n\n    fn br_in_tables(&self) -> bool {\n        self.br_in_tables\n    }\n\n    fn highlight_style(&self) -> HighlightStyle {\n        self.highlight_style.clone()\n    }\n\n    fn extract_metadata(&self) -> bool {\n        self.extract_metadata\n    }\n\n    fn whitespace_mode(&self) -> WhitespaceMode {\n        self.whitespace_mode.clone()\n    }\n\n    fn strip_newlines(&self) -> bool {\n        self.strip_newlines\n    }\n\n    fn wrap(&self) -> bool {\n        self.wrap\n    }\n\n    fn wrap_width(&self) -> usize {\n        self.wrap_width\n    }\n\n    fn convert_as_inline(&self) -> bool {\n        self.convert_as_inline\n    }\n\n    fn sub_symbol(&self) -> String {\n        self.sub_symbol.clone()\n    }\n\n    fn sup_symbol(&self) -> String {\n        self.sup_symbol.clone()\n    }\n\n    fn newline_style(&self) -> NewlineStyle {\n        self.newline_style.clone()\n    }\n\n    fn code_block_style(&self) -> CodeBlockStyle {\n        self.code_block_style.clone()\n    }\n\n    fn keep_inline_images_in(&self) -> Vec<String> {\n        self.keep_inline_images_in.clone()\n    }\n\n    fn preprocessing(&self) -> PreprocessingOptions {\n        self.preprocessing.clone()\n    }\n\n    fn encoding(&self) -> String {\n        self.encoding.clone()\n    }\n\n    fn debug(&self) -> bool {\n        self.debug\n    }\n\n    fn strip_tags(&self) -> Vec<String> {\n        self.strip_tags.clone()\n    }\n\n    fn preserve_tags(&self) -> Vec<String> {\n        self.preserve_tags.clone()\n    }\n\n    fn skip_images(&self) -> bool {\n        self.skip_images\n    }\n\n    fn link_style(&self) -> LinkStyle {\n        self.link_style.clone()\n    }\n\n    fn output_format(&self) -> OutputFormat {\n        self.output_format.clone()\n    }\n\n    fn include_document_structure(&self) -> bool {\n        self.include_document_structure\n    }\n\n    fn extract_images(&self) -> bool {\n        self.extract_images\n    }\n\n    fn max_image_size(&self) -> u64 {\n        self.max_image_size\n    }\n\n    fn capture_svg(&self) -> bool {\n        self.capture_svg\n    }\n\n    fn infer_dimensions(&self) -> bool {\n        self.infer_dimensions\n    }\n\n    fn max_depth(&self) -> Option<usize> {\n        self.max_depth\n    }\n\n    fn exclude_selectors(&self) -> Vec<String> {\n        self.exclude_selectors.clone()\n    }\n\n    fn visitor(&self) -> Option<VisitorHandle> {\n        self.visitor.clone()\n    }\n\n    fn apply_update(&self, update: ConversionOptionsUpdate) -> () {\n        ()\n    }\n}\n\n#[derive(Clone)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ConversionOptionsBuilder\")]\npub struct ConversionOptionsBuilder {\n    inner: Arc<html_to_markdown_rs::options::ConversionOptionsBuilder>,\n}\n\nunsafe impl IntoValueFromNative for ConversionOptionsBuilder {}\n\nimpl magnus::TryConvert for ConversionOptionsBuilder {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ConversionOptionsBuilder = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ConversionOptionsBuilder {}\n\nimpl ConversionOptionsBuilder {\n    fn strip_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().strip_tags(tags)),\n        }\n    }\n\n    fn preserve_tags(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().preserve_tags(tags)),\n        }\n    }\n\n    fn keep_inline_images_in(&self, tags: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().keep_inline_images_in(tags)),\n        }\n    }\n\n    fn exclude_selectors(&self, selectors: Vec<String>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().exclude_selectors(selectors)),\n        }\n    }\n\n    fn visitor(&self, visitor: Option<VisitorHandle>) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().visitor(visitor.as_ref().map(|v| &v.inner))),\n        }\n    }\n\n    fn preprocessing(&self, preprocessing: PreprocessingOptions) -> ConversionOptionsBuilder {\n        Self {\n            inner: Arc::new(self.inner.as_ref().clone().preprocessing(preprocessing.into())),\n        }\n    }\n\n    fn build(&self) -> ConversionOptions {\n        self.inner.as_ref().clone().build().into()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ConversionOptionsUpdate\")]\n#[serde(default)]\npub struct ConversionOptionsUpdate {\n    pub heading_style: Option<HeadingStyle>,\n    pub list_indent_type: Option<ListIndentType>,\n    pub list_indent_width: Option<usize>,\n    pub bullets: Option<String>,\n    pub strong_em_symbol: Option<String>,\n    pub escape_asterisks: Option<bool>,\n    pub escape_underscores: Option<bool>,\n    pub escape_misc: Option<bool>,\n    pub escape_ascii: Option<bool>,\n    pub code_language: Option<String>,\n    pub autolinks: Option<bool>,\n    pub default_title: Option<bool>,\n    pub br_in_tables: Option<bool>,\n    pub highlight_style: Option<HighlightStyle>,\n    pub extract_metadata: Option<bool>,\n    pub whitespace_mode: Option<WhitespaceMode>,\n    pub strip_newlines: Option<bool>,\n    pub wrap: Option<bool>,\n    pub wrap_width: Option<usize>,\n    pub convert_as_inline: Option<bool>,\n    pub sub_symbol: Option<String>,\n    pub sup_symbol: Option<String>,\n    pub newline_style: Option<NewlineStyle>,\n    pub code_block_style: Option<CodeBlockStyle>,\n    pub keep_inline_images_in: Option<Vec<String>>,\n    pub preprocessing: Option<PreprocessingOptionsUpdate>,\n    pub encoding: Option<String>,\n    pub debug: Option<bool>,\n    pub strip_tags: Option<Vec<String>>,\n    pub preserve_tags: Option<Vec<String>>,\n    pub skip_images: Option<bool>,\n    pub link_style: Option<LinkStyle>,\n    pub output_format: Option<OutputFormat>,\n    pub include_document_structure: Option<bool>,\n    pub extract_images: Option<bool>,\n    pub max_image_size: Option<u64>,\n    pub capture_svg: Option<bool>,\n    pub infer_dimensions: Option<bool>,\n    pub max_depth: Option<usize>,\n    pub exclude_selectors: Option<Vec<String>>,\n    pub visitor: Option<VisitorHandle>,\n}\n\nunsafe impl IntoValueFromNative for ConversionOptionsUpdate {}\n\nimpl magnus::TryConvert for ConversionOptionsUpdate {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ConversionOptionsUpdate = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ConversionOptionsUpdate {}\n\nimpl ConversionOptionsUpdate {\n    fn new(kwargs: magnus::RHash) -> Result<Self, magnus::Error> {\n        let ruby = unsafe { magnus::Ruby::get_unchecked() };\n        Ok(Self {\n            heading_style: kwargs\n                .get(ruby.to_symbol(\"heading_style\"))\n                .and_then(|v| HeadingStyle::try_convert(v).ok()),\n            list_indent_type: kwargs\n                .get(ruby.to_symbol(\"list_indent_type\"))\n                .and_then(|v| ListIndentType::try_convert(v).ok()),\n            list_indent_width: kwargs\n                .get(ruby.to_symbol(\"list_indent_width\"))\n                .and_then(|v| usize::try_convert(v).ok()),\n            bullets: kwargs\n                .get(ruby.to_symbol(\"bullets\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            strong_em_symbol: kwargs\n                .get(ruby.to_symbol(\"strong_em_symbol\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            escape_asterisks: kwargs\n                .get(ruby.to_symbol(\"escape_asterisks\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            escape_underscores: kwargs\n                .get(ruby.to_symbol(\"escape_underscores\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            escape_misc: kwargs\n                .get(ruby.to_symbol(\"escape_misc\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            escape_ascii: kwargs\n                .get(ruby.to_symbol(\"escape_ascii\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            code_language: kwargs\n                .get(ruby.to_symbol(\"code_language\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            autolinks: kwargs\n                .get(ruby.to_symbol(\"autolinks\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            default_title: kwargs\n                .get(ruby.to_symbol(\"default_title\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            br_in_tables: kwargs\n                .get(ruby.to_symbol(\"br_in_tables\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            highlight_style: kwargs\n                .get(ruby.to_symbol(\"highlight_style\"))\n                .and_then(|v| HighlightStyle::try_convert(v).ok()),\n            extract_metadata: kwargs\n                .get(ruby.to_symbol(\"extract_metadata\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            whitespace_mode: kwargs\n                .get(ruby.to_symbol(\"whitespace_mode\"))\n                .and_then(|v| WhitespaceMode::try_convert(v).ok()),\n            strip_newlines: kwargs\n                .get(ruby.to_symbol(\"strip_newlines\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            wrap: kwargs\n                .get(ruby.to_symbol(\"wrap\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            wrap_width: kwargs\n                .get(ruby.to_symbol(\"wrap_width\"))\n                .and_then(|v| usize::try_convert(v).ok()),\n            convert_as_inline: kwargs\n                .get(ruby.to_symbol(\"convert_as_inline\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            sub_symbol: kwargs\n                .get(ruby.to_symbol(\"sub_symbol\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            sup_symbol: kwargs\n                .get(ruby.to_symbol(\"sup_symbol\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            newline_style: kwargs\n                .get(ruby.to_symbol(\"newline_style\"))\n                .and_then(|v| NewlineStyle::try_convert(v).ok()),\n            code_block_style: kwargs\n                .get(ruby.to_symbol(\"code_block_style\"))\n                .and_then(|v| CodeBlockStyle::try_convert(v).ok()),\n            keep_inline_images_in: kwargs\n                .get(ruby.to_symbol(\"keep_inline_images_in\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok()),\n            preprocessing: kwargs\n                .get(ruby.to_symbol(\"preprocessing\"))\n                .and_then(|v| PreprocessingOptionsUpdate::try_convert(v).ok()),\n            encoding: kwargs\n                .get(ruby.to_symbol(\"encoding\"))\n                .and_then(|v| String::try_convert(v).ok()),\n            debug: kwargs\n                .get(ruby.to_symbol(\"debug\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            strip_tags: kwargs\n                .get(ruby.to_symbol(\"strip_tags\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok()),\n            preserve_tags: kwargs\n                .get(ruby.to_symbol(\"preserve_tags\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok()),\n            skip_images: kwargs\n                .get(ruby.to_symbol(\"skip_images\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            link_style: kwargs\n                .get(ruby.to_symbol(\"link_style\"))\n                .and_then(|v| LinkStyle::try_convert(v).ok()),\n            output_format: kwargs\n                .get(ruby.to_symbol(\"output_format\"))\n                .and_then(|v| OutputFormat::try_convert(v).ok()),\n            include_document_structure: kwargs\n                .get(ruby.to_symbol(\"include_document_structure\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            extract_images: kwargs\n                .get(ruby.to_symbol(\"extract_images\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            max_image_size: kwargs\n                .get(ruby.to_symbol(\"max_image_size\"))\n                .and_then(|v| u64::try_convert(v).ok()),\n            capture_svg: kwargs\n                .get(ruby.to_symbol(\"capture_svg\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            infer_dimensions: kwargs\n                .get(ruby.to_symbol(\"infer_dimensions\"))\n                .and_then(|v| bool::try_convert(v).ok()),\n            max_depth: kwargs\n                .get(ruby.to_symbol(\"max_depth\"))\n                .and_then(|v| usize::try_convert(v).ok()),\n            exclude_selectors: kwargs\n                .get(ruby.to_symbol(\"exclude_selectors\"))\n                .and_then(|v| <Vec<String>>::try_convert(v).ok()),\n            visitor: kwargs\n                .get(ruby.to_symbol(\"visitor\"))\n                .and_then(|v| VisitorHandle::try_convert(v).ok()),\n        })\n    }\n\n    fn heading_style(&self) -> Option<HeadingStyle> {\n        self.heading_style.clone()\n    }\n\n    fn list_indent_type(&self) -> Option<ListIndentType> {\n        self.list_indent_type.clone()\n    }\n\n    fn list_indent_width(&self) -> Option<usize> {\n        self.list_indent_width\n    }\n\n    fn bullets(&self) -> Option<String> {\n        self.bullets.clone()\n    }\n\n    fn strong_em_symbol(&self) -> Option<String> {\n        self.strong_em_symbol.clone()\n    }\n\n    fn escape_asterisks(&self) -> Option<bool> {\n        self.escape_asterisks\n    }\n\n    fn escape_underscores(&self) -> Option<bool> {\n        self.escape_underscores\n    }\n\n    fn escape_misc(&self) -> Option<bool> {\n        self.escape_misc\n    }\n\n    fn escape_ascii(&self) -> Option<bool> {\n        self.escape_ascii\n    }\n\n    fn code_language(&self) -> Option<String> {\n        self.code_language.clone()\n    }\n\n    fn autolinks(&self) -> Option<bool> {\n        self.autolinks\n    }\n\n    fn default_title(&self) -> Option<bool> {\n        self.default_title\n    }\n\n    fn br_in_tables(&self) -> Option<bool> {\n        self.br_in_tables\n    }\n\n    fn highlight_style(&self) -> Option<HighlightStyle> {\n        self.highlight_style.clone()\n    }\n\n    fn extract_metadata(&self) -> Option<bool> {\n        self.extract_metadata\n    }\n\n    fn whitespace_mode(&self) -> Option<WhitespaceMode> {\n        self.whitespace_mode.clone()\n    }\n\n    fn strip_newlines(&self) -> Option<bool> {\n        self.strip_newlines\n    }\n\n    fn wrap(&self) -> Option<bool> {\n        self.wrap\n    }\n\n    fn wrap_width(&self) -> Option<usize> {\n        self.wrap_width\n    }\n\n    fn convert_as_inline(&self) -> Option<bool> {\n        self.convert_as_inline\n    }\n\n    fn sub_symbol(&self) -> Option<String> {\n        self.sub_symbol.clone()\n    }\n\n    fn sup_symbol(&self) -> Option<String> {\n        self.sup_symbol.clone()\n    }\n\n    fn newline_style(&self) -> Option<NewlineStyle> {\n        self.newline_style.clone()\n    }\n\n    fn code_block_style(&self) -> Option<CodeBlockStyle> {\n        self.code_block_style.clone()\n    }\n\n    fn keep_inline_images_in(&self) -> Option<Vec<String>> {\n        self.keep_inline_images_in.clone()\n    }\n\n    fn preprocessing(&self) -> Option<PreprocessingOptionsUpdate> {\n        self.preprocessing.clone()\n    }\n\n    fn encoding(&self) -> Option<String> {\n        self.encoding.clone()\n    }\n\n    fn debug(&self) -> Option<bool> {\n        self.debug\n    }\n\n    fn strip_tags(&self) -> Option<Vec<String>> {\n        self.strip_tags.clone()\n    }\n\n    fn preserve_tags(&self) -> Option<Vec<String>> {\n        self.preserve_tags.clone()\n    }\n\n    fn skip_images(&self) -> Option<bool> {\n        self.skip_images\n    }\n\n    fn link_style(&self) -> Option<LinkStyle> {\n        self.link_style.clone()\n    }\n\n    fn output_format(&self) -> Option<OutputFormat> {\n        self.output_format.clone()\n    }\n\n    fn include_document_structure(&self) -> Option<bool> {\n        self.include_document_structure\n    }\n\n    fn extract_images(&self) -> Option<bool> {\n        self.extract_images\n    }\n\n    fn max_image_size(&self) -> Option<u64> {\n        self.max_image_size\n    }\n\n    fn capture_svg(&self) -> Option<bool> {\n        self.capture_svg\n    }\n\n    fn infer_dimensions(&self) -> Option<bool> {\n        self.infer_dimensions\n    }\n\n    fn max_depth(&self) -> Option<usize> {\n        self.max_depth.clone()\n    }\n\n    fn exclude_selectors(&self) -> Option<Vec<String>> {\n        self.exclude_selectors.clone()\n    }\n\n    fn visitor(&self) -> Option<VisitorHandle> {\n        self.visitor.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::PreprocessingOptions\")]\n#[serde(default)]\npub struct PreprocessingOptions {\n    pub enabled: bool,\n    pub preset: PreprocessingPreset,\n    pub remove_navigation: bool,\n    pub remove_forms: bool,\n}\n\nunsafe impl IntoValueFromNative for PreprocessingOptions {}\n\nimpl magnus::TryConvert for PreprocessingOptions {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &PreprocessingOptions = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for PreprocessingOptions {}\n\nimpl PreprocessingOptions {\n    fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled: enabled.unwrap_or(true),\n            preset: preset.unwrap_or_default(),\n            remove_navigation: remove_navigation.unwrap_or(true),\n            remove_forms: remove_forms.unwrap_or(true),\n        }\n    }\n\n    fn enabled(&self) -> bool {\n        self.enabled\n    }\n\n    fn preset(&self) -> PreprocessingPreset {\n        self.preset.clone()\n    }\n\n    fn remove_navigation(&self) -> bool {\n        self.remove_navigation\n    }\n\n    fn remove_forms(&self) -> bool {\n        self.remove_forms\n    }\n\n    fn apply_update(&self, update: PreprocessingOptionsUpdate) -> () {\n        ()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::PreprocessingOptionsUpdate\")]\n#[serde(default)]\npub struct PreprocessingOptionsUpdate {\n    pub enabled: Option<bool>,\n    pub preset: Option<PreprocessingPreset>,\n    pub remove_navigation: Option<bool>,\n    pub remove_forms: Option<bool>,\n}\n\nunsafe impl IntoValueFromNative for PreprocessingOptionsUpdate {}\n\nimpl magnus::TryConvert for PreprocessingOptionsUpdate {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &PreprocessingOptionsUpdate = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for PreprocessingOptionsUpdate {}\n\nimpl PreprocessingOptionsUpdate {\n    fn new(\n        enabled: Option<bool>,\n        preset: Option<PreprocessingPreset>,\n        remove_navigation: Option<bool>,\n        remove_forms: Option<bool>,\n    ) -> Self {\n        Self {\n            enabled,\n            preset,\n            remove_navigation,\n            remove_forms,\n        }\n    }\n\n    fn enabled(&self) -> Option<bool> {\n        self.enabled\n    }\n\n    fn preset(&self) -> Option<PreprocessingPreset> {\n        self.preset.clone()\n    }\n\n    fn remove_navigation(&self) -> Option<bool> {\n        self.remove_navigation\n    }\n\n    fn remove_forms(&self) -> Option<bool> {\n        self.remove_forms\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::DocumentStructure\")]\npub struct DocumentStructure {\n    pub nodes: Vec<DocumentNode>,\n    pub source_format: Option<String>,\n}\n\nunsafe impl IntoValueFromNative for DocumentStructure {}\n\nimpl magnus::TryConvert for DocumentStructure {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &DocumentStructure = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for DocumentStructure {}\n\nimpl DocumentStructure {\n    fn new(nodes: Vec<DocumentNode>, source_format: Option<String>) -> Self {\n        Self { nodes, source_format }\n    }\n\n    fn nodes(&self) -> Vec<DocumentNode> {\n        self.nodes.clone()\n    }\n\n    fn source_format(&self) -> Option<String> {\n        self.source_format.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::DocumentNode\")]\npub struct DocumentNode {\n    pub id: String,\n    pub content: NodeContent,\n    pub parent: Option<u32>,\n    pub children: Vec<u32>,\n    pub annotations: Vec<TextAnnotation>,\n    pub attributes: Option<HashMap<String, String>>,\n}\n\nunsafe impl IntoValueFromNative for DocumentNode {}\n\nimpl magnus::TryConvert for DocumentNode {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &DocumentNode = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for DocumentNode {}\n\nimpl DocumentNode {\n    fn new(\n        id: String,\n        content: NodeContent,\n        children: Vec<u32>,\n        annotations: Vec<TextAnnotation>,\n        parent: Option<u32>,\n        attributes: Option<HashMap<String, String>>,\n    ) -> Self {\n        Self {\n            id,\n            content,\n            parent,\n            children,\n            annotations,\n            attributes,\n        }\n    }\n\n    fn id(&self) -> String {\n        self.id.clone()\n    }\n\n    fn content(&self) -> NodeContent {\n        self.content.clone()\n    }\n\n    fn parent(&self) -> Option<u32> {\n        self.parent\n    }\n\n    fn children(&self) -> Vec<u32> {\n        self.children.clone()\n    }\n\n    fn annotations(&self) -> Vec<TextAnnotation> {\n        self.annotations.clone()\n    }\n\n    fn attributes(&self) -> Option<HashMap<String, String>> {\n        self.attributes.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::TextAnnotation\")]\npub struct TextAnnotation {\n    pub start: u32,\n    pub end: u32,\n    pub kind: AnnotationKind,\n}\n\nunsafe impl IntoValueFromNative for TextAnnotation {}\n\nimpl magnus::TryConvert for TextAnnotation {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &TextAnnotation = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for TextAnnotation {}\n\nimpl TextAnnotation {\n    fn new(start: u32, end: u32, kind: AnnotationKind) -> Self {\n        Self { start, end, kind }\n    }\n\n    fn start(&self) -> u32 {\n        self.start\n    }\n\n    fn end(&self) -> u32 {\n        self.end\n    }\n\n    fn kind(&self) -> AnnotationKind {\n        self.kind.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ConversionResult\")]\n#[serde(default)]\npub struct ConversionResult {\n    pub content: Option<String>,\n    pub document: Option<DocumentStructure>,\n    pub metadata: HtmlMetadata,\n    pub tables: Vec<TableData>,\n    pub images: Vec<String>,\n    pub warnings: Vec<ProcessingWarning>,\n}\n\nunsafe impl IntoValueFromNative for ConversionResult {}\n\nimpl magnus::TryConvert for ConversionResult {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ConversionResult = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ConversionResult {}\n\nimpl ConversionResult {\n    fn new(\n        content: Option<String>,\n        document: Option<DocumentStructure>,\n        metadata: Option<HtmlMetadata>,\n        tables: Option<Vec<TableData>>,\n        images: Option<Vec<String>>,\n        warnings: Option<Vec<ProcessingWarning>>,\n    ) -> Self {\n        Self {\n            content,\n            document,\n            metadata: metadata.unwrap_or_default(),\n            tables: tables.unwrap_or_default(),\n            images: images.unwrap_or_default(),\n            warnings: warnings.unwrap_or_default(),\n        }\n    }\n\n    fn content(&self) -> Option<String> {\n        self.content.clone()\n    }\n\n    fn document(&self) -> Option<DocumentStructure> {\n        self.document.clone()\n    }\n\n    fn metadata(&self) -> HtmlMetadata {\n        self.metadata.clone()\n    }\n\n    fn tables(&self) -> Vec<TableData> {\n        self.tables.clone()\n    }\n\n    fn images(&self) -> Vec<String> {\n        self.images.clone()\n    }\n\n    fn warnings(&self) -> Vec<ProcessingWarning> {\n        self.warnings.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::TableGrid\")]\n#[serde(default)]\npub struct TableGrid {\n    pub rows: u32,\n    pub cols: u32,\n    pub cells: Vec<GridCell>,\n}\n\nunsafe impl IntoValueFromNative for TableGrid {}\n\nimpl magnus::TryConvert for TableGrid {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &TableGrid = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for TableGrid {}\n\nimpl TableGrid {\n    fn new(rows: Option<u32>, cols: Option<u32>, cells: Option<Vec<GridCell>>) -> Self {\n        Self {\n            rows: rows.unwrap_or_default(),\n            cols: cols.unwrap_or_default(),\n            cells: cells.unwrap_or_default(),\n        }\n    }\n\n    fn rows(&self) -> u32 {\n        self.rows\n    }\n\n    fn cols(&self) -> u32 {\n        self.cols\n    }\n\n    fn cells(&self) -> Vec<GridCell> {\n        self.cells.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::GridCell\")]\npub struct GridCell {\n    pub content: String,\n    pub row: u32,\n    pub col: u32,\n    pub row_span: u32,\n    pub col_span: u32,\n    pub is_header: bool,\n}\n\nunsafe impl IntoValueFromNative for GridCell {}\n\nimpl magnus::TryConvert for GridCell {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &GridCell = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for GridCell {}\n\nimpl GridCell {\n    fn new(content: String, row: u32, col: u32, row_span: u32, col_span: u32, is_header: bool) -> Self {\n        Self {\n            content,\n            row,\n            col,\n            row_span,\n            col_span,\n            is_header,\n        }\n    }\n\n    fn content(&self) -> String {\n        self.content.clone()\n    }\n\n    fn row(&self) -> u32 {\n        self.row\n    }\n\n    fn col(&self) -> u32 {\n        self.col\n    }\n\n    fn row_span(&self) -> u32 {\n        self.row_span\n    }\n\n    fn col_span(&self) -> u32 {\n        self.col_span\n    }\n\n    fn is_header(&self) -> bool {\n        self.is_header\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::TableData\")]\npub struct TableData {\n    pub grid: TableGrid,\n    pub markdown: String,\n}\n\nunsafe impl IntoValueFromNative for TableData {}\n\nimpl magnus::TryConvert for TableData {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &TableData = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for TableData {}\n\nimpl TableData {\n    fn new(grid: TableGrid, markdown: String) -> Self {\n        Self { grid, markdown }\n    }\n\n    fn grid(&self) -> TableGrid {\n        self.grid.clone()\n    }\n\n    fn markdown(&self) -> String {\n        self.markdown.clone()\n    }\n}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::ProcessingWarning\")]\npub struct ProcessingWarning {\n    pub message: String,\n    pub kind: WarningKind,\n}\n\nunsafe impl IntoValueFromNative for ProcessingWarning {}\n\nimpl magnus::TryConvert for ProcessingWarning {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &ProcessingWarning = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for ProcessingWarning {}\n\nimpl ProcessingWarning {\n    fn new(message: String, kind: WarningKind) -> Self {\n        Self { message, kind }\n    }\n\n    fn message(&self) -> String {\n        self.message.clone()\n    }\n\n    fn kind(&self) -> WarningKind {\n        self.kind.clone()\n    }\n}\n\n#[derive(Clone)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::VisitorHandle\")]\npub struct VisitorHandle {\n    inner: Arc<html_to_markdown_rs::visitor::VisitorHandle>,\n}\n\nunsafe impl IntoValueFromNative for VisitorHandle {}\n\nimpl magnus::TryConvert for VisitorHandle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &VisitorHandle = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for VisitorHandle {}\n\nimpl VisitorHandle {}\n\n#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]\n#[magnus::wrap(class = \"HtmlToMarkdownRs::NodeContext\")]\npub struct NodeContext {\n    pub node_type: NodeType,\n    pub tag_name: String,\n    pub attributes: HashMap<String, String>,\n    pub depth: usize,\n    pub index_in_parent: usize,\n    pub parent_tag: Option<String>,\n    pub is_inline: bool,\n}\n\nunsafe impl IntoValueFromNative for NodeContext {}\n\nimpl magnus::TryConvert for NodeContext {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let r: &NodeContext = magnus::TryConvert::try_convert(val)?;\n        Ok(r.clone())\n    }\n}\nunsafe impl TryConvertOwned for NodeContext {}\n\nimpl NodeContext {\n    fn new(\n        node_type: NodeType,\n        tag_name: String,\n        attributes: HashMap<String, String>,\n        depth: usize,\n        index_in_parent: usize,\n        is_inline: bool,\n        parent_tag: Option<String>,\n    ) -> Self {\n        Self {\n            node_type,\n            tag_name,\n            attributes,\n            depth,\n            index_in_parent,\n            parent_tag,\n            is_inline,\n        }\n    }\n\n    fn node_type(&self) -> NodeType {\n        self.node_type.clone()\n    }\n\n    fn tag_name(&self) -> String {\n        self.tag_name.clone()\n    }\n\n    fn attributes(&self) -> HashMap<String, String> {\n        self.attributes.clone()\n    }\n\n    fn depth(&self) -> usize {\n        self.depth\n    }\n\n    fn index_in_parent(&self) -> usize {\n        self.index_in_parent\n    }\n\n    fn parent_tag(&self) -> Option<String> {\n        self.parent_tag.clone()\n    }\n\n    fn is_inline(&self) -> bool {\n        self.is_inline\n    }\n}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum TextDirection {\n    #[serde(rename = \"ltr\")]\n    LeftToRight,\n    #[serde(rename = \"rtl\")]\n    RightToLeft,\n    #[serde(rename = \"auto\")]\n    Auto,\n}\n\nimpl Default for TextDirection {\n    fn default() -> Self {\n        Self::LeftToRight\n    }\n}\n\nimpl magnus::IntoValue for TextDirection {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            TextDirection::LeftToRight => \"left_to_right\",\n            TextDirection::RightToLeft => \"right_to_left\",\n            TextDirection::Auto => \"auto\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for TextDirection {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"left_to_right\" => Ok(TextDirection::LeftToRight),\n            \"right_to_left\" => Ok(TextDirection::RightToLeft),\n            \"auto\" => Ok(TextDirection::Auto),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid TextDirection value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for TextDirection {}\nunsafe impl TryConvertOwned for TextDirection {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum LinkType {\n    Anchor,\n    Internal,\n    External,\n    Email,\n    Phone,\n    Other,\n}\n\nimpl Default for LinkType {\n    fn default() -> Self {\n        Self::Anchor\n    }\n}\n\nimpl magnus::IntoValue for LinkType {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            LinkType::Anchor => \"anchor\",\n            LinkType::Internal => \"internal\",\n            LinkType::External => \"external\",\n            LinkType::Email => \"email\",\n            LinkType::Phone => \"phone\",\n            LinkType::Other => \"other\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for LinkType {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"anchor\" => Ok(LinkType::Anchor),\n            \"internal\" => Ok(LinkType::Internal),\n            \"external\" => Ok(LinkType::External),\n            \"email\" => Ok(LinkType::Email),\n            \"phone\" => Ok(LinkType::Phone),\n            \"other\" => Ok(LinkType::Other),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid LinkType value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for LinkType {}\nunsafe impl TryConvertOwned for LinkType {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum ImageType {\n    DataUri,\n    InlineSvg,\n    External,\n    Relative,\n}\n\nimpl Default for ImageType {\n    fn default() -> Self {\n        Self::DataUri\n    }\n}\n\nimpl magnus::IntoValue for ImageType {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            ImageType::DataUri => \"data_uri\",\n            ImageType::InlineSvg => \"inline_svg\",\n            ImageType::External => \"external\",\n            ImageType::Relative => \"relative\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for ImageType {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"data_uri\" => Ok(ImageType::DataUri),\n            \"inline_svg\" => Ok(ImageType::InlineSvg),\n            \"external\" => Ok(ImageType::External),\n            \"relative\" => Ok(ImageType::Relative),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid ImageType value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for ImageType {}\nunsafe impl TryConvertOwned for ImageType {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum StructuredDataType {\n    #[serde(rename = \"json_ld\")]\n    JsonLd,\n    Microdata,\n    #[serde(rename = \"rdfa\")]\n    RDFa,\n}\n\nimpl Default for StructuredDataType {\n    fn default() -> Self {\n        Self::JsonLd\n    }\n}\n\nimpl magnus::IntoValue for StructuredDataType {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            StructuredDataType::JsonLd => \"json_ld\",\n            StructuredDataType::Microdata => \"microdata\",\n            StructuredDataType::RDFa => \"r_d_fa\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for StructuredDataType {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"json_ld\" => Ok(StructuredDataType::JsonLd),\n            \"microdata\" => Ok(StructuredDataType::Microdata),\n            \"r_d_fa\" => Ok(StructuredDataType::RDFa),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid StructuredDataType value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for StructuredDataType {}\nunsafe impl TryConvertOwned for StructuredDataType {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum PreprocessingPreset {\n    Minimal,\n    Standard,\n    Aggressive,\n}\n\nimpl Default for PreprocessingPreset {\n    fn default() -> Self {\n        Self::Minimal\n    }\n}\n\nimpl magnus::IntoValue for PreprocessingPreset {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            PreprocessingPreset::Minimal => \"minimal\",\n            PreprocessingPreset::Standard => \"standard\",\n            PreprocessingPreset::Aggressive => \"aggressive\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for PreprocessingPreset {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"minimal\" => Ok(PreprocessingPreset::Minimal),\n            \"standard\" => Ok(PreprocessingPreset::Standard),\n            \"aggressive\" => Ok(PreprocessingPreset::Aggressive),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid PreprocessingPreset value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for PreprocessingPreset {}\nunsafe impl TryConvertOwned for PreprocessingPreset {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum HeadingStyle {\n    Underlined,\n    Atx,\n    AtxClosed,\n}\n\nimpl Default for HeadingStyle {\n    fn default() -> Self {\n        Self::Underlined\n    }\n}\n\nimpl magnus::IntoValue for HeadingStyle {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            HeadingStyle::Underlined => \"underlined\",\n            HeadingStyle::Atx => \"atx\",\n            HeadingStyle::AtxClosed => \"atx_closed\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for HeadingStyle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"underlined\" => Ok(HeadingStyle::Underlined),\n            \"atx\" => Ok(HeadingStyle::Atx),\n            \"atx_closed\" => Ok(HeadingStyle::AtxClosed),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid HeadingStyle value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for HeadingStyle {}\nunsafe impl TryConvertOwned for HeadingStyle {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum ListIndentType {\n    Spaces,\n    Tabs,\n}\n\nimpl Default for ListIndentType {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\nimpl magnus::IntoValue for ListIndentType {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            ListIndentType::Spaces => \"spaces\",\n            ListIndentType::Tabs => \"tabs\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for ListIndentType {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"spaces\" => Ok(ListIndentType::Spaces),\n            \"tabs\" => Ok(ListIndentType::Tabs),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid ListIndentType value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for ListIndentType {}\nunsafe impl TryConvertOwned for ListIndentType {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum WhitespaceMode {\n    Normalized,\n    Strict,\n}\n\nimpl Default for WhitespaceMode {\n    fn default() -> Self {\n        Self::Normalized\n    }\n}\n\nimpl magnus::IntoValue for WhitespaceMode {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            WhitespaceMode::Normalized => \"normalized\",\n            WhitespaceMode::Strict => \"strict\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for WhitespaceMode {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"normalized\" => Ok(WhitespaceMode::Normalized),\n            \"strict\" => Ok(WhitespaceMode::Strict),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid WhitespaceMode value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for WhitespaceMode {}\nunsafe impl TryConvertOwned for WhitespaceMode {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum NewlineStyle {\n    Spaces,\n    Backslash,\n}\n\nimpl Default for NewlineStyle {\n    fn default() -> Self {\n        Self::Spaces\n    }\n}\n\nimpl magnus::IntoValue for NewlineStyle {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            NewlineStyle::Spaces => \"spaces\",\n            NewlineStyle::Backslash => \"backslash\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for NewlineStyle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"spaces\" => Ok(NewlineStyle::Spaces),\n            \"backslash\" => Ok(NewlineStyle::Backslash),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid NewlineStyle value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for NewlineStyle {}\nunsafe impl TryConvertOwned for NewlineStyle {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum CodeBlockStyle {\n    Indented,\n    Backticks,\n    Tildes,\n}\n\nimpl Default for CodeBlockStyle {\n    fn default() -> Self {\n        Self::Indented\n    }\n}\n\nimpl magnus::IntoValue for CodeBlockStyle {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            CodeBlockStyle::Indented => \"indented\",\n            CodeBlockStyle::Backticks => \"backticks\",\n            CodeBlockStyle::Tildes => \"tildes\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for CodeBlockStyle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"indented\" => Ok(CodeBlockStyle::Indented),\n            \"backticks\" => Ok(CodeBlockStyle::Backticks),\n            \"tildes\" => Ok(CodeBlockStyle::Tildes),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid CodeBlockStyle value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for CodeBlockStyle {}\nunsafe impl TryConvertOwned for CodeBlockStyle {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum HighlightStyle {\n    DoubleEqual,\n    Html,\n    Bold,\n    None,\n}\n\nimpl Default for HighlightStyle {\n    fn default() -> Self {\n        Self::DoubleEqual\n    }\n}\n\nimpl magnus::IntoValue for HighlightStyle {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            HighlightStyle::DoubleEqual => \"double_equal\",\n            HighlightStyle::Html => \"html\",\n            HighlightStyle::Bold => \"bold\",\n            HighlightStyle::None => \"none\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for HighlightStyle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"double_equal\" => Ok(HighlightStyle::DoubleEqual),\n            \"html\" => Ok(HighlightStyle::Html),\n            \"bold\" => Ok(HighlightStyle::Bold),\n            \"none\" => Ok(HighlightStyle::None),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid HighlightStyle value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for HighlightStyle {}\nunsafe impl TryConvertOwned for HighlightStyle {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum LinkStyle {\n    Inline,\n    Reference,\n}\n\nimpl Default for LinkStyle {\n    fn default() -> Self {\n        Self::Inline\n    }\n}\n\nimpl magnus::IntoValue for LinkStyle {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            LinkStyle::Inline => \"inline\",\n            LinkStyle::Reference => \"reference\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for LinkStyle {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"inline\" => Ok(LinkStyle::Inline),\n            \"reference\" => Ok(LinkStyle::Reference),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid LinkStyle value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for LinkStyle {}\nunsafe impl TryConvertOwned for LinkStyle {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum OutputFormat {\n    Markdown,\n    Djot,\n    Plain,\n}\n\nimpl Default for OutputFormat {\n    fn default() -> Self {\n        Self::Markdown\n    }\n}\n\nimpl magnus::IntoValue for OutputFormat {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            OutputFormat::Markdown => \"markdown\",\n            OutputFormat::Djot => \"djot\",\n            OutputFormat::Plain => \"plain\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for OutputFormat {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"markdown\" => Ok(OutputFormat::Markdown),\n            \"djot\" => Ok(OutputFormat::Djot),\n            \"plain\" => Ok(OutputFormat::Plain),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid OutputFormat value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for OutputFormat {}\nunsafe impl TryConvertOwned for OutputFormat {}\n\n#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]\n#[serde(tag = \"node_type\")]\npub enum NodeContent {\n    Heading {\n        level: u8,\n        text: String,\n    },\n    Paragraph {\n        text: String,\n    },\n    List {\n        ordered: bool,\n    },\n    ListItem {\n        text: String,\n    },\n    Table {\n        grid: TableGrid,\n    },\n    Image {\n        description: Option<String>,\n        src: Option<String>,\n        image_index: Option<u32>,\n    },\n    Code {\n        text: String,\n        language: Option<String>,\n    },\n    Quote,\n    DefinitionList,\n    DefinitionItem {\n        term: String,\n        definition: String,\n    },\n    RawBlock {\n        format: String,\n        content: String,\n    },\n    MetadataBlock {\n        entries: Vec<String>,\n    },\n    Group {\n        label: Option<String>,\n        heading_level: Option<u8>,\n        heading_text: Option<String>,\n    },\n}\n\nimpl Default for NodeContent {\n    fn default() -> Self {\n        Self::Heading {\n            level: Default::default(),\n            text: Default::default(),\n        }\n    }\n}\n\nimpl magnus::IntoValue for NodeContent {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        match serde_json::to_value(&self) {\n            Ok(v) => json_to_ruby(handle, v),\n            Err(_) => handle.qnil().into_value_with(handle),\n        }\n    }\n}\n\nimpl magnus::TryConvert for NodeContent {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        serde_json::from_str(&s)\n            .map_err(|e| magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_type_error(), e.to_string()))\n    }\n}\n\nunsafe impl IntoValueFromNative for NodeContent {}\nunsafe impl TryConvertOwned for NodeContent {}\n\n#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]\n#[serde(tag = \"annotation_type\")]\npub enum AnnotationKind {\n    Bold,\n    Italic,\n    Underline,\n    Strikethrough,\n    Code,\n    Subscript,\n    Superscript,\n    Highlight,\n    Link { url: String, title: Option<String> },\n}\n\nimpl Default for AnnotationKind {\n    fn default() -> Self {\n        Self::Bold\n    }\n}\n\nimpl magnus::IntoValue for AnnotationKind {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        match serde_json::to_value(&self) {\n            Ok(v) => json_to_ruby(handle, v),\n            Err(_) => handle.qnil().into_value_with(handle),\n        }\n    }\n}\n\nimpl magnus::TryConvert for AnnotationKind {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        serde_json::from_str(&s)\n            .map_err(|e| magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_type_error(), e.to_string()))\n    }\n}\n\nunsafe impl IntoValueFromNative for AnnotationKind {}\nunsafe impl TryConvertOwned for AnnotationKind {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum WarningKind {\n    ImageExtractionFailed,\n    EncodingFallback,\n    TruncatedInput,\n    MalformedHtml,\n    SanitizationApplied,\n    DepthLimitExceeded,\n}\n\nimpl Default for WarningKind {\n    fn default() -> Self {\n        Self::ImageExtractionFailed\n    }\n}\n\nimpl magnus::IntoValue for WarningKind {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            WarningKind::ImageExtractionFailed => \"image_extraction_failed\",\n            WarningKind::EncodingFallback => \"encoding_fallback\",\n            WarningKind::TruncatedInput => \"truncated_input\",\n            WarningKind::MalformedHtml => \"malformed_html\",\n            WarningKind::SanitizationApplied => \"sanitization_applied\",\n            WarningKind::DepthLimitExceeded => \"depth_limit_exceeded\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for WarningKind {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"image_extraction_failed\" => Ok(WarningKind::ImageExtractionFailed),\n            \"encoding_fallback\" => Ok(WarningKind::EncodingFallback),\n            \"truncated_input\" => Ok(WarningKind::TruncatedInput),\n            \"malformed_html\" => Ok(WarningKind::MalformedHtml),\n            \"sanitization_applied\" => Ok(WarningKind::SanitizationApplied),\n            \"depth_limit_exceeded\" => Ok(WarningKind::DepthLimitExceeded),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid WarningKind value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for WarningKind {}\nunsafe impl TryConvertOwned for WarningKind {}\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]\npub enum NodeType {\n    Text,\n    Element,\n    Heading,\n    Paragraph,\n    Div,\n    Blockquote,\n    Pre,\n    Hr,\n    List,\n    ListItem,\n    DefinitionList,\n    DefinitionTerm,\n    DefinitionDescription,\n    Table,\n    TableRow,\n    TableCell,\n    TableHeader,\n    TableBody,\n    TableHead,\n    TableFoot,\n    Link,\n    Image,\n    Strong,\n    Em,\n    Code,\n    Strikethrough,\n    Underline,\n    Subscript,\n    Superscript,\n    Mark,\n    Small,\n    Br,\n    Span,\n    Article,\n    Section,\n    Nav,\n    Aside,\n    Header,\n    Footer,\n    Main,\n    Figure,\n    Figcaption,\n    Time,\n    Details,\n    Summary,\n    Form,\n    Input,\n    Select,\n    Option,\n    Button,\n    Textarea,\n    Label,\n    Fieldset,\n    Legend,\n    Audio,\n    Video,\n    Picture,\n    Source,\n    Iframe,\n    Svg,\n    Canvas,\n    Ruby,\n    Rt,\n    Rp,\n    Abbr,\n    Kbd,\n    Samp,\n    Var,\n    Cite,\n    Q,\n    Del,\n    Ins,\n    Data,\n    Meter,\n    Progress,\n    Output,\n    Template,\n    Slot,\n    Html,\n    Head,\n    Body,\n    Title,\n    Meta,\n    LinkTag,\n    Style,\n    Script,\n    Base,\n    Custom,\n}\n\nimpl Default for NodeType {\n    fn default() -> Self {\n        Self::Text\n    }\n}\n\nimpl magnus::IntoValue for NodeType {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        let sym = match self {\n            NodeType::Text => \"text\",\n            NodeType::Element => \"element\",\n            NodeType::Heading => \"heading\",\n            NodeType::Paragraph => \"paragraph\",\n            NodeType::Div => \"div\",\n            NodeType::Blockquote => \"blockquote\",\n            NodeType::Pre => \"pre\",\n            NodeType::Hr => \"hr\",\n            NodeType::List => \"list\",\n            NodeType::ListItem => \"list_item\",\n            NodeType::DefinitionList => \"definition_list\",\n            NodeType::DefinitionTerm => \"definition_term\",\n            NodeType::DefinitionDescription => \"definition_description\",\n            NodeType::Table => \"table\",\n            NodeType::TableRow => \"table_row\",\n            NodeType::TableCell => \"table_cell\",\n            NodeType::TableHeader => \"table_header\",\n            NodeType::TableBody => \"table_body\",\n            NodeType::TableHead => \"table_head\",\n            NodeType::TableFoot => \"table_foot\",\n            NodeType::Link => \"link\",\n            NodeType::Image => \"image\",\n            NodeType::Strong => \"strong\",\n            NodeType::Em => \"em\",\n            NodeType::Code => \"code\",\n            NodeType::Strikethrough => \"strikethrough\",\n            NodeType::Underline => \"underline\",\n            NodeType::Subscript => \"subscript\",\n            NodeType::Superscript => \"superscript\",\n            NodeType::Mark => \"mark\",\n            NodeType::Small => \"small\",\n            NodeType::Br => \"br\",\n            NodeType::Span => \"span\",\n            NodeType::Article => \"article\",\n            NodeType::Section => \"section\",\n            NodeType::Nav => \"nav\",\n            NodeType::Aside => \"aside\",\n            NodeType::Header => \"header\",\n            NodeType::Footer => \"footer\",\n            NodeType::Main => \"main\",\n            NodeType::Figure => \"figure\",\n            NodeType::Figcaption => \"figcaption\",\n            NodeType::Time => \"time\",\n            NodeType::Details => \"details\",\n            NodeType::Summary => \"summary\",\n            NodeType::Form => \"form\",\n            NodeType::Input => \"input\",\n            NodeType::Select => \"select\",\n            NodeType::Option => \"option\",\n            NodeType::Button => \"button\",\n            NodeType::Textarea => \"textarea\",\n            NodeType::Label => \"label\",\n            NodeType::Fieldset => \"fieldset\",\n            NodeType::Legend => \"legend\",\n            NodeType::Audio => \"audio\",\n            NodeType::Video => \"video\",\n            NodeType::Picture => \"picture\",\n            NodeType::Source => \"source\",\n            NodeType::Iframe => \"iframe\",\n            NodeType::Svg => \"svg\",\n            NodeType::Canvas => \"canvas\",\n            NodeType::Ruby => \"ruby\",\n            NodeType::Rt => \"rt\",\n            NodeType::Rp => \"rp\",\n            NodeType::Abbr => \"abbr\",\n            NodeType::Kbd => \"kbd\",\n            NodeType::Samp => \"samp\",\n            NodeType::Var => \"var\",\n            NodeType::Cite => \"cite\",\n            NodeType::Q => \"q\",\n            NodeType::Del => \"del\",\n            NodeType::Ins => \"ins\",\n            NodeType::Data => \"data\",\n            NodeType::Meter => \"meter\",\n            NodeType::Progress => \"progress\",\n            NodeType::Output => \"output\",\n            NodeType::Template => \"template\",\n            NodeType::Slot => \"slot\",\n            NodeType::Html => \"html\",\n            NodeType::Head => \"head\",\n            NodeType::Body => \"body\",\n            NodeType::Title => \"title\",\n            NodeType::Meta => \"meta\",\n            NodeType::LinkTag => \"link_tag\",\n            NodeType::Style => \"style\",\n            NodeType::Script => \"script\",\n            NodeType::Base => \"base\",\n            NodeType::Custom => \"custom\",\n        };\n        handle.to_symbol(sym).into_value_with(handle)\n    }\n}\n\nimpl magnus::TryConvert for NodeType {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        match s.as_str() {\n            \"text\" => Ok(NodeType::Text),\n            \"element\" => Ok(NodeType::Element),\n            \"heading\" => Ok(NodeType::Heading),\n            \"paragraph\" => Ok(NodeType::Paragraph),\n            \"div\" => Ok(NodeType::Div),\n            \"blockquote\" => Ok(NodeType::Blockquote),\n            \"pre\" => Ok(NodeType::Pre),\n            \"hr\" => Ok(NodeType::Hr),\n            \"list\" => Ok(NodeType::List),\n            \"list_item\" => Ok(NodeType::ListItem),\n            \"definition_list\" => Ok(NodeType::DefinitionList),\n            \"definition_term\" => Ok(NodeType::DefinitionTerm),\n            \"definition_description\" => Ok(NodeType::DefinitionDescription),\n            \"table\" => Ok(NodeType::Table),\n            \"table_row\" => Ok(NodeType::TableRow),\n            \"table_cell\" => Ok(NodeType::TableCell),\n            \"table_header\" => Ok(NodeType::TableHeader),\n            \"table_body\" => Ok(NodeType::TableBody),\n            \"table_head\" => Ok(NodeType::TableHead),\n            \"table_foot\" => Ok(NodeType::TableFoot),\n            \"link\" => Ok(NodeType::Link),\n            \"image\" => Ok(NodeType::Image),\n            \"strong\" => Ok(NodeType::Strong),\n            \"em\" => Ok(NodeType::Em),\n            \"code\" => Ok(NodeType::Code),\n            \"strikethrough\" => Ok(NodeType::Strikethrough),\n            \"underline\" => Ok(NodeType::Underline),\n            \"subscript\" => Ok(NodeType::Subscript),\n            \"superscript\" => Ok(NodeType::Superscript),\n            \"mark\" => Ok(NodeType::Mark),\n            \"small\" => Ok(NodeType::Small),\n            \"br\" => Ok(NodeType::Br),\n            \"span\" => Ok(NodeType::Span),\n            \"article\" => Ok(NodeType::Article),\n            \"section\" => Ok(NodeType::Section),\n            \"nav\" => Ok(NodeType::Nav),\n            \"aside\" => Ok(NodeType::Aside),\n            \"header\" => Ok(NodeType::Header),\n            \"footer\" => Ok(NodeType::Footer),\n            \"main\" => Ok(NodeType::Main),\n            \"figure\" => Ok(NodeType::Figure),\n            \"figcaption\" => Ok(NodeType::Figcaption),\n            \"time\" => Ok(NodeType::Time),\n            \"details\" => Ok(NodeType::Details),\n            \"summary\" => Ok(NodeType::Summary),\n            \"form\" => Ok(NodeType::Form),\n            \"input\" => Ok(NodeType::Input),\n            \"select\" => Ok(NodeType::Select),\n            \"option\" => Ok(NodeType::Option),\n            \"button\" => Ok(NodeType::Button),\n            \"textarea\" => Ok(NodeType::Textarea),\n            \"label\" => Ok(NodeType::Label),\n            \"fieldset\" => Ok(NodeType::Fieldset),\n            \"legend\" => Ok(NodeType::Legend),\n            \"audio\" => Ok(NodeType::Audio),\n            \"video\" => Ok(NodeType::Video),\n            \"picture\" => Ok(NodeType::Picture),\n            \"source\" => Ok(NodeType::Source),\n            \"iframe\" => Ok(NodeType::Iframe),\n            \"svg\" => Ok(NodeType::Svg),\n            \"canvas\" => Ok(NodeType::Canvas),\n            \"ruby\" => Ok(NodeType::Ruby),\n            \"rt\" => Ok(NodeType::Rt),\n            \"rp\" => Ok(NodeType::Rp),\n            \"abbr\" => Ok(NodeType::Abbr),\n            \"kbd\" => Ok(NodeType::Kbd),\n            \"samp\" => Ok(NodeType::Samp),\n            \"var\" => Ok(NodeType::Var),\n            \"cite\" => Ok(NodeType::Cite),\n            \"q\" => Ok(NodeType::Q),\n            \"del\" => Ok(NodeType::Del),\n            \"ins\" => Ok(NodeType::Ins),\n            \"data\" => Ok(NodeType::Data),\n            \"meter\" => Ok(NodeType::Meter),\n            \"progress\" => Ok(NodeType::Progress),\n            \"output\" => Ok(NodeType::Output),\n            \"template\" => Ok(NodeType::Template),\n            \"slot\" => Ok(NodeType::Slot),\n            \"html\" => Ok(NodeType::Html),\n            \"head\" => Ok(NodeType::Head),\n            \"body\" => Ok(NodeType::Body),\n            \"title\" => Ok(NodeType::Title),\n            \"meta\" => Ok(NodeType::Meta),\n            \"link_tag\" => Ok(NodeType::LinkTag),\n            \"style\" => Ok(NodeType::Style),\n            \"script\" => Ok(NodeType::Script),\n            \"base\" => Ok(NodeType::Base),\n            \"custom\" => Ok(NodeType::Custom),\n            other => Err(magnus::Error::new(\n                unsafe { Ruby::get_unchecked() }.exception_arg_error(),\n                format!(\"invalid NodeType value: {other}\"),\n            )),\n        }\n    }\n}\n\nunsafe impl IntoValueFromNative for NodeType {}\nunsafe impl TryConvertOwned for NodeType {}\n\n#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]\npub enum VisitResult {\n    Continue,\n    Custom { _0: String },\n    Skip,\n    PreserveHtml,\n    Error { _0: String },\n}\n\nimpl Default for VisitResult {\n    fn default() -> Self {\n        Self::Continue\n    }\n}\n\nimpl magnus::IntoValue for VisitResult {\n    fn into_value_with(self, handle: &Ruby) -> magnus::Value {\n        match serde_json::to_value(&self) {\n            Ok(v) => json_to_ruby(handle, v),\n            Err(_) => handle.qnil().into_value_with(handle),\n        }\n    }\n}\n\nimpl magnus::TryConvert for VisitResult {\n    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {\n        let s: String = magnus::TryConvert::try_convert(val)?;\n        serde_json::from_str(&s)\n            .map_err(|e| magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_type_error(), e.to_string()))\n    }\n}\n\nunsafe impl IntoValueFromNative for VisitResult {}\nunsafe impl TryConvertOwned for VisitResult {}\n\n#[allow(clippy::missing_errors_doc)]\n#[allow(unused_variables)]\npub fn convert(html: String, options: Option<magnus::Value>) -> Result<ConversionResult, Error> {\n    let mut options_binding: ConversionOptions = match options {\n        Some(_v) if !_v.is_nil() => {\n            let _s: String = _v.funcall(\"to_json\", ()).map_err(|e| {\n                magnus::Error::new(\n                    unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(),\n                    e.to_string(),\n                )\n            })?;\n            serde_json::from_str(&_s).map_err(|e| {\n                magnus::Error::new(\n                    unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(),\n                    e.to_string(),\n                )\n            })?\n        }\n        _ => Default::default(),\n    };\n    let _visitor_rb: Option<magnus::Value> = options_binding.visitor.take();\n    let mut options_core: html_to_markdown_rs::ConversionOptions = options_binding.into();\n    if let Some(_v) = _visitor_rb {\n        let _bridge = RbHtmlVisitorBridge::new(_v);\n        options_core.visitor =\n            Some(std::rc::Rc::new(std::cell::RefCell::new(_bridge)) as html_to_markdown_rs::visitor::VisitorHandle);\n    }\n    html_to_markdown_rs::convert(&html, options_core)\n        .map(|val| val.into())\n        .map_err(|e| {\n            magnus::Error::new(\n                unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(),\n                e.to_string(),\n            )\n        })\n}\n\nfn nodecontext_to_rb_hash(ctx: &html_to_markdown_rs::visitor::NodeContext) -> magnus::RHash {\n    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n    let h = ruby.hash_new();\n    h.aset(ruby.to_symbol(\"node_type\"), format!(\"{:?}\", ctx.node_type)).ok();\n    h.aset(ruby.to_symbol(\"tag_name\"), ctx.tag_name.as_str()).ok();\n    h.aset(ruby.to_symbol(\"depth\"), ctx.depth as i64).ok();\n    h.aset(ruby.to_symbol(\"index_in_parent\"), ctx.index_in_parent as i64)\n        .ok();\n    h.aset(ruby.to_symbol(\"is_inline\"), ctx.is_inline).ok();\n    h.aset(\n        ruby.to_symbol(\"parent_tag\"),\n        ctx.parent_tag.as_deref().map(|s| ruby.str_new(s).as_value()),\n    )\n    .ok();\n    let attrs = ruby.hash_new();\n    for (k, v) in &ctx.attributes {\n        attrs.aset(ruby.str_new(k), ruby.str_new(v)).ok();\n    }\n    h.aset(ruby.to_symbol(\"attributes\"), attrs).ok();\n    h\n}\n\npub struct RbHtmlVisitorBridge {\n    rb_obj: magnus::Value,\n}\n\nimpl std::fmt::Debug for RbHtmlVisitorBridge {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"RbHtmlVisitorBridge\")\n    }\n}\n\nimpl RbHtmlVisitorBridge {\n    pub fn new(rb_obj: magnus::Value) -> Self {\n        Self { rb_obj }\n    }\n}\n\nimpl html_to_markdown_rs::visitor::HtmlVisitor for RbHtmlVisitorBridge {\n    fn visit_element_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_element_start\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_element_start\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_element_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_element_end\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_element_end\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_output)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_text(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_text\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_text\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_link(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _href: &str,\n        _text: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_link\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_link\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_href)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_text)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _title {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_image(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: &str,\n        _alt: &str,\n        _title: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_image\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_image\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_src)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_alt)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _title {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_heading(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _level: u32,\n        _text: &str,\n        _id: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_heading\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_heading\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                _level,\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_text)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _id {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_code_block(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _lang: Option<&str>,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_code_block\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_code_block\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _lang {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_code)\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_code_inline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _code: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_code_inline\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_code_inline\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_code)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_item(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _marker: &str,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_list_item\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_list_item\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                _ordered,\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_marker)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_text)\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_list_start\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_list_start\", (nodecontext_to_rb_hash(_ctx), _ordered));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _ordered: bool,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_list_end\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_list_end\",\n            (nodecontext_to_rb_hash(_ctx), _ordered, {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_output)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_table_start\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_table_start\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_row(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _cells: &[String],\n        _is_header: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_table_row\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_table_row\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let arr = unsafe { magnus::Ruby::get_unchecked() }.ary_new_capa(_cells.len());\n                    for item in _cells {\n                        let _ = arr.push(item.to_string());\n                    }\n                    arr\n                },\n                _is_header,\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_table_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_table_end\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_table_end\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_output)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_blockquote(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _content: &str,\n        _depth: usize,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_blockquote\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_blockquote\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_content)\n                },\n                _depth,\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_strong(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_strong\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_strong\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_emphasis(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_emphasis\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_emphasis\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_strikethrough(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_strikethrough\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_strikethrough\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_underline(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_underline\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_underline\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_subscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_subscript\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_subscript\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_superscript(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_superscript\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_superscript\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_mark(&mut self, _ctx: &html_to_markdown_rs::NodeContext, _text: &str) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_mark\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_mark\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_line_break(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_line_break\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> =\n            self.rb_obj.funcall(\"visit_line_break\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_horizontal_rule(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_horizontal_rule\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_horizontal_rule\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_custom_element(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _tag_name: &str,\n        _html: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_custom_element\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_custom_element\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_tag_name)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_html)\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_start(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self\n            .rb_obj\n            .respond_to(\"visit_definition_list_start\", false)\n            .unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_definition_list_start\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_term(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_definition_term\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_definition_term\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_description(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self\n            .rb_obj\n            .respond_to(\"visit_definition_description\", false)\n            .unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_definition_description\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_definition_list_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self\n            .rb_obj\n            .respond_to(\"visit_definition_list_end\", false)\n            .unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_definition_list_end\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_output)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_form(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _action: Option<&str>,\n        _method: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_form\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_form\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _action {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _method {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_input(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _input_type: &str,\n        _name: Option<&str>,\n        _value: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_input\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_input\",\n            (\n                nodecontext_to_rb_hash(_ctx),\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    ruby.str_new(_input_type)\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _name {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n                {\n                    let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                    match _value {\n                        Some(s) => ruby.str_new(s).as_value(),\n                        None => ruby.qnil().as_value(),\n                    }\n                },\n            ),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_button(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_button\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_button\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_audio(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_audio\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_audio\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                match _src {\n                    Some(s) => ruby.str_new(s).as_value(),\n                    None => ruby.qnil().as_value(),\n                }\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_video(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_video\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_video\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                match _src {\n                    Some(s) => ruby.str_new(s).as_value(),\n                    None => ruby.qnil().as_value(),\n                }\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_iframe(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _src: Option<&str>,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_iframe\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_iframe\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                match _src {\n                    Some(s) => ruby.str_new(s).as_value(),\n                    None => ruby.qnil().as_value(),\n                }\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_details(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _open: bool,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_details\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_details\", (nodecontext_to_rb_hash(_ctx), _open));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_summary(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_summary\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_summary\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figure_start(&mut self, _ctx: &html_to_markdown_rs::NodeContext) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_figure_start\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self\n            .rb_obj\n            .funcall(\"visit_figure_start\", (nodecontext_to_rb_hash(_ctx),));\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figcaption(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _text: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_figcaption\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_figcaption\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_text)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n\n    fn visit_figure_end(\n        &mut self,\n        _ctx: &html_to_markdown_rs::NodeContext,\n        _output: &str,\n    ) -> html_to_markdown_rs::VisitResult {\n        let responds = self.rb_obj.respond_to(\"visit_figure_end\", false).unwrap_or(false);\n        if !responds {\n            return html_to_markdown_rs::VisitResult::Continue;\n        }\n        let result: Result<magnus::Value, magnus::Error> = self.rb_obj.funcall(\n            \"visit_figure_end\",\n            (nodecontext_to_rb_hash(_ctx), {\n                let ruby = unsafe { magnus::Ruby::get_unchecked() };\n                ruby.str_new(_output)\n            }),\n        );\n        match result {\n            Err(_) => html_to_markdown_rs::VisitResult::Continue,\n            Ok(val) => {\n                let s: String = val.to_string();\n                match s.to_lowercase().as_str() {\n                    \"continue\" => html_to_markdown_rs::VisitResult::Continue,\n                    \"skip\" => html_to_markdown_rs::VisitResult::Skip,\n                    \"preserve_html\" | \"preservehtml\" => html_to_markdown_rs::VisitResult::PreserveHtml,\n                    other => html_to_markdown_rs::VisitResult::Custom(other.to_string()),\n                }\n            }\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentMetadata> for html_to_markdown_rs::metadata::DocumentMetadata {\n    fn from(val: DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::DocumentMetadata> for DocumentMetadata {\n    fn from(val: html_to_markdown_rs::metadata::DocumentMetadata) -> Self {\n        Self {\n            title: val.title,\n            description: val.description,\n            keywords: val.keywords,\n            author: val.author,\n            canonical_url: val.canonical_url,\n            base_href: val.base_href,\n            language: val.language,\n            text_direction: val.text_direction.map(Into::into),\n            open_graph: val.open_graph.into_iter().collect(),\n            twitter_card: val.twitter_card.into_iter().collect(),\n            meta_tags: val.meta_tags.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HeaderMetadata> for html_to_markdown_rs::metadata::HeaderMetadata {\n    fn from(val: HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HeaderMetadata> for HeaderMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HeaderMetadata) -> Self {\n        Self {\n            level: val.level,\n            text: val.text,\n            id: val.id,\n            depth: val.depth,\n            html_offset: val.html_offset,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<LinkMetadata> for html_to_markdown_rs::metadata::LinkMetadata {\n    fn from(val: LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::LinkMetadata> for LinkMetadata {\n    fn from(val: html_to_markdown_rs::metadata::LinkMetadata) -> Self {\n        Self {\n            href: val.href,\n            text: val.text,\n            title: val.title,\n            link_type: val.link_type.into(),\n            rel: val.rel,\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ImageMetadata> for html_to_markdown_rs::metadata::ImageMetadata {\n    fn from(val: ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: Default::default(),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::ImageMetadata> for ImageMetadata {\n    fn from(val: html_to_markdown_rs::metadata::ImageMetadata) -> Self {\n        Self {\n            src: val.src,\n            alt: val.alt,\n            title: val.title,\n            dimensions: val.dimensions.map(|t| {\n                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();\n                arr\n            }),\n            image_type: val.image_type.into(),\n            attributes: val.attributes.into_iter().collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<StructuredData> for html_to_markdown_rs::metadata::StructuredData {\n    fn from(val: StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::StructuredData> for StructuredData {\n    fn from(val: html_to_markdown_rs::metadata::StructuredData) -> Self {\n        Self {\n            data_type: val.data_type.into(),\n            raw_json: val.raw_json,\n            schema_type: val.schema_type,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<HtmlMetadata> for html_to_markdown_rs::metadata::HtmlMetadata {\n    fn from(val: HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::metadata::HtmlMetadata> for HtmlMetadata {\n    fn from(val: html_to_markdown_rs::metadata::HtmlMetadata) -> Self {\n        Self {\n            document: val.document.into(),\n            headers: val.headers.into_iter().map(Into::into).collect(),\n            links: val.links.into_iter().map(Into::into).collect(),\n            images: val.images.into_iter().map(Into::into).collect(),\n            structured_data: val.structured_data.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptions> for html_to_markdown_rs::options::ConversionOptions {\n    fn from(val: ConversionOptions) -> Self {\n        let mut __result = html_to_markdown_rs::options::ConversionOptions::default();\n        __result.heading_style = val.heading_style.into();\n        __result.list_indent_type = val.list_indent_type.into();\n        __result.list_indent_width = val.list_indent_width;\n        __result.bullets = val.bullets;\n        __result.strong_em_symbol = val.strong_em_symbol;\n        __result.escape_asterisks = val.escape_asterisks;\n        __result.escape_underscores = val.escape_underscores;\n        __result.escape_misc = val.escape_misc;\n        __result.escape_ascii = val.escape_ascii;\n        __result.code_language = val.code_language;\n        __result.autolinks = val.autolinks;\n        __result.default_title = val.default_title;\n        __result.br_in_tables = val.br_in_tables;\n        __result.highlight_style = val.highlight_style.into();\n        __result.extract_metadata = val.extract_metadata;\n        __result.whitespace_mode = val.whitespace_mode.into();\n        __result.strip_newlines = val.strip_newlines;\n        __result.wrap = val.wrap;\n        __result.wrap_width = val.wrap_width;\n        __result.convert_as_inline = val.convert_as_inline;\n        __result.sub_symbol = val.sub_symbol;\n        __result.sup_symbol = val.sup_symbol;\n        __result.newline_style = val.newline_style.into();\n        __result.code_block_style = val.code_block_style.into();\n        __result.keep_inline_images_in = val.keep_inline_images_in;\n        __result.preprocessing = val.preprocessing.into();\n        __result.encoding = val.encoding;\n        __result.debug = val.debug;\n        __result.strip_tags = val.strip_tags;\n        __result.preserve_tags = val.preserve_tags;\n        __result.skip_images = val.skip_images;\n        __result.link_style = val.link_style.into();\n        __result.output_format = val.output_format.into();\n        __result.include_document_structure = val.include_document_structure;\n        __result.extract_images = val.extract_images;\n        __result.max_image_size = val.max_image_size;\n        __result.capture_svg = val.capture_svg;\n        __result.infer_dimensions = val.infer_dimensions;\n        __result.max_depth = val.max_depth;\n        __result.exclude_selectors = val.exclude_selectors;\n        __result\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptions> for ConversionOptions {\n    fn from(val: html_to_markdown_rs::options::ConversionOptions) -> Self {\n        Self {\n            heading_style: val.heading_style.into(),\n            list_indent_type: val.list_indent_type.into(),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.to_string(),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.into(),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.into(),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.into(),\n            code_block_style: val.code_block_style.into(),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.into(),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.into(),\n            output_format: val.output_format.into(),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth,\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionOptionsUpdate> for html_to_markdown_rs::options::ConversionOptionsUpdate {\n    fn from(val: ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.and_then(|s| s.chars().next()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: (val.max_depth).map(Some),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(Into::into),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::ConversionOptionsUpdate> for ConversionOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::ConversionOptionsUpdate) -> Self {\n        Self {\n            heading_style: val.heading_style.map(Into::into),\n            list_indent_type: val.list_indent_type.map(Into::into),\n            list_indent_width: val.list_indent_width,\n            bullets: val.bullets,\n            strong_em_symbol: val.strong_em_symbol.map(|c| c.to_string()),\n            escape_asterisks: val.escape_asterisks,\n            escape_underscores: val.escape_underscores,\n            escape_misc: val.escape_misc,\n            escape_ascii: val.escape_ascii,\n            code_language: val.code_language,\n            autolinks: val.autolinks,\n            default_title: val.default_title,\n            br_in_tables: val.br_in_tables,\n            highlight_style: val.highlight_style.map(Into::into),\n            extract_metadata: val.extract_metadata,\n            whitespace_mode: val.whitespace_mode.map(Into::into),\n            strip_newlines: val.strip_newlines,\n            wrap: val.wrap,\n            wrap_width: val.wrap_width,\n            convert_as_inline: val.convert_as_inline,\n            sub_symbol: val.sub_symbol,\n            sup_symbol: val.sup_symbol,\n            newline_style: val.newline_style.map(Into::into),\n            code_block_style: val.code_block_style.map(Into::into),\n            keep_inline_images_in: val.keep_inline_images_in,\n            preprocessing: val.preprocessing.map(Into::into),\n            encoding: val.encoding,\n            debug: val.debug,\n            strip_tags: val.strip_tags,\n            preserve_tags: val.preserve_tags,\n            skip_images: val.skip_images,\n            link_style: val.link_style.map(Into::into),\n            output_format: val.output_format.map(Into::into),\n            include_document_structure: val.include_document_structure,\n            extract_images: val.extract_images,\n            max_image_size: val.max_image_size,\n            capture_svg: val.capture_svg,\n            infer_dimensions: val.infer_dimensions,\n            max_depth: val.max_depth.flatten(),\n            exclude_selectors: val.exclude_selectors,\n            visitor: val.visitor.map(|v| VisitorHandle { inner: Arc::new(v) }),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptions> for html_to_markdown_rs::options::PreprocessingOptions {\n    fn from(val: PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptions> for PreprocessingOptions {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptions) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.into(),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<PreprocessingOptionsUpdate> for html_to_markdown_rs::options::PreprocessingOptionsUpdate {\n    fn from(val: PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::options::PreprocessingOptionsUpdate> for PreprocessingOptionsUpdate {\n    fn from(val: html_to_markdown_rs::options::PreprocessingOptionsUpdate) -> Self {\n        Self {\n            enabled: val.enabled,\n            preset: val.preset.map(Into::into),\n            remove_navigation: val.remove_navigation,\n            remove_forms: val.remove_forms,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentStructure> for html_to_markdown_rs::DocumentStructure {\n    fn from(val: DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentStructure> for DocumentStructure {\n    fn from(val: html_to_markdown_rs::DocumentStructure) -> Self {\n        Self {\n            nodes: val.nodes.into_iter().map(Into::into).collect(),\n            source_format: val.source_format,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<DocumentNode> for html_to_markdown_rs::DocumentNode {\n    fn from(val: DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::DocumentNode> for DocumentNode {\n    fn from(val: html_to_markdown_rs::DocumentNode) -> Self {\n        Self {\n            id: val.id,\n            content: val.content.into(),\n            parent: val.parent,\n            children: val.children,\n            annotations: val.annotations.into_iter().map(Into::into).collect(),\n            attributes: val.attributes.map(|m| m.into_iter().collect()),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TextAnnotation> for html_to_markdown_rs::TextAnnotation {\n    fn from(val: TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TextAnnotation> for TextAnnotation {\n    fn from(val: html_to_markdown_rs::TextAnnotation) -> Self {\n        Self {\n            start: val.start,\n            end: val.end,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::needless_update)]\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ConversionResult> for html_to_markdown_rs::ConversionResult {\n    fn from(val: ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: Default::default(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n            ..Default::default()\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ConversionResult> for ConversionResult {\n    fn from(val: html_to_markdown_rs::ConversionResult) -> Self {\n        Self {\n            content: val.content,\n            document: val.document.map(Into::into),\n            metadata: val.metadata.into(),\n            tables: val.tables.into_iter().map(Into::into).collect(),\n            images: val.images.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            warnings: val.warnings.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableGrid> for html_to_markdown_rs::TableGrid {\n    fn from(val: TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableGrid> for TableGrid {\n    fn from(val: html_to_markdown_rs::TableGrid) -> Self {\n        Self {\n            rows: val.rows,\n            cols: val.cols,\n            cells: val.cells.into_iter().map(Into::into).collect(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<GridCell> for html_to_markdown_rs::GridCell {\n    fn from(val: GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::GridCell> for GridCell {\n    fn from(val: html_to_markdown_rs::GridCell) -> Self {\n        Self {\n            content: val.content,\n            row: val.row,\n            col: val.col,\n            row_span: val.row_span,\n            col_span: val.col_span,\n            is_header: val.is_header,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<TableData> for html_to_markdown_rs::TableData {\n    fn from(val: TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::TableData> for TableData {\n    fn from(val: html_to_markdown_rs::TableData) -> Self {\n        Self {\n            grid: val.grid.into(),\n            markdown: val.markdown,\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<ProcessingWarning> for html_to_markdown_rs::ProcessingWarning {\n    fn from(val: ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::ProcessingWarning> for ProcessingWarning {\n    fn from(val: html_to_markdown_rs::ProcessingWarning) -> Self {\n        Self {\n            message: val.message,\n            kind: val.kind.into(),\n        }\n    }\n}\n\n#[allow(clippy::redundant_closure, clippy::useless_conversion)]\nimpl From<html_to_markdown_rs::NodeContext> for NodeContext {\n    fn from(val: html_to_markdown_rs::NodeContext) -> Self {\n        Self {\n            node_type: val.node_type.into(),\n            tag_name: val.tag_name,\n            attributes: val.attributes.into_iter().collect(),\n            depth: val.depth,\n            index_in_parent: val.index_in_parent,\n            parent_tag: val.parent_tag,\n            is_inline: val.is_inline,\n        }\n    }\n}\n\nimpl From<TextDirection> for html_to_markdown_rs::metadata::TextDirection {\n    fn from(val: TextDirection) -> Self {\n        match val {\n            TextDirection::LeftToRight => Self::LeftToRight,\n            TextDirection::RightToLeft => Self::RightToLeft,\n            TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::TextDirection> for TextDirection {\n    fn from(val: html_to_markdown_rs::metadata::TextDirection) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::TextDirection::LeftToRight => Self::LeftToRight,\n            html_to_markdown_rs::metadata::TextDirection::RightToLeft => Self::RightToLeft,\n            html_to_markdown_rs::metadata::TextDirection::Auto => Self::Auto,\n        }\n    }\n}\n\nimpl From<LinkType> for html_to_markdown_rs::metadata::LinkType {\n    fn from(val: LinkType) -> Self {\n        match val {\n            LinkType::Anchor => Self::Anchor,\n            LinkType::Internal => Self::Internal,\n            LinkType::External => Self::External,\n            LinkType::Email => Self::Email,\n            LinkType::Phone => Self::Phone,\n            LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::LinkType> for LinkType {\n    fn from(val: html_to_markdown_rs::metadata::LinkType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::LinkType::Anchor => Self::Anchor,\n            html_to_markdown_rs::metadata::LinkType::Internal => Self::Internal,\n            html_to_markdown_rs::metadata::LinkType::External => Self::External,\n            html_to_markdown_rs::metadata::LinkType::Email => Self::Email,\n            html_to_markdown_rs::metadata::LinkType::Phone => Self::Phone,\n            html_to_markdown_rs::metadata::LinkType::Other => Self::Other,\n        }\n    }\n}\n\nimpl From<ImageType> for html_to_markdown_rs::metadata::ImageType {\n    fn from(val: ImageType) -> Self {\n        match val {\n            ImageType::DataUri => Self::DataUri,\n            ImageType::InlineSvg => Self::InlineSvg,\n            ImageType::External => Self::External,\n            ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::ImageType> for ImageType {\n    fn from(val: html_to_markdown_rs::metadata::ImageType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::ImageType::DataUri => Self::DataUri,\n            html_to_markdown_rs::metadata::ImageType::InlineSvg => Self::InlineSvg,\n            html_to_markdown_rs::metadata::ImageType::External => Self::External,\n            html_to_markdown_rs::metadata::ImageType::Relative => Self::Relative,\n        }\n    }\n}\n\nimpl From<StructuredDataType> for html_to_markdown_rs::metadata::StructuredDataType {\n    fn from(val: StructuredDataType) -> Self {\n        match val {\n            StructuredDataType::JsonLd => Self::JsonLd,\n            StructuredDataType::Microdata => Self::Microdata,\n            StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::metadata::StructuredDataType> for StructuredDataType {\n    fn from(val: html_to_markdown_rs::metadata::StructuredDataType) -> Self {\n        match val {\n            html_to_markdown_rs::metadata::StructuredDataType::JsonLd => Self::JsonLd,\n            html_to_markdown_rs::metadata::StructuredDataType::Microdata => Self::Microdata,\n            html_to_markdown_rs::metadata::StructuredDataType::RDFa => Self::RDFa,\n        }\n    }\n}\n\nimpl From<PreprocessingPreset> for html_to_markdown_rs::options::PreprocessingPreset {\n    fn from(val: PreprocessingPreset) -> Self {\n        match val {\n            PreprocessingPreset::Minimal => Self::Minimal,\n            PreprocessingPreset::Standard => Self::Standard,\n            PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::PreprocessingPreset> for PreprocessingPreset {\n    fn from(val: html_to_markdown_rs::options::PreprocessingPreset) -> Self {\n        match val {\n            html_to_markdown_rs::options::PreprocessingPreset::Minimal => Self::Minimal,\n            html_to_markdown_rs::options::PreprocessingPreset::Standard => Self::Standard,\n            html_to_markdown_rs::options::PreprocessingPreset::Aggressive => Self::Aggressive,\n        }\n    }\n}\n\nimpl From<HeadingStyle> for html_to_markdown_rs::options::HeadingStyle {\n    fn from(val: HeadingStyle) -> Self {\n        match val {\n            HeadingStyle::Underlined => Self::Underlined,\n            HeadingStyle::Atx => Self::Atx,\n            HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HeadingStyle> for HeadingStyle {\n    fn from(val: html_to_markdown_rs::options::HeadingStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HeadingStyle::Underlined => Self::Underlined,\n            html_to_markdown_rs::options::HeadingStyle::Atx => Self::Atx,\n            html_to_markdown_rs::options::HeadingStyle::AtxClosed => Self::AtxClosed,\n        }\n    }\n}\n\nimpl From<ListIndentType> for html_to_markdown_rs::options::ListIndentType {\n    fn from(val: ListIndentType) -> Self {\n        match val {\n            ListIndentType::Spaces => Self::Spaces,\n            ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::ListIndentType> for ListIndentType {\n    fn from(val: html_to_markdown_rs::options::ListIndentType) -> Self {\n        match val {\n            html_to_markdown_rs::options::ListIndentType::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::ListIndentType::Tabs => Self::Tabs,\n        }\n    }\n}\n\nimpl From<WhitespaceMode> for html_to_markdown_rs::options::WhitespaceMode {\n    fn from(val: WhitespaceMode) -> Self {\n        match val {\n            WhitespaceMode::Normalized => Self::Normalized,\n            WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::WhitespaceMode> for WhitespaceMode {\n    fn from(val: html_to_markdown_rs::options::WhitespaceMode) -> Self {\n        match val {\n            html_to_markdown_rs::options::WhitespaceMode::Normalized => Self::Normalized,\n            html_to_markdown_rs::options::WhitespaceMode::Strict => Self::Strict,\n        }\n    }\n}\n\nimpl From<NewlineStyle> for html_to_markdown_rs::options::NewlineStyle {\n    fn from(val: NewlineStyle) -> Self {\n        match val {\n            NewlineStyle::Spaces => Self::Spaces,\n            NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::NewlineStyle> for NewlineStyle {\n    fn from(val: html_to_markdown_rs::options::NewlineStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::NewlineStyle::Spaces => Self::Spaces,\n            html_to_markdown_rs::options::NewlineStyle::Backslash => Self::Backslash,\n        }\n    }\n}\n\nimpl From<CodeBlockStyle> for html_to_markdown_rs::options::CodeBlockStyle {\n    fn from(val: CodeBlockStyle) -> Self {\n        match val {\n            CodeBlockStyle::Indented => Self::Indented,\n            CodeBlockStyle::Backticks => Self::Backticks,\n            CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::CodeBlockStyle> for CodeBlockStyle {\n    fn from(val: html_to_markdown_rs::options::CodeBlockStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::CodeBlockStyle::Indented => Self::Indented,\n            html_to_markdown_rs::options::CodeBlockStyle::Backticks => Self::Backticks,\n            html_to_markdown_rs::options::CodeBlockStyle::Tildes => Self::Tildes,\n        }\n    }\n}\n\nimpl From<HighlightStyle> for html_to_markdown_rs::options::HighlightStyle {\n    fn from(val: HighlightStyle) -> Self {\n        match val {\n            HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            HighlightStyle::Html => Self::Html,\n            HighlightStyle::Bold => Self::Bold,\n            HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::HighlightStyle> for HighlightStyle {\n    fn from(val: html_to_markdown_rs::options::HighlightStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::HighlightStyle::DoubleEqual => Self::DoubleEqual,\n            html_to_markdown_rs::options::HighlightStyle::Html => Self::Html,\n            html_to_markdown_rs::options::HighlightStyle::Bold => Self::Bold,\n            html_to_markdown_rs::options::HighlightStyle::None => Self::None,\n        }\n    }\n}\n\nimpl From<LinkStyle> for html_to_markdown_rs::options::LinkStyle {\n    fn from(val: LinkStyle) -> Self {\n        match val {\n            LinkStyle::Inline => Self::Inline,\n            LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::LinkStyle> for LinkStyle {\n    fn from(val: html_to_markdown_rs::options::LinkStyle) -> Self {\n        match val {\n            html_to_markdown_rs::options::LinkStyle::Inline => Self::Inline,\n            html_to_markdown_rs::options::LinkStyle::Reference => Self::Reference,\n        }\n    }\n}\n\nimpl From<OutputFormat> for html_to_markdown_rs::options::OutputFormat {\n    fn from(val: OutputFormat) -> Self {\n        match val {\n            OutputFormat::Markdown => Self::Markdown,\n            OutputFormat::Djot => Self::Djot,\n            OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::options::OutputFormat> for OutputFormat {\n    fn from(val: html_to_markdown_rs::options::OutputFormat) -> Self {\n        match val {\n            html_to_markdown_rs::options::OutputFormat::Markdown => Self::Markdown,\n            html_to_markdown_rs::options::OutputFormat::Djot => Self::Djot,\n            html_to_markdown_rs::options::OutputFormat::Plain => Self::Plain,\n        }\n    }\n}\n\nimpl From<NodeContent> for html_to_markdown_rs::NodeContent {\n    fn from(val: NodeContent) -> Self {\n        match val {\n            NodeContent::Heading { level, text } => Self::Heading { level, text },\n            NodeContent::Paragraph { text } => Self::Paragraph { text },\n            NodeContent::List { ordered } => Self::List { ordered },\n            NodeContent::ListItem { text } => Self::ListItem { text },\n            NodeContent::Table { grid } => Self::Table { grid: grid.into() },\n            NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self::Image {\n                description,\n                src,\n                image_index,\n            },\n            NodeContent::Code { text, language } => Self::Code { text, language },\n            NodeContent::Quote => Self::Quote,\n            NodeContent::DefinitionList => Self::DefinitionList,\n            NodeContent::DefinitionItem { term, definition } => Self::DefinitionItem { term, definition },\n            NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },\n            NodeContent::MetadataBlock { entries } => Self::MetadataBlock {\n                entries: entries.iter().filter_map(|s| serde_json::from_str(s).ok()).collect(),\n            },\n            NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self::Group {\n                label,\n                heading_level,\n                heading_text,\n            },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeContent> for NodeContent {\n    fn from(val: html_to_markdown_rs::NodeContent) -> Self {\n        match val {\n            html_to_markdown_rs::NodeContent::Heading { level, text } => Self::Heading { level, text },\n            html_to_markdown_rs::NodeContent::Paragraph { text } => Self::Paragraph { text },\n            html_to_markdown_rs::NodeContent::List { ordered } => Self::List { ordered },\n            html_to_markdown_rs::NodeContent::ListItem { text } => Self::ListItem { text },\n            html_to_markdown_rs::NodeContent::Table { grid } => Self::Table { grid: grid.into() },\n            html_to_markdown_rs::NodeContent::Image {\n                description,\n                src,\n                image_index,\n            } => Self::Image {\n                description,\n                src,\n                image_index,\n            },\n            html_to_markdown_rs::NodeContent::Code { text, language } => Self::Code { text, language },\n            html_to_markdown_rs::NodeContent::Quote => Self::Quote,\n            html_to_markdown_rs::NodeContent::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeContent::DefinitionItem { term, definition } => {\n                Self::DefinitionItem { term, definition }\n            }\n            html_to_markdown_rs::NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },\n            html_to_markdown_rs::NodeContent::MetadataBlock { entries } => Self::MetadataBlock {\n                entries: entries.iter().map(|i| format!(\"{:?}\", i)).collect(),\n            },\n            html_to_markdown_rs::NodeContent::Group {\n                label,\n                heading_level,\n                heading_text,\n            } => Self::Group {\n                label,\n                heading_level,\n                heading_text,\n            },\n        }\n    }\n}\n\nimpl From<AnnotationKind> for html_to_markdown_rs::AnnotationKind {\n    fn from(val: AnnotationKind) -> Self {\n        match val {\n            AnnotationKind::Bold => Self::Bold,\n            AnnotationKind::Italic => Self::Italic,\n            AnnotationKind::Underline => Self::Underline,\n            AnnotationKind::Strikethrough => Self::Strikethrough,\n            AnnotationKind::Code => Self::Code,\n            AnnotationKind::Subscript => Self::Subscript,\n            AnnotationKind::Superscript => Self::Superscript,\n            AnnotationKind::Highlight => Self::Highlight,\n            AnnotationKind::Link { url, title } => Self::Link { url, title },\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::AnnotationKind> for AnnotationKind {\n    fn from(val: html_to_markdown_rs::AnnotationKind) -> Self {\n        match val {\n            html_to_markdown_rs::AnnotationKind::Bold => Self::Bold,\n            html_to_markdown_rs::AnnotationKind::Italic => Self::Italic,\n            html_to_markdown_rs::AnnotationKind::Underline => Self::Underline,\n            html_to_markdown_rs::AnnotationKind::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::AnnotationKind::Code => Self::Code,\n            html_to_markdown_rs::AnnotationKind::Subscript => Self::Subscript,\n            html_to_markdown_rs::AnnotationKind::Superscript => Self::Superscript,\n            html_to_markdown_rs::AnnotationKind::Highlight => Self::Highlight,\n            html_to_markdown_rs::AnnotationKind::Link { url, title } => Self::Link { url, title },\n        }\n    }\n}\n\nimpl From<WarningKind> for html_to_markdown_rs::WarningKind {\n    fn from(val: WarningKind) -> Self {\n        match val {\n            WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            WarningKind::EncodingFallback => Self::EncodingFallback,\n            WarningKind::TruncatedInput => Self::TruncatedInput,\n            WarningKind::MalformedHtml => Self::MalformedHtml,\n            WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::WarningKind> for WarningKind {\n    fn from(val: html_to_markdown_rs::WarningKind) -> Self {\n        match val {\n            html_to_markdown_rs::WarningKind::ImageExtractionFailed => Self::ImageExtractionFailed,\n            html_to_markdown_rs::WarningKind::EncodingFallback => Self::EncodingFallback,\n            html_to_markdown_rs::WarningKind::TruncatedInput => Self::TruncatedInput,\n            html_to_markdown_rs::WarningKind::MalformedHtml => Self::MalformedHtml,\n            html_to_markdown_rs::WarningKind::SanitizationApplied => Self::SanitizationApplied,\n            html_to_markdown_rs::WarningKind::DepthLimitExceeded => Self::DepthLimitExceeded,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::NodeType> for NodeType {\n    fn from(val: html_to_markdown_rs::NodeType) -> Self {\n        match val {\n            html_to_markdown_rs::NodeType::Text => Self::Text,\n            html_to_markdown_rs::NodeType::Element => Self::Element,\n            html_to_markdown_rs::NodeType::Heading => Self::Heading,\n            html_to_markdown_rs::NodeType::Paragraph => Self::Paragraph,\n            html_to_markdown_rs::NodeType::Div => Self::Div,\n            html_to_markdown_rs::NodeType::Blockquote => Self::Blockquote,\n            html_to_markdown_rs::NodeType::Pre => Self::Pre,\n            html_to_markdown_rs::NodeType::Hr => Self::Hr,\n            html_to_markdown_rs::NodeType::List => Self::List,\n            html_to_markdown_rs::NodeType::ListItem => Self::ListItem,\n            html_to_markdown_rs::NodeType::DefinitionList => Self::DefinitionList,\n            html_to_markdown_rs::NodeType::DefinitionTerm => Self::DefinitionTerm,\n            html_to_markdown_rs::NodeType::DefinitionDescription => Self::DefinitionDescription,\n            html_to_markdown_rs::NodeType::Table => Self::Table,\n            html_to_markdown_rs::NodeType::TableRow => Self::TableRow,\n            html_to_markdown_rs::NodeType::TableCell => Self::TableCell,\n            html_to_markdown_rs::NodeType::TableHeader => Self::TableHeader,\n            html_to_markdown_rs::NodeType::TableBody => Self::TableBody,\n            html_to_markdown_rs::NodeType::TableHead => Self::TableHead,\n            html_to_markdown_rs::NodeType::TableFoot => Self::TableFoot,\n            html_to_markdown_rs::NodeType::Link => Self::Link,\n            html_to_markdown_rs::NodeType::Image => Self::Image,\n            html_to_markdown_rs::NodeType::Strong => Self::Strong,\n            html_to_markdown_rs::NodeType::Em => Self::Em,\n            html_to_markdown_rs::NodeType::Code => Self::Code,\n            html_to_markdown_rs::NodeType::Strikethrough => Self::Strikethrough,\n            html_to_markdown_rs::NodeType::Underline => Self::Underline,\n            html_to_markdown_rs::NodeType::Subscript => Self::Subscript,\n            html_to_markdown_rs::NodeType::Superscript => Self::Superscript,\n            html_to_markdown_rs::NodeType::Mark => Self::Mark,\n            html_to_markdown_rs::NodeType::Small => Self::Small,\n            html_to_markdown_rs::NodeType::Br => Self::Br,\n            html_to_markdown_rs::NodeType::Span => Self::Span,\n            html_to_markdown_rs::NodeType::Article => Self::Article,\n            html_to_markdown_rs::NodeType::Section => Self::Section,\n            html_to_markdown_rs::NodeType::Nav => Self::Nav,\n            html_to_markdown_rs::NodeType::Aside => Self::Aside,\n            html_to_markdown_rs::NodeType::Header => Self::Header,\n            html_to_markdown_rs::NodeType::Footer => Self::Footer,\n            html_to_markdown_rs::NodeType::Main => Self::Main,\n            html_to_markdown_rs::NodeType::Figure => Self::Figure,\n            html_to_markdown_rs::NodeType::Figcaption => Self::Figcaption,\n            html_to_markdown_rs::NodeType::Time => Self::Time,\n            html_to_markdown_rs::NodeType::Details => Self::Details,\n            html_to_markdown_rs::NodeType::Summary => Self::Summary,\n            html_to_markdown_rs::NodeType::Form => Self::Form,\n            html_to_markdown_rs::NodeType::Input => Self::Input,\n            html_to_markdown_rs::NodeType::Select => Self::Select,\n            html_to_markdown_rs::NodeType::Option => Self::Option,\n            html_to_markdown_rs::NodeType::Button => Self::Button,\n            html_to_markdown_rs::NodeType::Textarea => Self::Textarea,\n            html_to_markdown_rs::NodeType::Label => Self::Label,\n            html_to_markdown_rs::NodeType::Fieldset => Self::Fieldset,\n            html_to_markdown_rs::NodeType::Legend => Self::Legend,\n            html_to_markdown_rs::NodeType::Audio => Self::Audio,\n            html_to_markdown_rs::NodeType::Video => Self::Video,\n            html_to_markdown_rs::NodeType::Picture => Self::Picture,\n            html_to_markdown_rs::NodeType::Source => Self::Source,\n            html_to_markdown_rs::NodeType::Iframe => Self::Iframe,\n            html_to_markdown_rs::NodeType::Svg => Self::Svg,\n            html_to_markdown_rs::NodeType::Canvas => Self::Canvas,\n            html_to_markdown_rs::NodeType::Ruby => Self::Ruby,\n            html_to_markdown_rs::NodeType::Rt => Self::Rt,\n            html_to_markdown_rs::NodeType::Rp => Self::Rp,\n            html_to_markdown_rs::NodeType::Abbr => Self::Abbr,\n            html_to_markdown_rs::NodeType::Kbd => Self::Kbd,\n            html_to_markdown_rs::NodeType::Samp => Self::Samp,\n            html_to_markdown_rs::NodeType::Var => Self::Var,\n            html_to_markdown_rs::NodeType::Cite => Self::Cite,\n            html_to_markdown_rs::NodeType::Q => Self::Q,\n            html_to_markdown_rs::NodeType::Del => Self::Del,\n            html_to_markdown_rs::NodeType::Ins => Self::Ins,\n            html_to_markdown_rs::NodeType::Data => Self::Data,\n            html_to_markdown_rs::NodeType::Meter => Self::Meter,\n            html_to_markdown_rs::NodeType::Progress => Self::Progress,\n            html_to_markdown_rs::NodeType::Output => Self::Output,\n            html_to_markdown_rs::NodeType::Template => Self::Template,\n            html_to_markdown_rs::NodeType::Slot => Self::Slot,\n            html_to_markdown_rs::NodeType::Html => Self::Html,\n            html_to_markdown_rs::NodeType::Head => Self::Head,\n            html_to_markdown_rs::NodeType::Body => Self::Body,\n            html_to_markdown_rs::NodeType::Title => Self::Title,\n            html_to_markdown_rs::NodeType::Meta => Self::Meta,\n            html_to_markdown_rs::NodeType::LinkTag => Self::LinkTag,\n            html_to_markdown_rs::NodeType::Style => Self::Style,\n            html_to_markdown_rs::NodeType::Script => Self::Script,\n            html_to_markdown_rs::NodeType::Base => Self::Base,\n            html_to_markdown_rs::NodeType::Custom => Self::Custom,\n        }\n    }\n}\n\nimpl From<html_to_markdown_rs::VisitResult> for VisitResult {\n    fn from(val: html_to_markdown_rs::VisitResult) -> Self {\n        match val {\n            html_to_markdown_rs::VisitResult::Continue => Self::Continue,\n            html_to_markdown_rs::VisitResult::Custom(_0) => Self::Custom { _0 },\n            html_to_markdown_rs::VisitResult::Skip => Self::Skip,\n            html_to_markdown_rs::VisitResult::PreserveHtml => Self::PreserveHtml,\n            html_to_markdown_rs::VisitResult::Error(_0) => Self::Error { _0 },\n        }\n    }\n}\n\n/// Convert a `html_to_markdown_rs::error::ConversionError` error to a Magnus runtime error.\n#[allow(dead_code)]\nfn conversion_error_to_magnus_err(e: html_to_markdown_rs::error::ConversionError) -> magnus::Error {\n    let msg = e.to_string();\n    magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)\n}\n\n#[magnus::init]\nfn init(ruby: &Ruby) -> Result<(), Error> {\n    let module = ruby.define_module(\"HtmlToMarkdownRs\")?;\n\n    let class = module.define_class(\"DocumentMetadata\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(DocumentMetadata::new, 11))?;\n    class.define_method(\"title\", method!(DocumentMetadata::title, 0))?;\n    class.define_method(\"description\", method!(DocumentMetadata::description, 0))?;\n    class.define_method(\"keywords\", method!(DocumentMetadata::keywords, 0))?;\n    class.define_method(\"author\", method!(DocumentMetadata::author, 0))?;\n    class.define_method(\"canonical_url\", method!(DocumentMetadata::canonical_url, 0))?;\n    class.define_method(\"base_href\", method!(DocumentMetadata::base_href, 0))?;\n    class.define_method(\"language\", method!(DocumentMetadata::language, 0))?;\n    class.define_method(\"text_direction\", method!(DocumentMetadata::text_direction, 0))?;\n    class.define_method(\"open_graph\", method!(DocumentMetadata::open_graph, 0))?;\n    class.define_method(\"twitter_card\", method!(DocumentMetadata::twitter_card, 0))?;\n    class.define_method(\"meta_tags\", method!(DocumentMetadata::meta_tags, 0))?;\n\n    let class = module.define_class(\"HeaderMetadata\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(HeaderMetadata::new, 5))?;\n    class.define_method(\"level\", method!(HeaderMetadata::level, 0))?;\n    class.define_method(\"text\", method!(HeaderMetadata::text, 0))?;\n    class.define_method(\"id\", method!(HeaderMetadata::id, 0))?;\n    class.define_method(\"depth\", method!(HeaderMetadata::depth, 0))?;\n    class.define_method(\"html_offset\", method!(HeaderMetadata::html_offset, 0))?;\n    class.define_method(\"is_valid\", method!(HeaderMetadata::is_valid, 0))?;\n\n    let class = module.define_class(\"LinkMetadata\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(LinkMetadata::new, 6))?;\n    class.define_method(\"href\", method!(LinkMetadata::href, 0))?;\n    class.define_method(\"text\", method!(LinkMetadata::text, 0))?;\n    class.define_method(\"title\", method!(LinkMetadata::title, 0))?;\n    class.define_method(\"link_type\", method!(LinkMetadata::link_type, 0))?;\n    class.define_method(\"rel\", method!(LinkMetadata::rel, 0))?;\n    class.define_method(\"attributes\", method!(LinkMetadata::attributes, 0))?;\n\n    let class = module.define_class(\"ImageMetadata\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(ImageMetadata::new, 6))?;\n    class.define_method(\"src\", method!(ImageMetadata::src, 0))?;\n    class.define_method(\"alt\", method!(ImageMetadata::alt, 0))?;\n    class.define_method(\"title\", method!(ImageMetadata::title, 0))?;\n    class.define_method(\"dimensions\", method!(ImageMetadata::dimensions, 0))?;\n    class.define_method(\"image_type\", method!(ImageMetadata::image_type, 0))?;\n    class.define_method(\"attributes\", method!(ImageMetadata::attributes, 0))?;\n\n    let class = module.define_class(\"StructuredData\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(StructuredData::new, 3))?;\n    class.define_method(\"data_type\", method!(StructuredData::data_type, 0))?;\n    class.define_method(\"raw_json\", method!(StructuredData::raw_json, 0))?;\n    class.define_method(\"schema_type\", method!(StructuredData::schema_type, 0))?;\n\n    let class = module.define_class(\"HtmlMetadata\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(HtmlMetadata::new, 5))?;\n    class.define_method(\"document\", method!(HtmlMetadata::document, 0))?;\n    class.define_method(\"headers\", method!(HtmlMetadata::headers, 0))?;\n    class.define_method(\"links\", method!(HtmlMetadata::links, 0))?;\n    class.define_method(\"images\", method!(HtmlMetadata::images, 0))?;\n    class.define_method(\"structured_data\", method!(HtmlMetadata::structured_data, 0))?;\n\n    let class = module.define_class(\"ConversionOptions\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(ConversionOptions::new, 1))?;\n    class.define_method(\"heading_style\", method!(ConversionOptions::heading_style, 0))?;\n    class.define_method(\"list_indent_type\", method!(ConversionOptions::list_indent_type, 0))?;\n    class.define_method(\"list_indent_width\", method!(ConversionOptions::list_indent_width, 0))?;\n    class.define_method(\"bullets\", method!(ConversionOptions::bullets, 0))?;\n    class.define_method(\"strong_em_symbol\", method!(ConversionOptions::strong_em_symbol, 0))?;\n    class.define_method(\"escape_asterisks\", method!(ConversionOptions::escape_asterisks, 0))?;\n    class.define_method(\"escape_underscores\", method!(ConversionOptions::escape_underscores, 0))?;\n    class.define_method(\"escape_misc\", method!(ConversionOptions::escape_misc, 0))?;\n    class.define_method(\"escape_ascii\", method!(ConversionOptions::escape_ascii, 0))?;\n    class.define_method(\"code_language\", method!(ConversionOptions::code_language, 0))?;\n    class.define_method(\"autolinks\", method!(ConversionOptions::autolinks, 0))?;\n    class.define_method(\"default_title\", method!(ConversionOptions::default_title, 0))?;\n    class.define_method(\"br_in_tables\", method!(ConversionOptions::br_in_tables, 0))?;\n    class.define_method(\"highlight_style\", method!(ConversionOptions::highlight_style, 0))?;\n    class.define_method(\"extract_metadata\", method!(ConversionOptions::extract_metadata, 0))?;\n    class.define_method(\"whitespace_mode\", method!(ConversionOptions::whitespace_mode, 0))?;\n    class.define_method(\"strip_newlines\", method!(ConversionOptions::strip_newlines, 0))?;\n    class.define_method(\"wrap\", method!(ConversionOptions::wrap, 0))?;\n    class.define_method(\"wrap_width\", method!(ConversionOptions::wrap_width, 0))?;\n    class.define_method(\"convert_as_inline\", method!(ConversionOptions::convert_as_inline, 0))?;\n    class.define_method(\"sub_symbol\", method!(ConversionOptions::sub_symbol, 0))?;\n    class.define_method(\"sup_symbol\", method!(ConversionOptions::sup_symbol, 0))?;\n    class.define_method(\"newline_style\", method!(ConversionOptions::newline_style, 0))?;\n    class.define_method(\"code_block_style\", method!(ConversionOptions::code_block_style, 0))?;\n    class.define_method(\n        \"keep_inline_images_in\",\n        method!(ConversionOptions::keep_inline_images_in, 0),\n    )?;\n    class.define_method(\"preprocessing\", method!(ConversionOptions::preprocessing, 0))?;\n    class.define_method(\"encoding\", method!(ConversionOptions::encoding, 0))?;\n    class.define_method(\"debug\", method!(ConversionOptions::debug, 0))?;\n    class.define_method(\"strip_tags\", method!(ConversionOptions::strip_tags, 0))?;\n    class.define_method(\"preserve_tags\", method!(ConversionOptions::preserve_tags, 0))?;\n    class.define_method(\"skip_images\", method!(ConversionOptions::skip_images, 0))?;\n    class.define_method(\"link_style\", method!(ConversionOptions::link_style, 0))?;\n    class.define_method(\"output_format\", method!(ConversionOptions::output_format, 0))?;\n    class.define_method(\n        \"include_document_structure\",\n        method!(ConversionOptions::include_document_structure, 0),\n    )?;\n    class.define_method(\"extract_images\", method!(ConversionOptions::extract_images, 0))?;\n    class.define_method(\"max_image_size\", method!(ConversionOptions::max_image_size, 0))?;\n    class.define_method(\"capture_svg\", method!(ConversionOptions::capture_svg, 0))?;\n    class.define_method(\"infer_dimensions\", method!(ConversionOptions::infer_dimensions, 0))?;\n    class.define_method(\"max_depth\", method!(ConversionOptions::max_depth, 0))?;\n    class.define_method(\"exclude_selectors\", method!(ConversionOptions::exclude_selectors, 0))?;\n    class.define_method(\"visitor\", method!(ConversionOptions::visitor, 0))?;\n    class.define_method(\"apply_update\", method!(ConversionOptions::apply_update, 1))?;\n\n    let class = module.define_class(\"ConversionOptionsBuilder\", ruby.class_object())?;\n    class.define_method(\"strip_tags\", method!(ConversionOptionsBuilder::strip_tags, 1))?;\n    class.define_method(\"preserve_tags\", method!(ConversionOptionsBuilder::preserve_tags, 1))?;\n    class.define_method(\n        \"keep_inline_images_in\",\n        method!(ConversionOptionsBuilder::keep_inline_images_in, 1),\n    )?;\n    class.define_method(\n        \"exclude_selectors\",\n        method!(ConversionOptionsBuilder::exclude_selectors, 1),\n    )?;\n    class.define_method(\"visitor\", method!(ConversionOptionsBuilder::visitor, 1))?;\n    class.define_method(\"preprocessing\", method!(ConversionOptionsBuilder::preprocessing, 1))?;\n    class.define_method(\"build\", method!(ConversionOptionsBuilder::build, 0))?;\n\n    let class = module.define_class(\"ConversionOptionsUpdate\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(ConversionOptionsUpdate::new, 1))?;\n    class.define_method(\"heading_style\", method!(ConversionOptionsUpdate::heading_style, 0))?;\n    class.define_method(\n        \"list_indent_type\",\n        method!(ConversionOptionsUpdate::list_indent_type, 0),\n    )?;\n    class.define_method(\n        \"list_indent_width\",\n        method!(ConversionOptionsUpdate::list_indent_width, 0),\n    )?;\n    class.define_method(\"bullets\", method!(ConversionOptionsUpdate::bullets, 0))?;\n    class.define_method(\n        \"strong_em_symbol\",\n        method!(ConversionOptionsUpdate::strong_em_symbol, 0),\n    )?;\n    class.define_method(\n        \"escape_asterisks\",\n        method!(ConversionOptionsUpdate::escape_asterisks, 0),\n    )?;\n    class.define_method(\n        \"escape_underscores\",\n        method!(ConversionOptionsUpdate::escape_underscores, 0),\n    )?;\n    class.define_method(\"escape_misc\", method!(ConversionOptionsUpdate::escape_misc, 0))?;\n    class.define_method(\"escape_ascii\", method!(ConversionOptionsUpdate::escape_ascii, 0))?;\n    class.define_method(\"code_language\", method!(ConversionOptionsUpdate::code_language, 0))?;\n    class.define_method(\"autolinks\", method!(ConversionOptionsUpdate::autolinks, 0))?;\n    class.define_method(\"default_title\", method!(ConversionOptionsUpdate::default_title, 0))?;\n    class.define_method(\"br_in_tables\", method!(ConversionOptionsUpdate::br_in_tables, 0))?;\n    class.define_method(\"highlight_style\", method!(ConversionOptionsUpdate::highlight_style, 0))?;\n    class.define_method(\n        \"extract_metadata\",\n        method!(ConversionOptionsUpdate::extract_metadata, 0),\n    )?;\n    class.define_method(\"whitespace_mode\", method!(ConversionOptionsUpdate::whitespace_mode, 0))?;\n    class.define_method(\"strip_newlines\", method!(ConversionOptionsUpdate::strip_newlines, 0))?;\n    class.define_method(\"wrap\", method!(ConversionOptionsUpdate::wrap, 0))?;\n    class.define_method(\"wrap_width\", method!(ConversionOptionsUpdate::wrap_width, 0))?;\n    class.define_method(\n        \"convert_as_inline\",\n        method!(ConversionOptionsUpdate::convert_as_inline, 0),\n    )?;\n    class.define_method(\"sub_symbol\", method!(ConversionOptionsUpdate::sub_symbol, 0))?;\n    class.define_method(\"sup_symbol\", method!(ConversionOptionsUpdate::sup_symbol, 0))?;\n    class.define_method(\"newline_style\", method!(ConversionOptionsUpdate::newline_style, 0))?;\n    class.define_method(\n        \"code_block_style\",\n        method!(ConversionOptionsUpdate::code_block_style, 0),\n    )?;\n    class.define_method(\n        \"keep_inline_images_in\",\n        method!(ConversionOptionsUpdate::keep_inline_images_in, 0),\n    )?;\n    class.define_method(\"preprocessing\", method!(ConversionOptionsUpdate::preprocessing, 0))?;\n    class.define_method(\"encoding\", method!(ConversionOptionsUpdate::encoding, 0))?;\n    class.define_method(\"debug\", method!(ConversionOptionsUpdate::debug, 0))?;\n    class.define_method(\"strip_tags\", method!(ConversionOptionsUpdate::strip_tags, 0))?;\n    class.define_method(\"preserve_tags\", method!(ConversionOptionsUpdate::preserve_tags, 0))?;\n    class.define_method(\"skip_images\", method!(ConversionOptionsUpdate::skip_images, 0))?;\n    class.define_method(\"link_style\", method!(ConversionOptionsUpdate::link_style, 0))?;\n    class.define_method(\"output_format\", method!(ConversionOptionsUpdate::output_format, 0))?;\n    class.define_method(\n        \"include_document_structure\",\n        method!(ConversionOptionsUpdate::include_document_structure, 0),\n    )?;\n    class.define_method(\"extract_images\", method!(ConversionOptionsUpdate::extract_images, 0))?;\n    class.define_method(\"max_image_size\", method!(ConversionOptionsUpdate::max_image_size, 0))?;\n    class.define_method(\"capture_svg\", method!(ConversionOptionsUpdate::capture_svg, 0))?;\n    class.define_method(\n        \"infer_dimensions\",\n        method!(ConversionOptionsUpdate::infer_dimensions, 0),\n    )?;\n    class.define_method(\"max_depth\", method!(ConversionOptionsUpdate::max_depth, 0))?;\n    class.define_method(\n        \"exclude_selectors\",\n        method!(ConversionOptionsUpdate::exclude_selectors, 0),\n    )?;\n    class.define_method(\"visitor\", method!(ConversionOptionsUpdate::visitor, 0))?;\n\n    let class = module.define_class(\"PreprocessingOptions\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(PreprocessingOptions::new, 4))?;\n    class.define_method(\"enabled\", method!(PreprocessingOptions::enabled, 0))?;\n    class.define_method(\"preset\", method!(PreprocessingOptions::preset, 0))?;\n    class.define_method(\"remove_navigation\", method!(PreprocessingOptions::remove_navigation, 0))?;\n    class.define_method(\"remove_forms\", method!(PreprocessingOptions::remove_forms, 0))?;\n    class.define_method(\"apply_update\", method!(PreprocessingOptions::apply_update, 1))?;\n\n    let class = module.define_class(\"PreprocessingOptionsUpdate\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(PreprocessingOptionsUpdate::new, 4))?;\n    class.define_method(\"enabled\", method!(PreprocessingOptionsUpdate::enabled, 0))?;\n    class.define_method(\"preset\", method!(PreprocessingOptionsUpdate::preset, 0))?;\n    class.define_method(\n        \"remove_navigation\",\n        method!(PreprocessingOptionsUpdate::remove_navigation, 0),\n    )?;\n    class.define_method(\"remove_forms\", method!(PreprocessingOptionsUpdate::remove_forms, 0))?;\n\n    let class = module.define_class(\"DocumentStructure\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(DocumentStructure::new, 2))?;\n    class.define_method(\"nodes\", method!(DocumentStructure::nodes, 0))?;\n    class.define_method(\"source_format\", method!(DocumentStructure::source_format, 0))?;\n\n    let class = module.define_class(\"DocumentNode\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(DocumentNode::new, 6))?;\n    class.define_method(\"id\", method!(DocumentNode::id, 0))?;\n    class.define_method(\"content\", method!(DocumentNode::content, 0))?;\n    class.define_method(\"parent\", method!(DocumentNode::parent, 0))?;\n    class.define_method(\"children\", method!(DocumentNode::children, 0))?;\n    class.define_method(\"annotations\", method!(DocumentNode::annotations, 0))?;\n    class.define_method(\"attributes\", method!(DocumentNode::attributes, 0))?;\n\n    let class = module.define_class(\"TextAnnotation\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(TextAnnotation::new, 3))?;\n    class.define_method(\"start\", method!(TextAnnotation::start, 0))?;\n    class.define_method(\"end\", method!(TextAnnotation::end, 0))?;\n    class.define_method(\"kind\", method!(TextAnnotation::kind, 0))?;\n\n    let class = module.define_class(\"ConversionResult\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(ConversionResult::new, 6))?;\n    class.define_method(\"content\", method!(ConversionResult::content, 0))?;\n    class.define_method(\"document\", method!(ConversionResult::document, 0))?;\n    class.define_method(\"metadata\", method!(ConversionResult::metadata, 0))?;\n    class.define_method(\"tables\", method!(ConversionResult::tables, 0))?;\n    class.define_method(\"images\", method!(ConversionResult::images, 0))?;\n    class.define_method(\"warnings\", method!(ConversionResult::warnings, 0))?;\n\n    let class = module.define_class(\"TableGrid\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(TableGrid::new, 3))?;\n    class.define_method(\"rows\", method!(TableGrid::rows, 0))?;\n    class.define_method(\"cols\", method!(TableGrid::cols, 0))?;\n    class.define_method(\"cells\", method!(TableGrid::cells, 0))?;\n\n    let class = module.define_class(\"GridCell\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(GridCell::new, 6))?;\n    class.define_method(\"content\", method!(GridCell::content, 0))?;\n    class.define_method(\"row\", method!(GridCell::row, 0))?;\n    class.define_method(\"col\", method!(GridCell::col, 0))?;\n    class.define_method(\"row_span\", method!(GridCell::row_span, 0))?;\n    class.define_method(\"col_span\", method!(GridCell::col_span, 0))?;\n    class.define_method(\"is_header\", method!(GridCell::is_header, 0))?;\n\n    let class = module.define_class(\"TableData\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(TableData::new, 2))?;\n    class.define_method(\"grid\", method!(TableData::grid, 0))?;\n    class.define_method(\"markdown\", method!(TableData::markdown, 0))?;\n\n    let class = module.define_class(\"ProcessingWarning\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(ProcessingWarning::new, 2))?;\n    class.define_method(\"message\", method!(ProcessingWarning::message, 0))?;\n    class.define_method(\"kind\", method!(ProcessingWarning::kind, 0))?;\n\n    let _class = module.define_class(\"VisitorHandle\", ruby.class_object())?;\n\n    let class = module.define_class(\"NodeContext\", ruby.class_object())?;\n    class.define_singleton_method(\"new\", function!(NodeContext::new, 7))?;\n    class.define_method(\"node_type\", method!(NodeContext::node_type, 0))?;\n    class.define_method(\"tag_name\", method!(NodeContext::tag_name, 0))?;\n    class.define_method(\"attributes\", method!(NodeContext::attributes, 0))?;\n    class.define_method(\"depth\", method!(NodeContext::depth, 0))?;\n    class.define_method(\"index_in_parent\", method!(NodeContext::index_in_parent, 0))?;\n    class.define_method(\"parent_tag\", method!(NodeContext::parent_tag, 0))?;\n    class.define_method(\"is_inline\", method!(NodeContext::is_inline, 0))?;\n\n    module.define_module_function(\"convert\", function!(convert, 2))?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "packages/ruby/html_to_markdown.gemspec",
    "content": "# frozen_string_literal: true\n\nGem::Specification.new do |spec|\n  spec.name = 'html-to-markdown'\n  spec.version = '3.4.0.pre.rc.25'\n  spec.authors       = ['Kreuzberg Team']\n  spec.summary       = 'High-performance HTML to Markdown converter'\n  spec.description   = 'High-performance HTML to Markdown converter'\n  spec.homepage      = 'https://github.com/kreuzberg-dev/html-to-markdown'\n  spec.license       = 'MIT'\n  spec.required_ruby_version = '>= 3.2.0'\n  spec.metadata['keywords'] = %w[html markdown converter].join(',')\n  spec.metadata['rubygems_mfa_required'] = 'true'\n\n  spec.files         = Dir.glob(%w[lib/**/* ext/**/* sig/**/* Steepfile])\n  spec.require_paths = ['lib']\n  spec.extensions    = ['ext/html_to_markdown_rb/extconf.rb']\n\n  spec.add_dependency 'rb_sys', '~> 0.9'\nend\n"
  },
  {
    "path": "packages/ruby/lib/html_to_markdown/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule HtmlToMarkdown\n  VERSION = '3.4.0.pre.rc.25'\nend\n"
  },
  {
    "path": "packages/ruby/lib/html_to_markdown.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'html_to_markdown/version'\nrequire 'html_to_markdown_rb'\nrequire 'json'\n\n# High-performance HTML to Markdown conversion.\n#\n# @example Simple conversion\n#   HtmlToMarkdown.convert('<h1>Hello</h1>') # => \"# Hello\\n\\n\"\n#\n# @example With options\n#   HtmlToMarkdown.convert('<h1>Hello</h1>', heading_style: 'atx')\nmodule HtmlToMarkdown\n  # Convert HTML to Markdown.\n  #\n  # @param html [String] The HTML content to convert.\n  # @param options [Hash] Optional conversion options.\n  #   Supported keys (all optional):\n  #   - :heading_style       - 'atx', 'atx_closed', 'setext', 'underlined'\n  #   - :code_block_style    - 'backticks', 'tildes', 'indented'\n  #   - :escape_asterisks    - Boolean\n  #   - :escape_underscores  - Boolean\n  #   - :escape_misc         - Boolean\n  #   - :escape_ascii        - Boolean\n  #   - :strip_newlines      - Boolean\n  #   - :keep_inline_images_in - Array of tag names\n  #   - :strip_tags          - Array of tag names to strip\n  #   - :preserve_tags       - Array of tag names to preserve verbatim\n  #   (and more, matching ConversionOptions fields)\n  # @return [String] The converted Markdown content.\n  def self.convert(html, options = {}, visitor = nil)\n    # The Rust FFI expects options as a JSON string; serialise the hash here\n    # rather than constructing a ConversionOptions object, which the generated\n    # FFI layer cannot coerce back to String (see issue #334).\n    opts_json = options.nil? || options.empty? ? nil : options.to_json\n    result = HtmlToMarkdownRs.convert(html, opts_json, visitor)\n    result.content || ''\n  end\nend\n"
  },
  {
    "path": "packages/ruby/sig/html_to_markdown/cli.rbs",
    "content": "module HtmlToMarkdown\n  module CLI\n    # Module method (module_function creates both module and instance methods)\n    #\n    # Run the CLI with the given arguments\n    #\n    # @param argv Command-line arguments (defaults to ARGV)\n    # @param stdout Output stream for standard output\n    # @param stderr Output stream for standard error\n    # @return Exit code (0 for success, non-zero for failure)\n    def self.run: (\n      ?Array[String] argv,\n      ?stdout: IO,\n      ?stderr: IO\n    ) -> Integer\n\n    # Instance method version (created by module_function)\n    def run: (\n      ?Array[String] argv,\n      ?stdout: IO,\n      ?stderr: IO\n    ) -> Integer\n  end\nend\n"
  },
  {
    "path": "packages/ruby/sig/html_to_markdown/cli_proxy.rbs",
    "content": "module HtmlToMarkdown\n  module CLIProxy\n    # Base error class\n    class Error < StandardError\n    end\n\n    # Error when CLI binary is not found\n    class MissingBinaryError < Error\n    end\n\n    # Error when CLI execution fails\n    class CLIExecutionError < Error\n      attr_reader stderr: String\n      attr_reader status: Integer?\n\n      def initialize: (String message, stderr: String, status: Integer?) -> void\n    end\n\n    # Module methods (module_function creates both module and instance methods)\n\n    # Execute CLI with given arguments\n    def self.call: (Array[String] argv) -> String\n\n    # Find the CLI binary in search paths\n    def self.find_cli_binary: () -> Pathname\n\n    # Get root path of the gem\n    def self.root_path: () -> Pathname\n\n    # Get lib path of the gem\n    def self.lib_path: () -> Pathname\n\n    # Get search paths for CLI binary\n    def self.search_paths: (String binary_name) -> Array[Pathname]\n\n    # Get error message for missing binary\n    def self.missing_binary_message: () -> String\n\n    # Instance method versions (created by module_function)\n\n    def call: (Array[String] argv) -> String\n    def find_cli_binary: () -> Pathname\n    def root_path: () -> Pathname\n    def lib_path: () -> Pathname\n    def search_paths: (String binary_name) -> Array[Pathname]\n    def missing_binary_message: () -> String\n  end\nend\n"
  },
  {
    "path": "packages/ruby/sig/open3.rbs",
    "content": "# Type signature for Open3 standard library\nmodule Open3\n  # Execute command and capture stdout, stderr, and status\n  #\n  # @param cmd Command to execute\n  # @param args Command arguments\n  # @return Array containing stdout (String), stderr (String), and status (Process::Status)\n  def self.capture3: (\n    String cmd,\n    *String args\n  ) -> [String, String, Process::Status]\nend\n"
  },
  {
    "path": "packages/ruby/sig/types.rbs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:a32dce65b375b22723e07b273bee24d2e4fdec585dac2b6bde184ba08e3c15f1\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\nmodule HtmlToMarkdown\n\n  VERSION: String\n\n  class DocumentMetadata\n    # Document-level metadata extracted from `<head>` and top-level elements.\n    #\n    # Contains all metadata typically used by search engines, social media platforms,\n    # and browsers for document indexing and presentation.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::DocumentMetadata;\n    # let doc = DocumentMetadata {\n    #     title: Some(\"My Article\".to_string()),\n    #     description: Some(\"A great article about Rust\".to_string()),\n    #     keywords: vec![\"rust\".to_string(), \"programming\".to_string()],\n    #     ..Default::default()\n    # };\n    #\n    # assert_eq!(doc.title, Some(\"My Article\".to_string()));\n    # ```\n\n    attr_accessor title: String\n    attr_accessor description: String\n    attr_accessor keywords: Array[String]\n    attr_accessor author: String\n    attr_accessor canonical_url: String\n    attr_accessor base_href: String\n    attr_accessor language: String\n    attr_accessor text_direction: TextDirection\n    attr_accessor open_graph: Hash[String, String]\n    attr_accessor twitter_card: Hash[String, String]\n    attr_accessor meta_tags: Hash[String, String]\n\n    def initialize: (?title: String, ?description: String, keywords: Array[String], ?author: String, ?canonical_url: String, ?base_href: String, ?language: String, ?text_direction: TextDirection, open_graph: Hash[String, String], twitter_card: Hash[String, String], meta_tags: Hash[String, String]) -> void\n  end\n\n  class HeaderMetadata\n    # Header element metadata with hierarchy tracking.\n    #\n    # Captures heading elements (h1-h6) with their text content, identifiers,\n    # and position in the document structure.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::HeaderMetadata;\n    # let header = HeaderMetadata {\n    #     level: 1,\n    #     text: \"Main Title\".to_string(),\n    #     id: Some(\"main-title\".to_string()),\n    #     depth: 0,\n    #     html_offset: 145,\n    # };\n    #\n    # assert_eq!(header.level, 1);\n    # assert!(header.is_valid());\n    # ```\n\n    attr_reader level: Integer\n    attr_reader text: String\n    attr_reader id: String\n    attr_reader depth: Integer\n    attr_reader html_offset: Integer\n\n    def initialize: (level: Integer, text: String, ?id: String, depth: Integer, html_offset: Integer) -> void\n    def is_valid: () -> bool\n  end\n\n  class LinkMetadata\n    # Hyperlink metadata with categorization and attributes.\n    #\n    # Represents `<a>` elements with parsed href values, text content, and link type classification.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::{LinkMetadata, LinkType};\n    # let link = LinkMetadata {\n    #     href: \"https://example.com\".to_string(),\n    #     text: \"Example\".to_string(),\n    #     title: Some(\"Visit Example\".to_string()),\n    #     link_type: LinkType::External,\n    #     rel: vec![\"nofollow\".to_string()],\n    #     attributes: Default::default(),\n    # };\n    #\n    # assert_eq!(link.link_type, LinkType::External);\n    # assert_eq!(link.text, \"Example\");\n    # ```\n\n    attr_reader href: String\n    attr_reader text: String\n    attr_reader title: String\n    attr_reader link_type: LinkType\n    attr_reader rel: Array[String]\n    attr_reader attributes: Hash[String, String]\n\n    def initialize: (href: String, text: String, ?title: String, link_type: LinkType, rel: Array[String], attributes: Hash[String, String]) -> void\n    def self.classify_link: (String href) -> LinkType\n  end\n\n  class ImageMetadata\n    # Image metadata with source and dimensions.\n    #\n    # Captures `<img>` elements and inline `<svg>` elements with metadata\n    # for image analysis and optimization.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::{ImageMetadata, ImageType};\n    # let img = ImageMetadata {\n    #     src: \"https://example.com/image.jpg\".to_string(),\n    #     alt: Some(\"An example image\".to_string()),\n    #     title: Some(\"Example\".to_string()),\n    #     dimensions: Some((800, 600)),\n    #     image_type: ImageType::External,\n    #     attributes: Default::default(),\n    # };\n    #\n    # assert_eq!(img.image_type, ImageType::External);\n    # ```\n\n    attr_reader src: String\n    attr_reader alt: String\n    attr_reader title: String\n    attr_reader dimensions: Array[Integer]\n    attr_reader image_type: ImageType\n    attr_reader attributes: Hash[String, String]\n\n    def initialize: (src: String, ?alt: String, ?title: String, ?dimensions: Array[Integer], image_type: ImageType, attributes: Hash[String, String]) -> void\n  end\n\n  class StructuredData\n    # Structured data block (JSON-LD, Microdata, or RDFa).\n    #\n    # Represents machine-readable structured data found in the document.\n    # JSON-LD blocks are collected as raw JSON strings for flexibility.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::{StructuredData, StructuredDataType};\n    # let schema = StructuredData {\n    #     data_type: StructuredDataType::JsonLd,\n    #     raw_json: r#\"{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}\"#.to_string(),\n    #     schema_type: Some(\"Article\".to_string()),\n    # };\n    #\n    # assert_eq!(schema.data_type, StructuredDataType::JsonLd);\n    # ```\n\n    attr_reader data_type: StructuredDataType\n    attr_reader raw_json: String\n    attr_reader schema_type: String\n\n    def initialize: (data_type: StructuredDataType, raw_json: String, ?schema_type: String) -> void\n  end\n\n  class HtmlMetadata\n    # Comprehensive metadata extraction result from HTML document.\n    #\n    # Contains all extracted metadata types in a single structure,\n    # suitable for serialization and transmission across language boundaries.\n    #\n    # # Examples\n    #\n    # ```\n    # # use html_to_markdown_rs::metadata::HtmlMetadata;\n    # let metadata = HtmlMetadata {\n    #     document: Default::default(),\n    #     headers: Vec::new(),\n    #     links: Vec::new(),\n    #     images: Vec::new(),\n    #     structured_data: Vec::new(),\n    # };\n    #\n    # assert!(metadata.headers.is_empty());\n    # ```\n\n    attr_accessor document: DocumentMetadata\n    attr_accessor headers: Array[HeaderMetadata]\n    attr_accessor links: Array[LinkMetadata]\n    attr_accessor images: Array[ImageMetadata]\n    attr_accessor structured_data: Array[StructuredData]\n\n    def initialize: (document: DocumentMetadata, headers: Array[HeaderMetadata], links: Array[LinkMetadata], images: Array[ImageMetadata], structured_data: Array[StructuredData]) -> void\n  end\n\n  class ConversionOptions\n    # Main conversion options for HTML to Markdown conversion.\n    #\n    # Use [`ConversionOptions::builder()`] to construct, or [`Default::default()`] for defaults.\n    #\n    # # Example\n    #\n    # ```text\n    # use html_to_markdown_rs::ConversionOptions;\n    #\n    # let options = ConversionOptions::builder()\n    #     .heading_style(HeadingStyle::Atx)\n    #     .wrap(true)\n    #     .wrap_width(100)\n    #     .build();\n    # ```\n\n    attr_accessor heading_style: HeadingStyle\n    attr_accessor list_indent_type: ListIndentType\n    attr_accessor list_indent_width: Integer\n    attr_accessor bullets: String\n    attr_accessor strong_em_symbol: String\n    attr_accessor escape_asterisks: bool\n    attr_accessor escape_underscores: bool\n    attr_accessor escape_misc: bool\n    attr_accessor escape_ascii: bool\n    attr_accessor code_language: String\n    attr_accessor autolinks: bool\n    attr_accessor default_title: bool\n    attr_accessor br_in_tables: bool\n    attr_accessor highlight_style: HighlightStyle\n    attr_accessor extract_metadata: bool\n    attr_accessor whitespace_mode: WhitespaceMode\n    attr_accessor strip_newlines: bool\n    attr_accessor wrap: bool\n    attr_accessor wrap_width: Integer\n    attr_accessor convert_as_inline: bool\n    attr_accessor sub_symbol: String\n    attr_accessor sup_symbol: String\n    attr_accessor newline_style: NewlineStyle\n    attr_accessor code_block_style: CodeBlockStyle\n    attr_accessor keep_inline_images_in: Array[String]\n    attr_accessor preprocessing: PreprocessingOptions\n    attr_accessor encoding: String\n    attr_accessor debug: bool\n    attr_accessor strip_tags: Array[String]\n    attr_accessor preserve_tags: Array[String]\n    attr_accessor skip_images: bool\n    attr_accessor link_style: LinkStyle\n    attr_accessor output_format: OutputFormat\n    attr_accessor include_document_structure: bool\n    attr_accessor extract_images: bool\n    attr_accessor max_image_size: Integer\n    attr_accessor capture_svg: bool\n    attr_accessor infer_dimensions: bool\n    attr_accessor max_depth: Integer\n    attr_accessor exclude_selectors: Array[String]\n    attr_accessor visitor: VisitorHandle\n\n    def initialize: (heading_style: HeadingStyle, list_indent_type: ListIndentType, list_indent_width: Integer, bullets: String, strong_em_symbol: String, escape_asterisks: bool, escape_underscores: bool, escape_misc: bool, escape_ascii: bool, code_language: String, autolinks: bool, default_title: bool, br_in_tables: bool, highlight_style: HighlightStyle, extract_metadata: bool, whitespace_mode: WhitespaceMode, strip_newlines: bool, wrap: bool, wrap_width: Integer, convert_as_inline: bool, sub_symbol: String, sup_symbol: String, newline_style: NewlineStyle, code_block_style: CodeBlockStyle, keep_inline_images_in: Array[String], preprocessing: PreprocessingOptions, encoding: String, debug: bool, strip_tags: Array[String], preserve_tags: Array[String], skip_images: bool, link_style: LinkStyle, output_format: OutputFormat, include_document_structure: bool, extract_images: bool, max_image_size: Integer, capture_svg: bool, infer_dimensions: bool, ?max_depth: Integer, exclude_selectors: Array[String], ?visitor: VisitorHandle) -> void\n    def apply_update: (ConversionOptionsUpdate update) -> void\n    def self.default: () -> ConversionOptions\n    def self.builder: () -> ConversionOptionsBuilder\n    def self.from_update: (ConversionOptionsUpdate update) -> ConversionOptions\n    def self.from: (ConversionOptionsUpdate update) -> ConversionOptions\n  end\n\n  class ConversionOptionsBuilder\n    # Builder for [`ConversionOptions`].\n    #\n    # All fields start with default values. Call `.build()` to produce the final options.\n\n    def strip_tags: (Array[String] tags) -> ConversionOptionsBuilder\n    def preserve_tags: (Array[String] tags) -> ConversionOptionsBuilder\n    def keep_inline_images_in: (Array[String] tags) -> ConversionOptionsBuilder\n    def exclude_selectors: (Array[String] selectors) -> ConversionOptionsBuilder\n    def visitor: (?VisitorHandle visitor) -> ConversionOptionsBuilder\n    def preprocessing: (PreprocessingOptions preprocessing) -> ConversionOptionsBuilder\n    def build: () -> ConversionOptions\n  end\n\n  class ConversionOptionsUpdate\n    # Partial update for `ConversionOptions`.\n    #\n    # Uses `Option<T>` fields for selective updates. Bindings use this to construct\n    # options from language-native types. Prefer [`ConversionOptionsBuilder`] for Rust code.\n\n    attr_accessor heading_style: HeadingStyle\n    attr_accessor list_indent_type: ListIndentType\n    attr_accessor list_indent_width: Integer\n    attr_accessor bullets: String\n    attr_accessor strong_em_symbol: String\n    attr_accessor escape_asterisks: bool\n    attr_accessor escape_underscores: bool\n    attr_accessor escape_misc: bool\n    attr_accessor escape_ascii: bool\n    attr_accessor code_language: String\n    attr_accessor autolinks: bool\n    attr_accessor default_title: bool\n    attr_accessor br_in_tables: bool\n    attr_accessor highlight_style: HighlightStyle\n    attr_accessor extract_metadata: bool\n    attr_accessor whitespace_mode: WhitespaceMode\n    attr_accessor strip_newlines: bool\n    attr_accessor wrap: bool\n    attr_accessor wrap_width: Integer\n    attr_accessor convert_as_inline: bool\n    attr_accessor sub_symbol: String\n    attr_accessor sup_symbol: String\n    attr_accessor newline_style: NewlineStyle\n    attr_accessor code_block_style: CodeBlockStyle\n    attr_accessor keep_inline_images_in: Array[String]\n    attr_accessor preprocessing: PreprocessingOptionsUpdate\n    attr_accessor encoding: String\n    attr_accessor debug: bool\n    attr_accessor strip_tags: Array[String]\n    attr_accessor preserve_tags: Array[String]\n    attr_accessor skip_images: bool\n    attr_accessor link_style: LinkStyle\n    attr_accessor output_format: OutputFormat\n    attr_accessor include_document_structure: bool\n    attr_accessor extract_images: bool\n    attr_accessor max_image_size: Integer\n    attr_accessor capture_svg: bool\n    attr_accessor infer_dimensions: bool\n    attr_accessor max_depth: Integer?\n    attr_accessor exclude_selectors: Array[String]\n    attr_accessor visitor: VisitorHandle\n\n    def initialize: (?heading_style: HeadingStyle, ?list_indent_type: ListIndentType, ?list_indent_width: Integer, ?bullets: String, ?strong_em_symbol: String, ?escape_asterisks: bool, ?escape_underscores: bool, ?escape_misc: bool, ?escape_ascii: bool, ?code_language: String, ?autolinks: bool, ?default_title: bool, ?br_in_tables: bool, ?highlight_style: HighlightStyle, ?extract_metadata: bool, ?whitespace_mode: WhitespaceMode, ?strip_newlines: bool, ?wrap: bool, ?wrap_width: Integer, ?convert_as_inline: bool, ?sub_symbol: String, ?sup_symbol: String, ?newline_style: NewlineStyle, ?code_block_style: CodeBlockStyle, ?keep_inline_images_in: Array[String], ?preprocessing: PreprocessingOptionsUpdate, ?encoding: String, ?debug: bool, ?strip_tags: Array[String], ?preserve_tags: Array[String], ?skip_images: bool, ?link_style: LinkStyle, ?output_format: OutputFormat, ?include_document_structure: bool, ?extract_images: bool, ?max_image_size: Integer, ?capture_svg: bool, ?infer_dimensions: bool, ?max_depth: Integer?, ?exclude_selectors: Array[String], ?visitor: VisitorHandle) -> void\n  end\n\n  class PreprocessingOptions\n    # HTML preprocessing options for document cleanup before conversion.\n\n    attr_accessor enabled: bool\n    attr_accessor preset: PreprocessingPreset\n    attr_accessor remove_navigation: bool\n    attr_accessor remove_forms: bool\n\n    def initialize: (enabled: bool, preset: PreprocessingPreset, remove_navigation: bool, remove_forms: bool) -> void\n    def apply_update: (PreprocessingOptionsUpdate update) -> void\n    def self.default: () -> PreprocessingOptions\n    def self.from_update: (PreprocessingOptionsUpdate update) -> PreprocessingOptions\n    def self.from: (PreprocessingOptionsUpdate update) -> PreprocessingOptions\n  end\n\n  class PreprocessingOptionsUpdate\n    # Partial update for `PreprocessingOptions`.\n    #\n    # This struct uses `Option<T>` to represent optional fields that can be selectively updated.\n    # Only specified fields (Some values) will override existing options; None values leave the\n    # corresponding fields unchanged when applied via [`PreprocessingOptions::apply_update`].\n\n    attr_accessor enabled: bool\n    attr_accessor preset: PreprocessingPreset\n    attr_accessor remove_navigation: bool\n    attr_accessor remove_forms: bool\n\n    def initialize: (?enabled: bool, ?preset: PreprocessingPreset, ?remove_navigation: bool, ?remove_forms: bool) -> void\n  end\n\n  class DocumentStructure\n    # A structured document tree representing the semantic content of an HTML document.\n    #\n    # Uses a flat node array with index-based parent/child references for efficient traversal.\n\n    attr_reader nodes: Array[DocumentNode]\n    attr_reader source_format: String\n\n    def initialize: (nodes: Array[DocumentNode], ?source_format: String) -> void\n  end\n\n  class DocumentNode\n    # A single node in the document tree.\n\n    attr_reader id: String\n    attr_reader content: NodeContent\n    attr_reader parent: Integer\n    attr_reader children: Array[Integer]\n    attr_reader annotations: Array[TextAnnotation]\n    attr_reader attributes: Hash[String, String]\n\n    def initialize: (id: String, content: NodeContent, ?parent: Integer, children: Array[Integer], annotations: Array[TextAnnotation], ?attributes: Hash[String, String]) -> void\n  end\n\n  class TextAnnotation\n    # An inline text annotation with byte-range offsets.\n    #\n    # Annotations describe formatting (bold, italic, etc.) and links within a node's text content.\n\n    attr_reader start: Integer\n    attr_reader end: Integer\n    attr_reader kind: AnnotationKind\n\n    def initialize: (start: Integer, end: Integer, kind: AnnotationKind) -> void\n  end\n\n  class ConversionResult\n    # The primary result of HTML conversion and extraction.\n    #\n    # Contains the converted text output, optional structured document tree,\n    # metadata, extracted tables, images, and processing warnings.\n    #\n    # # Example\n    #\n    # ```text\n    # use html_to_markdown_rs::{convert, ConversionOptions};\n    #\n    # let result = convert(\"<h1>Hello</h1><p>World</p>\", None)?;\n    # assert!(result.content.is_some());\n    # assert!(result.warnings.is_empty());\n    # ```\n\n    attr_accessor content: String\n    attr_accessor document: DocumentStructure\n    attr_accessor metadata: HtmlMetadata\n    attr_accessor tables: Array[TableData]\n    attr_accessor images: Array[String]\n    attr_accessor warnings: Array[ProcessingWarning]\n\n    def initialize: (?content: String, ?document: DocumentStructure, metadata: HtmlMetadata, tables: Array[TableData], images: Array[String], warnings: Array[ProcessingWarning]) -> void\n  end\n\n  class TableGrid\n    # A structured table grid with cell-level data including spans.\n\n    attr_accessor rows: Integer\n    attr_accessor cols: Integer\n    attr_accessor cells: Array[GridCell]\n\n    def initialize: (rows: Integer, cols: Integer, cells: Array[GridCell]) -> void\n  end\n\n  class GridCell\n    # A single cell in a table grid.\n\n    attr_reader content: String\n    attr_reader row: Integer\n    attr_reader col: Integer\n    attr_reader row_span: Integer\n    attr_reader col_span: Integer\n    attr_reader is_header: bool\n\n    def initialize: (content: String, row: Integer, col: Integer, row_span: Integer, col_span: Integer, is_header: bool) -> void\n  end\n\n  class TableData\n    # A top-level extracted table with both structured data and markdown representation.\n\n    attr_reader grid: TableGrid\n    attr_reader markdown: String\n\n    def initialize: (grid: TableGrid, markdown: String) -> void\n  end\n\n  class ProcessingWarning\n    # A non-fatal warning generated during HTML processing.\n\n    attr_reader message: String\n    attr_reader kind: WarningKind\n\n    def initialize: (message: String, kind: WarningKind) -> void\n  end\n\n  class VisitorHandle\n    # Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).\n    #\n    # This allows visitors to be passed around and shared while still being mutable.\n\n  end\n\n  class NodeContext\n    # Context information passed to all visitor methods.\n    #\n    # Provides comprehensive metadata about the current node being visited,\n    # including its type, attributes, position in the DOM tree, and parent context.\n\n    attr_reader node_type: NodeType\n    attr_reader tag_name: String\n    attr_reader attributes: Hash[String, String]\n    attr_reader depth: Integer\n    attr_reader index_in_parent: Integer\n    attr_reader parent_tag: String\n    attr_reader is_inline: bool\n\n    def initialize: (node_type: NodeType, tag_name: String, attributes: Hash[String, String], depth: Integer, index_in_parent: Integer, ?parent_tag: String, is_inline: bool) -> void\n  end\n\n  class TextDirection\n    # Text directionality of document content.\n    #\n    # Corresponds to the HTML `dir` attribute and `bdi` element directionality.\n\n    LeftToRight: Integer\n    RightToLeft: Integer\n    Auto: Integer\n  end\n\n  class LinkType\n    # Link classification based on href value and document context.\n    #\n    # Used to categorize links during extraction for filtering and analysis.\n\n    Anchor: Integer\n    Internal: Integer\n    External: Integer\n    Email: Integer\n    Phone: Integer\n    Other: Integer\n  end\n\n  class ImageType\n    # Image source classification for proper handling and processing.\n    #\n    # Determines whether an image is embedded (data URI), inline SVG, external, or relative.\n\n    DataUri: Integer\n    InlineSvg: Integer\n    External: Integer\n    Relative: Integer\n  end\n\n  class StructuredDataType\n    # Structured data format type.\n    #\n    # Identifies the schema/format used for structured data markup.\n\n    JsonLd: Integer\n    Microdata: Integer\n    RDFa: Integer\n  end\n\n  class PreprocessingPreset\n    # HTML preprocessing aggressiveness level.\n    #\n    # Controls the extent of cleanup performed before conversion. Higher levels remove more elements.\n\n    Minimal: Integer\n    Standard: Integer\n    Aggressive: Integer\n  end\n\n  class HeadingStyle\n    # Heading style options for Markdown output.\n    #\n    # Controls how headings (h1-h6) are rendered in the output Markdown.\n\n    Underlined: Integer\n    Atx: Integer\n    AtxClosed: Integer\n  end\n\n  class ListIndentType\n    # List indentation character type.\n    #\n    # Controls whether list items are indented with spaces or tabs.\n\n    Spaces: Integer\n    Tabs: Integer\n  end\n\n  class WhitespaceMode\n    # Whitespace handling strategy during conversion.\n    #\n    # Determines how sequences of whitespace characters (spaces, tabs, newlines) are processed.\n\n    Normalized: Integer\n    Strict: Integer\n  end\n\n  class NewlineStyle\n    # Line break syntax in Markdown output.\n    #\n    # Controls how soft line breaks (from `<br>` or line breaks in source) are rendered.\n\n    Spaces: Integer\n    Backslash: Integer\n  end\n\n  class CodeBlockStyle\n    # Code block fence style in Markdown output.\n    #\n    # Determines how code blocks (`<pre><code>`) are rendered in Markdown.\n\n    Indented: Integer\n    Backticks: Integer\n    Tildes: Integer\n  end\n\n  class HighlightStyle\n    # Highlight rendering style for `<mark>` elements.\n    #\n    # Controls how highlighted text is rendered in Markdown output.\n\n    DoubleEqual: Integer\n    Html: Integer\n    Bold: Integer\n    None: Integer\n  end\n\n  class LinkStyle\n    # Link rendering style in Markdown output.\n    #\n    # Controls whether links and images use inline `[text](url)` syntax or\n    # reference-style `[text][1]` syntax with definitions collected at the end.\n\n    Inline: Integer\n    Reference: Integer\n  end\n\n  class OutputFormat\n    # Output format for conversion.\n    #\n    # Specifies the target markup language format for the conversion output.\n\n    Markdown: Integer\n    Djot: Integer\n    Plain: Integer\n  end\n\n  class NodeContent\n    # The semantic content type of a document node.\n    #\n    # Uses internally tagged representation (`\"node_type\": \"heading\"`) for JSON serialization.\n\n    Heading: Integer\n    Paragraph: Integer\n    List: Integer\n    ListItem: Integer\n    Table: Integer\n    Image: Integer\n    Code: Integer\n    Quote: Integer\n    DefinitionList: Integer\n    DefinitionItem: Integer\n    RawBlock: Integer\n    MetadataBlock: Integer\n    Group: Integer\n  end\n\n  class AnnotationKind\n    # The type of an inline text annotation.\n    #\n    # Uses internally tagged representation (`\"annotation_type\": \"bold\"`) for JSON serialization.\n\n    Bold: Integer\n    Italic: Integer\n    Underline: Integer\n    Strikethrough: Integer\n    Code: Integer\n    Subscript: Integer\n    Superscript: Integer\n    Highlight: Integer\n    Link: Integer\n  end\n\n  class WarningKind\n    # Categories of processing warnings.\n\n    ImageExtractionFailed: Integer\n    EncodingFallback: Integer\n    TruncatedInput: Integer\n    MalformedHtml: Integer\n    SanitizationApplied: Integer\n    DepthLimitExceeded: Integer\n  end\n\n  class NodeType\n    # Node type enumeration covering all HTML element types.\n    #\n    # This enum categorizes all HTML elements that the converter recognizes,\n    # providing a coarse-grained classification for visitor dispatch.\n\n    Text: Integer\n    Element: Integer\n    Heading: Integer\n    Paragraph: Integer\n    Div: Integer\n    Blockquote: Integer\n    Pre: Integer\n    Hr: Integer\n    List: Integer\n    ListItem: Integer\n    DefinitionList: Integer\n    DefinitionTerm: Integer\n    DefinitionDescription: Integer\n    Table: Integer\n    TableRow: Integer\n    TableCell: Integer\n    TableHeader: Integer\n    TableBody: Integer\n    TableHead: Integer\n    TableFoot: Integer\n    Link: Integer\n    Image: Integer\n    Strong: Integer\n    Em: Integer\n    Code: Integer\n    Strikethrough: Integer\n    Underline: Integer\n    Subscript: Integer\n    Superscript: Integer\n    Mark: Integer\n    Small: Integer\n    Br: Integer\n    Span: Integer\n    Article: Integer\n    Section: Integer\n    Nav: Integer\n    Aside: Integer\n    Header: Integer\n    Footer: Integer\n    Main: Integer\n    Figure: Integer\n    Figcaption: Integer\n    Time: Integer\n    Details: Integer\n    Summary: Integer\n    Form: Integer\n    Input: Integer\n    Select: Integer\n    Option: Integer\n    Button: Integer\n    Textarea: Integer\n    Label: Integer\n    Fieldset: Integer\n    Legend: Integer\n    Audio: Integer\n    Video: Integer\n    Picture: Integer\n    Source: Integer\n    Iframe: Integer\n    Svg: Integer\n    Canvas: Integer\n    Ruby: Integer\n    Rt: Integer\n    Rp: Integer\n    Abbr: Integer\n    Kbd: Integer\n    Samp: Integer\n    Var: Integer\n    Cite: Integer\n    Q: Integer\n    Del: Integer\n    Ins: Integer\n    Data: Integer\n    Meter: Integer\n    Progress: Integer\n    Output: Integer\n    Template: Integer\n    Slot: Integer\n    Html: Integer\n    Head: Integer\n    Body: Integer\n    Title: Integer\n    Meta: Integer\n    LinkTag: Integer\n    Style: Integer\n    Script: Integer\n    Base: Integer\n    Custom: Integer\n  end\n\n  class VisitResult\n    # Result of a visitor callback.\n    #\n    # Allows visitors to control the conversion flow by either proceeding\n    # with default behavior, providing custom output, skipping elements,\n    # preserving HTML, or signaling errors.\n\n    Continue: Integer\n    Custom: Integer\n    Skip: Integer\n    PreserveHtml: Integer\n    Error: Integer\n  end\n\n  def self.convert: (String html, ?ConversionOptions options) -> ConversionResult\n\nend\n"
  },
  {
    "path": "packages/ruby/spec/html_to_markdown_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'spec_helper'\n\nRSpec.describe HtmlToMarkdown do\n  describe '.convert' do\n    it 'converts html without options' do\n      result = described_class.convert('<h1>Hello</h1>')\n      expect(result).to be_a(String)\n      expect(result).to include('Hello')\n    end\n\n    it 'converts html with an empty options hash' do\n      result = described_class.convert('<h1>Hello</h1>', {})\n      expect(result).to be_a(String)\n      expect(result).to include('Hello')\n    end\n\n    # Regression test for issue #334: alef regeneration reverts Rust FFI options fix.\n    # Passing any options previously raised TypeError because the Ruby wrapper was\n    # constructing a ConversionOptions object, which the Rust FFI (expecting a JSON\n    # string) cannot coerce — resulting in a TypeError on every call with options.\n    it 'does not raise TypeError when options are passed (regression #334)' do\n      expect { described_class.convert('<h1>Hello</h1>', heading_style: 'atx') }.not_to raise_error\n    end\n\n    it 'applies heading_style option' do\n      result = described_class.convert('<h1>Hello</h1>', heading_style: 'atx')\n      expect(result).to match(/^# Hello/)\n    end\n\n    it 'applies escape_asterisks option' do\n      result = described_class.convert('<p>1 * 2</p>', escape_asterisks: true)\n      expect(result).to include('\\*')\n    end\n\n    it 'applies strip_tags option' do\n      result = described_class.convert('<p>Hello</p><script>alert(1)</script>', strip_tags: ['script'])\n      expect(result).not_to include('alert')\n    end\n\n    it 'returns a String' do\n      expect(described_class.convert('<p>test</p>')).to be_a(String)\n      expect(described_class.convert('<p>test</p>', strip_newlines: true)).to be_a(String)\n    end\n  end\nend\n"
  },
  {
    "path": "packages/ruby/spec/spec_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'html_to_markdown'\n"
  },
  {
    "path": "packages/typescript/.npmignore",
    "content": "src\ntests\nvitest.config.ts\n"
  },
  {
    "path": "packages/typescript/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.4.0-rc.25\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nHigh-performance HTML to Markdown converter for Node.js and Bun with full TypeScript support.\nThis package wraps native `@kreuzberg/html-to-markdown` bindings and provides a type-safe API.\n\n## Installation\n\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\nRequires Node.js 18+ or Bun. Native bindings provide superior performance.\n\n**npm:**\n\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\n**pnpm:**\n\n```bash\npnpm add @kreuzberg/html-to-markdown\n```\n\n**yarn:**\n\n```bash\nyarn add @kreuzberg/html-to-markdown\n```\n\n**bun:**\n\n```bash\nbun add @kreuzberg/html-to-markdown\n```\n\nAlternatively, use the WebAssembly version for browser/edge environments:\n\n```bash\nnpm install @kreuzberg/html-to-markdown-wasm\n```\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document            | Size  | Latency | Throughput |\n| ------------------- | ----- | ------- | ---------- |\n| Lists (Timeline)    | 129KB | 0.58ms  | 222 MB/s   |\n| Tables (Countries)  | 360KB | 1.89ms  | 190 MB/s   |\n| Mixed (Python wiki) | 656KB | 4.21ms  | 156 MB/s   |\n\n## Quick Start\n\nBasic conversion:\n\n```typescript\nimport { convert } from \"@kreuzberg/html-to-markdown\";\n\nconst result = convert(\"<h1>Hello World</h1>\");\nconst markdown: string = result.content;\nconsole.log(markdown); // # Hello World\n```\n\nWith conversion options:\n\n```typescript\nimport { convert, ConversionOptions } from \"@kreuzberg/html-to-markdown\";\n\nconst options: ConversionOptions = {\n  headingStyle: \"atx\",\n  listIndentWidth: 2,\n  wrap: true,\n};\n\nconst result = convert(\"<h1>Title</h1><p>Content</p>\", options);\nconst markdown = result.content;\n```\n\n## API Reference\n\n### Core Function\n\n**`convert(html: string, options?: ConversionOptions, visitor?: Visitor): ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```typescript\nimport { convert, ConversionOptions } from \"@kreuzberg/html-to-markdown\";\n\nconst result = convert(html);\nconst markdown = result.content; // Converted Markdown string\nconst metadata = result.metadata; // Metadata (when extractMetadata: true)\nconst tables = result.tables; // Structured table data (when extractTables: true)\nconst document = result.document; // Document-level info\nconst images = result.images; // Extracted images\nconst warnings = result.warnings; // Any conversion warnings\n```\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element        | Markdown   | Djot       |\n| -------------- | ---------- | ---------- |\n| Strong         | `**text**` | `*text*`   |\n| Emphasis       | `*text*`   | `_text_`   |\n| Strikethrough  | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A        | `{+text+}` |\n| Highlighted    | N/A        | `{=text=}` |\n| Subscript      | N/A        | `~text~`   |\n| Superscript    | N/A        | `^text^`   |\n\n### Example Usage\n\n```typescript\nimport { convert, ConversionOptions } from \"@kreuzberg/html-to-markdown\";\n\nconst html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nconst markdown = convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nconst djot = convert(html, { outputFormat: \"djot\" });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n```typescript\nimport { convert } from \"@kreuzberg/html-to-markdown\";\n\nconst html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nconst plain = convert(html, { outputFormat: \"plain\" });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n```typescript\nimport { convert } from \"@kreuzberg/html-to-markdown\";\n\nconst html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\nconst result = convert(html, { extractMetadata: true });\n\nconsole.log(result.content); // Converted Markdown\nconsole.log(result.metadata?.document?.title); // Document title\nconsole.log(result.metadata?.headers); // All h1-h6 elements\nconsole.log(result.metadata?.links); // All hyperlinks\nconsole.log(result.metadata?.images); // All images with alt text\nconsole.log(result.metadata?.structuredData); // JSON-LD, Microdata, RDFa\n```\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n```typescript\nimport {\n  convert,\n  type Visitor,\n  type NodeContext,\n  type VisitResult,\n} from \"@kreuzberg/html-to-markdown\";\n\nconst visitor: Visitor = {\n  visitLink(ctx: NodeContext, href: string, text: string, title?: string): VisitResult {\n    // Rewrite CDN URLs\n    if (href.startsWith(\"https://old-cdn.com\")) {\n      href = href.replace(\"https://old-cdn.com\", \"https://new-cdn.com\");\n    }\n    return { type: \"custom\", output: `[${text}](${href})` };\n  },\n\n  visitImage(ctx: NodeContext, src: string, alt?: string, title?: string): VisitResult {\n    // Skip tracking pixels\n    if (src.includes(\"tracking\")) {\n      return { type: \"skip\" };\n    }\n    return { type: \"continue\" };\n  },\n};\n\nconst html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\nconst result = convert(html, {}, visitor);\nconst markdown = result.content;\n```\n\n## Examples\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **npm:** [npmjs.com/@kreuzberg/html-to-markdown](https://www.npmjs.com/package/@kreuzberg/html-to-markdown)\n- **WASM:** [npmjs.com/@kreuzberg/html-to-markdown-wasm](https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "packages/typescript/index.d.ts",
    "content": "/* auto-generated by NAPI-RS */\n/* eslint-disable */\nexport declare class JsConversionOptionsBuilder {\n  stripTags(tags: Array<string>): JsConversionOptionsBuilder;\n  preserveTags(tags: Array<string>): JsConversionOptionsBuilder;\n  keepInlineImagesIn(tags: Array<string>): JsConversionOptionsBuilder;\n  preprocessing(preprocessing: JsPreprocessingOptions): JsConversionOptionsBuilder;\n  build(): JsConversionOptions;\n}\n\nexport declare function convert(\n  html: string,\n  options?: JsConversionOptions | undefined | null,\n): JsConversionResult;\n\nexport interface JsAnnotationKind {\n  annotation_type: string;\n  url?: string;\n  title?: string;\n}\n\nexport declare enum JsCodeBlockStyle {\n  Indented = \"Indented\",\n  Backticks = \"Backticks\",\n  Tildes = \"Tildes\",\n}\n\nexport interface JsConversionOptions {\n  headingStyle?: JsHeadingStyle;\n  listIndentType?: JsListIndentType;\n  listIndentWidth?: number;\n  bullets?: string;\n  strongEmSymbol?: string;\n  escapeAsterisks?: boolean;\n  escapeUnderscores?: boolean;\n  escapeMisc?: boolean;\n  escapeAscii?: boolean;\n  codeLanguage?: string;\n  autolinks?: boolean;\n  defaultTitle?: boolean;\n  brInTables?: boolean;\n  highlightStyle?: JsHighlightStyle;\n  extractMetadata?: boolean;\n  whitespaceMode?: JsWhitespaceMode;\n  stripNewlines?: boolean;\n  wrap?: boolean;\n  wrapWidth?: number;\n  convertAsInline?: boolean;\n  subSymbol?: string;\n  supSymbol?: string;\n  newlineStyle?: JsNewlineStyle;\n  codeBlockStyle?: JsCodeBlockStyle;\n  keepInlineImagesIn?: Array<string>;\n  preprocessing?: JsPreprocessingOptions;\n  encoding?: string;\n  debug?: boolean;\n  stripTags?: Array<string>;\n  preserveTags?: Array<string>;\n  skipImages?: boolean;\n  linkStyle?: JsLinkStyle;\n  outputFormat?: JsOutputFormat;\n  includeDocumentStructure?: boolean;\n  extractImages?: boolean;\n  maxImageSize?: number;\n  captureSvg?: boolean;\n  inferDimensions?: boolean;\n}\n\nexport interface JsConversionOptionsUpdate {\n  headingStyle?: JsHeadingStyle;\n  listIndentType?: JsListIndentType;\n  listIndentWidth?: number;\n  bullets?: string;\n  strongEmSymbol?: string;\n  escapeAsterisks?: boolean;\n  escapeUnderscores?: boolean;\n  escapeMisc?: boolean;\n  escapeAscii?: boolean;\n  codeLanguage?: string;\n  autolinks?: boolean;\n  defaultTitle?: boolean;\n  brInTables?: boolean;\n  highlightStyle?: JsHighlightStyle;\n  extractMetadata?: boolean;\n  whitespaceMode?: JsWhitespaceMode;\n  stripNewlines?: boolean;\n  wrap?: boolean;\n  wrapWidth?: number;\n  convertAsInline?: boolean;\n  subSymbol?: string;\n  supSymbol?: string;\n  newlineStyle?: JsNewlineStyle;\n  codeBlockStyle?: JsCodeBlockStyle;\n  keepInlineImagesIn?: Array<string>;\n  preprocessing?: JsPreprocessingOptionsUpdate;\n  encoding?: string;\n  debug?: boolean;\n  stripTags?: Array<string>;\n  preserveTags?: Array<string>;\n  skipImages?: boolean;\n  linkStyle?: JsLinkStyle;\n  outputFormat?: JsOutputFormat;\n  includeDocumentStructure?: boolean;\n  extractImages?: boolean;\n  maxImageSize?: number;\n  captureSvg?: boolean;\n  inferDimensions?: boolean;\n}\n\nexport interface JsConversionResult {\n  content?: string;\n  document?: JsDocumentStructure;\n  metadata?: JsHtmlMetadata;\n  tables?: Array<JsTableData>;\n  images?: Array<string>;\n  warnings?: Array<JsProcessingWarning>;\n}\n\nexport interface JsDocumentMetadata {\n  title?: string;\n  description?: string;\n  keywords?: Array<string>;\n  author?: string;\n  canonicalUrl?: string;\n  baseHref?: string;\n  language?: string;\n  textDirection?: JsTextDirection;\n  openGraph?: Record<string, string>;\n  twitterCard?: Record<string, string>;\n  metaTags?: Record<string, string>;\n}\n\nexport interface JsDocumentNode {\n  id: string;\n  content: JsNodeContent;\n  parent?: number;\n  children: Array<number>;\n  annotations: Array<JsTextAnnotation>;\n  attributes?: Record<string, string>;\n}\n\nexport interface JsDocumentStructure {\n  nodes: Array<JsDocumentNode>;\n  sourceFormat?: string;\n}\n\nexport interface JsGridCell {\n  content: string;\n  row: number;\n  col: number;\n  rowSpan: number;\n  colSpan: number;\n  isHeader: boolean;\n}\n\nexport interface JsHeaderMetadata {\n  level: number;\n  text: string;\n  id?: string;\n  depth: number;\n  htmlOffset: number;\n}\n\nexport declare enum JsHeadingStyle {\n  Underlined = \"Underlined\",\n  Atx = \"Atx\",\n  AtxClosed = \"AtxClosed\",\n}\n\nexport declare enum JsHighlightStyle {\n  DoubleEqual = \"DoubleEqual\",\n  Html = \"Html\",\n  Bold = \"Bold\",\n  None = \"None\",\n}\n\nexport interface JsHtmlMetadata {\n  document?: JsDocumentMetadata;\n  headers?: Array<JsHeaderMetadata>;\n  links?: Array<JsLinkMetadata>;\n  images?: Array<JsImageMetadata>;\n  structuredData?: Array<JsStructuredData>;\n}\n\nexport interface JsImageMetadata {\n  src: string;\n  alt?: string;\n  title?: string;\n  dimensions?: string;\n  imageType: JsImageType;\n  attributes: Record<string, string>;\n}\n\nexport declare enum JsImageType {\n  DataUri = \"data_uri\",\n  InlineSvg = \"inline_svg\",\n  External = \"external\",\n  Relative = \"relative\",\n}\n\nexport interface JsLinkMetadata {\n  href: string;\n  text: string;\n  title?: string;\n  linkType: JsLinkType;\n  rel: Array<string>;\n  attributes: Record<string, string>;\n}\n\nexport declare enum JsLinkStyle {\n  Inline = \"Inline\",\n  Reference = \"Reference\",\n}\n\nexport declare enum JsLinkType {\n  Anchor = \"anchor\",\n  Internal = \"internal\",\n  External = \"external\",\n  Email = \"email\",\n  Phone = \"phone\",\n  Other = \"other\",\n}\n\nexport declare enum JsListIndentType {\n  Spaces = \"Spaces\",\n  Tabs = \"Tabs\",\n}\n\nexport interface JsMetadataConfig {\n  extractDocument?: boolean;\n  extractHeaders?: boolean;\n  extractLinks?: boolean;\n  extractImages?: boolean;\n  extractStructuredData?: boolean;\n  maxStructuredDataSize?: number;\n}\n\nexport interface JsMetadataConfigUpdate {\n  extractDocument?: boolean;\n  extractHeaders?: boolean;\n  extractLinks?: boolean;\n  extractImages?: boolean;\n  extractStructuredData?: boolean;\n  maxStructuredDataSize?: number;\n}\n\nexport declare enum JsNewlineStyle {\n  Spaces = \"Spaces\",\n  Backslash = \"Backslash\",\n}\n\nexport interface JsNodeContent {\n  node_type: string;\n  level?: number;\n  text?: string;\n  ordered?: boolean;\n  grid?: JsTableGrid;\n  description?: string;\n  src?: string;\n  imageIndex?: number;\n  language?: string;\n  term?: string;\n  definition?: string;\n  format?: string;\n  content?: string;\n  entries?: Array<Array<string>>;\n  label?: string;\n  headingLevel?: number;\n  headingText?: string;\n}\n\nexport declare enum JsOutputFormat {\n  Markdown = \"Markdown\",\n  Djot = \"Djot\",\n  Plain = \"Plain\",\n}\n\nexport interface JsPreprocessingOptions {\n  enabled?: boolean;\n  preset?: JsPreprocessingPreset;\n  removeNavigation?: boolean;\n  removeForms?: boolean;\n}\n\nexport interface JsPreprocessingOptionsUpdate {\n  enabled?: boolean;\n  preset?: JsPreprocessingPreset;\n  removeNavigation?: boolean;\n  removeForms?: boolean;\n}\n\nexport declare enum JsPreprocessingPreset {\n  Minimal = \"Minimal\",\n  Standard = \"Standard\",\n  Aggressive = \"Aggressive\",\n}\n\nexport interface JsProcessingWarning {\n  message: string;\n  kind: JsWarningKind;\n}\n\nexport interface JsStructuredData {\n  dataType: JsStructuredDataType;\n  rawJson: string;\n  schemaType?: string;\n}\n\nexport declare enum JsStructuredDataType {\n  JsonLd = \"json_ld\",\n  Microdata = \"microdata\",\n  RDFa = \"rd_fa\",\n}\n\nexport interface JsTableData {\n  grid: JsTableGrid;\n  markdown: string;\n}\n\nexport interface JsTableGrid {\n  rows?: number;\n  cols?: number;\n  cells?: Array<JsGridCell>;\n}\n\nexport interface JsTextAnnotation {\n  start: number;\n  end: number;\n  kind: JsAnnotationKind;\n}\n\nexport declare enum JsTextDirection {\n  LeftToRight = \"LeftToRight\",\n  RightToLeft = \"RightToLeft\",\n  Auto = \"Auto\",\n}\n\nexport declare enum JsWarningKind {\n  ImageExtractionFailed = \"image_extraction_failed\",\n  EncodingFallback = \"encoding_fallback\",\n  TruncatedInput = \"truncated_input\",\n  MalformedHtml = \"malformed_html\",\n  SanitizationApplied = \"sanitization_applied\",\n}\n\nexport declare enum JsWhitespaceMode {\n  Normalized = \"Normalized\",\n  Strict = \"Strict\",\n}\n"
  },
  {
    "path": "packages/typescript/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown\",\n  \"version\": \"3.4.0-rc.25\",\n  \"description\": \"High-performance HTML to Markdown converter\",\n  \"keywords\": [\n    \"converter\",\n    \"html\",\n    \"markdown\"\n  ],\n  \"homepage\": \"https://kreuzberg.dev\",\n  \"bugs\": \"https://github.com/kreuzberg-dev/html-to-markdown/issues\",\n  \"license\": \"MIT\",\n  \"author\": \"Kreuzberg Team\",\n  \"repository\": \"https://github.com/kreuzberg-dev/html-to-markdown\",\n  \"files\": [\n    \"dist\",\n    \"README.md\"\n  ],\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --project tsconfig.json\",\n    \"lint\": \"echo 'TypeScript type-checking requires native build; use task node:lint:check instead'\",\n    \"test\": \"node -e \\\"console.log('Add test command')\\\"\"\n  },\n  \"dependencies\": {\n    \"@kreuzberg/html-to-markdown-node\": \"3.4.0-rc.18\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.0\"\n  },\n  \"engines\": {\n    \"node\": \">= 18\"\n  }\n}\n"
  },
  {
    "path": "packages/typescript/src/helpers.ts",
    "content": "import { readFile } from \"node:fs/promises\";\nimport type { JsConversionOptions, JsConversionResult } from \"@kreuzberg/html-to-markdown-node\";\nimport { convert } from \"@kreuzberg/html-to-markdown-node\";\n\n/**\n * Convert an HTML file to Markdown.\n */\nexport async function convertFile(\n  path: string,\n  options?: Partial<JsConversionOptions>,\n): Promise<JsConversionResult> {\n  const html = await readFile(path, \"utf-8\");\n  return convert(html, options as JsConversionOptions | undefined);\n}\n\n/**\n * Convert a readable stream of HTML to Markdown.\n */\nexport async function convertStream(\n  stream: NodeJS.ReadableStream,\n  options?: Partial<JsConversionOptions>,\n): Promise<JsConversionResult> {\n  const chunks: Buffer[] = [];\n  for await (const chunk of stream) {\n    chunks.push(Buffer.from(chunk));\n  }\n  const html = Buffer.concat(chunks).toString(\"utf-8\");\n  return convert(html, options as JsConversionOptions | undefined);\n}\n"
  },
  {
    "path": "packages/typescript/src/index.ts",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\n// alef:hash:11eb3113cb4f4379bfc242d5fe72aa8d2c3321beb53008a533fb1001f0a89918\n\nexport {\n  convert,\n} from '@kreuzberg/html-to-markdown-node';\n\nexport type {\n  JsAnnotationKind,\n  JsCodeBlockStyle,\n  JsConversionOptions,\n  JsConversionOptionsBuilder,\n  JsConversionOptionsUpdate,\n  JsConversionResult,\n  JsDocumentMetadata,\n  JsDocumentNode,\n  JsDocumentStructure,\n  JsGridCell,\n  JsHeaderMetadata,\n  JsHeadingStyle,\n  JsHighlightStyle,\n  JsHtmlMetadata,\n  JsImageMetadata,\n  JsImageType,\n  JsLinkMetadata,\n  JsLinkStyle,\n  JsLinkType,\n  JsListIndentType,\n  JsNewlineStyle,\n  JsNodeContent,\n  JsNodeContext,\n  JsNodeType,\n  JsOutputFormat,\n  JsPreprocessingOptions,\n  JsPreprocessingOptionsUpdate,\n  JsPreprocessingPreset,\n  JsProcessingWarning,\n  JsStructuredData,\n  JsStructuredDataType,\n  JsTableData,\n  JsTableGrid,\n  JsTextAnnotation,\n  JsTextDirection,\n  JsVisitResult,\n  JsVisitorHandle,\n  JsWarningKind,\n  JsWhitespaceMode,\n} from '@kreuzberg/html-to-markdown-node';\nexport * from './helpers';\n"
  },
  {
    "path": "packages/typescript/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"emitDeclarationOnly\": false,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"types\": [\"node\"],\n    \"verbatimModuleSyntax\": false,\n    \"isolatedModules\": false,\n    \"paths\": {\n      \"@kreuzberg/html-to-markdown-node\": [\"../../crates/html-to-markdown-node/index.d.ts\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"dist\", \"tests\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/wasm/src/helpers.ts",
    "content": "import {\n  convert as wasmConvert,\n  WasmConversionOptions,\n  type WasmConversionResult,\n  type WasmHeadingStyle,\n  type WasmListIndentType,\n  type WasmWhitespaceMode,\n  type WasmNewlineStyle,\n  type WasmCodeBlockStyle,\n  type WasmHighlightStyle,\n  type WasmLinkStyle,\n  type WasmOutputFormat,\n  type WasmPreprocessingOptions,\n} from \"./wasm\";\n\n/**\n * Plain-object options for HTML to Markdown conversion.\n * All fields are optional — omitted fields use sensible defaults.\n */\nexport interface ConversionOptions {\n  headingStyle?: WasmHeadingStyle | null;\n  listIndentType?: WasmListIndentType | null;\n  listIndentWidth?: number | null;\n  bullets?: string | null;\n  strongEmSymbol?: string | null;\n  escapeAsterisks?: boolean | null;\n  escapeUnderscores?: boolean | null;\n  escapeMisc?: boolean | null;\n  escapeAscii?: boolean | null;\n  codeLanguage?: string | null;\n  autolinks?: boolean | null;\n  defaultTitle?: boolean | null;\n  brInTables?: boolean | null;\n  highlightStyle?: WasmHighlightStyle | null;\n  extractMetadata?: boolean | null;\n  whitespaceMode?: WasmWhitespaceMode | null;\n  stripNewlines?: boolean | null;\n  wrap?: boolean | null;\n  wrapWidth?: number | null;\n  convertAsInline?: boolean | null;\n  subSymbol?: string | null;\n  supSymbol?: string | null;\n  newlineStyle?: WasmNewlineStyle | null;\n  codeBlockStyle?: WasmCodeBlockStyle | null;\n  keepInlineImagesIn?: string[] | null;\n  preprocessing?: WasmPreprocessingOptions | null;\n  encoding?: string | null;\n  debug?: boolean | null;\n  stripTags?: string[] | null;\n  preserveTags?: string[] | null;\n  skipImages?: boolean | null;\n  linkStyle?: WasmLinkStyle | null;\n  outputFormat?: WasmOutputFormat | null;\n  includeDocumentStructure?: boolean | null;\n  extractImages?: boolean | null;\n  maxImageSize?: bigint | null;\n  captureSvg?: boolean | null;\n  inferDimensions?: boolean | null;\n  maxDepth?: number | null;\n}\n\n/**\n * Convert HTML to Markdown.\n *\n * Accepts an optional plain-object options parameter — no need to construct\n * a `WasmConversionOptions` class instance.\n *\n * @example\n * ```ts\n * import { convert } from \"@kreuzberg/html-to-markdown-wasm\";\n *\n * const result = convert(\"<h1>Hello</h1>\", { headingStyle: \"Atx\" });\n * console.log(result.content);\n * ```\n */\nexport function convert(html: string, options?: ConversionOptions | null): WasmConversionResult {\n  if (!options) {\n    return wasmConvert(html);\n  }\n\n  const wasmOpts = new WasmConversionOptions(\n    options.headingStyle,\n    options.listIndentType,\n    options.listIndentWidth,\n    options.bullets,\n    options.strongEmSymbol,\n    options.escapeAsterisks,\n    options.escapeUnderscores,\n    options.escapeMisc,\n    options.escapeAscii,\n    options.codeLanguage,\n    options.autolinks,\n    options.defaultTitle,\n    options.brInTables,\n    options.highlightStyle,\n    options.extractMetadata,\n    options.whitespaceMode,\n    options.stripNewlines,\n    options.wrap,\n    options.wrapWidth,\n    options.convertAsInline,\n    options.subSymbol,\n    options.supSymbol,\n    options.newlineStyle,\n    options.codeBlockStyle,\n    options.keepInlineImagesIn,\n    options.preprocessing,\n    options.encoding,\n    options.debug,\n    options.stripTags,\n    options.preserveTags,\n    options.skipImages,\n    options.linkStyle,\n    options.outputFormat,\n    options.includeDocumentStructure,\n    options.extractImages,\n    options.maxImageSize,\n    options.captureSvg,\n    options.inferDimensions,\n    options.maxDepth,\n  );\n\n  try {\n    return wasmConvert(html, wasmOpts);\n  } finally {\n    wasmOpts.free();\n  }\n}\n"
  },
  {
    "path": "packages/wasm/src/index.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:9670c07dd94c79045c79217c3d62a267b63bcbfaf98a9a0b5b186180723e3bfb\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\nexport {\n  ConversionError,\n  WasmAnnotationKind,\n  WasmCodeBlockStyle,\n  WasmConversionOptions,\n  WasmConversionOptionsBuilder,\n  WasmConversionOptionsUpdate,\n  WasmConversionResult,\n  WasmDocumentMetadata,\n  WasmDocumentNode,\n  WasmDocumentStructure,\n  WasmGridCell,\n  WasmHeaderMetadata,\n  WasmHeadingStyle,\n  WasmHighlightStyle,\n  WasmHtmlMetadata,\n  WasmImageMetadata,\n  WasmImageType,\n  WasmLinkMetadata,\n  WasmLinkStyle,\n  WasmLinkType,\n  WasmListIndentType,\n  WasmNewlineStyle,\n  WasmNodeContent,\n  WasmNodeContext,\n  WasmNodeType,\n  WasmOutputFormat,\n  WasmPreprocessingOptions,\n  WasmPreprocessingOptionsUpdate,\n  WasmPreprocessingPreset,\n  WasmProcessingWarning,\n  WasmStructuredData,\n  WasmStructuredDataType,\n  WasmTableData,\n  WasmTableGrid,\n  WasmTextAnnotation,\n  WasmTextDirection,\n  WasmVisitResult,\n  WasmVisitorHandle,\n  WasmWarningKind,\n  WasmWhitespaceMode,\n  convert,\n} from \"./wasm\";\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - crates/html-to-markdown-node\n  - crates/html-to-markdown-wasm\n\nignoredBuiltDependencies:\n  - esbuild\n  - wasm-pack\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [ \"setuptools>=69\" ]\n\n[project]\nname = \"html-to-markdown-workspace\"\nversion = \"0.0.0\"\ndescription = \"Workspace-only metadata for tooling.\"\nrequires-python = \">=3.10\"\nclassifiers = [\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n]\n\n[dependency-groups]\ndev = [\n  \"covdefaults>=2.3\",\n  \"jinja2>=3.1.6\",\n  \"mypy>=1.19.1\",\n  \"pre-commit>=4.5.1\",\n  \"pytest>=9.0.2\",\n  \"pytest-asyncio>=1.3\",\n  \"pytest-benchmark>=5.2.3\",\n  \"pytest-cov>=7\",\n  \"pytest-mock>=3.15.1\",\n  \"pyyaml>=6.0.3\",\n  \"ruff>=0.14.10\",\n  \"uv-bump\",\n]\ndoc = [\n  \"zensical\",\n]\n\n[tool.setuptools]\npackages = []\n\n[tool.uv]\ndefault-groups = [ \"dev\" ]\nsources.uv-bump = { git = \"https://github.com/Goldziher/uv-bump\" }\nworkspace.members = [ \"packages/python\" ]\ncache-keys = [\n  { file = \"pyproject.toml\" },\n  { file = \"Cargo.toml\" },\n  { file = \"Cargo.lock\" },\n  { file = \"packages/python/pyproject.toml\" },\n  { file = \"crates/**/Cargo.toml\" },\n  { file = \"crates/**/*.rs\" },\n]\n\n[tool.ruff]\ntarget-version = \"py310\"\nline-length = 120\nsrc = [ \"packages/python/html_to_markdown\", \"packages/python/tests\", \"scripts\" ]\nformat.docstring-code-line-length = 120\nformat.docstring-code-format = true\nlint.select = [ \"ALL\" ]\nlint.ignore = [\n  \"ANN401\",\n  \"ASYNC109\",\n  \"ASYNC110\",\n  \"BLE001\",\n  \"COM812\",\n  \"D100\",\n  \"D101\",\n  \"D102\",\n  \"D103\",\n  \"D104\",\n  \"D107\",\n  \"D205\",\n  \"D301\",\n  \"E501\",\n  \"EM\",\n  \"FBT\",\n  \"FIX\",      # Allow todo and fixme comments\n  \"ISC001\",\n  \"PD011\",\n  \"PGH003\",\n  \"PLR0913\",\n  \"PLR2004\",\n  \"S104\",\n  \"TD\",       # Allow todo and fixme comments\n  \"TRY\",\n]\nlint.per-file-ignores.\"e2e/python/**/*.*\" = [ \"D\", \"INP001\", \"PT018\", \"S\" ]\nlint.per-file-ignores.\"packages/python/tests/**/*.*\" = [ \"ARG\", \"D\", \"PD\", \"PT006\", \"PT013\", \"S\" ]\nlint.per-file-ignores.\"packages/python/tests/benchmark_*_test.py\" = [ \"T201\" ]\nlint.per-file-ignores.\"scripts/*.py\" = [ \"INP001\", \"S607\", \"T201\" ]\nlint.isort.known-first-party = [ \"html_to_markdown\", \"tests\" ]\nlint.pydocstyle.convention = \"pep257\"\n\n[tool.mypy]\npython_version = \"3.10\"\nstrict = true\nimplicit_reexport = false\nshow_error_codes = true\nwarn_return_any = true\nwarn_unused_configs = true\ndisable_error_code = [ \"import-untyped\", \"import-not-found\" ]\nnamespace_packages = true\nfiles = [ \"packages/python\" ]\nexplicit_package_bases = true\nmypy_path = [ \"packages/python\" ]\nexclude = [\n  \"^crates/\",\n  \"^e2e/\",\n  \"^test_apps/\",\n  \"^packages/typescript/\",\n  \"^packages/ruby/\",\n  \"^scripts/\",\n  \"^node_modules/\",\n  \"^dist/\",\n  \"^target/\",\n]\n\n[[tool.mypy.overrides]]\nmodule = [\n  \"tests.*\",\n  \"html_to_markdown.tests.*\",\n]\ndisallow_any_generics = false\ndisallow_untyped_decorators = false\n\n[tool.pytest]\nini_options.asyncio_mode = \"auto\"\nini_options.asyncio_default_fixture_loop_scope = \"function\"\nini_options.testpaths = [ \"packages/python/tests\" ]\nini_options.filterwarnings = [\n  \"error\",\n  \"ignore::pytest.PytestConfigWarning\",\n  \"ignore::pytest.PytestUnraisableExceptionWarning\",\n  \"ignore::ResourceWarning\",\n]\n\n[tool.coverage]\nrun.omit = [ \"packages/python/tests/*\" ]\nrun.source = [ \"packages/python/html_to_markdown\" ]\nreport.exclude_lines = [ \"if TYPE_CHECKING:\" ]\nreport.fail_under = 0\nreport.show_missing = true\n"
  },
  {
    "path": "readme_templates/language_package.md",
    "content": "# html-to-markdown\n\n{% include 'partials/_badges.md' %}\n\n{{ description }}\n\n## Installation\n\n{% include 'partials/_installation.md' %}\n\n{% if migration_guide %}\n{{ migration_guide }}\n{% endif %}\n\n{% if performance %}\n\n## Performance Snapshot\n\n{{ performance | render_performance_table(name) }}\n\n{% endif %}\n\n## Quick Start\n\n{% include 'partials/_quick_start.md' %}\n\n## API Reference\n\n{% include 'partials/_api_reference.md' %}\n\n{% include 'partials/_djot_output.md' %}\n\n{% include 'partials/_plain_text_output.md' %}\n\n{% if features.metadata_extraction %}\n\n## Metadata Extraction\n\n{% include 'partials/_metadata_extraction.md' %}\n{% endif %}\n\n{% if features.visitor_pattern %}\n\n## Visitor Pattern\n\n{% include 'partials/_visitor_pattern.md' %}\n{% endif %}\n\n## Examples\n\n\n{% include 'partials/_footer.md' %}\n"
  },
  {
    "path": "readme_templates/partials/_api_reference.md",
    "content": "### Core Function\n\n{% if language == 'python' %}\n**`convert(html: str, options?: ConversionOptions, visitor?: object) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nresult = convert(html)\nmarkdown = result.content           # Converted Markdown string\nmetadata = result.metadata          # Metadata (when extract_metadata=True)\ntables   = result.tables            # Structured table data\ndocument = result.document          # Document-level info\nimages   = result.images            # Extracted images\nwarnings = result.warnings          # Any conversion warnings\n```\n\n{% elif language == 'typescript' %}\n**`convert(html: string, options?: ConversionOptions, visitor?: Visitor): ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst result = convert(html);\nconst markdown  = result.content;    // Converted Markdown string\nconst metadata  = result.metadata;   // Metadata (when extractMetadata: true)\nconst tables    = result.tables;     // Structured table data (when extractTables: true)\nconst document  = result.document;   // Document-level info\nconst images    = result.images;     // Extracted images\nconst warnings  = result.warnings;   // Any conversion warnings\n```\n\n{% elif language == 'ruby' %}\n**`convert(html, options: nil, visitor: nil) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` hash with all results in a single call.\n\n```ruby\nrequire 'html_to_markdown'\n\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]       # Converted Markdown string\nmetadata = result[:metadata]      # Metadata (when extract_metadata: true)\ntables   = result[:tables]        # Structured table data (when extract_tables: true)\ndocument = result[:document]      # Document-level info\nimages   = result[:images]        # Extracted images\nwarnings = result[:warnings]      # Any conversion warnings\n```\n\n{% elif language == 'php' %}\n**`Converter::convert(string $html, ?ConversionOptions $options = null, ?VisitorInterface $visitor = null): array`**\n\nConverts HTML to Markdown. Returns an array `ConversionResult` with all results in a single call.\n\n```php\n<?php\nuse HtmlToMarkdown\\Service\\Converter;\n\n$result  = Converter::create()->convert($html);\n$markdown = $result['content'];    // Converted Markdown string\n$metadata = $result['metadata'];   // Metadata (when extractMetadata: true)\n$tables   = $result['tables'];     // Structured table data (when extractTables: true)\n$document = $result['document'];   // Document-level info\n$images   = $result['images'];     // Extracted images\n$warnings = $result['warnings'];   // Any conversion warnings\n```\n\n{% elif language == 'go' %}\n**`Convert(html string, options ...ConversionOptions) (ConversionResult, error)`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` struct with all results in a single call.\n\n```go\nresult, err := htmltomarkdown.Convert(html)\nmarkdown  := result.Content    // *string — converted Markdown\nmetadata  := result.Metadata   // *Metadata — when ExtractMetadata: true\ntables    := result.Tables     // []TableData — when ExtractTables: true\n```\n\n{% elif language == 'java' %}\n**`HtmlToMarkdown.convert(String html) : ConversionResult`**\n**`HtmlToMarkdown.convert(String html, ConversionOptions options) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```java\nConversionResult result = HtmlToMarkdown.convert(html);\nString   markdown = result.content();   // Converted Markdown string\nMetadata metadata = result.metadata();  // null unless extractMetadata(true)\nList<?>  tables   = result.tables();    // empty unless extractTables(true)\n```\n\n{% elif language == 'csharp' %}\n**`HtmlToMarkdownConverter.Convert(string html, ConversionOptions? options = null) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```csharp\nvar result   = HtmlToMarkdownConverter.Convert(html);\nvar markdown = result.Content;    // Converted Markdown string\nvar metadata = result.Metadata;   // null unless ExtractMetadata = true\nvar tables   = result.Tables;     // empty unless ExtractTables = true\n```\n\n{% elif language == 'elixir' %}\n**`HtmlToMarkdown.convert(html, options \\\\ nil) :: {:ok, ConversionResult.t()} | {:error, term()}`**\n\nConverts HTML to Markdown. Returns `{:ok, result}` where result is a struct with all results in a single call.\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(html)\nresult.content    # Converted Markdown string\nresult.metadata   # Metadata map (when extract_metadata: true)\nresult.tables     # Table data list (when extract_tables: true)\nresult.document   # Document-level info\nresult.images     # Extracted images\nresult.warnings   # Any conversion warnings\n```\n\n{% elif language == 'r' %}\n**`convert(html, options = NULL)`**\n\nConverts HTML to Markdown. Returns a named list `ConversionResult` with all results in a single call.\n\n```r\nresult   <- convert(html)\nmarkdown <- result$content    # Converted Markdown string\nmetadata <- result$metadata   # Metadata (when extract_metadata = TRUE)\ntables   <- result$tables     # Table data (when extract_tables = TRUE)\n```\n\n{% else %}\n{% endif %}\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n"
  },
  {
    "path": "readme_templates/partials/_badges.md",
    "content": "<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v{{ version }}\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"{{ banner_url }}\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"{{ discord_url }}\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n"
  },
  {
    "path": "readme_templates/partials/_djot_output.md",
    "content": "## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element | Markdown | Djot |\n|---------|----------|------|\n| Strong | `**text**` | `*text*` |\n| Emphasis | `*text*` | `_text_` |\n| Strikethrough | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A | `{+text+}` |\n| Highlighted | N/A | `{=text=}` |\n| Subscript | N/A | `~text~` |\n| Superscript | N/A | `^text^` |\n\n### Example Usage\n\n{% if language == 'python' %}\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = convert(html, ConversionOptions(output_format=\"djot\"))\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'typescript' %}\n\n```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nconst markdown = convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nconst djot = convert(html, { outputFormat: 'djot' });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'ruby' %}\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = HtmlToMarkdown.convert(html, output_format: 'djot')\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'php' %}\n\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\n$markdown = Converter::convert($html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\n$djot = Converter::convert($html, new ConversionOptions(outputFormat: 'djot'));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'go' %}\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n// Default Markdown output\nmarkdown, _ := htmltomarkdown.Convert(html)\n// Result: \"This is **bold** and *italic* text.\"\n\n// Note: Djot output format configuration is not yet supported in Go bindings\n```\n\n{% elif language == 'java' %}\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nString markdown = HtmlToMarkdown.convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nString djot = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.DJOT));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'csharp' %}\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nvar markdown = Converter.Convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nvar djot = Converter.Convert(html, new ConversionOptions { OutputFormat = \"djot\" });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% elif language == 'elixir' %}\n\n```elixir\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\n{:ok, markdown} = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\n{:ok, djot} = HtmlToMarkdown.convert(html, %{output_format: \"djot\"})\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\n{% endif %}\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n"
  },
  {
    "path": "readme_templates/partials/_footer.md",
    "content": "## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n{% if language == 'python' %}\n- **PyPI:** [pypi.org/project/html-to-markdown](https://pypi.org/project/html-to-markdown/)\n{% elif language == 'typescript' or language == 'node' %}\n- **npm:** [npmjs.com/@kreuzberg/html-to-markdown](https://www.npmjs.com/package/@kreuzberg/html-to-markdown)\n- **WASM:** [npmjs.com/@kreuzberg/html-to-markdown-wasm](https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm)\n{% elif language == 'ruby' %}\n- **RubyGems:** [rubygems.org/gems/html-to-markdown](https://rubygems.org/gems/html-to-markdown)\n{% elif language == 'php' %}\n- **Packagist:** [packagist.org/packages/kreuzberg-dev/html-to-markdown](https://packagist.org/packages/kreuzberg-dev/html-to-markdown)\n{% elif language == 'go' %}\n- **Go Packages:** [pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2](https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2)\n{% elif language == 'java' %}\n- **Maven Central:** [central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown](https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown)\n{% elif language == 'csharp' %}\n- **NuGet:** [nuget.org/packages/KreuzbergDev.HtmlToMarkdown](https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/)\n{% elif language == 'elixir' %}\n- **Hex.pm:** [hex.pm/packages/html_to_markdown](https://hex.pm/packages/html_to_markdown)\n{% endif %}\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "readme_templates/partials/_installation.md",
    "content": "```bash\n{{ install_command }}\n```\n\n{% if language == 'python' %}\nRequires Python 3.10+. Wheels are published for Linux, macOS, and Windows on PyPI.\n{% elif language == 'typescript' %}\n\nRequires Node.js 18+ or Bun. Native bindings provide superior performance.\n\n**npm:**\n\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\n**pnpm:**\n\n```bash\npnpm add @kreuzberg/html-to-markdown\n```\n\n**yarn:**\n\n```bash\nyarn add @kreuzberg/html-to-markdown\n```\n\n**bun:**\n\n```bash\nbun add @kreuzberg/html-to-markdown\n```\n\nAlternatively, use the WebAssembly version for browser/edge environments:\n\n```bash\nnpm install @kreuzberg/html-to-markdown-wasm\n```\n\n{% elif language == 'ruby' %}\n\nRequires Ruby 3.2+ with Magnus native extension bindings. Published for Linux, macOS.\n{% elif language == 'php' %}\n\nRequires PHP 8.2+. Install the native extension via PIE:\n\n```bash\npie install kreuzberg-dev/html-to-markdown\n```\n\nOr use Composer (requires ext-html_to_markdown):\n\n```bash\ncomposer require kreuzberg-dev/html-to-markdown\n```\n\n{% elif language == 'go' %}\n\nRequires Go 1.25+. After installing the package, run `go generate` to automatically download the platform-specific FFI library:\n\n```bash\ngo generate\n```\n\nThis downloads the native library from GitHub releases and generates the necessary CGO flags. The library is cached in `~/.html-to-markdown/` for subsequent builds.\n\nAlternatively, you can manually set `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables if you prefer to manage the FFI library yourself.\n{% elif language == 'java' %}\n\nRequires Java 25+ with Panama FFI support.\n\n**Maven:**\n\n```xml\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>{{ version }}</version>\n</dependency>\n```\n\n**Gradle (Kotlin DSL):**\n\n```kotlin\nimplementation(\"dev.kreuzberg:html-to-markdown:{{ version }}\")\n```\n\n{% elif language == 'csharp' %}\n\nRequires .NET 8.0+ SDK.\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n\n{% elif language == 'elixir' %}\n\nRequires Elixir 1.19+ and OTP 28. Add to your `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:html_to_markdown, \"~> {{ version }}\"}\n  ]\nend\n```\n\n{% elif language == 'r' %}\n\nRequires R 4.3+ and a Rust toolchain (cargo, rustc).\n\n```r\ninstall.packages(\"htmltomarkdown\")\n```\n\nOr install the development version from GitHub:\n\n```r\ndevtools::install_github(\"kreuzberg-dev/html-to-markdown\", subdir = \"packages/r\")\n```\n\n{% endif %}\n"
  },
  {
    "path": "readme_templates/partials/_metadata_extraction.md",
    "content": "The metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n{% if language == 'python' %}\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = convert(html, ConversionOptions(extract_metadata=True))\n\nprint(result.content)                          # Converted Markdown\nprint(result.metadata.document.title)          # Document title\nprint(result.metadata.headers)                 # All h1-h6 elements\nprint(result.metadata.links)                   # All hyperlinks\nprint(result.metadata.images)                  # All images with alt text\nprint(result.metadata.structured_data)         # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'typescript' %}\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\nconst result = convert(html, { extractMetadata: true });\n\nconsole.log(result.content);                      // Converted Markdown\nconsole.log(result.metadata?.document?.title);    // Document title\nconsole.log(result.metadata?.headers);            // All h1-h6 elements\nconsole.log(result.metadata?.links);              // All hyperlinks\nconsole.log(result.metadata?.images);             // All images with alt text\nconsole.log(result.metadata?.structuredData);     // JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'ruby' %}\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = HtmlToMarkdown.convert(html, extract_metadata: true)\n\nputs result[:content]                             # Converted Markdown\nputs result[:metadata][:document][:title]         # Document title\nputs result[:metadata][:headers]                  # All h1-h6 elements\nputs result[:metadata][:links]                    # All hyperlinks\nputs result[:metadata][:images]                   # All images with alt text\nputs result[:metadata][:structured_data]          # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'php' %}\n\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(extractMetadata: true)\n);\n\necho $result['content'];                          // Converted Markdown\necho $result['metadata']->document->title;        // Document title\nprint_r($result['metadata']->headers);            // All h1-h6 elements\nprint_r($result['metadata']->links);              // All hyperlinks\nprint_r($result['metadata']->images);             // All images with alt text\nprint_r($result['metadata']->structured_data);    // JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'go' %}\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    html := `<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">`\n    result, err := htmltomarkdown.Convert(html, htmltomarkdown.ConversionOptions{ExtractMetadata: true})\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    fmt.Println(*result.Content)                  // Converted Markdown\n    fmt.Println(result.Metadata.Title)            // Document title\n    fmt.Println(result.Metadata.Headers)          // All h1-h6 elements\n    fmt.Println(result.Metadata.Links)            // All hyperlinks\n    fmt.Println(result.Metadata.Images)           // All images with alt text\n}\n```\n\n{% elif language == 'java' %}\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class Main {\n    public static void main(String[] args) {\n        String html = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\";\n        ConversionOptions options = ConversionOptions.builder()\n            .extractMetadata(true)\n            .build();\n        ConversionResult result = HtmlToMarkdown.convert(html, options);\n\n        System.out.println(result.content());                          // Converted Markdown\n        System.out.println(result.metadata().getDocument().getTitle()); // Document title\n        System.out.println(result.metadata().getHeaders());            // All h1-h6 elements\n        System.out.println(result.metadata().getLinks());              // All hyperlinks\n        System.out.println(result.metadata().getImages());             // All images with alt text\n    }\n}\n```\n\n{% elif language == 'csharp' %}\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\";\nvar result = HtmlToMarkdownConverter.Convert(html, new ConversionOptions { ExtractMetadata = true });\n\nConsole.WriteLine(result.Content);                                    // Converted Markdown\nConsole.WriteLine(result.Metadata?.Document?.Title);                  // Document title\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Headers ?? [])); // All h1-h6 elements\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Links ?? []));   // All hyperlinks\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Images ?? []));  // All images with alt text\n```\n\n{% elif language == 'elixir' %}\n\n```elixir\nhtml = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\"\nopts = %HtmlToMarkdown.Options{extract_metadata: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nIO.puts(result.content)                           # Converted Markdown\nIO.inspect(result.metadata[\"document\"][\"title\"])  # Document title\nIO.inspect(result.metadata[\"headers\"])            # All h1-h6 elements\nIO.inspect(result.metadata[\"links\"])              # All hyperlinks\nIO.inspect(result.metadata[\"images\"])             # All images with alt text\nIO.inspect(result.metadata[\"structured_data\"])    # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'r' %}\n\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nopts <- conversion_options(extract_metadata = TRUE)\nresult <- convert(html, opts)\n\ncat(result$content)                    # Converted Markdown\nresult$metadata$document$title        # Document title\nresult$metadata$headers                # All h1-h6 elements\nresult$metadata$links                  # All hyperlinks\nresult$metadata$images                 # All images with alt text\n```\n\n{% endif %}\n"
  },
  {
    "path": "readme_templates/partials/_plain_text_output.md",
    "content": "## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n{% if language == 'python' %}\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = convert(html, ConversionOptions(output_format=\"plain\"))\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'typescript' %}\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nconst plain = convert(html, { outputFormat: 'plain' });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'ruby' %}\n\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = HtmlToMarkdown.convert(html, output_format: 'plain')\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'php' %}\n\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n$plain = Converter::convert($html, new ConversionOptions(outputFormat: 'plain'));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'go' %}\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain, _ := htmltomarkdown.Convert(html, htmltomarkdown.WithOutputFormat(\"plain\"))\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'java' %}\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nString plain = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.PLAIN));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'csharp' %}\n\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nvar plain = Converter.Convert(html, new ConversionOptions { OutputFormat = \"plain\" });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'elixir' %}\n\n```elixir\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n{:ok, plain} = HtmlToMarkdown.convert(html, %{output_format: \"plain\"})\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% elif language == 'r' %}\n\n```r\nhtml <- \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nresult <- convert(html, options = list(output_format = \"plain\"))\nplain <- result$content\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n{% endif %}\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n"
  },
  {
    "path": "readme_templates/partials/_quick_start.md",
    "content": "Basic conversion:\n\n{{ snippets.basic_usage | include_snippet(language) }}\n\n{% if snippets.with_options %}\nWith conversion options:\n\n{{ snippets.with_options | include_snippet(language) }}\n{% endif %}\n"
  },
  {
    "path": "readme_templates/partials/_visitor_pattern.md",
    "content": "The visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n{% if language == 'python' %}\n\n```python\nfrom html_to_markdown import convert\n\nclass MyVisitor:\n    def visit_link(self, ctx, href, text, title):\n        # Rewrite CDN URLs\n        if href.startswith(\"https://old-cdn.com\"):\n            href = href.replace(\"https://old-cdn.com\", \"https://new-cdn.com\")\n        return {\"type\": \"custom\", \"output\": f\"[{text}]({href})\"}\n\n    def visit_image(self, ctx, src, alt, title):\n        # Skip tracking pixels\n        if \"tracking\" in src:\n            return {\"type\": \"skip\"}\n        return {\"type\": \"continue\"}\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = convert(html, visitor=MyVisitor())\nmarkdown = result.content\n```\n\n{% elif language == 'typescript' %}\n\n```typescript\nimport { convert, type Visitor, type NodeContext, type VisitResult } from '@kreuzberg/html-to-markdown';\n\nconst visitor: Visitor = {\n  visitLink(ctx: NodeContext, href: string, text: string, title?: string): VisitResult {\n    // Rewrite CDN URLs\n    if (href.startsWith('https://old-cdn.com')) {\n      href = href.replace('https://old-cdn.com', 'https://new-cdn.com');\n    }\n    return { type: 'custom', output: `[${text}](${href})` };\n  },\n\n  visitImage(ctx: NodeContext, src: string, alt?: string, title?: string): VisitResult {\n    // Skip tracking pixels\n    if (src.includes('tracking')) {\n      return { type: 'skip' };\n    }\n    return { type: 'continue' };\n  },\n};\n\nconst html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\nconst result = convert(html, {}, visitor);\nconst markdown = result.content;\n```\n\n{% elif language == 'ruby' %}\n\n```ruby\nrequire 'html_to_markdown'\n\nclass MyVisitor\n  def visit_link(ctx, href, text, title = nil)\n    # Rewrite CDN URLs\n    if href.start_with?('https://old-cdn.com')\n      href = href.sub('https://old-cdn.com', 'https://new-cdn.com')\n    end\n    { type: :custom, output: \"[#{text}](#{href})\" }\n  end\n\n  def visit_image(ctx, src, alt = nil, title = nil)\n    # Skip tracking pixels\n    src.include?('tracking') ? { type: :skip } : { type: :continue }\n  end\nend\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = HtmlToMarkdown.convert(html, visitor: MyVisitor.new)\nmarkdown = result[:content]\n```\n\n{% elif language == 'php' %}\n\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\nuse HtmlToMarkdown\\Visitor\\AbstractVisitor;\nuse HtmlToMarkdown\\Visitor\\NodeContext;\nuse HtmlToMarkdown\\Visitor\\VisitResult;\n\nclass MyVisitor extends AbstractVisitor\n{\n    public function visitLink(NodeContext $ctx, string $href, string $text, ?string $title): array\n    {\n        // Rewrite CDN URLs\n        if (str_starts_with($href, 'https://old-cdn.com')) {\n            $href = str_replace('https://old-cdn.com', 'https://new-cdn.com', $href);\n        }\n        return VisitResult::custom(\"[{$text}]({$href})\");\n    }\n\n    public function visitImage(NodeContext $ctx, string $src, ?string $alt, ?string $title): array\n    {\n        // Skip tracking pixels\n        return str_contains($src, 'tracking') ? VisitResult::skip() : VisitResult::continue();\n    }\n}\n\n$html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(visitor: new MyVisitor())\n);\n$markdown = $result['content'];\n```\n\n{% elif language == 'elixir' %}\n\n```elixir\ndefmodule MyVisitor do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_ctx, href, text, _title) do\n    # Rewrite CDN URLs\n    href = if String.starts_with?(href, \"https://old-cdn.com\") do\n      String.replace(href, \"https://old-cdn.com\", \"https://new-cdn.com\")\n    else\n      href\n    end\n    {:custom, \"[#{text}](#{href})\"}\n  end\n\n  @impl true\n  def handle_image(_ctx, src, _alt, _title) do\n    # Skip tracking pixels\n    if String.contains?(src, \"tracking\"), do: :skip, else: :continue\n  end\nend\n\nhtml = \"<a href=\\\"https://old-cdn.com/file.pdf\\\">Download</a>\"\nopts = %HtmlToMarkdown.Options{visitor: MyVisitor}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\nresult.content\n```\n\n{% elif language == 'r' %}\n\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nopts <- conversion_options(extract_metadata = FALSE)\nresult <- convert(html, opts)\ncat(result$content)\n```\n\n{% endif %}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.95\"\ncomponents = [\"rust-src\", \"rustfmt\", \"clippy\"]\ntargets = [\"wasm32-unknown-unknown\"]\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2024\"\nmax_width = 120\nhard_tabs = false\ntab_spaces = 4\nnewline_style = \"Auto\"\nuse_small_heuristics = \"Default\"\nreorder_imports = true\nreorder_modules = true\nremove_nested_parens = true\nmatch_block_trailing_comma = false\nuse_field_init_shorthand = true\nuse_try_shorthand = true\n"
  },
  {
    "path": "scripts/build-demo.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\necho \"🔨 Building WASM package...\"\ncd crates/html-to-markdown-wasm\nwasm-pack build --target web --out-dir dist-web\n\necho \"📦 Copying files to docs/...\"\ncd ../..\ncp crates/html-to-markdown-wasm/dist-web/html_to_markdown_wasm.js docs/\ncp crates/html-to-markdown-wasm/dist-web/html_to_markdown_wasm_bg.wasm docs/\n\necho \"✅ Demo updated successfully!\"\necho \"\"\necho \"To test locally, run:\"\necho \"  cd docs && python3 -m http.server 8000\"\necho \"\"\necho \"Then open http://localhost:8000 in your browser\"\n"
  },
  {
    "path": "scripts/ci/elixir/install-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nmix deps.get\n"
  },
  {
    "path": "scripts/ci/elixir/install-hex-rebar.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nmix local.hex --force\nmix local.rebar --force\n"
  },
  {
    "path": "scripts/ci/elixir/run-credo.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nenv MIX_ENV=dev mix credo --strict\n"
  },
  {
    "path": "scripts/ci/elixir/run-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nrepo_root=\"$(cd \"${script_dir}/../../..\" && pwd)\"\n\n\"${repo_root}/scripts/publish/elixir/stage-rust-core.sh\"\n\nenv MIX_ENV=test mix test\n"
  },
  {
    "path": "scripts/ci/go/detect-go-modules.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nmapfile -t all_modules < <(git ls-files -- '*/go.mod' | sort)\nif [[ \"${#all_modules[@]}\" -eq 0 ]]; then\n  echo \"modules=[]\" >>\"$GITHUB_OUTPUT\"\n  exit 0\nfi\n\n# Filter out test-only modules (directories with only _test.go files and no\n# non-test .go files). golangci-lint cannot analyse these and would fail with\n# \"no go files to analyze\".\nmodules=()\nfor mod in \"${all_modules[@]}\"; do\n  dir=$(dirname \"$mod\")\n  if git ls-files -- \"${dir}/\"'*.go' | grep -qv '_test\\.go$'; then\n    modules+=(\"$mod\")\n  fi\ndone\n\nif [[ \"${#modules[@]}\" -eq 0 ]]; then\n  echo \"modules=[]\" >>\"$GITHUB_OUTPUT\"\n  exit 0\nfi\n\njson=$(printf '%s\\n' \"${modules[@]}\" | sed 's|/go\\.mod$||' | jq -R -s -c 'split(\"\\n\") | map(select(length > 0))')\necho \"modules=$json\" >>\"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": "scripts/ci/go/install-golangci-lint.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\necho \"$HOME/go/bin\" >>\"$GITHUB_PATH\"\ngo install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@\"${GOLANGCI_LINT_VERSION}\"\n"
  },
  {
    "path": "scripts/ci/go/run-golangci-lint.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../../..\" && pwd)\"\n\ngolangci-lint run --config \"${ROOT_DIR}/.golangci.yml\" ./...\n"
  },
  {
    "path": "scripts/ci/node/test-napi-cargo.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd crates/html-to-markdown-node >/dev/null\ncargo test\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/node/test-napi.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd crates/html-to-markdown-node >/dev/null\npnpm test\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/node/test-typescript.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/typescript >/dev/null\npnpm test\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/php/run-php-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ -n \"${EXTENSION_PATH:-}\" ]]; then\n  ini_file=\"$(mktemp)\"\n  echo \"extension=${EXTENSION_PATH}\" >\"${ini_file}\"\n  export PHPRC=\"${ini_file}\"\nfi\n\npushd packages/php >/dev/null\ncomposer run test\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/php/run-phpstan.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/php >/dev/null\ncomposer run phpstan\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/php/set-php-config.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\necho \"PHP_CONFIG=$(command -v php-config)\" >>\"$GITHUB_ENV\"\n"
  },
  {
    "path": "scripts/ci/python/build-cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nbinary_name=\"html-to-markdown\"\nif [[ \"${RUNNER_OS:-}\" == \"Windows\" ]]; then\n  binary_name=\"html-to-markdown.exe\"\nfi\nrm -f \"target/release/${binary_name}\"\ncargo build --release --package html-to-markdown-cli\n"
  },
  {
    "path": "scripts/ci/python/run-pytest.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nPYTEST_ADDOPTS=\"${PYTEST_ADDOPTS:--vv --maxfail=1 --durations=25}\"\n\nIFS=\" \" read -r -a pytest_addopts <<<\"$PYTEST_ADDOPTS\"\nbinary_name=\"html-to-markdown\"\nif [[ \"${RUNNER_OS:-}\" == \"Windows\" ]]; then\n  binary_name=\"html-to-markdown.exe\"\nfi\nexport HTML_TO_MARKDOWN_CLI=\"${PWD}/target/release/${binary_name}\"\nuv pip install --editable packages/python\nuv run pytest \"${pytest_addopts[@]}\"\n"
  },
  {
    "path": "scripts/ci/r/install-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif command -v apt-get >/dev/null 2>&1; then\n  sudo apt-get update\n  sudo apt-get install -y --no-install-recommends libuv1-dev\nfi\n\nRscript -e 'for (pkg in c(\"devtools\", \"testthat\", \"rextendr\", \"lintr\", \"styler\", \"covr\", \"remotes\")) { if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg, repos = \"https://cran.r-project.org\") }'\n"
  },
  {
    "path": "scripts/ci/r/run-lintr.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nrepo_root=\"$(cd \"${script_dir}/../../..\" && pwd)\"\n\ncd \"${repo_root}/packages/r\" && Rscript -e 'lints <- lintr::lint_package(); if (length(lints) > 0) { print(lints); quit(status = 1) }'\n"
  },
  {
    "path": "scripts/ci/r/run-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nrepo_root=\"$(cd \"${script_dir}/../../..\" && pwd)\"\n\n\"${repo_root}/scripts/publish/r/stage-rust-core.sh\"\n\ncd \"${repo_root}/packages/r\"\n\n# Ensure devtools is available (may be missing after cache invalidation)\nRscript -e 'if (!requireNamespace(\"devtools\", quietly = TRUE)) install.packages(\"devtools\", repos = \"https://cran.r-project.org\")'\n\nRscript -e 'devtools::test()'\n"
  },
  {
    "path": "scripts/ci/r/vendor-core-crate.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nVendor html-to-markdown-rs core crate into R package.\n\nThis script:\n1. Reads workspace.dependencies and version from root Cargo.toml\n2. Copies crates/html-to-markdown/ to packages/r/src/rust/vendor/html-to-markdown-rs/\n3. Replaces workspace = true with explicit values in the vendored Cargo.toml\n\"\"\"\n\nimport os\nimport re\nimport shutil\nimport sys\nfrom pathlib import Path\n\ntry:\n    import tomllib\nexcept ImportError:\n    import tomli as tomllib  # type: ignore\n\n\ndef get_repo_root() -> Path:\n    \"\"\"Get repository root directory.\"\"\"\n    repo_root_env = os.environ.get(\"REPO_ROOT\")\n    if repo_root_env:\n        return Path(repo_root_env)\n\n    script_dir = Path(__file__).parent.absolute()\n    return (script_dir / \"..\" / \"..\" / \"..\").resolve()\n\n\ndef read_toml(path: Path) -> dict[str, object]:\n    \"\"\"Read a TOML file and return its contents.\"\"\"\n    with path.open(\"rb\") as f:\n        return tomllib.load(f)\n\n\ndef get_workspace_config(repo_root: Path) -> tuple[str, dict[str, object], dict[str, object]]:\n    \"\"\"Extract version, package metadata, and dependencies from root Cargo.toml.\"\"\"\n    data = read_toml(repo_root / \"Cargo.toml\")\n    ws = data.get(\"workspace\", {})\n    version = ws.get(\"package\", {}).get(\"version\", \"0.0.0\")\n    pkg = ws.get(\"package\", {})\n    deps = ws.get(\"dependencies\", {})\n    return version, pkg, deps\n\n\ndef format_dependency(name: str, dep_spec: object) -> str:\n    \"\"\"Format a dependency spec for Cargo.toml.\"\"\"\n    if isinstance(dep_spec, str):\n        return f'{name} = \"{dep_spec}\"'\n    if isinstance(dep_spec, dict):\n        parts: list[str] = []\n\n        package = dep_spec.get(\"package\")\n        if package:\n            parts.append(f'package = \"{package}\"')\n\n        version = dep_spec.get(\"version\", \"\")\n        parts.append(f'version = \"{version}\"')\n\n        features = dep_spec.get(\"features\", [])\n        if features:\n            features_str = \", \".join(f'\"{f}\"' for f in features)\n            parts.append(f\"features = [{features_str}]\")\n\n        default_features = dep_spec.get(\"default-features\")\n        if default_features is False:\n            parts.append(\"default-features = false\")\n\n        spec_str = \", \".join(parts)\n        return f\"{name} = {{ {spec_str} }}\"\n\n    return f'{name} = \"{dep_spec}\"'\n\n\ndef _replace_package_fields(content: str, version: str, pkg: dict[str, object]) -> str:\n    \"\"\"Replace package-level workspace inheritance fields.\"\"\"\n    content = re.sub(r\"^version\\.workspace = true$\", f'version = \"{version}\"', content, flags=re.MULTILINE)\n    content = re.sub(\n        r\"^edition\\.workspace = true$\", f'edition = \"{pkg.get(\"edition\", \"2024\")}\"', content, flags=re.MULTILINE\n    )\n    content = re.sub(\n        r\"^rust-version\\.workspace = true$\",\n        f'rust-version = \"{pkg.get(\"rust-version\", \"1.85\")}\"',\n        content,\n        flags=re.MULTILINE,\n    )\n\n    authors = pkg.get(\"authors\", [])\n    if authors:\n        authors_str = \", \".join(f'\"{a}\"' for a in authors)\n        content = re.sub(r\"^authors\\.workspace = true$\", f\"authors = [{authors_str}]\", content, flags=re.MULTILINE)\n\n    for field in (\"license\", \"repository\", \"homepage\", \"documentation\"):\n        default = \"MIT\" if field == \"license\" else \"\"\n        content = re.sub(\n            rf\"^{field}\\.workspace = true$\",\n            f'{field} = \"{pkg.get(field, default)}\"',\n            content,\n            flags=re.MULTILINE,\n        )\n\n    # Replace workspace lints\n    return re.sub(r\"^\\[lints\\]\\nworkspace = true\\n?\", \"\", content, flags=re.MULTILINE)\n\n\ndef _make_fields_replacer(dep_name: str, dep_spec: object) -> callable:\n    \"\"\"Create a regex replacer that merges workspace dep spec with extra fields.\"\"\"\n\n    def replacer(match: re.Match[str]) -> str:\n        other_fields_str = match.group(1).strip()\n        base_spec = format_dependency(dep_name, dep_spec)\n\n        if \" = { \" not in base_spec:\n            version_val = base_spec.split(\" = \", 1)[1].strip('\"')\n            spec_part = f'version = \"{version_val}\"'\n        else:\n            spec_part = base_spec.split(\" = { \", 1)[1].rstrip(\"}\")\n\n        existing_keys: set[str] = set()\n        for raw_part in spec_part.split(\",\"):\n            stripped = raw_part.strip()\n            if \"=\" in stripped:\n                existing_keys.add(stripped.split(\"=\")[0].strip())\n\n        filtered_fields: list[str] = []\n        for raw_field in other_fields_str.split(\",\"):\n            stripped = raw_field.strip()\n            if stripped and \"=\" in stripped:\n                if stripped.split(\"=\")[0].strip() not in existing_keys:\n                    filtered_fields.append(stripped)\n            elif stripped:\n                filtered_fields.append(stripped)\n\n        if filtered_fields:\n            return f\"{dep_name} = {{ {spec_part}, {', '.join(filtered_fields)} }}\"\n        return f\"{dep_name} = {{ {spec_part} }}\"\n\n    return replacer\n\n\ndef replace_workspace_refs(toml_path: Path, version: str, pkg: dict[str, object], deps: dict[str, object]) -> None:\n    \"\"\"Replace workspace references with explicit values in vendored Cargo.toml.\"\"\"\n    with toml_path.open() as f:\n        content = f.read()\n\n    content = _replace_package_fields(content, version, pkg)\n\n    # Replace dependency-level workspace references\n    for name, dep_spec in deps.items():\n        pattern_dotted = rf\"^{re.escape(name)}\\.workspace = true$\"\n        content = re.sub(pattern_dotted, format_dependency(name, dep_spec), content, flags=re.MULTILINE)\n\n        pattern_simple = rf\"^{re.escape(name)} = \\{{ workspace = true \\}}$\"\n        content = re.sub(pattern_simple, format_dependency(name, dep_spec), content, flags=re.MULTILINE)\n\n        pattern_extra = rf\"^{re.escape(name)} = \\{{ workspace = true, (.+?) \\}}$\"\n        content = re.sub(pattern_extra, _make_fields_replacer(name, dep_spec), content, flags=re.MULTILINE | re.DOTALL)\n\n    with toml_path.open(\"w\") as f:\n        f.write(content)\n\n\ndef main() -> None:\n    \"\"\"Vendor the html-to-markdown-rs core crate into the R package.\"\"\"\n    repo_root = get_repo_root()\n    src_crate = repo_root / \"crates\" / \"html-to-markdown\"\n    dest_vendor = repo_root / \"packages\" / \"r\" / \"src\" / \"rust\" / \"vendor\" / \"html-to-markdown-rs\"\n\n    print(\"=== Vendoring html-to-markdown-rs core crate ===\")\n\n    if not src_crate.exists():\n        print(f\"Error: Source crate not found at {src_crate}\", file=sys.stderr)\n        sys.exit(1)\n\n    version, pkg, deps = get_workspace_config(repo_root)\n    print(f\"Workspace version: {version}\")\n\n    # Clean existing vendor directory\n    if dest_vendor.exists():\n        shutil.rmtree(dest_vendor)\n        print(\"Cleaned existing vendor directory\")\n\n    # Copy crate source\n    shutil.copytree(src_crate, dest_vendor)\n    print(\"Copied crates/html-to-markdown/ -> vendor/html-to-markdown-rs/\")\n\n    # Clean build artifacts from copied crate\n    for artifact_dir in [\"target\", \".fastembed_cache\"]:\n        artifact = dest_vendor / artifact_dir\n        if artifact.exists():\n            shutil.rmtree(artifact)\n\n    for pattern in [\"*.swp\", \"*.bak\", \"*.tmp\", \"*~\"]:\n        for f in dest_vendor.rglob(pattern):\n            f.unlink()\n\n    # Replace workspace references with explicit values\n    vendor_toml = dest_vendor / \"Cargo.toml\"\n    if vendor_toml.exists():\n        replace_workspace_refs(vendor_toml, version, pkg, deps)\n        print(\"Updated vendor/html-to-markdown-rs/Cargo.toml\")\n\n    print(f\"\\nVendoring complete (version: {version})\")\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception as e:\n        print(f\"Error: {e}\", file=sys.stderr)\n        sys.exit(1)\n"
  },
  {
    "path": "scripts/ci/ruby/run-rbs-validate.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/ruby >/dev/null\nbundle exec rbs validate\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/ruby/run-rspec-unix.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nbundle exec rspec --format progress\n"
  },
  {
    "path": "scripts/ci/ruby/run-rspec-windows.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$unixPath = ridk exec bash -lc \"cygpath -au '$env:GITHUB_WORKSPACE/packages/ruby'\"\nridk exec bash -lc \"cd $unixPath && export RUSTUP_TOOLCHAIN=stable-gnu CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ && bundle exec rspec --format progress\"\n"
  },
  {
    "path": "scripts/ci/ruby/run-rubocop.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/ruby >/dev/null\nbundle exec rubocop --config .rubocop.yml\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/ruby/run-steep.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nbundle exec steep check\n"
  },
  {
    "path": "scripts/ci/ruby/vendor-core-crate.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nVendor html-to-markdown-rs core crate into Ruby package.\nUsed by: ci-ruby.yaml, publish.yaml - Vendor core crate step\n\nThis script:\n1. Reads workspace.dependencies from root Cargo.toml\n2. Copies core crate to packages/ruby/vendor/html-to-markdown-rs/\n3. Replaces workspace = true with explicit versions\n4. Generates vendor/Cargo.toml with proper workspace setup\n5. Updates native extension Cargo.toml to use vendored crate\n\"\"\"\n\nimport os\nimport re\nimport shutil\nimport sys\nfrom pathlib import Path\n\ntry:\n    import tomllib\nexcept ImportError:\n    import tomli as tomllib  # type: ignore\n\n\ndef get_repo_root() -> Path:\n    \"\"\"Get repository root directory.\"\"\"\n    repo_root_env = os.environ.get(\"REPO_ROOT\")\n    if repo_root_env:\n        return Path(repo_root_env)\n\n    script_dir = Path(__file__).parent.absolute()\n    return (script_dir / \"..\" / \"..\" / \"..\").resolve()\n\n\ndef read_toml(path: Path) -> dict[str, object]:\n    \"\"\"Read TOML file.\"\"\"\n    with open(path, \"rb\") as f:\n        return tomllib.load(f)\n\n\ndef get_workspace_deps(repo_root: Path) -> dict[str, object]:\n    \"\"\"Extract workspace.dependencies from root Cargo.toml.\"\"\"\n    cargo_toml_path = repo_root / \"Cargo.toml\"\n    data = read_toml(cargo_toml_path)\n    return data.get(\"workspace\", {}).get(\"dependencies\", {})\n\n\ndef get_workspace_version(repo_root: Path) -> str:\n    \"\"\"Extract version from workspace.package.\"\"\"\n    cargo_toml_path = repo_root / \"Cargo.toml\"\n    data = read_toml(cargo_toml_path)\n    return data.get(\"workspace\", {}).get(\"package\", {}).get(\"version\", \"0.0.0\")\n\n\ndef get_workspace_metadata(repo_root: Path) -> dict[str, str]:\n    \"\"\"Extract metadata from workspace.package.\"\"\"\n    cargo_toml_path = repo_root / \"Cargo.toml\"\n    data = read_toml(cargo_toml_path)\n    pkg = data.get(\"workspace\", {}).get(\"package\", {})\n    return {\n        \"version\": pkg.get(\"version\", \"0.0.0\"),\n        \"edition\": pkg.get(\"edition\", \"2024\"),\n        \"rust-version\": pkg.get(\"rust-version\", \"1.85\"),\n        \"authors\": pkg.get(\"authors\", [\"Na'aman Hirschfeld <naaman@kreuzberg.dev>\"]),\n        \"license\": pkg.get(\"license\", \"MIT\"),\n        \"repository\": pkg.get(\"repository\", \"https://github.com/kreuzberg-dev/html-to-markdown\"),\n        \"homepage\": pkg.get(\"homepage\", \"https://kreuzberg.dev\"),\n    }\n\n\ndef format_dependency(name: str, dep_spec: object) -> str:\n    \"\"\"Format a dependency spec for Cargo.toml.\"\"\"\n    if isinstance(dep_spec, str):\n        return f'{name} = \"{dep_spec}\"'\n    if isinstance(dep_spec, dict):\n        version: str = dep_spec.get(\"version\", \"\")\n        package: str | None = dep_spec.get(\"package\")\n        features: list[str] = dep_spec.get(\"features\", [])\n        default_features: bool | None = dep_spec.get(\"default-features\")\n        optional: bool | None = dep_spec.get(\"optional\")\n        path: str | None = dep_spec.get(\"path\")\n\n        parts: list[str] = []\n\n        if package:\n            parts.append(f'package = \"{package}\"')\n        if path:\n            parts.append(f'path = \"{path}\"')\n        if version:\n            parts.append(f'version = \"{version}\"')\n        if features:\n            features_str = \", \".join(f'\"{f}\"' for f in features)\n            parts.append(f\"features = [{features_str}]\")\n        if default_features is False:\n            parts.append(\"default-features = false\")\n        elif default_features is True:\n            parts.append(\"default-features = true\")\n        if optional is True:\n            parts.append(\"optional = true\")\n        elif optional is False:\n            parts.append(\"optional = false\")\n\n        spec_str = \", \".join(parts)\n        return f\"{name} = {{ {spec_str} }}\"\n\n    return f'{name} = \"{dep_spec}\"'\n\n\ndef replace_workspace_deps_in_toml(\n    toml_path: Path, workspace_deps: dict[str, object]\n) -> None:\n    \"\"\"Replace workspace = true with explicit versions in a Cargo.toml file.\"\"\"\n    with open(toml_path) as f:\n        content = f.read()\n\n    for name, dep_spec in workspace_deps.items():\n        # Dotted key: name.workspace = true\n        pattern0 = rf\"^{re.escape(name)}\\.workspace = true$\"\n        content = re.sub(\n            pattern0, format_dependency(name, dep_spec), content, flags=re.MULTILINE\n        )\n\n        # Simple: name = { workspace = true }\n        pattern1 = rf\"^{re.escape(name)} = \\{{ workspace = true \\}}$\"\n        content = re.sub(\n            pattern1, format_dependency(name, dep_spec), content, flags=re.MULTILINE\n        )\n\n        # Complex: name = { workspace = true, ... }\n        def replace_with_fields(match: re.Match[str]) -> str:\n            other_fields_str = match.group(1).strip()\n            base_spec = format_dependency(name, dep_spec)\n            if \" = { \" not in base_spec:\n                version_val = base_spec.split(\" = \", 1)[1].strip('\"')\n                spec_part = f'version = \"{version_val}\"'\n            else:\n                spec_part = base_spec.split(\" = { \", 1)[1].rstrip(\"} \").rstrip(\"}\")\n\n            # Extract existing keys from workspace spec using bracket-aware parsing\n            workspace_fields: dict[str, str] = {}\n            bracket_depth = 0\n            current_field = \"\"\n            for char in spec_part:\n                if char == \"[\":\n                    bracket_depth += 1\n                    current_field += char\n                elif char == \"]\":\n                    bracket_depth -= 1\n                    current_field += char\n                elif char == \",\" and bracket_depth == 0:\n                    field = current_field.strip()\n                    if field and \"=\" in field:\n                        key, val = field.split(\"=\", 1)\n                        workspace_fields[key.strip()] = val.strip()\n                    current_field = \"\"\n                else:\n                    current_field += char\n\n            if current_field.strip():\n                field = current_field.strip()\n                if field and \"=\" in field:\n                    key, val = field.split(\"=\", 1)\n                    workspace_fields[key.strip()] = val.strip()\n\n            # Extract crate-specific keys\n            crate_fields: dict[str, str] = {}\n            bracket_depth = 0\n            current_field = \"\"\n            for char in other_fields_str:\n                if char == \"[\":\n                    bracket_depth += 1\n                    current_field += char\n                elif char == \"]\":\n                    bracket_depth -= 1\n                    current_field += char\n                elif char == \",\" and bracket_depth == 0:\n                    field = current_field.strip()\n                    if field and \"=\" in field:\n                        key, val = field.split(\"=\", 1)\n                        crate_fields[key.strip()] = val.strip()\n                    current_field = \"\"\n                else:\n                    current_field += char\n\n            if current_field.strip():\n                field = current_field.strip()\n                if field and \"=\" in field:\n                    key, val = field.split(\"=\", 1)\n                    crate_fields[key.strip()] = val.strip()\n\n            # Merge: crate-specific fields override workspace fields\n            merged_fields = {**workspace_fields, **crate_fields}\n            merged_parts = [f\"{k} = {v}\" for k, v in merged_fields.items()]\n            merged_spec = \", \".join(merged_parts)\n\n            return f\"{name} = {{ {merged_spec} }}\"\n\n        pattern2 = rf\"^{re.escape(name)} = \\{{ workspace = true, (.+?) \\}}$\"\n        content = re.sub(\n            pattern2, replace_with_fields, content, flags=re.MULTILINE | re.DOTALL\n        )\n\n    with open(toml_path, \"w\") as f:\n        f.write(content)\n\n\ndef generate_vendor_cargo_toml(\n    repo_root: Path,\n    workspace_deps: dict[str, object],\n    metadata: dict[str, str],\n    copied_crates: list[str],\n) -> None:\n    \"\"\"Generate vendor/Cargo.toml with workspace setup.\"\"\"\n    deps_lines: list[str] = []\n    for name, dep_spec in sorted(workspace_deps.items()):\n        # Skip deps with paths (they're workspace-internal)\n        if isinstance(dep_spec, dict) and \"path\" in dep_spec:\n            continue\n        deps_lines.append(format_dependency(name, dep_spec))\n\n    deps_str = \"\\n\".join(deps_lines)\n    members_str = \", \".join(f'\"{m}\"' for m in copied_crates)\n    authors_str = \", \".join(f'\"{a}\"' for a in metadata[\"authors\"])\n\n    vendor_toml = f\"\"\"[workspace]\nmembers = [{members_str}]\nresolver = \"2\"\n\n[workspace.package]\nversion = \"{metadata['version']}\"\nedition = \"{metadata['edition']}\"\nrust-version = \"{metadata['rust-version']}\"\nauthors = [{authors_str}]\nlicense = \"{metadata['license']}\"\nrepository = \"{metadata['repository']}\"\nhomepage = \"{metadata['homepage']}\"\n\n[workspace.dependencies]\n{deps_str}\n\"\"\"\n\n    vendor_dir = repo_root / \"packages\" / \"ruby\" / \"vendor\"\n    vendor_dir.mkdir(parents=True, exist_ok=True)\n\n    toml_path = vendor_dir / \"Cargo.toml\"\n    with open(toml_path, \"w\") as f:\n        f.write(vendor_toml)\n\n\ndef main() -> None:\n    \"\"\"Main vendoring function.\"\"\"\n    repo_root: Path = get_repo_root()\n\n    print(\"=== Vendoring html-to-markdown-rs core crate ===\")\n\n    workspace_deps: dict[str, object] = get_workspace_deps(repo_root)\n    metadata: dict[str, str] = get_workspace_metadata(repo_root)\n    core_version: str = metadata[\"version\"]\n\n    print(f\"Core version: {core_version}\")\n    print(f\"Workspace dependencies: {len(workspace_deps)}\")\n\n    vendor_base: Path = repo_root / \"packages\" / \"ruby\" / \"vendor\"\n\n    # Clean only crate directories, preserving vendor/bundle/ (Bundler gems)\n    crate_names = [\"html-to-markdown-rs\"]\n    for name in crate_names:\n        crate_path = vendor_base / name\n        if crate_path.exists():\n            shutil.rmtree(crate_path)\n    # Clean the vendor Cargo.toml (will be regenerated)\n    vendor_cargo = vendor_base / \"Cargo.toml\"\n    if vendor_cargo.exists():\n        vendor_cargo.unlink()\n    # Clean old vendor/html-to-markdown if it exists (legacy name)\n    legacy = vendor_base / \"html-to-markdown\"\n    if legacy.exists():\n        shutil.rmtree(legacy)\n    print(\"Cleaned vendor crate directories\")\n\n    vendor_base.mkdir(parents=True, exist_ok=True)\n\n    # Copy core crate\n    crates_to_copy: list[tuple[str, str]] = [\n        (\"crates/html-to-markdown\", \"html-to-markdown-rs\"),\n    ]\n\n    copied_crates: list[str] = []\n    for src_rel, dest_name in crates_to_copy:\n        src: Path = repo_root / src_rel\n        dest: Path = vendor_base / dest_name\n        if src.exists():\n            try:\n                shutil.copytree(src, dest)\n                copied_crates.append(dest_name)\n                print(f\"Copied {dest_name}\")\n            except Exception as e:\n                print(f\"Warning: Failed to copy {dest_name}: {e}\", file=sys.stderr)\n        else:\n            print(f\"Warning: Source directory not found: {src_rel}\")\n\n    # Clean build artifacts from vendored crates\n    artifact_dirs: list[str] = [\"target\"]\n    temp_patterns: list[str] = [\"*.swp\", \"*.bak\", \"*.tmp\", \"*~\"]\n\n    for crate_dir in copied_crates:\n        crate_path: Path = vendor_base / crate_dir\n        if crate_path.exists():\n            for artifact_dir in artifact_dirs:\n                artifact: Path = crate_path / artifact_dir\n                if artifact.exists():\n                    shutil.rmtree(artifact)\n            for pattern in temp_patterns:\n                for f in crate_path.rglob(pattern):\n                    f.unlink()\n\n    print(\"Cleaned build artifacts\")\n\n    # Update workspace inheritance in vendored Cargo.toml files\n    for crate_dir in copied_crates:\n        crate_toml = vendor_base / crate_dir / \"Cargo.toml\"\n        if crate_toml.exists():\n            with open(crate_toml) as f:\n                content = f.read()\n\n            content = re.sub(\n                r\"^version\\.workspace = true$\",\n                f'version = \"{core_version}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^edition\\.workspace = true$\",\n                f'edition = \"{metadata[\"edition\"]}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^rust-version\\.workspace = true$\",\n                f'rust-version = \"{metadata[\"rust-version\"]}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            authors_toml = \", \".join(f'\"{a}\"' for a in metadata[\"authors\"])\n            content = re.sub(\n                r\"^authors\\.workspace = true$\",\n                f\"authors = [{authors_toml}]\",\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^license\\.workspace = true$\",\n                f'license = \"{metadata[\"license\"]}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^repository\\.workspace = true$\",\n                f'repository = \"{metadata[\"repository\"]}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^homepage\\.workspace = true$\",\n                f'homepage = \"{metadata[\"homepage\"]}\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^documentation\\.workspace = true$\",\n                'documentation = \"https://docs.rs/html-to-markdown-rs\"',\n                content,\n                flags=re.MULTILINE,\n            )\n            content = re.sub(\n                r\"^readme\\.workspace = true$\",\n                'readme = \"README.md\"',\n                content,\n                flags=re.MULTILINE,\n            )\n\n            # Remove [lints] workspace = true section\n            content = re.sub(\n                r\"\\[lints\\]\\s*\\nworkspace\\s*=\\s*true\\s*\\n?\",\n                \"\",\n                content,\n                flags=re.MULTILINE,\n            )\n\n            with open(crate_toml, \"w\") as f:\n                f.write(content)\n\n            replace_workspace_deps_in_toml(crate_toml, workspace_deps)\n            print(f\"Updated {crate_dir}/Cargo.toml\")\n\n    # Generate vendor/Cargo.toml with workspace setup\n    generate_vendor_cargo_toml(repo_root, workspace_deps, metadata, copied_crates)\n    print(\"Generated vendor/Cargo.toml\")\n\n    ext_dir = repo_root / \"packages\" / \"ruby\" / \"ext\" / \"html_to_markdown_rb\"\n\n    # Outer Cargo.toml (used by rb_sys/mkmf at gem install time)\n    # From: path = \"../../../../crates/html-to-markdown\"\n    # To:   path = \"../../vendor/html-to-markdown-rs\"\n    outer_toml = ext_dir / \"Cargo.toml\"\n    if outer_toml.exists():\n        with open(outer_toml) as f:\n            content = f.read()\n        content = re.sub(\n            r'path = \"\\.\\./\\.\\./\\.\\./\\.\\./crates/html-to-markdown\"',\n            'path = \"../../vendor/html-to-markdown-rs\"',\n            content,\n        )\n        with open(outer_toml, \"w\") as f:\n            f.write(content)\n        print(\"Updated ext/html_to_markdown_rb/Cargo.toml to use vendored crate\")\n\n    # Inner native/Cargo.toml (used by rake-compiler-dock cross-compile)\n    # From: path = \"../../../../../crates/html-to-markdown\"\n    # To:   path = \"../../../vendor/html-to-markdown-rs\"\n    native_toml = ext_dir / \"native\" / \"Cargo.toml\"\n    if native_toml.exists():\n        with open(native_toml) as f:\n            content = f.read()\n        content = re.sub(\n            r'path = \"\\.\\./\\.\\./\\.\\./\\.\\./\\.\\./crates/html-to-markdown\"',\n            'path = \"../../../vendor/html-to-markdown-rs\"',\n            content,\n        )\n        with open(native_toml, \"w\") as f:\n            f.write(content)\n        print(\"Updated ext/html_to_markdown_rb/native/Cargo.toml to use vendored crate\")\n\n    print(f\"\\nVendoring complete (core version: {core_version})\")\n    print(f\"Copied crates: {', '.join(sorted(copied_crates))}\")\n\n    if \"html-to-markdown-rs\" in copied_crates:\n        print(\"Extension Cargo.toml files updated to use vendored crate\")\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception as e:\n        print(f\"Error: {e}\", file=sys.stderr)\n        sys.exit(1)\n"
  },
  {
    "path": "scripts/ci/rust/check-fmt.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo fmt --check\n"
  },
  {
    "path": "scripts/ci/rust/install-cargo-llvm-cov.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo install cargo-llvm-cov --locked --force\n"
  },
  {
    "path": "scripts/ci/rust/run-clippy.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo clippy -- -D warnings\n"
  },
  {
    "path": "scripts/ci/rust/run-llvm-cov.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo llvm-cov --workspace --exclude html-to-markdown-py --exclude html-to-markdown-rb --exclude html-to-markdown-php --exclude html-to-markdown-node --exclude html-to-markdown-wasm --exclude html-to-markdown-wasm-wasmtime-tests --all-features --lcov --output-path coverage.lcov\n"
  },
  {
    "path": "scripts/ci/rust/run-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\necho \"Running Rust tests with full debugging...\"\ncargo test --release --no-default-features --workspace --exclude html-to-markdown-rb --exclude html-to-markdown-php --exclude html-to-markdown-wasm-wasmtime-tests -vv -- --nocapture --test-threads=1\n"
  },
  {
    "path": "scripts/ci/smoke/capture-php-config.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\necho \"PHP_CONFIG=$(command -v php-config)\" >>\"$GITHUB_ENV\"\n"
  },
  {
    "path": "scripts/ci/smoke/install-pnpm-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm install\n"
  },
  {
    "path": "scripts/ci/validate/install-elixir-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/elixir >/dev/null\nmix deps.get\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/validate/install-ruby-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/ruby >/dev/null\nbundle install --jobs 4 --retry 3\npopd >/dev/null\n"
  },
  {
    "path": "scripts/ci/validate/run-prek.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nexport SKIP=${SKIP:-golangci-lint-packages,golangci-lint-examples}\nprek run --show-diff-on-failure --color=always --all-files\n"
  },
  {
    "path": "scripts/ci/validate/run-rust-checks.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo fmt --check\ncargo clippy --workspace --exclude html-to-markdown-rb --exclude html-to-markdown-php -- -D warnings\n"
  },
  {
    "path": "scripts/ci/wasm/run-wasmtime-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo test -p html-to-markdown-wasm-wasmtime-tests\n"
  },
  {
    "path": "scripts/ci/wasm/test-wasm-bundle.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Skip if no test files exist (unit tests moved to e2e)\nif ! find . -name '*.spec.ts' -o -name '*.test.ts' | grep -q .; then\n  echo \"No test files found, skipping vitest (tests are in e2e/)\"\n  exit 0\nfi\n\nVITEST_TIMEOUT_MS=\"${VITEST_TIMEOUT_MS:-60000}\"\n\npnpm vitest run \\\n  --reporter=verbose \\\n  --pool=threads \\\n  --maxWorkers=1 \\\n  --exclude=\".venv/**\" \\\n  --test-timeout=\"${VITEST_TIMEOUT_MS}\"\n"
  },
  {
    "path": "scripts/ci/wasm/test-wasm-rust.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo test\n"
  },
  {
    "path": "scripts/common/enable-corepack.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncorepack enable\n"
  },
  {
    "path": "scripts/common/ensure-wasm-target.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrustup target add wasm32-unknown-unknown\nrustc --print target-libdir --target wasm32-unknown-unknown\n"
  },
  {
    "path": "scripts/common/install-maven-latest.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n: \"${RUNNER_TEMP:?RUNNER_TEMP is required}\"\n: \"${GITHUB_PATH:?GITHUB_PATH is required}\"\n\n# Pin Maven 3.9.x — Maven 4.x breaks central-publishing-maven-plugin deploy lifecycle\nmaven_version=\"3.9.11\"\n\ntmp_dir=\"${RUNNER_TEMP}\"\nif [[ \"${RUNNER_OS:-}\" == \"Windows\" ]]; then\n  if command -v cygpath >/dev/null 2>&1; then\n    tmp_dir=\"$(cygpath -u \"${RUNNER_TEMP}\")\"\n  fi\nfi\n\nmaven_dir=\"${tmp_dir}/apache-maven-${maven_version}\"\narchive_path=\"${tmp_dir}/maven.tar.gz\"\n\nif [[ ! -d \"${maven_dir}\" ]]; then\n  if ! curl -fsSL --retry 3 --retry-all-errors --retry-delay 5 \\\n    \"https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${maven_version}/apache-maven-${maven_version}-bin.tar.gz\" \\\n    -o \"${archive_path}\"; then\n    curl -fsSL --retry 3 --retry-all-errors --retry-delay 5 \\\n      \"https://archive.apache.org/dist/maven/maven-3/${maven_version}/binaries/apache-maven-${maven_version}-bin.tar.gz\" \\\n      -o \"${archive_path}\"\n  fi\n  tar -xzf \"${archive_path}\" -C \"${tmp_dir}\"\nfi\n\necho \"${maven_dir}/bin\" >>\"${GITHUB_PATH}\"\n"
  },
  {
    "path": "scripts/common/install-wasm-pack.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif command -v wasm-pack >/dev/null 2>&1; then\n  exit 0\nfi\n\nif command -v cargo >/dev/null 2>&1; then\n  cargo install wasm-pack --locked\nelse\n  curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh\nfi\n"
  },
  {
    "path": "scripts/generate_visitor_callbacks.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Generate visitor callback code from YAML schema.\n\nThis script generates FFI visitor callback implementations for multiple languages\nfrom a central YAML schema definition. It eliminates ~2,450 lines of duplicated\ncode across Rust, Go, C#, and Java layers.\n\nUsage:\n    python scripts/generate_visitor_callbacks.py\n\nGenerated files:\n    - crates/html-to-markdown-ffi/src/visitor/registry_generated.rs\n    - packages/go/v2/htmltomarkdown/visitor_generated.go\n    - packages/csharp/HtmlToMarkdown/Visitor/VisitorBridgeGenerated.cs\n    - packages/java/src/main/java/sh/kreuzberg/htmltomarkdown/visitor/VisitorBridgeGenerated.java\n\nRequirements:\n    - PyYAML (pip install pyyaml)\n    - Jinja2 (pip install jinja2)\n\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\ntry:\n    import yaml\n    from jinja2 import Environment, FileSystemLoader, select_autoescape\nexcept ImportError as e:\n    print(f\"Error: Missing required dependency: {e}\", file=sys.stderr)\n    print(\"Install dependencies: pip install pyyaml jinja2\", file=sys.stderr)\n    sys.exit(1)\n\n\ndef load_schema(schema_path: Path) -> dict[str, Any]:\n    \"\"\"Load and validate the YAML schema.\"\"\"\n    if not schema_path.exists():\n        raise FileNotFoundError(f\"Schema file not found: {schema_path}\")\n\n    with schema_path.open(encoding=\"utf-8\") as f:\n        schema = yaml.safe_load(f)\n\n    # Validate required keys\n    required_keys = [\"version\", \"metadata\", \"types\", \"callbacks\", \"generation\"]\n    for key in required_keys:\n        if key not in schema:\n            raise ValueError(f\"Missing required key in schema: {key}\")\n\n    return schema\n\n\ndef setup_jinja_env(template_dir: Path) -> Environment:\n    \"\"\"Set up Jinja2 environment with template directory.\"\"\"\n    if not template_dir.exists():\n        raise FileNotFoundError(f\"Template directory not found: {template_dir}\")\n\n    return Environment(\n        loader=FileSystemLoader(str(template_dir)),\n        autoescape=select_autoescape(disabled_extensions=(\"j2\",)),\n        trim_blocks=True,\n        lstrip_blocks=True,\n    )\n\n\ndef generate_code(\n    env: Environment,\n    template_name: str,\n    output_path: Path,\n    context: dict[str, Any],\n) -> None:\n    \"\"\"Generate code from template and write to output file.\"\"\"\n    template = env.get_template(template_name)\n    rendered = template.render(**context)\n\n    # Ensure output directory exists\n    output_path.parent.mkdir(parents=True, exist_ok=True)\n\n    # Write generated code\n    with output_path.open(\"w\", encoding=\"utf-8\") as f:\n        f.write(rendered)\n\n    print(f\"✓ Generated: {output_path}\")\n\n\ndef main() -> None:\n    \"\"\"Generate visitor callback code for all target languages.\"\"\"\n    # Locate project root (go up from scripts/ to project root)\n    script_dir = Path(__file__).parent.resolve()\n    project_root = script_dir.parent\n\n    # Define paths\n    schema_path = project_root / \"crates/html-to-markdown-ffi/visitor_callbacks.yaml\"\n    template_dir = project_root / \"crates/html-to-markdown-ffi/templates\"\n\n    print(\"HTML-to-Markdown Visitor Callback Code Generator\")\n    print(\"=\" * 60)\n    print(f\"Schema: {schema_path.relative_to(project_root)}\")\n    print(f\"Templates: {template_dir.relative_to(project_root)}\")\n    print()\n\n    # Load schema\n    try:\n        schema = load_schema(schema_path)\n    except Exception as e:\n        print(f\"Error loading schema: {e}\", file=sys.stderr)\n        sys.exit(1)\n\n    # Set up Jinja2 environment\n    try:\n        env = setup_jinja_env(template_dir)\n    except Exception as e:\n        print(f\"Error setting up templates: {e}\", file=sys.stderr)\n        sys.exit(1)\n\n    # Prepare template context\n    context = {\n        \"version\": schema[\"version\"],\n        \"metadata\": schema[\"metadata\"],\n        \"types\": schema[\"types\"],\n        \"callbacks\": schema[\"callbacks\"],\n    }\n\n    # Generate code for each target language\n    targets = schema[\"generation\"][\"targets\"]\n    total_callbacks = schema[\"metadata\"][\"total_callbacks\"]\n\n    print(f\"Generating code for {total_callbacks} callbacks across {len(targets)} languages:\")\n    print()\n\n    generated_files = []\n    for lang_name, lang_config in targets.items():\n        template_file = Path(lang_config[\"template\"]).name\n        output_file = project_root / lang_config[\"output_file\"]\n\n        try:\n            generate_code(env, template_file, output_file, context)\n            generated_files.append(output_file)\n        except Exception as e:\n            print(f\"✗ Error generating {lang_name}: {e}\", file=sys.stderr)\n            sys.exit(1)\n\n    # Summary\n    print()\n    print(\"=\" * 60)\n    print(f\"✓ Successfully generated {len(generated_files)} files\")\n    print()\n    print(\"Expected reductions:\")\n    for reduction in schema[\"metadata\"][\"expected_reduction\"].values():\n        print(f\"  • {reduction}\")\n    print()\n    print(\"Next steps:\")\n    print(\"  1. Review generated files\")\n    print(\"  2. Run: task rust:build\")\n    print(\"  3. Run: task test\")\n    print(\"  4. Commit changes\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/preferred-ruby.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\nif [[ -n \"${HTML_TO_MARKDOWN_RUBY:-}\" && -x \"${HTML_TO_MARKDOWN_RUBY}\" ]]; then\n  exec \"${HTML_TO_MARKDOWN_RUBY}\" \"$@\"\nfi\nif command -v brew >/dev/null 2>&1; then\n  brew_ruby_dir=$(brew --prefix ruby 2>/dev/null || true)\n  if [[ -n \"$brew_ruby_dir\" && -x \"$brew_ruby_dir/bin/ruby\" ]]; then\n    exec \"$brew_ruby_dir/bin/ruby\" \"$@\"\n  fi\nfi\nif command -v rbenv >/dev/null 2>&1; then\n  rb_path=\"$(rbenv which ruby 2>/dev/null || true)\"\n  if [[ -n \"$rb_path\" && -x \"$rb_path\" ]]; then\n    exec \"$rb_path\" \"$@\"\n  fi\nfi\nif command -v ruby >/dev/null 2>&1; then\n  exec \"$(command -v ruby)\" \"$@\"\nfi\nprintf 'Error: Ruby interpreter not found. Please install Ruby 3.x\\n' >&2\nexit 1\n"
  },
  {
    "path": "scripts/preferred-rustc.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif command -v rustup >/dev/null 2>&1; then\n  rustup_rustc=\"$(rustup which rustc 2>/dev/null || true)\"\n  if [[ -n \"$rustup_rustc\" && -x \"$rustup_rustc\" ]]; then\n    exec \"$rustup_rustc\" \"$@\"\n  fi\nfi\n\nif [[ -x \"$HOME/.cargo/bin/rustc\" ]]; then\n  exec \"$HOME/.cargo/bin/rustc\" \"$@\"\nfi\n\nif command -v rustc >/dev/null 2>&1; then\n  exec \"$(command -v rustc)\" \"$@\"\nfi\n\nprintf 'Error: rustc not found. Please install Rust via rustup.\\n' >&2\nexit 1\n"
  },
  {
    "path": "scripts/prepare_ruby_gem.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"fileutils\"\nrequire \"pathname\"\n\nroot = Pathname.new(__dir__).parent\n\nbinary_name = Gem.win_platform? ? \"html-to-markdown.exe\" : \"html-to-markdown\"\nsource = root.join(\"target\", \"release\", binary_name)\n\n# CLI binary should already be built before vendoring\n# This avoids package collision with the vendored html-to-markdown-rs crate\nunless source.file?\n  abort \"CLI binary not found at #{source}. Please build it first with: cargo build --release --package html-to-markdown-cli\"\nend\n\nputs \"Using CLI binary at #{source}\"\n\nbin_dir = root.join(\"packages\", \"ruby\", \"lib\", \"bin\")\nFileUtils.mkdir_p(bin_dir)\n\nplain_binary = bin_dir.join(\"html-to-markdown\")\nwindows_binary = bin_dir.join(\"html-to-markdown.exe\")\n\n[plain_binary, windows_binary].each do |path|\n  next unless path.exist?\n\n  FileUtils.rm_f(path)\nend\n\ndest = bin_dir.join(binary_name)\nFileUtils.cp(source, dest)\nFileUtils.chmod(0o755, dest) unless Gem.win_platform?\n\nputs \"Copied CLI binary to #{dest}\"\n"
  },
  {
    "path": "scripts/prepare_wheel.py",
    "content": "import shutil\nimport subprocess\nimport sys\nfrom pathlib import Path\n\ntry:\n    import tomllib  # Python 3.11+  # type: ignore[import-not-found]\nexcept ImportError:\n    import tomli as tomllib  # Python 3.10  # type: ignore[import-not-found]\n\n\ndef main() -> None:\n    repo_root = Path(__file__).resolve().parent.parent\n\n    print(\"Building html-to-markdown CLI binary...\")\n    subprocess.run(\n        [\"cargo\", \"build\", \"--release\", \"--package\", \"html-to-markdown-cli\"],\n        check=True,\n        cwd=repo_root,\n    )\n\n    binary_name = \"html-to-markdown.exe\" if sys.platform == \"win32\" else \"html-to-markdown\"\n    source = repo_root / \"target\" / \"release\" / binary_name\n\n    if not source.exists():\n        msg = f\"CLI binary not found at {source}\"\n        raise FileNotFoundError(msg)\n\n    print(f\"Found CLI binary at {source}\")\n\n    package_root = repo_root / \"packages\" / \"python\" / \"html_to_markdown\"\n    package_bin_dir = package_root / \"bin\"\n    package_bin_dir.mkdir(parents=True, exist_ok=True)\n\n    dest = package_bin_dir / binary_name\n    shutil.copy(source, dest)\n    print(f\"Copied CLI binary to {dest}\")\n\n    if sys.platform != \"win32\":\n        dest.chmod(0o755)\n        print(f\"Made binary executable: {dest}\")\n\n    license_src = repo_root / \"LICENSE\"\n    license_dest = repo_root / \"packages\" / \"python\" / \"LICENSE\"\n    if license_src.exists():\n        shutil.copy(license_src, license_dest)\n        print(f\"Copied LICENSE to {license_dest}\")\n\n    with (repo_root / \"Cargo.toml\").open(\"rb\") as f:\n        cargo_toml = tomllib.load(f)\n    version = cargo_toml[\"workspace\"][\"package\"][\"version\"]\n\n    data_dir_name = repo_root / \"packages\" / \"python\" / f\"html_to_markdown-{version}.data\"\n    scripts_dir = data_dir_name / \"scripts\"\n    scripts_dir.mkdir(parents=True, exist_ok=True)\n\n    scripts_dest = scripts_dir / binary_name\n    shutil.copy(source, scripts_dest)\n    print(f\"Copied CLI binary to {scripts_dest} for PATH installation\")\n\n    if sys.platform != \"win32\":\n        scripts_dest.chmod(0o755)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/publish/cli/build-cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\nuse_cross=\"${USE_CROSS:-false}\"\n\nif [[ \"${use_cross}\" == \"true\" ]]; then\n  cross build --release --target \"${target}\" --package html-to-markdown-cli\nelse\n  cargo build --release --target \"${target}\" --package html-to-markdown-cli\nfi\n"
  },
  {
    "path": "scripts/publish/cli/configure-cross-linker.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\n\nif [[ \"${target}\" != \"aarch64-unknown-linux-gnu\" ]]; then\n  exit 0\nfi\n\n{\n  echo \"CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc\"\n  echo \"CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++\"\n  echo \"AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar\"\n  echo \"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc\"\n} >>\"${GITHUB_ENV}\"\n"
  },
  {
    "path": "scripts/publish/cli/install-build-deps-linux.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\n\nsudo apt-get update\nsudo apt-get install -y pkg-config libssl-dev\ncase \"${target}\" in\nx86_64-unknown-linux-musl)\n  sudo apt-get install -y musl-tools\n  ;;\naarch64-unknown-linux-gnu)\n  sudo apt-get install -y gcc-aarch64-linux-gnu\n  ;;\nesac\n"
  },
  {
    "path": "scripts/publish/cli/install-cross.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo install cross --git https://github.com/cross-rs/cross --locked\n"
  },
  {
    "path": "scripts/publish/cli/package-cli-artifact.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$target = $env:TARGET\nif (-not $target) { throw \"TARGET is required\" }\n$stage = \"cli-$target\"\nRemove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue\nNew-Item -ItemType Directory -Path $stage | Out-Null\nCopy-Item \"target/$target/release/html-to-markdown.exe\" $stage\nCopy-Item LICENSE $stage\nCopy-Item README.md $stage\nCompress-Archive -Path \"$stage/*\" -DestinationPath \"$stage.zip\" -Force\nRemove-Item -Recurse -Force $stage\n"
  },
  {
    "path": "scripts/publish/cli/package-cli-artifact.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\nstage=\"cli-${target}\"\nrm -rf \"${stage}\"\nmkdir -p \"${stage}\"\ncp \"target/${target}/release/html-to-markdown\" \"${stage}/\"\ncp LICENSE \"${stage}/\"\ncp README.md \"${stage}/\"\ntar -czf \"${stage}.tar.gz\" \"${stage}\"\nrm -rf \"${stage}\"\n"
  },
  {
    "path": "scripts/publish/common/add-rust-target.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${RUST_TARGET:?RUST_TARGET is required}\"\nrustup target add \"${target}\"\n"
  },
  {
    "path": "scripts/publish/common/ensure-target-commit.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget_sha=\"${TARGET_SHA:-}\"\n\nif [[ -z \"${target_sha}\" ]]; then\n  exit 0\nfi\n\ngit checkout --progress --force \"${target_sha}\"\n"
  },
  {
    "path": "scripts/publish/crates/package-crates.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrelease_version=\"${RELEASE_VERSION:-unknown}\"\n\ncargo package -p html-to-markdown-rs --allow-dirty\n\ncli_packaged=0\ncli_status=0\ncargo package -p html-to-markdown-cli --allow-dirty --no-verify || cli_status=$?\n\nif [[ \"${cli_status}\" -eq 0 ]]; then\n  cli_packaged=1\nelse\n  echo \"::warning::Skipping html-to-markdown-cli crate packaging; html-to-markdown-rs ${release_version} is not yet available on crates.io.\"\n  if [[ -n \"${GITHUB_STEP_SUMMARY:-}\" ]]; then\n    {\n      echo \"### html-to-markdown-cli crate\"\n      echo \"\"\n      echo \"- Packaging skipped because html-to-markdown-rs ${release_version} is not yet published to crates.io.\"\n    } >>\"${GITHUB_STEP_SUMMARY}\"\n  fi\nfi\n\nmkdir -p crate-artifacts\ncp target/package/html-to-markdown-rs-*.crate crate-artifacts/\nif [[ \"${cli_packaged}\" -eq 1 ]]; then\n  cp target/package/html-to-markdown-cli-*.crate crate-artifacts/\nfi\n"
  },
  {
    "path": "scripts/publish/crates/publish-cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npublish_log=$(mktemp)\nset +e\ncargo publish -p html-to-markdown-cli --token \"${CARGO_TOKEN:?CARGO_TOKEN is required}\" 2>&1 | tee \"${publish_log}\"\nstatus=${PIPESTATUS[0]}\nset -e\n\nif [[ \"${status}\" -ne 0 ]]; then\n  if grep -q \"already uploaded\" \"${publish_log}\" || grep -q \"is already published\" \"${publish_log}\" || grep -q \"already exists\" \"${publish_log}\"; then\n    echo \"::notice::html-to-markdown-cli already published; skipping.\"\n  else\n    exit \"${status}\"\n  fi\nfi\n"
  },
  {
    "path": "scripts/publish/crates/publish-rs.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npublish_log=$(mktemp)\nset +e\ncargo publish -p html-to-markdown-rs --token \"${CARGO_TOKEN:?CARGO_TOKEN is required}\" 2>&1 | tee \"${publish_log}\"\nstatus=${PIPESTATUS[0]}\nset -e\n\nif [[ \"${status}\" -ne 0 ]]; then\n  if grep -q \"already uploaded\" \"${publish_log}\" || grep -q \"is already published\" \"${publish_log}\" || grep -q \"already exists\" \"${publish_log}\"; then\n    echo \"::notice::html-to-markdown-rs already published; skipping.\"\n  else\n    exit \"${status}\"\n  fi\nfi\n"
  },
  {
    "path": "scripts/publish/crates/verify-cargo-version.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag_version=\"${TAG_VERSION:?TAG_VERSION is required}\"\ncargo_version=$(grep '^version = ' Cargo.toml | head -1 | sed -E 's/version = \"(.*)\"/\\1/')\n\nif [[ \"${cargo_version}\" != \"${tag_version}\" ]]; then\n  echo \"Version mismatch! Cargo: ${cargo_version}, tag: ${tag_version}\" >&2\n  exit 1\nfi\n\necho \"Cargo.toml version matches tag: ${cargo_version}\"\n"
  },
  {
    "path": "scripts/publish/crates/wait-for-indexing.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nseconds=\"${WAIT_SECONDS:-30}\"\nsleep \"${seconds}\"\n"
  },
  {
    "path": "scripts/publish/csharp/pack.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ -d \"dist/csharp-ffi\" ]]; then\n  scripts/publish/csharp/stage-ffi.sh \"dist/csharp-ffi\" \"packages/csharp/HtmlToMarkdown\"\nfi\n\ndotnet pack packages/csharp/HtmlToMarkdown.csproj --configuration Release --output artifacts/csharp\n"
  },
  {
    "path": "scripts/publish/csharp/restore.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nPROJECT_PATH=\"${1:-packages/csharp/HtmlToMarkdown/HtmlToMarkdown.csproj}\"\n\ndotnet restore \"$PROJECT_PATH\"\n"
  },
  {
    "path": "scripts/publish/csharp/stage-ffi.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nsrc_dir=\"${1:?SRC_DIR is required}\"\nproject_dir=\"${2:-packages/csharp/HtmlToMarkdown}\"\n\nrm -rf \"${project_dir}/runtimes\"\nmkdir -p \"${project_dir}/runtimes\"\n\nfor rid in \"${src_dir}\"/*; do\n  [[ -d \"${rid}\" ]] || continue\n  rid_name=\"$(basename \"${rid}\")\"\n  mkdir -p \"${project_dir}/runtimes/${rid_name}/native\"\n  cp -f \"${rid}/native/\"* \"${project_dir}/runtimes/${rid_name}/native/\"\ndone\n"
  },
  {
    "path": "scripts/publish/elixir/build-hex-package.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# With rustler_precompiled, the Hex package only contains Elixir code + checksum file.\n# Precompiled NIF binaries are downloaded from GitHub releases at install time.\n# No Rust source vendoring needed.\n\npushd packages/elixir >/dev/null\nmix hex.build\npopd >/dev/null\n"
  },
  {
    "path": "scripts/publish/elixir/install-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/elixir >/dev/null\nmix deps.get\npopd >/dev/null\n"
  },
  {
    "path": "scripts/publish/elixir/install-hex-rebar.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nmix local.hex --force\nmix local.rebar --force\n"
  },
  {
    "path": "scripts/publish/elixir/run-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nscripts/publish/elixir/stage-rust-core.sh\n\npushd packages/elixir >/dev/null\nenv MIX_ENV=test mix test\npopd >/dev/null\n"
  },
  {
    "path": "scripts/publish/elixir/stage-rust-core.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../../..\" && pwd)\"\n\nSRC_DIR=\"${ROOT_DIR}/crates/html-to-markdown\"\nDEST_DIR=\"${ROOT_DIR}/packages/elixir/native/html_to_markdown_elixir/vendor/html-to-markdown-rs\"\n\nVERSION=\"$(\n  python3 - \"${ROOT_DIR}/Cargo.toml\" <<'PY'\nimport re\nfrom pathlib import Path\nimport sys\n\ntext = Path(sys.argv[1]).read_text(encoding=\"utf-8\")\nin_workspace_pkg = False\nfor line in text.splitlines():\n    if line.strip() == \"[workspace.package]\":\n        in_workspace_pkg = True\n        continue\n    if in_workspace_pkg and line.startswith(\"[\") and line.strip().startswith(\"[\") and line.strip() != \"[workspace.package]\":\n        in_workspace_pkg = False\n    if in_workspace_pkg:\n        m = re.match(r'version\\s*=\\s*\"([^\"]+)\"\\s*$', line.strip())\n        if m:\n            print(m.group(1))\n            raise SystemExit(0)\nraise SystemExit(\"Failed to find [workspace.package] version in Cargo.toml\")\nPY\n)\"\n\nif [[ ! -d \"${SRC_DIR}\" ]]; then\n  echo \"Missing Rust core crate at ${SRC_DIR}\" >&2\n  exit 1\nfi\n\nrm -rf \"${DEST_DIR}\"\nmkdir -p \"$(dirname \"${DEST_DIR}\")\"\n\nif command -v rsync >/dev/null 2>&1; then\n  rsync -a --delete --exclude target --exclude .git \"${SRC_DIR}/\" \"${DEST_DIR}/\"\nelse\n  cp -R \"${SRC_DIR}\" \"${DEST_DIR}\"\n  rm -rf \"${DEST_DIR}/target\" \"${DEST_DIR}/.git\" || true\nfi\n\npython3 - \"${DEST_DIR}/Cargo.toml\" \"${VERSION}\" <<'PY'\nimport re\nimport sys\nfrom pathlib import Path\n\npath = Path(sys.argv[1])\nversion = sys.argv[2]\ntext = path.read_text(encoding=\"utf-8\")\n\nreplacements = {\n    r\"^version\\.workspace\\s*=\\s*true\\s*$\": f'version = \"{version}\"',\n    r\"^edition\\.workspace\\s*=\\s*true\\s*$\": 'edition = \"2024\"',\n    r\"^authors\\.workspace\\s*=\\s*true\\s*$\": 'authors = [\"Na\\'aman Hirschfeld <nhirschfeld@gmail.com>\"]',\n    r\"^license\\.workspace\\s*=\\s*true\\s*$\": 'license = \"MIT\"',\n    r\"^repository\\.workspace\\s*=\\s*true\\s*$\": 'repository = \"https://github.com/kreuzberg-dev/html-to-markdown\"',\n    r\"^homepage\\.workspace\\s*=\\s*true\\s*$\": 'homepage = \"https://github.com/kreuzberg-dev/html-to-markdown\"',\n    r\"^documentation\\.workspace\\s*=\\s*true\\s*$\": 'documentation = \"https://docs.rs/html-to-markdown-rs\"',\n    r\"^rust-version\\.workspace\\s*=\\s*true\\s*$\": 'rust-version = \"1.85\"',\n    r\"^\\[lints\\]\\s*\\nworkspace\\s*=\\s*true\\s*$\": '[lints]\\nrust.unsafe_code = \"forbid\"\\nrust.missing_docs = \"warn\"\\nrust.unused_must_use = \"deny\"',\n    r\"^tl\\.workspace\\s*=\\s*true\\s*$\": 'tl = { package = \"astral-tl\", version = \"0.7.11\" }',\n    r\"^regex\\.workspace\\s*=\\s*true\\s*$\": 'regex = \"1.12\"',\n    r\"^once_cell\\.workspace\\s*=\\s*true\\s*$\": 'once_cell = \"1.21\"',\n    r\"^thiserror\\.workspace\\s*=\\s*true\\s*$\": 'thiserror = \"2.0\"',\n    r\"^base64\\.workspace\\s*=\\s*true\\s*$\": 'base64 = \"0.22\"',\n    r\"^ahash\\.workspace\\s*=\\s*true\\s*$\": 'ahash = \"0.8\"',\n    r\"^html5ever\\.workspace\\s*=\\s*true\\s*$\": 'html5ever = \"0.38.0\"',\n    r\"^async-trait\\s*=\\s*{\\s*workspace\\s*=\\s*true,\\s*optional\\s*=\\s*true\\s*}\\s*$\": 'async-trait = { version = \"0.1\", optional = true }',\n}\n\nfor pattern, replacement in replacements.items():\n    text = re.sub(pattern, replacement, text, flags=re.MULTILINE)\n\npath.write_text(text, encoding=\"utf-8\")\nPY\n\n# Add #![allow(unused)] to all converter module files as inner attribute\n# since visitor feature gates many imports that are unused when visitor is disabled\nfind \"${DEST_DIR}/src/converter\" -type f -name \"*.rs\" -print0 | while IFS= read -r -d '' file; do\n  # Check if file already has #![allow(unused)]\n  if ! grep -q \"^\\s*#!\\[allow(unused)\" \"$file\"; then\n    # Add #![allow(unused)] at the very beginning of the file (before doc comments)\n    python3 - \"$file\" <<'PYFIX'\nimport sys\nfrom pathlib import Path\n\npath = Path(sys.argv[1])\ntext = path.read_text(encoding=\"utf-8\")\n\n# Add inner attribute at the very beginning\ntext = '#![allow(unused)]\\n' + text\n\npath.write_text(text, encoding=\"utf-8\")\nPYFIX\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/elixir/vendor-dependencies.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\necho \"=== Staging Elixir native dependencies ===\"\n\n# Stage the Rust core crate source into vendor/html-to-markdown-rs.\n# Transitive dependencies are NOT vendored — Cargo fetches them from\n# crates.io when the NIF is compiled during package installation.\n# This keeps the Hex tarball well under the 16 MB compressed size limit.\necho \"Staging Rust core crate...\"\n\"$SCRIPT_DIR/stage-rust-core.sh\"\n\nREPO_ROOT=\"$SCRIPT_DIR/../../..\"\nVENDOR_DIR=\"$REPO_ROOT/packages/elixir/native/html_to_markdown_elixir/vendor\"\n\necho \"Package size:\"\ndu -sh \"$VENDOR_DIR\" 2>/dev/null || true\n"
  },
  {
    "path": "scripts/publish/ensure-github-release-exists.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\ntag=\"${1:?Release tag argument required}\"\n\nif ! gh release view \"$tag\" >/dev/null 2>&1; then\n  gh release create \"$tag\" --title \"$tag\" --generate-notes\n  echo \"Created release $tag\"\nfi\n"
  },
  {
    "path": "scripts/publish/generate_elixir_checksums.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nVERSION=\"${1:?Usage: $0 <version>}\"\nREPO=\"kreuzberg-dev/html-to-markdown\"\nCHECKSUM_FILE=\"packages/elixir/checksum-Elixir.HtmlToMarkdown.Native.exs\"\n\nTARGETS=(\n  \"aarch64-apple-darwin\"\n  \"aarch64-unknown-linux-gnu\"\n  \"x86_64-unknown-linux-gnu\"\n)\n\nNIF_VERSIONS=(\"2.16\" \"2.17\")\n\nTMPDIR=$(mktemp -d)\ntrap 'rm -rf \"$TMPDIR\"' EXIT\n\necho \"Generating checksums for v${VERSION}...\"\nCHECKSUMS=()\n\nfor TARGET in \"${TARGETS[@]}\"; do\n  for NIF_VERSION in \"${NIF_VERSIONS[@]}\"; do\n    if [[ \"$TARGET\" == *\"windows\"* ]]; then EXT=\"dll\"; else EXT=\"so\"; fi\n    FILENAME=\"libhtml_to_markdown_nif-v${VERSION}-nif-${NIF_VERSION}-${TARGET}.${EXT}.tar.gz\"\n    URL=\"https://github.com/${REPO}/releases/download/v${VERSION}/${FILENAME}\"\n    echo \"Downloading: $FILENAME\"\n    if curl -fsSL -o \"${TMPDIR}/${FILENAME}\" \"$URL\"; then\n      if command -v sha256sum &>/dev/null; then\n        CHECKSUM=$(sha256sum \"${TMPDIR}/${FILENAME}\" | cut -d' ' -f1)\n      else\n        CHECKSUM=$(shasum -a 256 \"${TMPDIR}/${FILENAME}\" | cut -d' ' -f1)\n      fi\n      CHECKSUMS+=(\"  \\\"${FILENAME}\\\" => \\\"sha256:${CHECKSUM}\\\",\")\n    else\n      echo \"  ERROR: Failed to download $FILENAME\"\n      exit 1\n    fi\n  done\ndone\n\nmapfile -t SORTED < <(printf '%s\\n' \"${CHECKSUMS[@]}\" | sort)\n{\n  echo \"%{\"\n  for C in \"${SORTED[@]}\"; do echo \"$C\"; done\n  echo \"}\"\n} >\"$CHECKSUM_FILE\"\n\necho \"Generated checksums for ${#SORTED[@]} files.\"\ncat \"$CHECKSUM_FILE\"\n"
  },
  {
    "path": "scripts/publish/go/create-module-tag.sh",
    "content": "#!/usr/bin/env bash\n# Creates the Go submodule tag required for Go proxy to recognize the module.\n# For modules in subdirectories, Go requires tags in the format: {subdir}/{version}\n# e.g., packages/go/v3.23.0 for module github.com/kreuzberg-dev/html-to-markdown/packages/go/v3@v2.23.0\n#\n# Usage: create-module-tag.sh <version> [--dry-run]\n# Example: create-module-tag.sh v2.23.0\n\nset -euo pipefail\n\nVERSION=\"${1:?Version argument required (e.g. v2.25.1)}\"\n\n# Ensure version starts with 'v'\nif [[ ! \"$VERSION\" =~ ^v ]]; then\n  VERSION=\"v${VERSION}\"\nfi\n\n# The Go submodule tag format for modules in subdirectories\n# Module path: github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\n# Tag format: packages/go/{version} (the /v2 is part of the module path, not the tag prefix)\nGO_TAG=\"packages/go/${VERSION}\"\n\necho \"Creating Go module tag: ${GO_TAG}\"\necho \"  For module: github.com/kreuzberg-dev/html-to-markdown/packages/go/v3@${VERSION}\"\n\n# Check if Go tag already exists locally\nif git rev-parse \"$GO_TAG\" >/dev/null 2>&1; then\n  echo \"::notice::Go module tag $GO_TAG already exists locally; skipping.\"\n  exit 0\nfi\n\n# Check if tag exists on remote\nif git ls-remote --tags origin | grep -q \"refs/tags/${GO_TAG}$\"; then\n  echo \"::notice::Go module tag $GO_TAG already exists on remote; skipping.\"\n  exit 0\nfi\n\nif [[ \"${2:-}\" == \"--dry-run\" ]]; then\n  echo \"[DRY RUN] Would create tag: ${GO_TAG} -> ${VERSION}\"\n  exit 0\nfi\n\ngit tag \"$GO_TAG\" \"$VERSION\"\ngit push origin \"$GO_TAG\"\n\necho \"Go module tag created and pushed: ${GO_TAG}\"\n\n# Trigger Go proxy to fetch the module (optional but speeds up availability)\necho \"Triggering Go proxy fetch...\"\nGOPROXY_URL=\"https://proxy.golang.org/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/@v/${VERSION}.info\"\nif curl -sf \"${GOPROXY_URL}\" >/dev/null 2>&1; then\n  echo \"Go proxy successfully fetched module version\"\nelse\n  echo \"Note: Go proxy may take a few minutes to index the new version\"\nfi\n\necho \"\"\necho \"Go module published successfully!\"\necho \"  Module: github.com/kreuzberg-dev/html-to-markdown/packages/go/v3@${VERSION}\"\necho \"  Install: go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3@${VERSION}\"\n"
  },
  {
    "path": "scripts/publish/java/copy-native-libs.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# This script copies downloaded native FFI libraries into the Maven resources directory\n# so they can be bundled in the published JAR\n\nartifacts_dir=\"${1:?ARTIFACTS_DIR is required (e.g. java-ffi-artifacts)}\"\nresources_dir=\"packages/java/src/main/resources/natives\"\n\necho \"Copying native libraries from ${artifacts_dir} to ${resources_dir}\"\n\nmkdir -p \"${resources_dir}\"\n\n# Remove any existing native libraries\nif [[ -d \"${resources_dir:?}\" ]]; then\n  rm -rf \"${resources_dir:?}\"/*\nfi\n\n# Copy all platform-specific native libraries\nfor platform_dir in \"${artifacts_dir}\"/*; do\n  if [[ -d \"${platform_dir}\" ]]; then\n    platform_name=\"$(basename \"${platform_dir}\")\"\n    echo \"  Copying ${platform_name}...\"\n    mkdir -p \"${resources_dir}/${platform_name}\"\n    cp -r \"${platform_dir}/native/.\" \"${resources_dir}/${platform_name}/\"\n  fi\ndone\n\necho \"Native libraries copied successfully:\"\nfind \"${resources_dir}\" -type f -name \"*.so\" -o -name \"*.dylib\" -o -name \"*.dll\" | while read -r lib; do\n  echo \"  - ${lib}\"\n  ls -lh \"${lib}\"\ndone\n"
  },
  {
    "path": "scripts/publish/maven/patch-legacy-gpg-args.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\npom_file=\"${1:-packages/java/pom.xml}\"\n\nif [ ! -f \"$pom_file\" ]; then\n  echo \"Error: pom.xml not found: $pom_file\" >&2\n  exit 1\nfi\n\nif grep -q '<arg>--pinentry-mode</arg>' \"$pom_file\"; then\n  sed -i 's/<arg>--pinentry-mode<\\/arg>\\s*<arg>loopback<\\/arg>/<arg>--pinentry-mode=loopback<\\/arg>/g' \"$pom_file\"\n  echo \"Patched legacy GPG pinentry argument format in $pom_file\"\nelse\n  echo \"No legacy GPG arguments found in $pom_file\"\nfi\n"
  },
  {
    "path": "scripts/publish/maven/prefer-gpg2.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif command -v gpg2 >/dev/null 2>&1; then\n  mkdir -p \"${HOME}/.local/bin\"\n  printf '#!/usr/bin/env bash\\nexec gpg2 \"$@\"\\n' >\"${HOME}/.local/bin/gpg\"\n  chmod +x \"${HOME}/.local/bin/gpg\"\n  echo \"${HOME}/.local/bin\" >>\"${GITHUB_PATH}\"\n  echo \"PATH=${HOME}/.local/bin:${PATH}\" >>\"${GITHUB_ENV}\"\nfi\n"
  },
  {
    "path": "scripts/publish/node/build-native-module.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$target = $env:TARGET\nif (-not $target) { throw \"TARGET is required\" }\n$args = @('--platform', '--release', '--target', $target, '--output-dir', './artifacts')\nif ($env:USE_NAPI_CROSS -eq 'true') { $args += '--use-napi-cross' }\nif ($env:USE_CROSS -eq 'true') { $args += '--use-cross' }\npnpm --filter ./crates/html-to-markdown-node exec napi build @args\n"
  },
  {
    "path": "scripts/publish/node/build-native-module.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\nuse_cross=\"${USE_CROSS:-false}\"\nuse_napi_cross=\"${USE_NAPI_CROSS:-false}\"\n\nargs=(--platform --release --target \"${target}\" --output-dir ./artifacts)\nif [[ \"${use_napi_cross}\" == \"true\" ]]; then\n  args+=(--use-napi-cross)\nfi\nif [[ \"${use_cross}\" == \"true\" ]]; then\n  args+=(--use-cross)\nfi\n\npnpm --filter ./crates/html-to-markdown-node exec napi build \"${args[@]}\"\n"
  },
  {
    "path": "scripts/publish/node/clean-npm-dir.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nRemove-Item -Recurse -Force crates/html-to-markdown-node\\npm -ErrorAction SilentlyContinue\n"
  },
  {
    "path": "scripts/publish/node/clean-npm-dir.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrm -rf crates/html-to-markdown-node/npm\n"
  },
  {
    "path": "scripts/publish/node/create-npm-package-structure.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm --filter ./crates/html-to-markdown-node exec napi create-npm-dirs\n"
  },
  {
    "path": "scripts/publish/node/generate-typescript-defs.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm --filter ./crates/html-to-markdown-node exec napi build --release\nmkdir -p typescript-defs\ncp crates/html-to-markdown-node/index.js crates/html-to-markdown-node/index.d.ts typescript-defs/\n"
  },
  {
    "path": "scripts/publish/node/install-node-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm install --filter ./crates/html-to-markdown-node...\n"
  },
  {
    "path": "scripts/publish/node/pack-platform-packages.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncd crates/html-to-markdown-node/npm\nfor dir in */; do\n  if [ -f \"$dir/package.json\" ]; then\n    (cd \"$dir\" && npm pack && mv ./*.tgz ..)\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/node/package-artifacts.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$target = $env:TARGET\nif (-not $target) { throw \"TARGET is required\" }\npnpm --filter ./crates/html-to-markdown-node exec napi artifacts --output-dir ./artifacts\nif (-Not (Test-Path crates/html-to-markdown-node\\npm)) { throw \"npm artifact directory missing\" }\ntar -czf \"node-bindings-$target.tar.gz\" -C crates/html-to-markdown-node npm\n"
  },
  {
    "path": "scripts/publish/node/package-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntarget=\"${TARGET:?TARGET is required}\"\n\npnpm --filter ./crates/html-to-markdown-node exec napi artifacts --output-dir ./artifacts\ntest -d crates/html-to-markdown-node/npm || {\n  echo \"npm artifact directory missing\"\n  exit 1\n}\ntar -czf \"node-bindings-${target}.tar.gz\" -C crates/html-to-markdown-node npm\n"
  },
  {
    "path": "scripts/publish/node/prepare-artifact-directory.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrm -rf crates/html-to-markdown-node/npm\nmkdir -p crates/html-to-markdown-node\nfor pkg in node-artifacts/*.tar.gz; do\n  tar -xzf \"${pkg}\" -C crates/html-to-markdown-node\ndone\ncp typescript-defs/index.js typescript-defs/index.d.ts crates/html-to-markdown-node/\n"
  },
  {
    "path": "scripts/publish/node/prepublish-main-package.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npkg_dir=\"${1:-crates/html-to-markdown-node}\"\n\nif [ ! -d \"$pkg_dir\" ]; then\n  echo \"Package directory not found: $pkg_dir\" >&2\n  exit 1\nfi\n\nnpm_dir=\"$pkg_dir/npm\"\nif [ ! -d \"$npm_dir\" ]; then\n  echo \"Platform npm directory not found: $npm_dir\" >&2\n  exit 1\nfi\n\nif ! command -v jq >/dev/null 2>&1; then\n  echo \"jq is required to stage optionalDependencies for the main Node package\" >&2\n  exit 1\nfi\n\ntmp_pkg_json=\"$(mktemp)\"\ntrap 'rm -f \"$tmp_pkg_json\"' EXIT\n\noptional_deps_json='{}'\n\nshopt -s nullglob\nfor platform_pkg_json in \"$npm_dir\"/*/package.json; do\n  name=\"$(jq -r '.name // empty' \"$platform_pkg_json\")\"\n  version=\"$(jq -r '.version // empty' \"$platform_pkg_json\")\"\n\n  if [ -z \"$name\" ] || [ -z \"$version\" ]; then\n    echo \"Invalid platform package.json: $platform_pkg_json\" >&2\n    exit 1\n  fi\n\n  optional_deps_json=\"$(jq -c --arg n \"$name\" --arg v \"$version\" '. + {($n): $v}' <<<\"$optional_deps_json\")\"\ndone\n\nif [ \"$optional_deps_json\" = \"{}\" ]; then\n  echo \"No platform packages found under $npm_dir\" >&2\n  exit 1\nfi\n\njq --argjson deps \"$optional_deps_json\" '.optionalDependencies = $deps' \"$pkg_dir/package.json\" >\"$tmp_pkg_json\"\nmv \"$tmp_pkg_json\" \"$pkg_dir/package.json\"\n\necho \"Injected optionalDependencies into $pkg_dir/package.json:\"\njq '.optionalDependencies' \"$pkg_dir/package.json\"\n"
  },
  {
    "path": "scripts/publish/python/build-cli-for-sdist.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncargo build --release --package html-to-markdown-cli\n"
  },
  {
    "path": "scripts/publish/python/build-sdist.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/python >/dev/null\nmaturin sdist --out dist/\npopd >/dev/null\n"
  },
  {
    "path": "scripts/publish/python/install-build-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npython -m pip install --upgrade pip\npip install maturin\n"
  },
  {
    "path": "scripts/publish/python/prepare-sdist-with-cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npython scripts/prepare_wheel.py\n"
  },
  {
    "path": "scripts/publish/r/already-published-summary.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nversion=\"${VERSION:?VERSION is required}\"\necho \"CRAN package ${version} already published; skipping.\" >>\"${GITHUB_STEP_SUMMARY}\"\n"
  },
  {
    "path": "scripts/publish/r/build-cran-package.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$SCRIPT_DIR/../../..\"\n\n# Vendor all dependencies (stages core crate + vendors transitive deps + creates vendor.tar.xz)\n\"$SCRIPT_DIR/vendor-dependencies.sh\"\n\n# Build the R source package\necho \"\"\necho \"=== Building R CRAN source package ===\"\ncd \"$REPO_ROOT/packages/r\"\nR CMD build .\n\necho \"\"\necho \"=== Build complete ===\"\nls -lh htmltomarkdown_*.tar.gz\n"
  },
  {
    "path": "scripts/publish/r/run-tests.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$SCRIPT_DIR/../../..\"\n\n# Stage the Rust core crate\n\"$SCRIPT_DIR/stage-rust-core.sh\"\n\n# Install the R package\necho \"Installing R package...\"\nR CMD INSTALL \"$REPO_ROOT/packages/r\"\n\n# Run tests\necho \"Running tests...\"\ncd \"$REPO_ROOT/packages/r\"\nRscript -e 'devtools::test()'\n"
  },
  {
    "path": "scripts/publish/r/stage-rust-core.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../../..\" && pwd)\"\n\nSRC_DIR=\"${ROOT_DIR}/crates/html-to-markdown\"\nDEST_DIR=\"${ROOT_DIR}/packages/r/src/rust/vendor/html-to-markdown-rs\"\n\nVERSION=\"$(\n  python3 - \"${ROOT_DIR}/Cargo.toml\" <<'PY'\nimport re\nfrom pathlib import Path\nimport sys\n\ntext = Path(sys.argv[1]).read_text(encoding=\"utf-8\")\nin_workspace_pkg = False\nfor line in text.splitlines():\n    if line.strip() == \"[workspace.package]\":\n        in_workspace_pkg = True\n        continue\n    if in_workspace_pkg and line.startswith(\"[\") and line.strip().startswith(\"[\") and line.strip() != \"[workspace.package]\":\n        in_workspace_pkg = False\n    if in_workspace_pkg:\n        m = re.match(r'version\\s*=\\s*\"([^\"]+)\"\\s*$', line.strip())\n        if m:\n            print(m.group(1))\n            raise SystemExit(0)\nraise SystemExit(\"Failed to find [workspace.package] version in Cargo.toml\")\nPY\n)\"\n\nif [[ ! -d \"${SRC_DIR}\" ]]; then\n  echo \"Missing Rust core crate at ${SRC_DIR}\" >&2\n  exit 1\nfi\n\nrm -rf \"${DEST_DIR}\"\nmkdir -p \"$(dirname \"${DEST_DIR}\")\"\n\nif command -v rsync >/dev/null 2>&1; then\n  rsync -a --delete --exclude target --exclude .git \"${SRC_DIR}/\" \"${DEST_DIR}/\"\nelse\n  cp -R \"${SRC_DIR}\" \"${DEST_DIR}\"\n  rm -rf \"${DEST_DIR}/target\" \"${DEST_DIR}/.git\" || true\nfi\n\npython3 - \"${DEST_DIR}/Cargo.toml\" \"${VERSION}\" <<'PY'\nimport re\nimport sys\nfrom pathlib import Path\n\npath = Path(sys.argv[1])\nversion = sys.argv[2]\ntext = path.read_text(encoding=\"utf-8\")\n\nreplacements = {\n    r\"^version\\.workspace\\s*=\\s*true\\s*$\": f'version = \"{version}\"',\n    r\"^edition\\.workspace\\s*=\\s*true\\s*$\": 'edition = \"2024\"',\n    r\"^authors\\.workspace\\s*=\\s*true\\s*$\": 'authors = [\"Na\\'aman Hirschfeld <nhirschfeld@gmail.com>\"]',\n    r\"^license\\.workspace\\s*=\\s*true\\s*$\": 'license = \"MIT\"',\n    r\"^repository\\.workspace\\s*=\\s*true\\s*$\": 'repository = \"https://github.com/kreuzberg-dev/html-to-markdown\"',\n    r\"^homepage\\.workspace\\s*=\\s*true\\s*$\": 'homepage = \"https://github.com/kreuzberg-dev/html-to-markdown\"',\n    r\"^documentation\\.workspace\\s*=\\s*true\\s*$\": 'documentation = \"https://docs.rs/html-to-markdown-rs\"',\n    r\"^rust-version\\.workspace\\s*=\\s*true\\s*$\": 'rust-version = \"1.85\"',\n    r\"^\\[lints\\]\\s*\\nworkspace\\s*=\\s*true\\s*$\": '[lints]\\nrust.unsafe_code = \"forbid\"\\nrust.missing_docs = \"warn\"\\nrust.unused_must_use = \"deny\"',\n    r\"^tl\\.workspace\\s*=\\s*true\\s*$\": 'tl = { package = \"astral-tl\", version = \"0.7.11\" }',\n    r\"^regex\\.workspace\\s*=\\s*true\\s*$\": 'regex = \"1.12\"',\n    r\"^once_cell\\.workspace\\s*=\\s*true\\s*$\": 'once_cell = \"1.21\"',\n    r\"^thiserror\\.workspace\\s*=\\s*true\\s*$\": 'thiserror = \"2.0\"',\n    r\"^base64\\.workspace\\s*=\\s*true\\s*$\": 'base64 = \"0.22\"',\n    r\"^ahash\\.workspace\\s*=\\s*true\\s*$\": 'ahash = \"0.8\"',\n    r\"^html5ever\\.workspace\\s*=\\s*true\\s*$\": 'html5ever = \"0.36\"',\n    r\"^async-trait\\s*=\\s*{\\s*workspace\\s*=\\s*true,\\s*optional\\s*=\\s*true\\s*}\\s*$\": 'async-trait = { version = \"0.1\", optional = true }',\n}\n\nfor pattern, replacement in replacements.items():\n    text = re.sub(pattern, replacement, text, flags=re.MULTILINE)\n\npath.write_text(text, encoding=\"utf-8\")\nPY\n\n# Add #![allow(unused)] to all converter module files as inner attribute\n# since visitor feature gates many imports that are unused when visitor is disabled\nfind \"${DEST_DIR}/src/converter\" -type f -name \"*.rs\" -print0 | while IFS= read -r -d '' file; do\n  # Check if file already has #![allow(unused)]\n  if ! grep -q \"^\\s*#!\\[allow(unused)\" \"$file\"; then\n    # Add #![allow(unused)] at the very beginning of the file (before doc comments)\n    python3 - \"$file\" <<'PYFIX'\nimport sys\nfrom pathlib import Path\n\npath = Path(sys.argv[1])\ntext = path.read_text(encoding=\"utf-8\")\n\n# Add inner attribute at the very beginning\ntext = '#![allow(unused)]\\n' + text\n\npath.write_text(text, encoding=\"utf-8\")\nPYFIX\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/r/vendor-dependencies.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$SCRIPT_DIR/../../..\"\nR_PKG=\"$REPO_ROOT/packages/r\"\nRUST_DIR=\"$R_PKG/src/rust\"\n\necho \"=== Vendoring R package dependencies ===\"\n\n# Step 1: Stage the Rust core crate (copies html-to-markdown to vendor/)\necho \"Step 1: Staging Rust core crate...\"\n\"$SCRIPT_DIR/stage-rust-core.sh\"\n\n# Step 2: Vendor all transitive dependencies using cargo vendor\necho \"\"\necho \"Step 2: Vendoring all transitive dependencies...\"\ncd \"$RUST_DIR\"\n\necho \"Running cargo vendor...\"\ncargo vendor vendor >/dev/null\n\n# Step 3: Create vendor-config.toml (source replacement for vendored builds)\necho \"Step 3: Creating vendor-config.toml...\"\ncat >vendor-config.toml <<'TOML'\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.vendored-sources]\ndirectory = \"vendor\"\nTOML\n\n# Step 4: Add .cargo-checksum.json for html-to-markdown-rs (path dep needs it)\necho \"Step 4: Creating checksum for html-to-markdown-rs...\"\necho '{\"files\":{}}' >vendor/html-to-markdown-rs/.cargo-checksum.json\n\n# Step 5: Clean up unnecessary files to reduce tarball size\necho \"Step 5: Cleaning up vendored dependencies...\"\n\n# Remove test/bench/doc directories (but keep root-level files they may be included from)\nwhile IFS= read -r dir; do\n  rm -rf \"$dir\"\ndone < <(find vendor -type d \\( -name \"tests\" -o -name \"benches\" -o -name \"examples\" -o -name \"docs\" -o -name \".github\" -o -name \"ci\" \\) 2>/dev/null)\n\n# Remove documentation and metadata files, but be careful about root-level README\n# that may be referenced by macros like include_str!\nfind vendor -type f -path \"*/tests/*\" -o -path \"*/benches/*\" \\( \\\n  -name \"*.md\" -o \\\n  -name \"LICENSE*\" -o \\\n  -name \"CHANGELOG*\" \\\n  \\) -delete 2>/dev/null || true\n\nfind vendor -type f \\( \\\n  -name \".git*\" -o \\\n  -name \".cargo-ok\" -o \\\n  -name \"*.html\" -o \\\n  -name \"*.yml\" -o \\\n  -name \"*.yaml\" \\\n  \\) -delete 2>/dev/null || true\n\n# Remove LICENSE/CHANGELOG at crate root only if not referenced in build scripts\nfind vendor -maxdepth 2 -type f \\( \\\n  -name \"LICENSE*\" -o \\\n  -name \"CHANGELOG*\" \\\n  \\) -delete 2>/dev/null || true\n\n# Remove static libraries (pre-built binaries not needed for source distribution)\nfind vendor -type f -name \"*.a\" -delete 2>/dev/null || true\n\n# Step 5b: Regenerate .cargo-checksum.json for all crates to match actual files\n# by removing test references from the checksums\necho \"Regenerating .cargo-checksum.json files...\"\npython3 - \"$RUST_DIR/vendor\" <<'PY_CHECKSUM'\nimport json\nimport sys\nfrom pathlib import Path\n\nvendor_dir = Path(sys.argv[1])\n\nfor checksum_path in sorted(vendor_dir.glob('*/.cargo-checksum.json')):\n    try:\n        data = json.loads(checksum_path.read_text())\n        # Remove files that no longer exist (tests, benches, etc)\n        files = data.get('files', {})\n        filtered_files = {}\n\n        crate_dir = checksum_path.parent\n        for file_path in files:\n            full_path = crate_dir / file_path\n            if full_path.exists():\n                filtered_files[file_path] = files[file_path]\n\n        if len(filtered_files) != len(files):\n            data['files'] = filtered_files\n            checksum_path.write_text(json.dumps(data))\n            removed = len(files) - len(filtered_files)\n            # print(f\"  {checksum_path.parent.name}: removed {removed} missing file(s)\")\n    except Exception as e:\n        print(f\"  Warning: Could not update checksum for {checksum_path.parent.name}: {e}\")\nPY_CHECKSUM\n\n# Step 6: Generate inst/AUTHORS from vendored crate metadata\necho \"Step 6: Generating inst/AUTHORS...\"\nmkdir -p \"$R_PKG/inst\"\npython3 - \"$RUST_DIR/vendor\" \"$R_PKG/inst/AUTHORS\" <<'PY'\nimport sys\nimport tomllib\nfrom pathlib import Path\n\nvendor_dir = Path(sys.argv[1])\nout_path = Path(sys.argv[2])\n\nlines = [\n    \"# Authors of vendored Rust crates\",\n    \"\",\n    \"This file lists the authors of the Rust crates vendored in this package.\",\n    \"\",\n    \"The htmltomarkdown R package includes Rust code from the following crates:\",\n    \"\",\n]\n\nentries = []\nfor cargo_toml in sorted(vendor_dir.glob(\"*/Cargo.toml\")):\n    try:\n        data = tomllib.loads(cargo_toml.read_text(encoding=\"utf-8\"))\n        pkg = data.get(\"package\", {})\n        name = pkg.get(\"name\", cargo_toml.parent.name)\n        version = pkg.get(\"version\", \"?\")\n        authors = pkg.get(\"authors\", [])\n        license_val = pkg.get(\"license\", \"unknown\")\n        author_str = \", \".join(authors) if authors else \"(no authors listed)\"\n        entries.append(f\"{name} {version} ({license_val}): {author_str}\")\n    except Exception:\n        continue\n\nlines.extend(entries)\nlines.append(\"\")\nout_path.write_text(\"\\n\".join(lines), encoding=\"utf-8\")\nprint(f\"  Generated {len(entries)} crate entries in inst/AUTHORS\")\nPY\n\n# Step 7: Create vendor.tar.xz\necho \"Step 7: Creating vendor.tar.xz...\"\nrm -f vendor.tar.xz\ntar -cJ -f vendor.tar.xz vendor\n\n# Step 8: Remove extracted vendor directory\necho \"Step 8: Cleaning up extracted vendor directory...\"\nrm -rf vendor\n\n# Summary\ncrate_count=$(tar -tf vendor.tar.xz | grep -c '^vendor/[^/]*/Cargo.toml$' || true)\ntarball_size=$(du -h vendor.tar.xz | cut -f1)\necho \"\"\necho \"=== Vendoring complete ===\"\necho \"  Vendored ${crate_count} crates\"\necho \"  Tarball size: ${tarball_size}\"\necho \"  Output: ${RUST_DIR}/vendor.tar.xz\"\n"
  },
  {
    "path": "scripts/publish/ruby/already-published-summary.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nversion=\"${VERSION:?VERSION is required}\"\necho \"RubyGem version ${version} already published; skipping.\" >>\"${GITHUB_STEP_SUMMARY}\"\n"
  },
  {
    "path": "scripts/publish/ruby/build-gem-unix.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$SCRIPT_DIR/../../..\"\n\n# Clean up any vendored files and build artifacts from previous runs\nrm -rf packages/ruby/vendor/html-to-markdown-rs packages/ruby/vendor/Cargo.toml packages/ruby/pkg\ngit restore \\\n  packages/ruby/ext/html_to_markdown_rb/Cargo.toml \\\n  packages/ruby/ext/html_to_markdown_rb/native/Cargo.toml \\\n  2>/dev/null || true\n\n# Build CLI binary BEFORE vendoring to avoid package collision\necho \"Building CLI binary before vendoring...\"\ncargo build --release --package html-to-markdown-cli\n\n# Copy CLI binary into gem\nruby \"$REPO_ROOT/scripts/prepare_ruby_gem.rb\"\n\n# Vendor core crate using Python script (like kreuzberg)\necho \"Vendoring core crate...\"\npython3 \"$REPO_ROOT/scripts/ci/ruby/vendor-core-crate.py\"\n\npushd packages/ruby >/dev/null\nbundle install\n\n# Ensure source gem packages the vendored crate so end-users on platforms\n# without a precompiled gem (e.g., aarch64-linux fallback) can build from\n# source. The alef-generated gemspec only globs lib/ ext/ sig/, missing\n# vendor/. Inject a one-line glob extension before rake build.\n# Fixes #325.\nif grep -q \"Dir.glob(%w\\[lib/\" html_to_markdown.gemspec && ! grep -q \"vendor/\\*\\*/\\*\" html_to_markdown.gemspec; then\n  sed -i.bak 's|Dir.glob(%w\\[\\([^]]*\\)\\])|Dir.glob(%w[\\1 vendor/**/*])|' html_to_markdown.gemspec\n  rm -f html_to_markdown.gemspec.bak\nfi\n\n# Build source gem\nbundle exec rake build\n\n# Build native platform gem with precompiled extension\nbundle exec rake compile\npopd >/dev/null\n\n# Detect platform for build-native-gem.rb\ncase \"$(uname -s)-$(uname -m)\" in\nLinux-x86_64) PLATFORM=\"x86_64-linux\" ;;\nLinux-aarch64) PLATFORM=\"aarch64-linux\" ;;\nDarwin-arm64) PLATFORM=\"arm64-darwin\" ;;\nDarwin-x86_64) PLATFORM=\"x86_64-linux\" ;;\n*)\n  echo \"WARNING: Unknown platform, skipping native gem build\"\n  exit 0\n  ;;\nesac\n\nruby \"$SCRIPT_DIR/build-native-gem.rb\" \"$PLATFORM\"\n"
  },
  {
    "path": "scripts/publish/ruby/build-gem-windows.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$workspace = ridk exec bash -lc \"cygpath -au '$env:GITHUB_WORKSPACE'\"\n$gemdir = \"$workspace/packages/ruby\"\n\n# Build CLI binary BEFORE vendoring to avoid package collision\nWrite-Host \"Building CLI binary before vendoring...\"\nridk exec bash -lc \"cd $workspace && export RUSTUP_TOOLCHAIN=stable-gnu && cargo build --release --package html-to-markdown-cli\"\n\n# Copy CLI binary into gem\nridk exec bash -lc \"cd $workspace && export RUSTUP_TOOLCHAIN=stable-gnu && ruby scripts/prepare_ruby_gem.rb\"\n\n# Vendor core crate using Python script (like kreuzberg)\nridk exec bash -lc \"cd $workspace && python3 scripts/ci/ruby/vendor-core-crate.py\"\n\n# Ensure source gem packages vendor/ — fixes #325\nridk exec bash -lc \"cd $gemdir && if grep -q 'Dir.glob(%w\\[lib/' html_to_markdown.gemspec && ! grep -q 'vendor/\\*\\*/\\*' html_to_markdown.gemspec; then sed -i.bak 's|Dir.glob(%w\\[\\([^]]*\\)\\])|Dir.glob(%w[\\1 vendor/**/*])|' html_to_markdown.gemspec && rm -f html_to_markdown.gemspec.bak; fi\"\n\n# Build source gem\nridk exec bash -lc \"cd $gemdir && export RUSTUP_TOOLCHAIN=stable-gnu CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ && bundle exec rake build\"\n"
  },
  {
    "path": "scripts/publish/ruby/build-native-gem.rb",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\n# Build a platform-specific pre-compiled gem.\n#\n# Usage: ruby build-native-gem.rb <platform>\n#\n# Platforms:\n#   x86_64-linux\n#   aarch64-linux\n#   arm64-darwin\n\nrequire 'rubygems'\nrequire 'rubygems/package'\nrequire 'fileutils'\n\nplatform = ARGV[0] or abort \"Usage: #{$PROGRAM_NAME} <platform>\"\n\nVALID_PLATFORMS = %w[x86_64-linux aarch64-linux arm64-darwin].freeze\nunless VALID_PLATFORMS.include?(platform)\n  abort \"ERROR: Invalid platform '#{platform}'. Valid: #{VALID_PLATFORMS.join(', ')}\"\nend\n\n# Work from the Ruby package directory\ngem_dir = File.expand_path('../../../packages/ruby', __dir__)\nDir.chdir(gem_dir)\n\n# Validate compiled native library exists\nnative_extensions = Dir.glob('lib/**/*.{so,bundle,dylib}')\nif native_extensions.empty?\n  abort \"ERROR: No compiled native extensions found in lib/. Run 'rake compile' first.\"\nend\n\nputs \"Found native extensions: #{native_extensions.join(', ')}\"\n\n# Load the gemspec\nspec = Gem::Specification.load('html_to_markdown.gemspec')\nabort 'ERROR: Could not load html_to_markdown.gemspec' unless spec\n\n# Set platform (transforms source gem into platform gem)\nspec.platform = Gem::Platform.new(platform)\n\n# Remove extensions field — pre-compiled gems skip install-time compilation\nspec.extensions = []\n\n# Ensure native artifacts are in the file list\nnative_extensions.each do |ext|\n  spec.files << ext unless spec.files.include?(ext)\nend\n\n# Remove vendor/ and ext/ source files — not needed in platform gems\nspec.files.reject! { |f| f.start_with?('vendor/') || f.start_with?('ext/') }\n\n# Remove rb_sys runtime dependency — only needed for source compilation\nspec.dependencies.reject! { |d| d.name == 'rb_sys' }\n\nspec.files.uniq!\n\nputs \"Building gem: #{spec.name}-#{spec.version}-#{spec.platform}\"\nputs \"Files: #{spec.files.length} (native: #{native_extensions.length})\"\n\n# Build the gem\nFileUtils.mkdir_p('pkg')\ngem_file = Gem::Package.build(spec)\nFileUtils.mv(gem_file, \"pkg/#{gem_file}\") if File.exist?(gem_file) && !File.exist?(\"pkg/#{gem_file}\")\n\nputs \"Built: pkg/#{gem_file}\"\n"
  },
  {
    "path": "scripts/publish/ruby/configure-bindgen-windows.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [[ -z \"${RI_DEVKIT:-}\" ]]; then\n  if [[ -d \"/ucrt64\" ]]; then\n    RI_DEVKIT=\"/ucrt64\"\n  elif [[ -d \"C:/msys64\" ]]; then\n    RI_DEVKIT=\"C:/msys64\"\n  else\n    echo \"RI_DEVKIT is unset and no default devkit path found\" >&2\n    exit 1\n  fi\nfi\n\nRI_DEVKIT_POSIX=\"${RI_DEVKIT//\\\\/\\/}\"\nMSYSTEM_PREFIX=\"${MSYSTEM_PREFIX:-/ucrt64}\"\necho \"BINDGEN_EXTRA_CLANG_ARGS=--target=x86_64-pc-windows-gnu --sysroot=${RI_DEVKIT_POSIX}${MSYSTEM_PREFIX}\" >>\"$GITHUB_ENV\"\n"
  },
  {
    "path": "scripts/publish/ruby/install-deps-unix.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npushd packages/ruby >/dev/null\nbundle install --jobs 4 --retry 3\npopd >/dev/null\n"
  },
  {
    "path": "scripts/publish/ruby/install-deps-windows.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$workspace = ridk exec bash -lc \"cygpath -au '$env:GITHUB_WORKSPACE'\"\n$gemdir = \"$workspace/packages/ruby\"\nridk exec bash -lc \"cd $gemdir && export RUSTUP_TOOLCHAIN=stable-gnu CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ && bundle install --jobs 4 --retry 3\"\n"
  },
  {
    "path": "scripts/publish/ruby/install-msys2-toolchain.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nridk exec pacman -S --needed --noconfirm base-devel mingw-w64-ucrt-x86_64-toolchain\n"
  },
  {
    "path": "scripts/publish/ruby/install-rust-gnu.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nrustup toolchain install stable-gnu --profile minimal --no-self-update\n"
  },
  {
    "path": "scripts/publish/ruby/remove-cached-cli.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nrm -f packages/ruby/lib/bin/html-to-markdown*\n"
  },
  {
    "path": "scripts/publish/typescript/build-package.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncd packages/typescript\n\n# Build only the TypeScript part (native bindings are already published)\n# The native bindings should already be available from npm at this point\n# since we depend on publish-node completing first\npnpm exec tsc --project tsconfig.json\n\necho \"TypeScript wrapper package built successfully\"\n"
  },
  {
    "path": "scripts/publish/upload-c-ffi-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag=\"${TAG:?TAG is required}\"\n\nshopt -s nullglob\narchives=(dist/c-ffi/*/*.{tar.gz,zip})\n\nif [ ${#archives[@]} -eq 0 ]; then\n  echo \"ERROR: No artifact files found in dist/c-ffi/\"\n  exit 1\nfi\n\nfor archive in \"${archives[@]}\"; do\n  if [ -f \"$archive\" ]; then\n    gh release upload \"${tag}\" \"${archive}\" --clobber\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/upload-cli-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag=\"${TAG:?TAG is required}\"\n\nshopt -s nullglob\nfor archive in dist/cli/*/*.{tar.gz,zip}; do\n  if [ -f \"$archive\" ]; then\n    gh release upload \"${tag}\" \"${archive}\" --clobber\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/upload-elixir-package.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag=\"${TAG:?TAG is required}\"\n\nshopt -s nullglob\nfor pkg in dist/elixir/*.tar; do\n  gh release upload \"${tag}\" \"${pkg}\" --clobber\ndone\n"
  },
  {
    "path": "scripts/publish/upload-go-ffi-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag=\"${TAG:?TAG is required}\"\n\nshopt -s nullglob\nfor archive in dist/go-ffi/*/*.{tar.gz,zip}; do\n  if [ -f \"$archive\" ]; then\n    gh release upload \"${tag}\" \"${archive}\" --clobber\n  fi\ndone\n"
  },
  {
    "path": "scripts/publish/upload-homebrew-bottles.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\ntag=\"${1:?Release tag argument required}\"\nartifacts_dir=\"${2:-dist/homebrew}\"\n\nif [ ! -d \"$artifacts_dir\" ]; then\n  echo \"Error: Artifacts directory not found: $artifacts_dir\" >&2\n  exit 1\nfi\n\nexisting_assets=\"$(mktemp)\"\ntrap 'rm -f \"$existing_assets\"' EXIT\n\ngh release view \"$tag\" --json assets | jq -r '.assets[].name' >\"$existing_assets\" 2>/dev/null || true\n\nbottle_count=0\nfor file in \"$artifacts_dir\"/html-to-markdown-*.bottle.tar.gz; do\n  if [ -f \"$file\" ]; then\n    base=\"$(basename \"$file\")\"\n    if grep -Fxq \"$base\" \"$existing_assets\"; then\n      echo \"Skipping $base (already uploaded)\"\n    else\n      gh release upload \"$tag\" \"$file\"\n      echo \"Uploaded $base\"\n      ((bottle_count++)) || true\n    fi\n  fi\ndone\n\nif [ \"$bottle_count\" -eq 0 ]; then\n  echo \"Note: No new bottles uploaded (all may already exist in release)\"\nfi\n\necho \"Homebrew bottles upload complete for $tag\"\n"
  },
  {
    "path": "scripts/publish/upload-php-pie.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ntag=\"${TAG:?TAG is required}\"\n\nshopt -s nullglob\npackages=(\n  dist/php-package/php_*.tgz\n  dist/php-package/php_*.tgz.sha256\n  dist/php-package/php_*.zip\n  dist/php-package/php_*.zip.sha256\n)\n\nif [[ ${#packages[@]} -eq 0 ]]; then\n  echo \"::warning::No PHP package artifacts found in dist/php-package/\" >&2\n  exit 0\nfi\n\nfor file in \"${packages[@]}\"; do\n  echo \"Uploading: ${file}\"\n  gh release upload \"${tag}\" \"${file}\" --clobber\ndone\n"
  },
  {
    "path": "scripts/publish/validate-and-compute-metadata.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nevent=\"${GITHUB_EVENT_NAME}\"\ntag_input=\"${INPUT_TAG:-}\"\ndry_run_input_env=\"${INPUT_DRY_RUN:-false}\"\nref_input_env=\"${INPUT_REF:-}\"\nrelease_tag=\"${EVENT_RELEASE_TAG:-}\"\ndispatch_tag=\"${EVENT_DISPATCH_TAG:-}\"\ndispatch_dry_run=\"${EVENT_DISPATCH_DRY_RUN:-}\"\ndispatch_ref=\"${EVENT_DISPATCH_REF:-}\"\n\ncase \"${event}\" in\nworkflow_dispatch)\n  tag=\"${tag_input}\"\n  dry_run_input=\"${dry_run_input_env}\"\n  ref_input=\"${ref_input_env}\"\n  ;;\nrelease)\n  tag=\"${release_tag}\"\n  dry_run_input=\"false\"\n  ref_input=\"refs/tags/${tag}\"\n  ;;\nrepository_dispatch)\n  tag=\"${dispatch_tag}\"\n  dry_run_input=\"${dispatch_dry_run}\"\n  ref_input=\"${dispatch_ref}\"\n  ;;\n*)\n  tag=\"${GITHUB_REF_NAME:-}\"\n  dry_run_input=\"false\"\n  ref_input=\"\"\n  if [[ \"${tag}\" == *-pre* ]]; then\n    dry_run_input=\"true\"\n  fi\n  ;;\nesac\n\nif [[ -z \"${tag}\" ]]; then\n  echo \"Release tag could not be determined\" >&2\n  exit 1\nfi\n\nif [[ \"${tag}\" != v* ]]; then\n  echo \"Tag must start with 'v' (e.g., v2.6.0)\" >&2\n  exit 1\nfi\n\nversion=\"${tag#v}\"\n\nif [[ -n \"${ref_input}\" ]]; then\n  ref=\"${ref_input}\"\nelse\n  ref=\"refs/tags/${tag}\"\nfi\n\nif [[ \"${ref}\" =~ ^[0-9a-f]{40}$ ]]; then\n  checkout_ref=\"refs/heads/main\"\n  target_sha=\"${ref}\"\nelif [[ \"${ref}\" =~ ^refs/ ]]; then\n  checkout_ref=\"${ref}\"\n  target_sha=\"\"\nelif [[ -n \"${tag}\" && \"${ref}\" == \"${tag}\" ]]; then\n  checkout_ref=\"refs/tags/${ref}\"\n  target_sha=\"\"\nelse\n  checkout_ref=\"refs/heads/${ref}\"\n  target_sha=\"\"\nfi\n\nif [[ \"${ref}\" =~ ^[0-9a-f]{40}$ ]]; then\n  matrix_ref=\"main\"\nelif [[ \"${ref}\" =~ ^refs/heads/(.+)$ ]]; then\n  matrix_ref=\"${BASH_REMATCH[1]}\"\nelif [[ \"${ref}\" =~ ^refs/tags/(.+)$ ]]; then\n  matrix_ref=\"${BASH_REMATCH[1]}\"\nelse\n  matrix_ref=\"${ref}\"\nfi\n\ndry_run=\"${dry_run_input}\"\nif [[ -z \"${dry_run}\" ]]; then\n  dry_run=\"false\"\nfi\n\nif [[ \"${ref}\" =~ ^refs/tags/ ]]; then\n  is_tag=\"true\"\nelse\n  is_tag=\"false\"\nfi\n\ncat <<JSON >release-metadata.json\n{\n  \"tag\": \"${tag}\",\n  \"version\": \"${version}\",\n  \"ref\": \"${ref}\",\n  \"checkout_ref\": \"${checkout_ref}\",\n  \"target_sha\": \"${target_sha}\",\n  \"matrix_ref\": \"${matrix_ref}\",\n  \"dry_run\": ${dry_run},\n  \"is_tag\": ${is_tag}\n}\nJSON\n\n{\n  echo \"tag=${tag}\"\n  echo \"version=${version}\"\n  echo \"ref=${ref}\"\n  echo \"dry_run=${dry_run}\"\n  echo \"checkout_ref=${checkout_ref}\"\n  echo \"target_sha=${target_sha}\"\n  echo \"matrix_ref=${matrix_ref}\"\n  echo \"is_tag=${is_tag}\"\n} >>\"${GITHUB_OUTPUT}\"\n"
  },
  {
    "path": "scripts/publish/wasm/build-bundles.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm --filter @kreuzberg/html-to-markdown-wasm run build:all\n"
  },
  {
    "path": "scripts/publish/wasm/extract-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncd wasm-artifacts\nfor tarball in *.tar.gz; do\n  tar -xzf \"${tarball}\" -C ../crates/html-to-markdown-wasm\ndone\n"
  },
  {
    "path": "scripts/publish/wasm/install-deps.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\npnpm install --filter @kreuzberg/html-to-markdown-wasm...\n"
  },
  {
    "path": "scripts/publish/wasm/package-artifacts.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nout_dir=\"wasm-artifacts\"\nrm -rf \"${out_dir}\"\nmkdir -p \"${out_dir}\"\nfor folder in dist dist-node dist-web; do\n  if [ -d \"crates/html-to-markdown-wasm/${folder}\" ]; then\n    tar -czf \"${out_dir}/html-to-markdown-${folder}.tar.gz\" -C crates/html-to-markdown-wasm \"${folder}\"\n  fi\ndone\n"
  },
  {
    "path": "scripts/readme_config.yaml",
    "content": "version: \"3.1.0\"\nlicense: MIT\ndiscord_url: https://discord.gg/pXxagNK2zN\nbanner_url: https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\n\nlanguages:\n  python:\n    name: Python\n    template: language_package.md.jinja\n    package_manager:\n      - pip\n    package_name: html-to-markdown\n    install_command: \"pip install html-to-markdown\"\n    description: |\n      High-performance HTML to Markdown converter with a clean Python API (powered by a Rust core).\n      The same engine also drives the Node.js, Ruby, PHP, and WebAssembly bindings, so rendered Markdown\n      stays identical across runtimes. Wheels are published for Linux, macOS, and Windows.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          latency: \"0.62ms\"\n          throughput: \"208 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          latency: \"2.02ms\"\n          throughput: \"178 MB/s\"\n        - name: \"Mixed (Python wiki)\"\n          size: \"656KB\"\n          latency: \"4.56ms\"\n          throughput: \"144 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n\n  typescript:\n    name: \"TypeScript (Node.js)\"\n    template: language_package.md.jinja\n    output_path: \"packages/typescript/README.md\"\n    package_manager:\n      - npm\n      - pnpm\n      - yarn\n      - bun\n    package_name: \"@kreuzberg/html-to-markdown\"\n    install_command: \"npm install @kreuzberg/html-to-markdown\"\n    description: |\n      High-performance HTML to Markdown converter for Node.js and Bun with full TypeScript support.\n      This package wraps native `@kreuzberg/html-to-markdown-node` bindings and provides a type-safe API.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          latency: \"0.58ms\"\n          throughput: \"222 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          latency: \"1.89ms\"\n          throughput: \"190 MB/s\"\n        - name: \"Mixed (Python wiki)\"\n          size: \"656KB\"\n          latency: \"4.21ms\"\n          throughput: \"156 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: \"2.19.0\"\n\n  ruby:\n    name: Ruby\n    template: language_package.md.jinja\n    output_path: \"packages/ruby/README.md\"\n    package_manager:\n      - gem\n      - bundler\n    package_name: html-to-markdown\n    install_command: \"gem install html-to-markdown\"\n    description: |\n      Blazing-fast HTML to Markdown conversion for Ruby, powered by the same Rust engine used by our Python, Node.js, WebAssembly, and PHP packages.\n      Ship identical Markdown across every runtime while enjoying native extension performance with Magnus bindings.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: true\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          latency: \"0.71ms\"\n          throughput: \"182 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          latency: \"2.15ms\"\n          throughput: \"167 MB/s\"\n        - name: \"Mixed (Python wiki)\"\n          size: \"656KB\"\n          latency: \"4.89ms\"\n          throughput: \"134 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n\n  php:\n    name: PHP\n    template: language_package.md.jinja\n    package_manager:\n      - composer\n      - pie\n    package_name: kreuzberg-dev/html-to-markdown\n    install_command: \"composer require kreuzberg-dev/html-to-markdown\"\n    description: |\n      High-performance HTML to Markdown converter with typed PHP bindings powered by a Rust core.\n      Provides a type-safe API with full PHPStan level 9 support, modern PHP 8.2+ features, and comprehensive metadata extraction.\n\n      Note: The package was previously published as `goldziher/html-to-markdown`, which still works for backward compatibility.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          ops_sec: 3346\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          ops_sec: 973\n        - name: \"Medium (Python)\"\n          size: \"657KB\"\n          ops_sec: 485\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n\n  go:\n    name: Go\n    template: language_package.md.jinja\n    output_path: \"packages/go/v3/README.md\"\n    package_manager:\n      - go\n    package_name: github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\n    install_command: \"go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n    description: |\n      High-performance HTML to Markdown converter with Go bindings to the Rust core library.\n      Supports automatic downloading of prebuilt FFI libraries for Linux, macOS, and Windows with customizable caching.\n    features:\n      visitor_pattern: false\n      metadata_extraction: false\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"Convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          latency: \"0.46ms\"\n          throughput: \"277.5 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          latency: \"1.37ms\"\n          throughput: \"262.1 MB/s\"\n        - name: \"Mixed (Python wiki)\"\n          size: \"656KB\"\n          latency: \"2.75ms\"\n          throughput: \"237.9 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n\n  java:\n    name: Java\n    template: language_package.md.jinja\n    output_path: \"packages/java/README.md\"\n    package_manager:\n      - maven\n      - gradle\n    package_name: dev.kreuzberg:html-to-markdown\n    install_command: |\n      <dependency>\n          <groupId>dev.kreuzberg</groupId>\n          <artifactId>html-to-markdown</artifactId>\n          <version>3.1.0</version>\n          <classifier>linux</classifier> <!-- or macos, windows -->\n      </dependency>\n    description: |\n      High-performance HTML to Markdown converter with Java Panama FFI bindings to the Rust core.\n      Uses Foreign Function & Memory API for zero-dependency, thread-safe conversion with full metadata extraction support.\n    features:\n      visitor_pattern: false\n      metadata_extraction: false\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          ops_sec: 2308\n          throughput: \"291.5 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          ops_sec: 773\n          throughput: \"272.0 MB/s\"\n        - name: \"Mixed (Python)\"\n          size: \"656KB\"\n          ops_sec: 403\n          throughput: \"258.5 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: \"2.19.0\"\n\n  csharp:\n    name: \"C# / .NET\"\n    template: language_package.md.jinja\n    output_path: \"packages/csharp/README.md\"\n    package_manager:\n      - nuget\n    package_name: KreuzbergDev.HtmlToMarkdown\n    install_command: \"dotnet add package KreuzbergDev.HtmlToMarkdown\"\n    description: |\n      High-performance HTML to Markdown converter with C#/.NET bindings using P/Invoke to the Rust core.\n      Provides type-safe record-based APIs for metadata extraction, visitor patterns, and thread-safe concurrent conversion.\n    features:\n      visitor_pattern: false\n      metadata_extraction: false\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"Convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          ops_sec: 3111\n          throughput: \"392.9 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          ops_sec: 853\n          throughput: \"300.1 MB/s\"\n        - name: \"Mixed (Python)\"\n          size: \"656KB\"\n          ops_sec: 456\n          throughput: \"292.3 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: \"2.19.0\"\n\n  elixir:\n    name: Elixir\n    template: language_package.md.jinja\n    output_path: \"packages/elixir/README.md\"\n    package_manager:\n      - mix\n    package_name: html_to_markdown\n    install_command: \"Add {:html_to_markdown, \\\"~> 3.0\\\"} to mix.exs deps\"\n    description: |\n      Elixir bindings for the Rust html-to-markdown engine. The package exposes a fast HTML to Markdown converter implemented with Rustler.\n      Ship identical Markdown across every runtime while enjoying native performance with Rustler NIF bindings.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          ops_sec: 2547\n          throughput: \"321.7 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          ops_sec: 835\n          throughput: \"293.8 MB/s\"\n        - name: \"Medium (Python)\"\n          size: \"656KB\"\n          ops_sec: 439\n          throughput: \"281.5 MB/s\"\n        - name: \"Large (Rust)\"\n          size: \"567KB\"\n          ops_sec: 485\n          throughput: \"268.7 MB/s\"\n        - name: \"Small (Intro)\"\n          size: \"463KB\"\n          ops_sec: 581\n          throughput: \"262.9 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n\n  r:\n    name: R\n    template: language_package.md.jinja\n    output_path: \"packages/r/README.md\"\n    package_manager:\n      - install.packages\n    package_name: htmltomarkdown\n    install_command: 'install.packages(\"htmltomarkdown\")'\n    description: |\n      High-performance HTML to Markdown converter with R bindings powered by a Rust core via extendr.\n      Ship identical Markdown across every runtime while enjoying native performance with extendr bindings.\n    features:\n      visitor_pattern: true\n      metadata_extraction: true\n      cli_proxy: false\n    performance:\n      platform: \"Apple M4\"\n      function: \"convert()\"\n      note: \"Real Wikipedia documents\"\n      benchmarks:\n        - name: \"Lists (Timeline)\"\n          size: \"129KB\"\n          latency: \"0.68ms\"\n          throughput: \"190 MB/s\"\n        - name: \"Tables (Countries)\"\n          size: \"360KB\"\n          latency: \"2.10ms\"\n          throughput: \"171 MB/s\"\n        - name: \"Mixed (Python wiki)\"\n          size: \"656KB\"\n          latency: \"4.75ms\"\n          throughput: \"138 MB/s\"\n    snippets:\n      basic_usage: \"getting-started/basic_usage.md\"\n      with_options: \"getting-started/with_options.md\"\n      metadata_extraction: \"metadata/basic_extraction.md\"\n      visitor_basic: \"visitor/basic_visitor.md\"\n      table_extraction: \"table-extraction/basic_extraction.md\"\n    migration_version: null\n"
  },
  {
    "path": "scripts/readme_templates/language_package.md.jinja",
    "content": "# html-to-markdown\n\n{% include 'partials/_badges.md.jinja' %}\n\n{{ description }}\n\n## Installation\n\n{% include 'partials/_installation.md.jinja' %}\n\n{% if migration_guide %}\n{{ migration_guide }}\n{% endif %}\n\n{% if performance %}\n## Performance Snapshot\n\n{{ performance | render_performance_table(name) }}\n\n{% endif %}\n\n## Quick Start\n\n{% include 'partials/_quick_start.md.jinja' %}\n\n## API Reference\n\n{% include 'partials/_api_reference.md.jinja' %}\n\n{% include 'partials/_djot_output.md.jinja' %}\n\n{% include 'partials/_plain_text_output.md.jinja' %}\n\n{% if features.metadata_extraction %}\n## Metadata Extraction\n\n{% include 'partials/_metadata_extraction.md.jinja' %}\n{% endif %}\n\n{% if features.visitor_pattern %}\n## Visitor Pattern\n\n{% include 'partials/_visitor_pattern.md.jinja' %}\n{% endif %}\n\n## Examples\n\n\n{% include 'partials/_footer.md.jinja' %}\n"
  },
  {
    "path": "scripts/readme_templates/partials/_api_reference.md.jinja",
    "content": "### Core Function\n\n{% if language == 'python' %}\n**`convert(html: str, options?: ConversionOptions, visitor?: object) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nresult = convert(html)\nmarkdown = result.content           # Converted Markdown string\nmetadata = result.metadata          # Metadata (when extract_metadata=True)\ntables   = result.tables            # Structured table data\ndocument = result.document          # Document-level info\nimages   = result.images            # Extracted images\nwarnings = result.warnings          # Any conversion warnings\n```\n\n{% elif language == 'typescript' %}\n**`convert(html: string, options?: ConversionOptions, visitor?: Visitor): ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` object with all results in a single call.\n\n```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst result = convert(html);\nconst markdown  = result.content;    // Converted Markdown string\nconst metadata  = result.metadata;   // Metadata (when extractMetadata: true)\nconst tables    = result.tables;     // Structured table data (when extractTables: true)\nconst document  = result.document;   // Document-level info\nconst images    = result.images;     // Extracted images\nconst warnings  = result.warnings;   // Any conversion warnings\n```\n\n{% elif language == 'ruby' %}\n**`convert(html, options: nil, visitor: nil) -> ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` hash with all results in a single call.\n\n```ruby\nrequire 'html_to_markdown'\n\nresult = HtmlToMarkdown.convert(html)\nmarkdown = result[:content]       # Converted Markdown string\nmetadata = result[:metadata]      # Metadata (when extract_metadata: true)\ntables   = result[:tables]        # Structured table data (when extract_tables: true)\ndocument = result[:document]      # Document-level info\nimages   = result[:images]        # Extracted images\nwarnings = result[:warnings]      # Any conversion warnings\n```\n\n{% elif language == 'php' %}\n**`Converter::convert(string $html, ?ConversionOptions $options = null, ?VisitorInterface $visitor = null): array`**\n\nConverts HTML to Markdown. Returns an array `ConversionResult` with all results in a single call.\n\n```php\n<?php\nuse HtmlToMarkdown\\Service\\Converter;\n\n$result  = Converter::create()->convert($html);\n$markdown = $result['content'];    // Converted Markdown string\n$metadata = $result['metadata'];   // Metadata (when extractMetadata: true)\n$tables   = $result['tables'];     // Structured table data (when extractTables: true)\n$document = $result['document'];   // Document-level info\n$images   = $result['images'];     // Extracted images\n$warnings = $result['warnings'];   // Any conversion warnings\n```\n\n{% elif language == 'go' %}\n**`Convert(html string, options ...ConversionOptions) (ConversionResult, error)`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` struct with all results in a single call.\n\n```go\nresult, err := htmltomarkdown.Convert(html)\nmarkdown  := result.Content    // *string — converted Markdown\nmetadata  := result.Metadata   // *Metadata — when ExtractMetadata: true\ntables    := result.Tables     // []TableData — when ExtractTables: true\n```\n\n{% elif language == 'java' %}\n**`HtmlToMarkdown.convert(String html) : ConversionResult`**\n**`HtmlToMarkdown.convert(String html, ConversionOptions options) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```java\nConversionResult result = HtmlToMarkdown.convert(html);\nString   markdown = result.content();   // Converted Markdown string\nMetadata metadata = result.metadata();  // null unless extractMetadata(true)\nList<?>  tables   = result.tables();    // empty unless extractTables(true)\n```\n\n{% elif language == 'csharp' %}\n**`HtmlToMarkdownConverter.Convert(string html, ConversionOptions? options = null) : ConversionResult`**\n\nConverts HTML to Markdown. Returns a `ConversionResult` record with all results in a single call.\n\n```csharp\nvar result   = HtmlToMarkdownConverter.Convert(html);\nvar markdown = result.Content;    // Converted Markdown string\nvar metadata = result.Metadata;   // null unless ExtractMetadata = true\nvar tables   = result.Tables;     // empty unless ExtractTables = true\n```\n\n{% elif language == 'elixir' %}\n**`HtmlToMarkdown.convert(html, options \\\\ nil) :: {:ok, ConversionResult.t()} | {:error, term()}`**\n\nConverts HTML to Markdown. Returns `{:ok, result}` where result is a struct with all results in a single call.\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(html)\nresult.content    # Converted Markdown string\nresult.metadata   # Metadata map (when extract_metadata: true)\nresult.tables     # Table data list (when extract_tables: true)\nresult.document   # Document-level info\nresult.images     # Extracted images\nresult.warnings   # Any conversion warnings\n```\n\n{% elif language == 'r' %}\n**`convert(html, options = NULL)`**\n\nConverts HTML to Markdown. Returns a named list `ConversionResult` with all results in a single call.\n\n```r\nresult   <- convert(html)\nmarkdown <- result$content    # Converted Markdown string\nmetadata <- result$metadata   # Metadata (when extract_metadata = TRUE)\ntables   <- result$tables     # Table data (when extract_tables = TRUE)\n```\n\n{% else %}\n{% endif %}\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n"
  },
  {
    "path": "scripts/readme_templates/partials/_badges.md.jinja",
    "content": "<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v{{ version }}\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"{{ banner_url }}\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"{{ discord_url }}\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n"
  },
  {
    "path": "scripts/readme_templates/partials/_djot_output.md.jinja",
    "content": "## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element | Markdown | Djot |\n|---------|----------|------|\n| Strong | `**text**` | `*text*` |\n| Emphasis | `*text*` | `_text_` |\n| Strikethrough | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A | `{+text+}` |\n| Highlighted | N/A | `{=text=}` |\n| Subscript | N/A | `~text~` |\n| Superscript | N/A | `^text^` |\n\n### Example Usage\n\n{% if language == 'python' %}\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = convert(html, ConversionOptions(output_format=\"djot\"))\n# Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'typescript' %}\n```typescript\nimport { convert, ConversionOptions } from '@kreuzberg/html-to-markdown';\n\nconst html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nconst markdown = convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nconst djot = convert(html, { outputFormat: 'djot' });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'ruby' %}\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\nmarkdown = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\ndjot = HtmlToMarkdown.convert(html, output_format: 'djot')\n# Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'php' %}\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\n$markdown = Converter::convert($html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\n$djot = Converter::convert($html, new ConversionOptions(outputFormat: 'djot'));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'go' %}\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n// Default Markdown output\nmarkdown, _ := htmltomarkdown.Convert(html)\n// Result: \"This is **bold** and *italic* text.\"\n\n// Note: Djot output format configuration is not yet supported in Go bindings\n```\n{% elif language == 'java' %}\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nString markdown = HtmlToMarkdown.convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nString djot = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.DJOT));\n// Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'csharp' %}\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n// Default Markdown output\nvar markdown = Converter.Convert(html);\n// Result: \"This is **bold** and *italic* text.\"\n\n// Djot output\nvar djot = Converter.Convert(html, new ConversionOptions { OutputFormat = \"djot\" });\n// Result: \"This is *bold* and _italic_ text.\"\n```\n{% elif language == 'elixir' %}\n```elixir\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\n{:ok, markdown} = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\n{:ok, djot} = HtmlToMarkdown.convert(html, %{output_format: \"djot\"})\n# Result: \"This is *bold* and _italic_ text.\"\n```\n{% endif %}\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n"
  },
  {
    "path": "scripts/readme_templates/partials/_footer.md.jinja",
    "content": "## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n{% if language == 'python' %}\n- **PyPI:** [pypi.org/project/html-to-markdown](https://pypi.org/project/html-to-markdown/)\n{% elif language == 'typescript' or language == 'node' %}\n- **npm:** [npmjs.com/@kreuzberg/html-to-markdown](https://www.npmjs.com/package/@kreuzberg/html-to-markdown)\n- **WASM:** [npmjs.com/@kreuzberg/html-to-markdown-wasm](https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm)\n{% elif language == 'ruby' %}\n- **RubyGems:** [rubygems.org/gems/html-to-markdown](https://rubygems.org/gems/html-to-markdown)\n{% elif language == 'php' %}\n- **Packagist:** [packagist.org/packages/kreuzberg-dev/html-to-markdown](https://packagist.org/packages/kreuzberg-dev/html-to-markdown)\n{% elif language == 'go' %}\n- **Go Packages:** [pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2](https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v2)\n{% elif language == 'java' %}\n- **Maven Central:** [central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown](https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown)\n{% elif language == 'csharp' %}\n- **NuGet:** [nuget.org/packages/KreuzbergDev.HtmlToMarkdown](https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/)\n{% elif language == 'elixir' %}\n- **Hex.pm:** [hex.pm/packages/html_to_markdown](https://hex.pm/packages/html_to_markdown)\n{% endif %}\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "scripts/readme_templates/partials/_installation.md.jinja",
    "content": "```bash\n{{ install_command }}\n```\n\n{% if language == 'python' %}\nRequires Python 3.10+. Wheels are published for Linux, macOS, and Windows on PyPI.\n{% elif language == 'typescript' %}\n\nRequires Node.js 18+ or Bun. Native bindings provide superior performance.\n\n**npm:**\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\n**pnpm:**\n```bash\npnpm add @kreuzberg/html-to-markdown\n```\n\n**yarn:**\n```bash\nyarn add @kreuzberg/html-to-markdown\n```\n\n**bun:**\n```bash\nbun add @kreuzberg/html-to-markdown\n```\n\nAlternatively, use the WebAssembly version for browser/edge environments:\n\n```bash\nnpm install @kreuzberg/html-to-markdown-wasm\n```\n{% elif language == 'ruby' %}\n\nRequires Ruby 3.2+ with Magnus native extension bindings. Published for Linux, macOS.\n{% elif language == 'php' %}\n\nRequires PHP 8.2+. Install the native extension via PIE:\n\n```bash\npie install kreuzberg-dev/html-to-markdown\n```\n\nOr use Composer (requires ext-html_to_markdown):\n\n```bash\ncomposer require kreuzberg-dev/html-to-markdown\n```\n{% elif language == 'go' %}\n\nRequires Go 1.25+. After installing the package, run `go generate` to automatically download the platform-specific FFI library:\n\n```bash\ngo generate\n```\n\nThis downloads the native library from GitHub releases and generates the necessary CGO flags. The library is cached in `~/.html-to-markdown/` for subsequent builds.\n\nAlternatively, you can manually set `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables if you prefer to manage the FFI library yourself.\n{% elif language == 'java' %}\n\nRequires Java 25+ with Panama FFI support.\n\n**Maven:**\n```xml\n<dependency>\n    <groupId>dev.kreuzberg</groupId>\n    <artifactId>html-to-markdown</artifactId>\n    <version>{{ version }}</version>\n</dependency>\n```\n\n**Gradle (Kotlin DSL):**\n```kotlin\nimplementation(\"dev.kreuzberg:html-to-markdown:{{ version }}\")\n```\n{% elif language == 'csharp' %}\n\nRequires .NET 8.0+ SDK.\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n{% elif language == 'elixir' %}\n\nRequires Elixir 1.19+ and OTP 28. Add to your `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:html_to_markdown, \"~> {{ version }}\"}\n  ]\nend\n```\n{% elif language == 'r' %}\n\nRequires R 4.3+ and a Rust toolchain (cargo, rustc).\n\n```r\ninstall.packages(\"htmltomarkdown\")\n```\n\nOr install the development version from GitHub:\n\n```r\ndevtools::install_github(\"kreuzberg-dev/html-to-markdown\", subdir = \"packages/r\")\n```\n{% endif %}\n"
  },
  {
    "path": "scripts/readme_templates/partials/_metadata_extraction.md.jinja",
    "content": "The metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n{% if language == 'python' %}\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = convert(html, ConversionOptions(extract_metadata=True))\n\nprint(result.content)                          # Converted Markdown\nprint(result.metadata.document.title)          # Document title\nprint(result.metadata.headers)                 # All h1-h6 elements\nprint(result.metadata.links)                   # All hyperlinks\nprint(result.metadata.images)                  # All images with alt text\nprint(result.metadata.structured_data)         # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'typescript' %}\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\nconst result = convert(html, { extractMetadata: true });\n\nconsole.log(result.content);                      // Converted Markdown\nconsole.log(result.metadata?.document?.title);    // Document title\nconsole.log(result.metadata?.headers);            // All h1-h6 elements\nconsole.log(result.metadata?.links);              // All hyperlinks\nconsole.log(result.metadata?.images);             // All images with alt text\nconsole.log(result.metadata?.structuredData);     // JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'ruby' %}\n```ruby\nrequire 'html_to_markdown'\n\nhtml = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nresult = HtmlToMarkdown.convert(html, extract_metadata: true)\n\nputs result[:content]                             # Converted Markdown\nputs result[:metadata][:document][:title]         # Document title\nputs result[:metadata][:headers]                  # All h1-h6 elements\nputs result[:metadata][:links]                    # All hyperlinks\nputs result[:metadata][:images]                   # All images with alt text\nputs result[:metadata][:structured_data]          # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'php' %}\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\n\n$html = '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(extractMetadata: true)\n);\n\necho $result['content'];                          // Converted Markdown\necho $result['metadata']->document->title;        // Document title\nprint_r($result['metadata']->headers);            // All h1-h6 elements\nprint_r($result['metadata']->links);              // All hyperlinks\nprint_r($result['metadata']->images);             // All images with alt text\nprint_r($result['metadata']->structured_data);    // JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'go' %}\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n)\n\nfunc main() {\n    html := `<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">`\n    result, err := htmltomarkdown.Convert(html, htmltomarkdown.ConversionOptions{ExtractMetadata: true})\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    fmt.Println(*result.Content)                  // Converted Markdown\n    fmt.Println(result.Metadata.Title)            // Document title\n    fmt.Println(result.Metadata.Headers)          // All h1-h6 elements\n    fmt.Println(result.Metadata.Links)            // All hyperlinks\n    fmt.Println(result.Metadata.Images)           // All images with alt text\n}\n```\n\n{% elif language == 'java' %}\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\n\npublic class Main {\n    public static void main(String[] args) {\n        String html = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\";\n        ConversionOptions options = ConversionOptions.builder()\n            .extractMetadata(true)\n            .build();\n        ConversionResult result = HtmlToMarkdown.convert(html, options);\n\n        System.out.println(result.content());                          // Converted Markdown\n        System.out.println(result.metadata().getDocument().getTitle()); // Document title\n        System.out.println(result.metadata().getHeaders());            // All h1-h6 elements\n        System.out.println(result.metadata().getLinks());              // All hyperlinks\n        System.out.println(result.metadata().getImages());             // All images with alt text\n    }\n}\n```\n\n{% elif language == 'csharp' %}\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\";\nvar result = HtmlToMarkdownConverter.Convert(html, new ConversionOptions { ExtractMetadata = true });\n\nConsole.WriteLine(result.Content);                                    // Converted Markdown\nConsole.WriteLine(result.Metadata?.Document?.Title);                  // Document title\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Headers ?? [])); // All h1-h6 elements\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Links ?? []));   // All hyperlinks\nConsole.WriteLine(string.Join(\", \", result.Metadata?.Images ?? []));  // All images with alt text\n```\n\n{% elif language == 'elixir' %}\n```elixir\nhtml = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\"\nopts = %HtmlToMarkdown.Options{extract_metadata: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nIO.puts(result.content)                           # Converted Markdown\nIO.inspect(result.metadata[\"document\"][\"title\"])  # Document title\nIO.inspect(result.metadata[\"headers\"])            # All h1-h6 elements\nIO.inspect(result.metadata[\"links\"])              # All hyperlinks\nIO.inspect(result.metadata[\"images\"])             # All images with alt text\nIO.inspect(result.metadata[\"structured_data\"])    # JSON-LD, Microdata, RDFa\n```\n\n{% elif language == 'r' %}\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<h1>Article</h1><img src=\"test.jpg\" alt=\"test\">'\nopts <- conversion_options(extract_metadata = TRUE)\nresult <- convert(html, opts)\n\ncat(result$content)                    # Converted Markdown\nresult$metadata$document$title        # Document title\nresult$metadata$headers                # All h1-h6 elements\nresult$metadata$links                  # All hyperlinks\nresult$metadata$images                 # All images with alt text\n```\n\n{% endif %}\n"
  },
  {
    "path": "scripts/readme_templates/partials/_plain_text_output.md.jinja",
    "content": "## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n{% if language == 'python' %}\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = convert(html, ConversionOptions(output_format=\"plain\"))\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'typescript' %}\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nconst plain = convert(html, { outputFormat: 'plain' });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'ruby' %}\n```ruby\nrequire 'html_to_markdown'\n\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain = HtmlToMarkdown.convert(html, output_format: 'plain')\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'php' %}\n```php\nuse HtmlToMarkdown\\Converter;\nuse HtmlToMarkdown\\ConversionOptions;\n\n$html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\n$plain = Converter::convert($html, new ConversionOptions(outputFormat: 'plain'));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'go' %}\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v2/htmltomarkdown\"\n\nhtml := \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nplain, _ := htmltomarkdown.Convert(html, htmltomarkdown.WithOutputFormat(\"plain\"))\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'java' %}\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\nimport dev.kreuzberg.htmltomarkdown.OutputFormat;\n\nString html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nString plain = HtmlToMarkdown.convert(html,\n    new ConversionOptions().setOutputFormat(OutputFormat.PLAIN));\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'csharp' %}\n```csharp\nusing HtmlToMarkdown;\n\nvar html = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\";\n\nvar plain = Converter.Convert(html, new ConversionOptions { OutputFormat = \"plain\" });\n// Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'elixir' %}\n```elixir\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n{:ok, plain} = HtmlToMarkdown.convert(html, %{output_format: \"plain\"})\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% elif language == 'r' %}\n```r\nhtml <- \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\nresult <- convert(html, options = list(output_format = \"plain\"))\nplain <- result$content\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n{% endif %}\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n"
  },
  {
    "path": "scripts/readme_templates/partials/_quick_start.md.jinja",
    "content": "Basic conversion:\n\n{{ snippets.basic_usage | include_snippet(language) }}\n\n{% if snippets.with_options %}\nWith conversion options:\n\n{{ snippets.with_options | include_snippet(language) }}\n{% endif %}\n"
  },
  {
    "path": "scripts/readme_templates/partials/_visitor_pattern.md.jinja",
    "content": "The visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n{% if language == 'python' %}\n```python\nfrom html_to_markdown import convert\n\nclass MyVisitor:\n    def visit_link(self, ctx, href, text, title):\n        # Rewrite CDN URLs\n        if href.startswith(\"https://old-cdn.com\"):\n            href = href.replace(\"https://old-cdn.com\", \"https://new-cdn.com\")\n        return {\"type\": \"custom\", \"output\": f\"[{text}]({href})\"}\n\n    def visit_image(self, ctx, src, alt, title):\n        # Skip tracking pixels\n        if \"tracking\" in src:\n            return {\"type\": \"skip\"}\n        return {\"type\": \"continue\"}\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = convert(html, visitor=MyVisitor())\nmarkdown = result.content\n```\n\n{% elif language == 'typescript' %}\n```typescript\nimport { convert, type Visitor, type NodeContext, type VisitResult } from '@kreuzberg/html-to-markdown';\n\nconst visitor: Visitor = {\n  visitLink(ctx: NodeContext, href: string, text: string, title?: string): VisitResult {\n    // Rewrite CDN URLs\n    if (href.startsWith('https://old-cdn.com')) {\n      href = href.replace('https://old-cdn.com', 'https://new-cdn.com');\n    }\n    return { type: 'custom', output: `[${text}](${href})` };\n  },\n\n  visitImage(ctx: NodeContext, src: string, alt?: string, title?: string): VisitResult {\n    // Skip tracking pixels\n    if (src.includes('tracking')) {\n      return { type: 'skip' };\n    }\n    return { type: 'continue' };\n  },\n};\n\nconst html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\nconst result = convert(html, {}, visitor);\nconst markdown = result.content;\n```\n\n{% elif language == 'ruby' %}\n```ruby\nrequire 'html_to_markdown'\n\nclass MyVisitor\n  def visit_link(ctx, href, text, title = nil)\n    # Rewrite CDN URLs\n    if href.start_with?('https://old-cdn.com')\n      href = href.sub('https://old-cdn.com', 'https://new-cdn.com')\n    end\n    { type: :custom, output: \"[#{text}](#{href})\" }\n  end\n\n  def visit_image(ctx, src, alt = nil, title = nil)\n    # Skip tracking pixels\n    src.include?('tracking') ? { type: :skip } : { type: :continue }\n  end\nend\n\nhtml = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nresult = HtmlToMarkdown.convert(html, visitor: MyVisitor.new)\nmarkdown = result[:content]\n```\n\n{% elif language == 'php' %}\n```php\n<?php\nuse HtmlToMarkdown\\Config\\ConversionOptions;\nuse HtmlToMarkdown\\Service\\Converter;\nuse HtmlToMarkdown\\Visitor\\AbstractVisitor;\nuse HtmlToMarkdown\\Visitor\\NodeContext;\nuse HtmlToMarkdown\\Visitor\\VisitResult;\n\nclass MyVisitor extends AbstractVisitor\n{\n    public function visitLink(NodeContext $ctx, string $href, string $text, ?string $title): array\n    {\n        // Rewrite CDN URLs\n        if (str_starts_with($href, 'https://old-cdn.com')) {\n            $href = str_replace('https://old-cdn.com', 'https://new-cdn.com', $href);\n        }\n        return VisitResult::custom(\"[{$text}]({$href})\");\n    }\n\n    public function visitImage(NodeContext $ctx, string $src, ?string $alt, ?string $title): array\n    {\n        // Skip tracking pixels\n        return str_contains($src, 'tracking') ? VisitResult::skip() : VisitResult::continue();\n    }\n}\n\n$html = '<a href=\"https://old-cdn.com/file.pdf\">Download</a>';\n$result = Converter::create()->convert(\n    $html,\n    new ConversionOptions(visitor: new MyVisitor())\n);\n$markdown = $result['content'];\n```\n\n{% elif language == 'elixir' %}\n```elixir\ndefmodule MyVisitor do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_ctx, href, text, _title) do\n    # Rewrite CDN URLs\n    href = if String.starts_with?(href, \"https://old-cdn.com\") do\n      String.replace(href, \"https://old-cdn.com\", \"https://new-cdn.com\")\n    else\n      href\n    end\n    {:custom, \"[#{text}](#{href})\"}\n  end\n\n  @impl true\n  def handle_image(_ctx, src, _alt, _title) do\n    # Skip tracking pixels\n    if String.contains?(src, \"tracking\"), do: :skip, else: :continue\n  end\nend\n\nhtml = \"<a href=\\\"https://old-cdn.com/file.pdf\\\">Download</a>\"\nopts = %HtmlToMarkdown.Options{visitor: MyVisitor}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\nresult.content\n```\n\n{% elif language == 'r' %}\n```r\nlibrary(htmltomarkdown)\n\nhtml <- '<a href=\"https://old-cdn.com/file.pdf\">Download</a>'\nopts <- conversion_options(extract_metadata = FALSE)\nresult <- convert(html, opts)\ncat(result$content)\n```\n\n{% endif %}\n"
  },
  {
    "path": "scripts/update_dotnet_packages.py",
    "content": "import json\nimport subprocess\nimport sys\n\n\ndef load_outdated_packages(project_path: str) -> list[tuple[str, str]]:\n    result = subprocess.run(  # noqa: S603\n        [\"dotnet\", \"list\", project_path, \"package\", \"--outdated\", \"--format\", \"json\"],\n        check=True,\n        capture_output=True,\n        text=True,\n    )\n    data = json.loads(result.stdout)\n    packages: dict[str, str] = {}\n    for project in data.get(\"projects\", []):\n        for framework in project.get(\"frameworks\", []) or []:\n            for package in framework.get(\"topLevelPackages\", []) or []:\n                latest = package.get(\"latestVersion\")\n                if not latest:\n                    continue\n                resolved = package.get(\"resolvedVersion\") or package.get(\"requestedVersion\")\n                if resolved == latest:\n                    continue\n                package_id = package.get(\"id\")\n                if package_id:\n                    packages[package_id] = latest\n    return sorted(packages.items())\n\n\ndef update_project(project_path: str) -> bool:\n    updated = False\n    for package_id, latest_version in load_outdated_packages(project_path):\n        subprocess.run(  # noqa: S603\n            [\n                \"dotnet\",\n                \"add\",\n                project_path,\n                \"package\",\n                package_id,\n                \"--version\",\n                latest_version,\n            ],\n            check=True,\n        )\n        updated = True\n    return updated\n\n\ndef main() -> int:\n    if len(sys.argv) < 2:\n        print(\"Usage: update_dotnet_packages.py <path> [<path>...]\", file=sys.stderr)\n        return 2\n    any_updates = False\n    for project_path in sys.argv[1:]:\n        if update_project(project_path):\n            any_updates = True\n    if not any_updates:\n        print(\"No .NET package updates found.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "skills/html-to-markdown/SKILL.md",
    "content": "---\nname: html-to-markdown\ndescription: >-\n  Convert HTML to Markdown, Djot, or plain text with structured extraction.\n  Use when writing code that calls html-to-markdown APIs in Rust, Python,\n  TypeScript, Go, Ruby, PHP, Java, C#, Elixir, R, C, or WASM.\n  Covers installation, conversion, configuration, metadata extraction,\n  document structure, and CLI usage.\nlicense: MIT\nmetadata:\n  author: kreuzberg-dev\n  version: \"3.2.0\"\n  repository: https://github.com/kreuzberg-dev/html-to-markdown\n---\n\n# html-to-markdown\n\nhtml-to-markdown is a high-performance HTML to Markdown converter with a Rust core and 12 native language bindings. It converts HTML to CommonMark Markdown, Djot, or plain text in a single pass, optionally extracting metadata, tables, inline images, and a structured document tree.\n\nUse this skill when writing code that:\n\n- Converts HTML strings or files to Markdown, Djot, or plain text\n- Extracts metadata (title, OG tags, headers, links, images, structured data) from HTML\n- Extracts structured table data from HTML\n- Extracts inline images (data URIs, SVGs) from HTML\n- Uses preprocessing to clean noisy HTML (ads, navigation, forms) before conversion\n- Implements custom element conversion with the visitor pattern\n\n## Installation\n\n### Python\n\n```bash\npip install html-to-markdown\n```\n\n### Node.js / TypeScript\n\n```bash\nnpm install @kreuzberg/html-to-markdown\n```\n\n### Rust\n\n```toml\n# Cargo.toml\n[dependencies]\nhtml-to-markdown-rs = \"3\"\n# Default features: [\"metadata\"]\n# Optional: features = [\"metadata\", \"inline-images\", \"visitor\", \"async-visitor\", \"serde\"]\n# Full: features = [\"full\"]\n```\n\n### Go\n\n```bash\ngo get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3\n```\n\nImport path: `github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown`\n\n### Ruby\n\n```bash\ngem install html-to-markdown\n```\n\n### PHP\n\n```bash\ncomposer require kreuzberg-dev/html-to-markdown\n```\n\n### Java (Maven)\n\n```xml\n<dependency>\n  <groupId>dev.kreuzberg</groupId>\n  <artifactId>html-to-markdown</artifactId>\n  <version>3.2.0</version>\n</dependency>\n```\n\n### C# (.NET)\n\n```bash\ndotnet add package KreuzbergDev.HtmlToMarkdown\n```\n\n### Elixir\n\n```elixir\n# mix.exs\n{:html_to_markdown, \"~> 3.0\"}\n```\n\n### R\n\n```r\ninstall.packages(\"htmltomarkdown\")\n```\n\n### CLI\n\n```bash\n# Install via cargo\ncargo install html-to-markdown-cli\n\n# Basic usage\nhtml-to-markdown input.html                          # convert file to stdout\nhtml-to-markdown input.html -o output.md             # convert file to output file\ncat file.html | html-to-markdown                     # read from stdin\nhtml-to-markdown --url https://example.com           # fetch and convert URL\n\n# JSON output (ConversionResult as JSON with content, tables, metadata, images, warnings)\nhtml-to-markdown --json input.html\nhtml-to-markdown --json --include-structure input.html   # include document structure\n\n# Key flags\nhtml-to-markdown --extract-inline-images input.html  # include inline image data in JSON output\nhtml-to-markdown --show-warnings input.html          # print non-fatal warnings to stderr\nhtml-to-markdown --no-content --json input.html      # extract only (no markdown text, just metadata/tables)\nhtml-to-markdown --heading-style atx input.html      # set heading style\nhtml-to-markdown --preprocess input.html             # preprocess HTML before converting\n```\n\n### WASM\n\n```bash\nnpm install @kreuzberg/html-to-markdown-wasm\n```\n\n## Quick Start\n\n### Rust\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\nlet html = \"<h1>Hello World</h1><p>This is a paragraph.</p>\";\nlet result = convert(html, None)?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n// Output: # Hello World\\n\\nThis is a paragraph.\\n\n```\n\n### Python\n\n```python\nfrom html_to_markdown import convert\n\nhtml = \"<h1>Hello World</h1><p>This is a paragraph.</p>\"\nresult = convert(html)\nprint(result.content)\n# Output: # Hello World\\n\\nThis is a paragraph.\\n\n```\n\n### TypeScript / Node.js\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst html = '<h1>Hello World</h1><p>This is a paragraph.</p>';\nconst result = JSON.parse(convert(html));\nconsole.log(result.content);\n// Output: # Hello World\\n\\nThis is a paragraph.\\n\n```\n\n### CLI\n\n```bash\n# From file\nhtml-to-markdown input.html\n\n# From file, save to output\nhtml-to-markdown input.html -o output.md\n\n# From stdin\ncat file.html | html-to-markdown\n\n# JSON output (ConversionResult as JSON)\nhtml-to-markdown --json input.html\n\n# JSON output with full structure (tables, metadata, images)\nhtml-to-markdown --json --include-structure input.html\n\n# Show warnings\nhtml-to-markdown --show-warnings input.html\n\n# Fetch URL\nhtml-to-markdown --url https://example.com > output.md\n```\n\n## ConversionResult Fields\n\nAll languages return the same data structure (as a dict, object, or struct).\n\n| Field | Rust Type | Python | TypeScript | Description |\n|-------|-----------|--------|------------|-------------|\n| `content` | `Option<String>` | `str \\| None` | `string \\| null` | Converted text (Markdown/Djot/plain). `None` only in extraction-only mode. |\n| `document` | `Option<DocumentStructure>` | `None` (not yet wired) | `null` (not yet wired) | Structured document tree when `include_document_structure=true` |\n| `metadata` | `HtmlMetadata` | `HtmlMetadata \\| None` | JSON object or null | Extracted HTML metadata (title, OG, headers, links, images, structured data). Requires `metadata` feature. |\n| `tables` | `Vec<TableData>` | `list[TableData]` | `array` | Extracted tables with `grid` (structured cells) and `markdown` fields |\n| `images` | `Vec<InlineImage>` | `list` | `array` | Extracted inline images (data URIs, SVGs). Requires `inline-images` feature. |\n| `warnings` | `Vec<ProcessingWarning>` | `list[ProcessingWarning]` | `array` | Non-fatal processing warnings with `message` and `kind` fields |\n\n## Configuration\n\nAll languages expose the same options. See [references/configuration.md](references/configuration.md) for the complete options table.\n\n### Rust (builder pattern)\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions, HeadingStyle, CodeBlockStyle, OutputFormat};\n\nlet options = ConversionOptions::builder()\n    .heading_style(HeadingStyle::Atx)\n    .code_block_style(CodeBlockStyle::Backticks)\n    .autolinks(true)\n    .wrap(true)\n    .wrap_width(100)\n    .output_format(OutputFormat::Markdown)\n    .build();\n\nlet result = convert(html, Some(options))?;\n```\n\n### Python (dataclass)\n\n```python\nfrom html_to_markdown import convert, ConversionOptions, PreprocessingOptions\n\noptions = ConversionOptions(\n    heading_style=\"atx\",\n    code_block_style=\"backticks\",\n    autolinks=True,\n    wrap=True,\n    wrap_width=100,\n)\npreprocessing = PreprocessingOptions(\n    enabled=True,\n    preset=\"aggressive\",\n)\n\nresult = convert(html, options, preprocessing)\nprint(result.content)\n```\n\n### TypeScript / Node.js (object)\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown';\n\nconst options = {\n    headingStyle: 'Atx',\n    codeBlockStyle: 'Backticks',\n    autolinks: true,\n    wrap: true,\n    wrapWidth: 100,\n};\n\nconst result = JSON.parse(convert(html, options));\n```\n\n### Go (JSON options)\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n\nresult, err := htmltomarkdown.Convert(html)\n// result.Content contains the markdown string\n// result.Tables contains structured table data\n```\n\n### Ruby (hash)\n\n```ruby\nrequire 'html_to_markdown'\n\nresult = HtmlToMarkdown.convert(html)\nputs result[:content]\n\n# With options\nresult = HtmlToMarkdown.convert(html, {\n  heading_style: \"atx\",\n  code_block_style: \"backticks\",\n})\n```\n\n## Metadata Extraction\n\nThe `metadata` field (requires `metadata` feature / default in Python/Node) contains:\n\n```python\n# Python example\nfrom html_to_markdown import convert\n\nhtml = \"\"\"\n<html lang=\"en\">\n  <head>\n    <title>My Article</title>\n    <meta name=\"description\" content=\"An article\">\n    <meta property=\"og:title\" content=\"OG Title\">\n  </head>\n  <body>\n    <h1>Main Heading</h1>\n    <a href=\"https://example.com\">Link</a>\n    <img src=\"photo.jpg\" alt=\"A photo\">\n  </body>\n</html>\n\"\"\"\nresult = convert(html)\nmeta = result.metadata\n\n# Document-level metadata\nprint(meta.document.title)           # \"My Article\"\nprint(meta.document.description)     # \"An article\"\nprint(meta.document.language)        # \"en\"\nprint(meta.document.open_graph)      # {\"title\": \"OG Title\"}\n\n# Headers\nprint(meta.headers[0].level)         # 1\nprint(meta.headers[0].text)          # \"Main Heading\"\n\n# Links\nprint(meta.links[0].href)            # \"https://example.com\"\nprint(meta.links[0].link_type)       # \"external\"\n\n# Images\nprint(meta.images[0].alt)            # \"A photo\"\nprint(meta.images[0].image_type)     # \"external\"\n```\n\nAll metadata is available in `result.metadata` (Python/Rust/Go/Ruby/Elixir) or `JSON.parse(convert(html)).metadata` (TypeScript) from the single `convert()` call.\n\n## Document Structure Extraction\n\nEnable `include_document_structure=True` to get a structured semantic tree:\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\nresult = convert(html, ConversionOptions(include_document_structure=True))\ndoc = result.document  # DocumentStructure with nodes array\n# Each node has: id, content (node_type + fields), parent, children, annotations\n```\n\nNode types include: `heading`, `paragraph`, `list`, `list_item`, `table`, `image`, `code`, `quote`, `group`, `metadata_block`.\n\n## Common Pitfalls\n\n1. **`convert()` returns a result object, not a string.** Access `.content` (Rust/Python) or `JSON.parse()` the return (Node.js) to get the Markdown string.\n2. **Node.js `convert()` returns a JSON string.** Always `JSON.parse(convert(html))` — the native NAPI binding returns serialized JSON to avoid type conversion overhead.\n3. **Python `convert()` returns a `ConversionResult` object.** Use `result.content` for the Markdown text, not `str(result)` or `result[\"content\"]`.\n4. **Rust builder pattern required.** Use `ConversionOptions::builder().field(value).build()` — direct struct construction omits future fields silently.\n5. **`metadata` requires the `metadata` feature.** In Rust, the `metadata` feature is included in `default`. In bindings (Python, Node, etc.), metadata is always available.\n6. **Python `PreprocessingOptions` is a separate parameter.** Pass it as the second argument to `convert()`, not inside `ConversionOptions`.\n7. **`include_document_structure` must be enabled explicitly.** The `document` field is `None` by default to avoid overhead.\n8. **Inline image extraction requires `extract_images=True`.** The `images` field is empty unless `ConversionOptions(extract_images=True)` is set.\n9. **CLI `--json` outputs JSON, not Markdown.** When `--json` is used, output is the full `ConversionResult` JSON. Use `html-to-markdown input.html` (without `--json`) for plain Markdown output.\n10. **Go `Convert()` returns `ExtractionResult` struct.** Access `.Content` (string) not the struct itself.\n\n## Additional Resources\n\n- **[Rust API Reference](references/rust-api.md)** — Complete Rust function signatures, types, feature flags\n- **[Python API Reference](references/python-api.md)** — All Python functions, dataclasses, type hints\n- **[TypeScript API Reference](references/typescript-api.md)** — All TypeScript functions, interfaces, Buffer support\n- **[Other Bindings](references/other-bindings.md)** — Go, Ruby, PHP, Java, C#, Elixir, R, WASM, C FFI\n- **[Configuration Reference](references/configuration.md)** — All 30+ ConversionOptions fields with defaults\n- **[CLI Reference](references/cli-reference.md)** — All CLI flags and usage examples\n\nGitHub: <https://github.com/kreuzberg-dev/html-to-markdown>\n"
  },
  {
    "path": "skills/html-to-markdown/references/cli-reference.md",
    "content": "# CLI Reference\n\nBinary name: `html-to-markdown`\n\n## Installation\n\n```bash\ncargo install html-to-markdown-cli\n# or download from GitHub releases\n```\n\n## Usage\n\n```text\nhtml-to-markdown [OPTIONS] [FILE]\n```\n\n`FILE` is the path to an input HTML file. Use `-` or omit to read from stdin.\n\n## Input / Output\n\n| Flag | Short | Description |\n|------|-------|-------------|\n| `[FILE]` | | Input HTML file. Omit or use `-` for stdin. |\n| `--url <URL>` | | Fetch HTML from a URL. Conflicts with FILE. |\n| `--user-agent <UA>` | | User-Agent header when using `--url`. Default mimics a real browser. Requires `--url`. |\n| `--output <FILE>` | `-o` | Output file. Default: stdout. |\n\n## Heading Options\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--heading-style <STYLE>` | `atx`, `underlined`, `atx-closed` | `atx` | `atx`: `# h1`. `underlined`: `h1\\n===`. `atx-closed`: `# h1 #`. |\n\n## List Options\n\n| Flag | Short | Values | Default | Description |\n|------|-------|--------|---------|-------------|\n| `--list-indent-type <TYPE>` | | `spaces`, `tabs` | `spaces` | Indentation type for nested lists. |\n| `--list-indent-width <N>` | | 1–8 | `2` | Spaces per indent level. Ignored with `tabs`. |\n| `--bullets <CHARS>` | `-b` | string | `\"-*+\"` | Bullet characters cycling through nesting levels. E.g. `\"*+-\"`. |\n\n## Text Formatting\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--strong-em-symbol <CHAR>` | `*`, `_` | `*` | Symbol for bold/italic emphasis. |\n| `--escape-asterisks` | flag | off | Escape `*` in plain text. |\n| `--escape-underscores` | flag | off | Escape `_` in plain text. |\n| `--escape-misc` | flag | off | Escape `[]()#` and other Markdown metacharacters. |\n| `--escape-ascii` | flag | off | Escape all ASCII punctuation (strict CommonMark compliance). |\n| `--sub-symbol <SYMBOL>` | string | `\"\"` | Symbol wrapping `<sub>` text. E.g. `\"~\"`. |\n| `--sup-symbol <SYMBOL>` | string | `\"\"` | Symbol wrapping `<sup>` text. E.g. `\"^\"`. |\n| `--newline-style <STYLE>` | `backslash`, `spaces` | `spaces` | `<br>` representation: `backslash` (`\\`+newline) or `spaces` (two trailing spaces). |\n\n## Code Blocks\n\n| Flag | Short | Values | Default | Description |\n|------|-------|--------|---------|-------------|\n| `--code-block-style <STYLE>` | | `backticks`, `indented`, `tildes` | `backticks` | Code block fence style. |\n| `--code-language <LANG>` | `-l` | string | `\"\"` | Default language for fenced code blocks. |\n\n## Links\n\n| Flag | Short | Default | Description |\n|------|-------|---------|-------------|\n| `--autolinks` | `-a` | off (CLI default; Rust library default is `true`) | Convert bare URLs to `<url>` autolinks when text equals href. |\n| `--default-title` | | off | Use href as link title when no `title` attribute exists. |\n\n## Images\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--keep-inline-images-in <ELEMENTS>` | comma-separated tag names | none | Keep images as Markdown in these parent elements. E.g. `\"a,strong\"`. |\n\n## Tables\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--br-in-tables` | off | Use `<br>` in table cells instead of converting to spaces. |\n\n## Highlighting\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--highlight-style <STYLE>` | `double-equal`, `html`, `bold`, `none` | `double-equal` | `<mark>` rendering. `double-equal` → `==text==`. |\n\n## Metadata\n\nMetadata is extracted by default and included in JSON output when `--json` is used. Use the flags below to control which metadata fields are populated.\n\n| Flag | Description |\n|------|-------------|\n| `--extract-metadata` | Extract title and meta tags as an HTML comment header in Markdown output (plain text mode). |\n| `--extract-document` | Extract document-level metadata (title, description, charset, lang, etc.) into `metadata.document`. |\n| `--extract-headers` | Extract h1-h6 headers with hierarchy into `metadata.headers`. |\n| `--extract-links` | Extract anchor tags with link type classification into `metadata.links`. |\n| `--extract-images` | Extract img tag metadata (not inline image data) into `metadata.images`. |\n| `--extract-structured-data` | Extract JSON-LD, Microdata, and RDFa blocks into `metadata.structured_data`. |\n\nMetadata is returned in `result.metadata` within the JSON output (use `--json` to see it):\n\n```json\n{\n    \"content\": \"# Title\\n\\nContent\\n\",\n    \"metadata\": {\n        \"document\": { \"title\": \"...\", \"language\": \"en\" },\n        \"headers\": [...],\n        \"links\": [...],\n        \"images\": [...],\n        \"structured_data\": [...]\n    }\n}\n```\n\n## Whitespace\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--whitespace-mode <MODE>` | `normalized`, `strict` | `normalized` | Whitespace handling. `normalized` collapses spaces; `strict` preserves as-is. |\n| `--strip-newlines` | flag | off | Remove all newlines from input HTML before processing. |\n\n## Wrapping\n\n| Flag | Short | Values | Default | Description |\n|------|-------|--------|---------|-------------|\n| `--wrap` | `-w` | flag | off | Enable text wrapping. |\n| `--wrap-width <N>` | | 20–500 | `80` | Column width when `--wrap` is enabled. |\n\n## Element Handling\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--convert-as-inline` | flag | off | Treat block elements as inline (no paragraph breaks). |\n| `--strip-tags <TAGS>` | comma-separated | none | HTML tags to strip entirely (output only text content). E.g. `\"script,style\"`. |\n\n## Preprocessing\n\n| Flag | Values | Default | Description |\n|------|--------|---------|-------------|\n| `--preprocess` / `-p` | flag | off | Enable HTML preprocessing (removes navigation, ads, forms, etc.). |\n| `--preset <LEVEL>` | `minimal`, `standard`, `aggressive` | `standard` | Preprocessing aggressiveness. Requires `--preprocess`. |\n| `--keep-navigation` | flag | off | Don't remove `<nav>`, menus during preprocessing. Requires `--preprocess`. |\n| `--keep-forms` | flag | off | Don't remove `<form>`, `<input>` during preprocessing. Requires `--preprocess`. |\n\n## Parsing\n\n| Flag | Short | Default | Description |\n|------|-------|---------|-------------|\n| `--encoding <ENCODING>` | `-e` | `utf-8` | Input character encoding. E.g. `latin-1`. |\n\n## Output Format\n\n| Flag | Short | Values | Default | Description |\n|------|-------|--------|---------|-------------|\n| `--output-format <FORMAT>` | `-f` | `markdown`, `djot` | `markdown` | Output markup format. |\n\n## JSON Output\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--json` | off | Output the full `ConversionResult` as JSON instead of plain Markdown. JSON has keys: `content`, `tables`, `metadata`, `images`, `warnings`. |\n| `--include-structure` | off | Include document structure tree in JSON output (`document` key). Requires `--json`. |\n| `--extract-inline-images` | off | Include extracted inline image data in JSON output (`images` key). Requires `--json`. |\n| `--show-warnings` | off | Print non-fatal processing warnings to stderr. |\n| `--no-content` | off | Suppress Markdown text output — only extract metadata/tables/images. Useful with `--json` for extraction-only mode. |\n\nWhen `--json` is used, stdout receives JSON:\n\n```json\n{\n    \"content\": \"# Title\\n\\nContent\\n\",\n    \"metadata\": {\n        \"document\": { \"title\": \"...\", \"language\": \"en\" },\n        \"headers\": [...],\n        \"links\": [...],\n        \"images\": [...],\n        \"structured_data\": [...]\n    },\n    \"tables\": [...],\n    \"images\": [...],\n    \"warnings\": [...]\n}\n```\n\n## Debugging\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--debug` | off | Output diagnostic warnings and information. |\n\n## Meta\n\n| Flag | Description |\n|------|-------------|\n| `--generate-completion <SHELL>` | Generate shell completion script. SHELL: `bash`, `zsh`, `fish`, `powershell`, `elvish`. |\n| `--generate-man` | Generate man page to stdout. |\n| `--version` | Print version. |\n| `--help` / `-h` | Print help. |\n\n## Examples\n\n```bash\n# Basic conversion from stdin\necho '<h1>Title</h1><p>Content</p>' | html-to-markdown\n\n# Convert file to stdout\nhtml-to-markdown input.html\n\n# Convert and save to file\nhtml-to-markdown input.html -o output.md\n\n# Fetch URL and convert\nhtml-to-markdown --url https://example.com > output.md\n\n# Fetch URL with custom user agent\nhtml-to-markdown --url https://example.com --user-agent \"MyBot/1.0\"\n\n# JSON output (ConversionResult with content, tables, metadata, images, warnings)\nhtml-to-markdown --json input.html\n\n# JSON output with document structure\nhtml-to-markdown --json --include-structure input.html\n\n# JSON output with inline images extracted\nhtml-to-markdown --json --extract-inline-images input.html\n\n# Extraction-only mode (no markdown text, just metadata/tables)\nhtml-to-markdown --json --no-content input.html\n\n# Show warnings to stderr\nhtml-to-markdown --show-warnings input.html\n\n# Full metadata extraction to file\nhtml-to-markdown --json \\\n    --extract-document --extract-headers --extract-links --extract-images \\\n    input.html -o output.json\n\n# Web scraping with aggressive preprocessing\nhtml-to-markdown page.html --preprocess --preset aggressive\n\n# Custom heading and list styles\nhtml-to-markdown input.html \\\n    --heading-style atx \\\n    --bullets '*' \\\n    --list-indent-width 2\n\n# Discord/Slack-friendly output (2-space indents, backtick code blocks)\nhtml-to-markdown input.html \\\n    --list-indent-width 2 \\\n    --code-block-style backticks\n\n# Djot output format\nhtml-to-markdown input.html --output-format djot\n\n# Generate shell completions\nhtml-to-markdown --generate-completion bash > html-to-markdown.bash\nhtml-to-markdown --generate-completion zsh > _html-to-markdown\n\n# Generate man page\nhtml-to-markdown --generate-man > html-to-markdown.1\n```\n\n## Exit Codes\n\n| Code | Meaning |\n|------|---------|\n| 0 | Success |\n| 1 | Conversion error or I/O error |\n| 2 | Invalid arguments |\n"
  },
  {
    "path": "skills/html-to-markdown/references/configuration.md",
    "content": "# Configuration Reference\n\nAll `ConversionOptions` fields with their types, defaults, and descriptions.\n\nIn Rust, use `ConversionOptions::builder()` or direct struct construction.\nIn Python, use the `ConversionOptions` dataclass.\nIn TypeScript/Node.js, use `JsConversionOptions` interface (camelCase).\n\n## ConversionOptions Fields\n\n| Rust Field | Python | TypeScript | Type | Default | Description |\n|------------|--------|------------|------|---------|-------------|\n| `heading_style` | `heading_style` | `headingStyle` | enum | `atx` | Heading format: `atx` (`# h1`), `underlined` (`===`), `atxClosed` (`# h1 #`) |\n| `list_indent_type` | `list_indent_type` | `listIndentType` | enum | `spaces` | List indentation: `spaces` or `tabs` |\n| `list_indent_width` | `list_indent_width` | `listIndentWidth` | int | `2` | Spaces per list indent level (ignored when `list_indent_type = tabs`) |\n| `bullets` | `bullets` | `bullets` | string | `\"-*+\"` | Bullet characters cycling through nesting levels. Characters cycle across nesting levels. Use `\"*+-\"` for a different order. |\n| `strong_em_symbol` | `strong_em_symbol` | `strongEmSymbol` | char | `'*'` | Symbol for bold/italic emphasis: `'*'` or `'_'` |\n| `escape_asterisks` | `escape_asterisks` | `escapeAsterisks` | bool | `false` | Escape `*` in plain text to prevent unintended formatting |\n| `escape_underscores` | `escape_underscores` | `escapeUnderscores` | bool | `false` | Escape `_` in plain text |\n| `escape_misc` | `escape_misc` | `escapeMisc` | bool | `false` | Escape `[]()#` and other Markdown metacharacters |\n| `escape_ascii` | `escape_ascii` | `escapeAscii` | bool | `false` | Escape all ASCII punctuation (strict CommonMark compliance) |\n| `code_language` | `code_language` | `codeLanguage` | string | `\"\"` | Default language hint for fenced code blocks with no language annotation |\n| `autolinks` | `autolinks` | `autolinks` | bool | `true` | Convert bare URLs to `<url>` autolinks when link text equals href |\n| `default_title` | `default_title` | `defaultTitle` | bool | `false` | Use href as link title when no `title` attribute exists |\n| `br_in_tables` | `br_in_tables` | `brInTables` | bool | `false` | Render `<br>` inside table cells as literal `<br>` tags instead of spaces |\n| `highlight_style` | `highlight_style` | `highlightStyle` | enum | `doubleEqual` | `<mark>` rendering: `doubleEqual` (`==text==`), `html`, `bold`, `none` |\n| `extract_metadata` | `extract_metadata` | `extractMetadata` | bool | `true` | Extract `<head>` metadata into result. Requires `metadata` feature in Rust. |\n| `whitespace_mode` | `whitespace_mode` | `whitespaceMode` | enum | `normalized` | Whitespace handling: `normalized` (collapse to single space) or `strict` (preserve as-is) |\n| `strip_newlines` | `strip_newlines` | `stripNewlines` | bool | `false` | Remove all newlines from HTML input before processing (useful for minified HTML) |\n| `wrap` | `wrap` | `wrap` | bool | `false` | Enable line wrapping at `wrap_width` columns |\n| `wrap_width` | `wrap_width` | `wrapWidth` | int | `80` | Column width for wrapping when `wrap = true`. Range: 20–500. |\n| `convert_as_inline` | `convert_as_inline` | `convertAsInline` | bool | `false` | Treat all block elements as inline content (no paragraph breaks) |\n| `sub_symbol` | `sub_symbol` | `subSymbol` | string | `\"\"` | Symbol wrapping `<sub>` text. E.g. `\"~\"` → `~text~`. Empty = no wrapping. |\n| `sup_symbol` | `sup_symbol` | `supSymbol` | string | `\"\"` | Symbol wrapping `<sup>` text. E.g. `\"^\"` → `^text^`. Empty = no wrapping. |\n| `newline_style` | `newline_style` | `newlineStyle` | enum | `spaces` | `<br>` representation: `spaces` (two trailing spaces + newline) or `backslash` (`\\` + newline) |\n| `code_block_style` | `code_block_style` | `codeBlockStyle` | enum | `backticks` | Code block style: `backticks` (```), `indented` (4 spaces), `tildes` (~~~). |\n| `keep_inline_images_in` | `keep_inline_images_in` | `keepInlineImagesIn` | list/array | `[]` | HTML tag names where `<img>` children remain as Markdown (not converted to alt text) |\n| `preprocessing` | (separate param) | `preprocessing` | object | see below | HTML preprocessing config. In Python, pass as separate `PreprocessingOptions` argument. |\n| `encoding` | `encoding` | `encoding` | string | `\"utf-8\"` | Expected character encoding for input HTML |\n| `debug` | `debug` | `debug` | bool | `false` | Emit diagnostic warnings about unhandled elements |\n| `strip_tags` | `strip_tags` | `stripTags` | list/array | `[]` | HTML tag names whose content is stripped entirely from output (text not preserved) |\n| `preserve_tags` | `preserve_tags` | `preserveTags` | list/array | `[]` | HTML tag names preserved verbatim as raw HTML in output |\n| `skip_images` | `skip_images` | `skipImages` | bool | `false` | Omit all `<img>` elements from output entirely |\n| `output_format` | `output_format` | `outputFormat` | enum | `markdown` | Output format: `markdown` (CommonMark), `djot`, `plain` (text only) |\n| `include_document_structure` | `include_document_structure` | n/a (Node: not in `JsConversionOptions`) | bool | `false` | Include structured document tree in `ConversionResult.document`. Python/Rust only. |\n| `extract_images` | `extract_images` | `extractImages` | bool | `false` | Extract inline data URI images and SVGs. Requires `inline-images` Rust feature. |\n| `max_image_size` | `max_image_size` | `maxImageSize` | u64/int | `5242880` | Maximum decoded image size in bytes (default 5 MiB). Requires `inline-images`. |\n| `capture_svg` | `capture_svg` | `captureSvg` | bool | `false` | Capture `<svg>` elements as images. Requires `inline-images`. |\n| `infer_dimensions` | `infer_dimensions` | `inferDimensions` | bool | `true` | Infer image width/height from data URI. Requires `inline-images`. |\n\n## PreprocessingOptions Fields\n\nPreprocessing runs before conversion to clean noisy HTML (ads, navigation, forms).\n\n| Rust Field | Python | TypeScript | Type | Default | Description |\n|------------|--------|------------|------|---------|-------------|\n| `enabled` | `enabled` | `enabled` | bool | `true` | Enable HTML preprocessing globally |\n| `preset` | `preset` | `preset` | enum | `standard` | Aggressiveness: `minimal`, `standard`, `aggressive` |\n| `remove_navigation` | `remove_navigation` | `removeNavigation` | bool | `true` | Remove `<nav>`, breadcrumbs, menus, sidebars |\n| `remove_forms` | `remove_forms` | `removeForms` | bool | `true` | Remove `<form>`, `<input>`, `<button>`, etc. |\n\n## Enum Values\n\n### HeadingStyle\n\nTypeScript values are PascalCase strings (NAPI-RS `const enum`).\n\n| Value | Rust | Python | TypeScript | Output |\n|-------|------|--------|------------|--------|\n| ATX | `HeadingStyle::Atx` | `\"atx\"` | `'Atx'` | `# H1`, `## H2`, ... |\n| Underlined | `HeadingStyle::Underlined` | `\"underlined\"` | `'Underlined'` | `H1\\n===`, `H2\\n---` |\n| ATX Closed | `HeadingStyle::AtxClosed` | `\"atx_closed\"` | `'AtxClosed'` | `# H1 #`, `## H2 ##` |\n\n### CodeBlockStyle\n\n| Value | Rust | Python | TypeScript | Output |\n|-------|------|--------|------------|--------|\n| Indented | `CodeBlockStyle::Indented` | `\"indented\"` | `'Indented'` | 4-space indent |\n| Backticks | `CodeBlockStyle::Backticks` | `\"backticks\"` | `'Backticks'` | ` ``` ` fence |\n| Tildes | `CodeBlockStyle::Tildes` | `\"tildes\"` | `'Tildes'` | `~~~` fence |\n\n### OutputFormat\n\n| Value | Rust | Python | TypeScript | Description |\n|-------|------|--------|------------|-------------|\n| Markdown | `OutputFormat::Markdown` | `\"markdown\"` | `'Markdown'` | CommonMark Markdown (default) |\n| Djot | `OutputFormat::Djot` | `\"djot\"` | `'Djot'` | Djot lightweight markup |\n| Plain | `OutputFormat::Plain` | `\"plain\"` | `'Plain'` | Visible text only, no markup |\n\n### WhitespaceMode\n\n| Value | Rust | Python | TypeScript | Description |\n|-------|------|--------|------------|-------------|\n| Normalized | `WhitespaceMode::Normalized` | `\"normalized\"` | `'Normalized'` | Collapse to single space (default, matches browser) |\n| Strict | `WhitespaceMode::Strict` | `\"strict\"` | `'Strict'` | Preserve all whitespace as-is |\n\n### NewlineStyle\n\n| Value | Rust | Python | TypeScript | `<br>` becomes |\n|-------|------|--------|------------|----------------|\n| Spaces | `NewlineStyle::Spaces` | `\"spaces\"` | `'Spaces'` | `\\n` (two trailing spaces) |\n| Backslash | `NewlineStyle::Backslash` | `\"backslash\"` | `'Backslash'` | `\\\\\\n` |\n\n### HighlightStyle\n\n| Value | Rust | Python | TypeScript | `<mark>text</mark>` becomes |\n|-------|------|--------|------------|-------------------------------|\n| DoubleEqual | `HighlightStyle::DoubleEqual` | `\"double-equal\"` | `'DoubleEqual'` | `==text==` (default) |\n| Html | `HighlightStyle::Html` | `\"html\"` | `'Html'` | `<mark>text</mark>` |\n| Bold | `HighlightStyle::Bold` | `\"bold\"` | `'Bold'` | `**text**` |\n| None | `HighlightStyle::None` | `\"none\"` | `'None'` | `text` (plain) |\n\n### PreprocessingPreset\n\n| Value | Rust | Python | TypeScript | Description |\n|-------|------|--------|------------|-------------|\n| Minimal | `PreprocessingPreset::Minimal` | `\"minimal\"` | `'Minimal'` | Remove only scripts and styles |\n| Standard | `PreprocessingPreset::Standard` | `\"standard\"` | `'Standard'` | Remove navigation, forms, noise (default) |\n| Aggressive | `PreprocessingPreset::Aggressive` | `\"aggressive\"` | `'Aggressive'` | Maximum cleanup for web scraping |\n\n## MetadataConfig Fields\n\nControls what metadata is extracted. Metadata is returned in `ConversionResult.metadata` from the single `convert()` call (requires the `metadata` feature, which is enabled by default).\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `extract_document` | bool | `true` | Title, description, language, OG tags, etc. |\n| `extract_headers` | bool | `true` | h1-h6 headers with level, text, id |\n| `extract_links` | bool | `true` | Anchor tags with href, text, link_type |\n| `extract_images` | bool | `true` | img tags with src, alt, title, image_type |\n| `extract_structured_data` | bool | `true` | JSON-LD, Microdata, RDFa blocks |\n| `max_structured_data_size` | usize | `10000` | Max bytes per structured data block |\n\n## Notes\n\n- In **Python**, `PreprocessingOptions` is a separate `@dataclass` passed as the second argument to `convert()`, not nested inside `ConversionOptions`.\n- In **TypeScript/Node.js**, preprocessing is nested inside `JsConversionOptions.preprocessing`.\n- In **Rust**, preprocessing is a field inside `ConversionOptions` struct (`options.preprocessing`).\n- The `metadata` feature is **enabled by default** in the Rust crate (`features = [\"metadata\"]`). In Python and Node.js bindings, metadata is always available.\n- TypeScript/Node.js enum values are **PascalCase strings** (e.g. `'Atx'` not `'atx'`). This is a NAPI-RS `const enum` requirement — lowercase values will be rejected at runtime.\n- Inline image extraction in Node.js is configured via `extractImages`, `maxImageSize`, `captureSvg`, and `inferDimensions` options passed to `convert()`. Extracted images appear in the result's `images` array.\n"
  },
  {
    "path": "skills/html-to-markdown/references/other-bindings.md",
    "content": "# Other Language Bindings\n\nBrief reference for Go, Ruby, PHP, Java, C#, Elixir, R, WASM, and C FFI.\n\n---\n\n## Go\n\n**Module:** `github.com/kreuzberg-dev/html-to-markdown/packages/go/v3`\n**Package:** `htmltomarkdown`\n**Install:** `go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3`\n\nUses cgo with the C FFI layer. Options are passed as JSON strings internally.\n\n```go\nimport \"github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\"\n\n// Primary function — returns *ConversionResult\nresult, err := htmltomarkdown.Convert(html)\nif err != nil {\n    log.Fatal(err)\n}\nfmt.Println(result.Content)        // markdown string\nfmt.Println(len(result.Tables))    // extracted tables\nfmt.Println(len(result.Warnings))  // processing warnings\n\n// With options (variadic)\nresult, err = htmltomarkdown.Convert(html, &htmltomarkdown.ConversionOptions{\n    HeadingStyle: \"atx\",\n})\n\n// Metadata is in result.Metadata when metadata extraction is enabled\nfmt.Println(result.Metadata)\n\n// Tables are always in result.Tables\nfor _, table := range result.Tables {\n    fmt.Println(table.Markdown)\n}\n```\n\n### Go ConversionOptions\n\nOptions are passed as a pointer to `ConversionOptions`. Fields use Go naming conventions (PascalCase). Pass `nil` or omit the argument for defaults.\n\n---\n\n## Ruby\n\n**Gem:** `html-to-markdown`\n**Install:** `gem install html-to-markdown`\n**Require:** `require 'html_to_markdown'`\n\nUses Magnus (native extension via Rust).\n\n```ruby\nrequire 'html_to_markdown'\n\n# Primary function — returns a Hash\nresult = HtmlToMarkdown.convert(html)\nputs result[:content]          # markdown string\nputs result[:tables].length    # extracted tables\nputs result[:warnings].length  # processing warnings\nputs result[:metadata]         # metadata hash (or nil)\n\n# With options (Hash)\nresult = HtmlToMarkdown.convert(html, {\n    heading_style: \"atx\",\n    code_block_style: \"backticks\",\n    autolinks: true,\n})\n\n# Metadata — in result[:metadata]\nresult = HtmlToMarkdown.convert(html)\nmetadata = result[:metadata]\n\n# Inline images — set extract_images: true\nresult = HtmlToMarkdown.convert(html, { extract_images: true })\nimages = result[:images]\n\n# Tables — always in result[:tables]\nresult[:tables].each { |t| puts t[:markdown] }\n\n# Reusable options handle (performance)\nhandle = HtmlToMarkdown.options({ heading_style: \"atx\" })\nresult = HtmlToMarkdown.convert(html, handle)\n```\n\n### Ruby convert() return Hash\n\n```ruby\n{\n    content: String,              # markdown text\n    document: nil,                # not yet wired\n    metadata: Hash | nil,         # HtmlMetadata\n    tables: Array,                # [{grid: {...}, markdown: \"...\"}]\n    images: Array,                # inline images (if extract_images: true)\n    warnings: Array               # [{message: \"...\", kind: \"...\"}]\n}\n```\n\n---\n\n## PHP\n\n**Composer:** `kreuzberg-dev/html-to-markdown`\n**Install:** `composer require kreuzberg-dev/html-to-markdown`\n**PHP requirement:** 8.4+\n\nUses ext-php-rs (native PHP extension). The facade class is `HtmlToMarkdownRs` under the `Html\\To\\Markdown\\Rs` namespace.\n\n```php\n<?php\ndeclare(strict_types=1);\n\nuse Html\\To\\Markdown\\Rs\\HtmlToMarkdownRs;\n\n// Primary function — returns ConversionResult with content, metadata, tables, images, warnings\n$result = HtmlToMarkdownRs::convert('<h1>Hello</h1>');\n$markdown = $result->content;\n\n// With options (ConversionOptions object)\nuse Html\\To\\Markdown\\Rs\\ConversionOptions;\n\n$options = new ConversionOptions();\n$options->headingStyle = 'atx';\n$options->codeBlockStyle = 'backticks';\n$options->autolinks = true;\n\n$result = HtmlToMarkdownRs::convert('<h1>Hello</h1>', $options);\n\n// Metadata — in $result->metadata\n$metadata = $result->metadata;\necho $metadata->document->title;\n\n// Tables — always in $result->tables\nforeach ($result->tables as $table) {\n    echo $table->markdown;\n}\n\n// Inline images — set extractImages: true in options\n$options->extractImages = true;\n$result = HtmlToMarkdownRs::convert('<img src=\"data:...\" />', $options);\n$images = $result->images;\n```\n\n---\n\n## Java\n\n**Maven:** `dev.kreuzberg.htmltomarkdown:html-to-markdown-rs`\n**GroupId:** `dev.kreuzberg.htmltomarkdown`\n**ArtifactId:** `html-to-markdown-rs`\n**Java requirement:** 21+ (uses Panama FFM API)\n\n```xml\n<dependency>\n  <groupId>dev.kreuzberg.htmltomarkdown</groupId>\n  <artifactId>html-to-markdown-rs</artifactId>\n  <version>3.2.0</version>\n</dependency>\n```\n\n```java\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport dev.kreuzberg.htmltomarkdown.ConversionResult;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdownRsException;\n\n// Primary function — returns ConversionResult\ntry {\n    ConversionResult result = HtmlToMarkdown.convert(\"<h1>Hello</h1>\");\n    System.out.println(result.content());    // markdown string\n    System.out.println(result.tables());     // List<TableData>\n    System.out.println(result.warnings());   // List<ProcessingWarning>\n    System.out.println(result.metadata());   // metadata map (when enabled)\n} catch (HtmlToMarkdownRsException e) {\n    System.err.println(\"Conversion failed: \" + e.getMessage());\n}\n\n// With options\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\nConversionOptions options = new ConversionOptions();\noptions.setHeadingStyle(\"atx\");\nConversionResult result = HtmlToMarkdown.convert(\"<h1>Hello</h1>\", options);\n\n// Tables — always in result.tables()\nfor (var table : result.tables()) {\n    System.out.println(table.markdown());\n}\n```\n\n---\n\n## C# (.NET)\n\n**NuGet:** `KreuzbergDev.HtmlToMarkdown`\n**Install:** `dotnet add package KreuzbergDev.HtmlToMarkdown`\n**.NET requirement:** 6+\n\nThe static entry point class is `HtmlToMarkdownRs`. The exception type is `HtmlToMarkdownRsException`.\n\n```csharp\nusing HtmlToMarkdown;\n\n// Primary function\nvar result = HtmlToMarkdownRs.Convert(\"<h1>Hello</h1>\", null);\nConsole.WriteLine(result.Content);         // markdown string\nConsole.WriteLine(result.Tables.Count);    // table count\nConsole.WriteLine(result.Warnings.Count);  // warning count\nConsole.WriteLine(result.Metadata?.Document?.Title);  // metadata (when enabled)\n\n// With options\nvar options = new ConversionOptions { ExtractImages = true };\nvar result2 = HtmlToMarkdownRs.Convert(html, options);\nforeach (var image in result2.Images) {\n    Console.WriteLine(image.Format);\n}\n\n// Tables — always in result.Tables\nforeach (var table in result.Tables) {\n    Console.WriteLine(table.Markdown);\n}\n```\n\n### HtmlToMarkdownRsException\n\n```csharp\ntry {\n    var result = HtmlToMarkdownRs.Convert(html, null);\n} catch (HtmlToMarkdownRsException e) {\n    Console.Error.WriteLine($\"Conversion failed: {e.Message}\");\n}\n```\n\n---\n\n## Elixir\n\n**Hex:** `html_to_markdown`\n**Module:** `HtmlToMarkdown`\n**Elixir requirement:** 1.14+ (uses Rustler NIFs)\n\n```elixir\n# mix.exs\n{:html_to_markdown, \"~> 3.0\"}\n```\n\n```elixir\n# Primary function — returns {:ok, map()} | {:error, term()}\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1>\")\nIO.puts result.content      # markdown string\nIO.inspect result.tables    # list of table maps\nIO.inspect result.warnings  # list of warning maps\nIO.inspect result.metadata  # metadata map (when enabled)\n\n# Bang variant (raises on error)\nresult = HtmlToMarkdown.convert!(\"<h1>Hello</h1>\")\n\n# With options\n{:ok, result} = HtmlToMarkdown.convert(html, %{\n    heading_style: \"atx\",\n    code_block_style: \"backticks\",\n})\n\n# Metadata — in result.metadata\n{:ok, result} = HtmlToMarkdown.convert(html)\nmetadata = result.metadata\n\n# Tables — always in result.tables\nEnum.each(result.tables, fn table -> IO.puts table.markdown end)\n\n# Inline images — set extract_images: true\n{:ok, result} = HtmlToMarkdown.convert(html, %{extract_images: true})\nimages = result.images\n\n# Options handle (reuse for performance)\n{:ok, handle} = HtmlToMarkdown.create_options_handle(%{heading_style: \"atx\"})\n{:ok, result} = HtmlToMarkdown.convert(html, handle)\n```\n\n---\n\n## R\n\n**CRAN:** `htmltomarkdown`\n**Install:** `install.packages(\"htmltomarkdown\")`\n**R requirement:** 4.1+\n\nUses extendr (Rust bindings for R).\n\n```r\nlibrary(htmltomarkdown)\n\n# Primary function\nresult <- convert(\"<h1>Hello</h1>\")\ncat(result$content)          # markdown string\nlength(result$tables)        # table count\n\n# With options (named list)\nresult <- convert(\"<h1>Hello</h1>\", list(\n    heading_style = \"atx\",\n    code_block_style = \"backticks\"\n))\n\n# Metadata — in result$metadata\nresult <- convert(\"<h1>Hello</h1>\")\nmetadata <- result$metadata\n\n# Tables — always in result$tables\nfor (table in result$tables) {\n    cat(table$markdown)\n}\n\n# Inline images — set extract_images = TRUE\nresult <- convert(\"<img src='data:...' />\", list(extract_images = TRUE))\nimages <- result$images\n\n# Options handle (performance)\nhandle <- create_options_handle(list(heading_style = \"atx\"))\nresult <- convert_handle(\"<h1>Hello</h1>\", handle)\n```\n\n---\n\n## WASM\n\n**Package:** `@kreuzberg/html-to-markdown-wasm` (built with wasm-pack)\n\n```javascript\nimport init, { convert, convertBytes, createConversionOptionsHandle, convertWithOptionsHandle } from '@kreuzberg/html-to-markdown-wasm';\n\nawait init(); // initialize WASM module\n\n// convert() — returns JSON string, always JSON.parse() the result\nconst result = JSON.parse(convert('<h1>Hello</h1>', {}));\nconsole.log(result.content);    // markdown string\nconsole.log(result.tables);     // extracted tables\nconsole.log(result.metadata);   // metadata (when enabled)\n\n// convertBytes() — accepts Uint8Array\nconst encoder = new TextEncoder();\nconst bytes = encoder.encode('<h1>Hello</h1>');\nconst result2 = JSON.parse(convertBytes(bytes, {}));\n\n// Metadata — in result.metadata when extract_metadata is enabled\nconst result3 = JSON.parse(convert('<h1>Hello</h1>', { extractMetadata: true }));\nconsole.log(result3.metadata);\n\n// Tables — always in result.tables\nfor (const table of result.tables) {\n    console.log(table.markdown);\n}\n\n// Options handle (reuse for performance)\nconst handle = createConversionOptionsHandle({ headingStyle: 'atx' });\nconst json = convertWithOptionsHandle('<h1>Hello</h1>', handle);\n```\n\n---\n\n## Node.js / npm\n\n**Package:** `@kreuzberg/html-to-markdown-node`\n**Install:** `npm install @kreuzberg/html-to-markdown-node`\n\nUses NAPI-RS. Platform-specific native binaries are delivered as optional dependencies.\n\n```json\n{\n  \"optionalDependencies\": {\n    \"@kreuzberg/html-to-markdown-node-darwin-arm64\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-darwin-x64\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-linux-arm64-gnu\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-linux-arm64-musl\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-linux-x64-gnu\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-linux-x64-musl\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-linux-arm-gnueabihf\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-win32-x64-msvc\": \"3.2.0\",\n    \"@kreuzberg/html-to-markdown-node-win32-arm64-msvc\": \"3.2.0\"\n  }\n}\n```\n\n---\n\n## C FFI\n\n**Crate:** `html-to-markdown-ffi`\n**Header:** `html_to_markdown.h` (generated by cbindgen)\n\nUsed internally by Go, Java, and C# bindings. All exported symbols use the `htm_` prefix. Direct C usage:\n\n```c\n#include \"html_to_markdown.h\"\n\n// htm_convert() — returns an opaque HTMConversionResult handle\nHTMConversionOptions *opts = htm_conversion_options_from_json(\"{\\\"headingStyle\\\":\\\"atx\\\"}\");\nHTMConversionResult *result = htm_convert(html_cstr, opts);\nhtm_conversion_options_free(opts);\n\nif (result) {\n    // Extract fields as malloc'd strings\n    char *content = htm_conversion_result_content(result);\n    // use content ...\n    htm_free_string(content);\n\n    htm_conversion_result_free(result);\n}\n\n// Error handling\nint32_t code = htm_last_error_code();\nconst char *msg = htm_last_error_context(); // borrowed — do NOT free\n\n// Version\nconst char *ver = htm_version(); // static — do NOT free\n```\n\n**Key FFI contracts:**\n\n- All exported functions use the `htm_` prefix.\n- Strings returned by `htm_conversion_result_content()` and similar field accessors must be freed with `htm_free_string()`. Never use the system `free()`.\n- `htm_last_error_context()` and `htm_version()` return borrowed/static pointers — do NOT free them.\n- Every `_free()` function has a matching allocator (`htm_convert` → `htm_conversion_result_free`, `htm_conversion_options_from_json` → `htm_conversion_options_free`).\n"
  },
  {
    "path": "skills/html-to-markdown/references/python-api.md",
    "content": "# Python API Reference\n\nPackage name: `html-to-markdown`\nImport: `from html_to_markdown import ...`\nPython requirement: 3.10+\n\n## Primary Function\n\n```python\ndef convert(\n    html: str,\n    options: ConversionOptions | None = None,\n) -> ConversionResult:\n    ...\n```\n\nReturns a `ConversionResult` dataclass with all extracted data in a single pass. Pass `PreprocessingOptions` via `ConversionOptions.preprocessing`.\n\n```python\nfrom html_to_markdown import convert, ConversionOptions, PreprocessingOptions\n\n# Simple\nresult = convert(\"<h1>Hello</h1><p>World</p>\")\nprint(result.content)        # \"# Hello\\n\\nWorld\\n\"\nprint(result.tables)         # []\nprint(result.warnings)       # []\nprint(result.metadata)       # HtmlMetadata or None\n\n# With options\nresult = convert(\n    html,\n    options=ConversionOptions(\n        heading_style=\"atx\",\n        code_block_style=\"backticks\",\n        preprocessing=PreprocessingOptions(enabled=True, preset=\"aggressive\"),\n    ),\n)\nprint(result.content)\n```\n\n## ConversionResult (dataclass)\n\n```python\nfrom html_to_markdown import ConversionResult\n\n@dataclass\nclass ConversionResult:\n    content: str | None = None           # Converted markdown/djot/plain text\n    document: Any | None = None          # Document structure (populated when include_document_structure=True)\n    metadata: Any | None = None          # HtmlMetadata or None\n    tables: list[Any] = field(default_factory=list)   # Extracted tables\n    images: list[str] = field(default_factory=list)   # Extracted inline images (if extract_images=True)\n    warnings: list[Any] = field(default_factory=list) # Non-fatal warnings\n```\n\n## ConversionOptions (dataclass)\n\n```python\nfrom html_to_markdown import ConversionOptions\n\n@dataclass\nclass ConversionOptions:\n    # Headings\n    heading_style: str = \"atx\"           # \"underlined\" | \"atx\" | \"atx_closed\"\n\n    # Lists\n    list_indent_type: str = \"spaces\"     # \"spaces\" | \"tabs\"\n    list_indent_width: int = 2\n    bullets: str = \"-*+\"\n\n    # Emphasis\n    strong_em_symbol: str = \"*\"          # \"*\" or \"_\"\n\n    # Escaping\n    escape_asterisks: bool = False\n    escape_underscores: bool = False\n    escape_misc: bool = False\n    escape_ascii: bool = False\n\n    # Code\n    code_language: str = \"\"\n    code_block_style: str = \"backticks\"  # \"indented\" | \"backticks\" | \"tildes\"\n\n    # Links\n    autolinks: bool = True\n    default_title: bool = False\n    link_style: str = \"inline\"           # \"inline\" | \"reference\"\n\n    # Images\n    keep_inline_images_in: list[str] = field(default_factory=list)\n    skip_images: bool = False\n    extract_images: bool = False\n    max_image_size: int = 5_242_880     # 5 MiB\n    capture_svg: bool = False\n    infer_dimensions: bool = True\n\n    # Tables\n    br_in_tables: bool = False\n\n    # Highlight\n    highlight_style: str = \"double_equal\"  # \"double_equal\" | \"html\" | \"bold\" | \"none\"\n\n    # Metadata\n    extract_metadata: bool = True\n\n    # Whitespace\n    whitespace_mode: str = \"normalized\"  # \"normalized\" | \"strict\"\n    strip_newlines: bool = False\n\n    # Wrapping\n    wrap: bool = False\n    wrap_width: int = 80\n\n    # Element handling\n    strip_tags: list[str] = field(default_factory=list)\n    preserve_tags: list[str] = field(default_factory=list)\n    convert_as_inline: bool = False\n\n    # Subscript / superscript\n    sub_symbol: str = \"\"\n    sup_symbol: str = \"\"\n\n    # Newlines\n    newline_style: str = \"spaces\"        # \"spaces\" | \"backslash\"\n\n    # Output format\n    output_format: str = \"markdown\"      # \"markdown\" | \"djot\" | \"plain\"\n\n    # Document structure\n    include_document_structure: bool = False\n\n    # Preprocessing (pass PreprocessingOptions instance)\n    preprocessing: Any | None = None\n\n    # Encoding and debug\n    encoding: str = \"utf-8\"\n    debug: bool = False\n```\n\n## PreprocessingOptions (dataclass)\n\n```python\nfrom html_to_markdown import PreprocessingOptions\n\n@dataclass\nclass PreprocessingOptions:\n    enabled: bool = True\n    preset: str = \"standard\"             # \"minimal\" | \"standard\" | \"aggressive\"\n    remove_navigation: bool = True\n    remove_forms: bool = True\n```\n\n## New Public Types in v3.2.0\n\nThe following types are now exported from `html_to_markdown` directly:\n\n```python\nfrom html_to_markdown import (\n    CodeBlockStyle,        # Enum: INDENTED, BACKTICKS, TILDES\n    ConversionResult,      # Dataclass: result of convert()\n    DocumentMetadata,      # Dataclass: document-level metadata\n    HeadingStyle,          # Enum: UNDERLINED, ATX, ATX_CLOSED\n    HighlightStyle,        # Enum: DOUBLE_EQUAL, HTML, BOLD, NONE\n    HtmlMetadata,          # Dataclass: full metadata extraction result\n    LinkStyle,             # Enum: INLINE, REFERENCE\n    ListIndentType,        # Enum: SPACES, TABS\n    MetadataConfig,        # Dataclass: metadata extraction configuration\n    NewlineStyle,          # Enum: SPACES, BACKSLASH\n    OutputFormat,          # Enum: MARKDOWN, DJOT, PLAIN\n    PreprocessingPreset,   # Enum: MINIMAL, STANDARD, AGGRESSIVE\n    TableGrid,             # Dataclass: table grid structure\n    TextDirection,         # Enum: LEFT_TO_RIGHT, RIGHT_TO_LEFT, AUTO\n    WhitespaceMode,        # Enum: NORMALIZED, STRICT\n)\n```\n\n## Accessing Metadata, Tables, and Images\n\nAll structured data is in the `ConversionResult` dataclass returned by `convert()`. Use `ConversionOptions` fields to control what is extracted:\n\n```python\nfrom html_to_markdown import convert, ConversionOptions\n\n# Metadata — enabled by default\nresult = convert(html)\nmeta = result.metadata\nprint(meta.document.title)\nprint(meta.headers)\nprint(meta.links)\n\n# Tables — always present in result\nfor table in result.tables:\n    print(table.markdown)\n\n# Inline images — set extract_images=True\nresult = convert(html, ConversionOptions(extract_images=True))\nfor image in result.images:\n    print(image)\n\n# Document structure — set include_document_structure=True\nresult = convert(html, ConversionOptions(include_document_structure=True))\ndoc = result.document\n\n# Plain string output\nmarkdown: str = result.content\n```\n\n## MetadataConfig\n\n```python\nfrom html_to_markdown import MetadataConfig\n\nconfig = MetadataConfig(\n    extract_document=True,\n    extract_headers=True,\n    extract_links=True,\n    extract_images=True,\n    extract_structured_data=True,\n    max_structured_data_size=0,   # 0 = unlimited\n)\n```\n\n## Error Handling\n\n```python\nfrom html_to_markdown import convert\nfrom html_to_markdown.exceptions import (\n    ConversionError,       # Base exception for all conversion errors\n    ParseError,            # HTML parsing error\n    SanitizationError,     # HTML sanitization error\n    ConfigError,           # Invalid configuration\n    IoError,               # I/O error\n    PanicError,            # Panic caught during conversion\n    InvalidInputError,     # Invalid input data (binary data, invalid UTF-8)\n    OtherError,            # Generic conversion error\n)\n\ntry:\n    result = convert(html)\nexcept InvalidInputError as e:\n    print(f\"Invalid input: {e}\")\nexcept ConfigError as e:\n    print(f\"Bad options: {e}\")\nexcept ConversionError as e:\n    print(f\"Conversion failed: {e}\")\n```\n\n## Async Tip\n\nThe `convert()` function is synchronous but releases the GIL (Python GVL) during the Rust computation. For CPU-bound workloads, use `asyncio.to_thread()` or a thread pool:\n\n```python\nimport asyncio\nfrom html_to_markdown import convert\n\nasync def convert_async(html: str):\n    return await asyncio.to_thread(convert, html)\n```\n"
  },
  {
    "path": "skills/html-to-markdown/references/rust-api.md",
    "content": "# Rust API Reference\n\nCrate name: `html-to-markdown-rs`\n\n## Cargo.toml\n\n```toml\n[dependencies]\nhtml-to-markdown-rs = \"3\"\n# Default features include: metadata\n# Available feature flags:\n#   metadata       - HtmlMetadata extraction (default)\n#   inline-images  - Inline image/SVG extraction\n#   visitor        - Custom element visitor pattern\n#   async-visitor  - Async visitor support (implies visitor)\n#   serde          - Serde serialize/deserialize for options/results\n#   full           - All features: inline-images, metadata, visitor, async-visitor, serde\n```\n\n## Primary Function\n\n```rust\npub fn convert(\n    html: &str,\n    options: Option<ConversionOptions>,\n) -> Result<ConversionResult>\n```\n\nReturns a `ConversionResult` containing converted text, tables, metadata, images, and warnings. This is the single entry point for all conversions.\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\n// Simple conversion\nlet result = convert(\"<h1>Hello</h1>\", None)?;\nprintln!(\"{}\", result.content.unwrap_or_default());\n\n// With options\nlet opts = ConversionOptions::builder()\n    .heading_style(HeadingStyle::Atx)\n    .build();\nlet result = convert(\"<h1>Hello</h1>\", Some(opts))?;\n```\n\n## ConversionResult\n\n`ConversionResult` derives `Serialize` and `Deserialize` (always, not gated on a feature).\n\n```rust\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ConversionResult {\n    /// Converted text output (Markdown, Djot, or plain text).\n    /// None only when output_format is OutputFormat::None (extraction-only mode).\n    pub content: Option<String>,\n\n    /// Structured document tree. Populated when include_document_structure = true.\n    pub document: Option<DocumentStructure>,\n\n    /// Extracted HTML metadata. Requires \"metadata\" feature (default).\n    #[cfg(feature = \"metadata\")]\n    pub metadata: HtmlMetadata,\n\n    /// Extracted tables with structured cell data and markdown representation.\n    pub tables: Vec<TableData>,\n\n    /// Extracted inline images (data URIs and SVGs). Requires \"inline-images\" feature.\n    #[cfg(feature = \"inline-images\")]\n    pub images: Vec<InlineImage>,\n\n    /// Non-fatal processing warnings.\n    pub warnings: Vec<ProcessingWarning>,\n}\n```\n\n## ConversionOptions Builder\n\n`ConversionOptions::builder()` returns `ConversionOptionsBuilder` (now public, exported from crate root). Call `.build()` to produce final options.\n\n```rust\nuse html_to_markdown_rs::{\n    ConversionOptions, HeadingStyle, ListIndentType, CodeBlockStyle,\n    NewlineStyle, HighlightStyle, OutputFormat, WhitespaceMode,\n    PreprocessingOptions, PreprocessingPreset,\n};\n\nlet options = ConversionOptions::builder()\n    // Output control\n    .output_format(OutputFormat::Markdown)      // Markdown | Djot | Plain\n    .include_document_structure(false)\n    .extract_metadata(true)\n    .extract_images(false)                       // requires inline-images feature\n\n    // Markdown formatting\n    .heading_style(HeadingStyle::Atx)           // Atx | Underlined | AtxClosed\n    .list_indent_type(ListIndentType::Spaces)   // Spaces | Tabs\n    .list_indent_width(2usize)\n    .bullets(\"-*+\")                               // default \"-*+\"; cycles per nesting level\n    .strong_em_symbol('*')                       // '*' or '_'\n    .code_block_style(CodeBlockStyle::Backticks) // Indented | Backticks | Tildes (default: Backticks)\n    .newline_style(NewlineStyle::Spaces)         // Spaces | Backslash\n    .highlight_style(HighlightStyle::DoubleEqual) // DoubleEqual | Html | Bold | None\n    .code_language(\"\")                           // default language for fenced code blocks\n    .autolinks(true)\n    .default_title(false)\n    .br_in_tables(false)\n    .sub_symbol(\"\")                              // e.g. \"~\"\n    .sup_symbol(\"\")                              // e.g. \"^\"\n\n    // Escaping\n    .escape_asterisks(false)\n    .escape_underscores(false)\n    .escape_misc(false)\n    .escape_ascii(false)\n\n    // Whitespace / wrapping\n    .whitespace_mode(WhitespaceMode::Normalized) // Normalized | Strict\n    .strip_newlines(false)\n    .wrap(false)\n    .wrap_width(80usize)\n\n    // Element handling\n    .convert_as_inline(false)\n    .skip_images(false)\n    .strip_tags(vec![])                          // tag names to strip (text only)\n    .preserve_tags(vec![])                       // tag names to preserve as HTML\n    .keep_inline_images_in(vec![])               // parent tags where images stay inline\n\n    // Inline image extraction (requires inline-images feature)\n    .max_image_size(5_242_880u64)               // 5 MiB default\n    .capture_svg(false)\n    .infer_dimensions(true)\n\n    // Preprocessing\n    .preprocessing(PreprocessingOptions {\n        enabled: false,\n        preset: PreprocessingPreset::Standard,  // Minimal | Standard | Aggressive\n        remove_navigation: true,\n        remove_forms: true,\n    })\n\n    // Encoding and debug\n    .encoding(\"utf-8\")\n    .debug(false)\n\n    .build();\n```\n\n## ConversionOptions Fields (Direct Access)\n\n`ConversionOptions` is a plain struct — all fields are public. You can also construct it directly:\n\n```rust\nlet options = ConversionOptions {\n    heading_style: HeadingStyle::Atx,\n    list_indent_width: 4,\n    ..ConversionOptions::default()\n};\n```\n\n## Accessing Metadata, Images, and Tables\n\nAll structured data is returned in the single `ConversionResult` from `convert()`. Enable the relevant options to populate each field:\n\n```rust\nuse html_to_markdown_rs::{convert, ConversionOptions};\n\n// Metadata — available by default (requires \"metadata\" feature, which is default)\nlet result = convert(html, None)?;\nlet meta = &result.metadata;\nprintln!(\"{:?}\", meta.document.title);\n\n// Tables — always populated in the result\nfor table in &result.tables {\n    println!(\"{}\", table.markdown);\n    println!(\"{:?}\", table.grid.cells);\n}\n\n// Inline images — requires \"inline-images\" feature and extract_images: true\nlet opts = ConversionOptions::builder()\n    .extract_images(true)\n    .build();\nlet result = convert(html, Some(opts))?;\nfor image in &result.images {\n    println!(\"{:?}\", image.format);\n}\n\n// Custom visitor — pass via options (requires \"visitor\" feature)\nlet opts = ConversionOptions::builder()\n    // visitor is configured as part of options\n    .build();\n\n// Document structure\nlet opts = ConversionOptions::builder()\n    .include_document_structure(true)\n    .build();\nlet result = convert(html, Some(opts))?;\nif let Some(doc) = &result.document {\n    println!(\"{} nodes\", doc.nodes.len());\n}\n\n// Plain string output\nlet result = convert(html, None)?;\nlet markdown: String = result.content.unwrap_or_default();\n```\n\n## JSON Configuration (requires `serde` or `metadata` feature)\n\n```rust\npub fn conversion_options_from_json(json: &str) -> Result<ConversionOptions>\npub fn conversion_options_update_from_json(json: &str) -> Result<ConversionOptionsUpdate>\npub fn metadata_config_from_json(json: &str) -> Result<MetadataConfig>  // metadata feature\npub fn inline_image_config_from_json(json: &str) -> Result<InlineImageConfig>  // inline-images\n```\n\n## DocumentStructure Types\n\n```rust\npub struct DocumentStructure {\n    pub nodes: Vec<DocumentNode>,\n    pub source_format: Option<String>,  // always \"html\"\n}\n\npub struct DocumentNode {\n    pub id: String,\n    pub content: NodeContent,\n    pub parent: Option<u32>,\n    pub children: Vec<u32>,\n    pub annotations: Vec<TextAnnotation>,\n    pub attributes: Option<HashMap<String, String>>,\n}\n\npub enum NodeContent {\n    Heading { level: u8, text: String },\n    Paragraph { text: String },\n    List { ordered: bool },\n    ListItem { text: String },\n    Table { grid: TableGrid },\n    Image { description: Option<String>, src: Option<String>, image_index: Option<u32> },\n    Code { text: String, language: Option<String> },\n    Quote,\n    DefinitionList,\n    DefinitionItem { term: String, definition: String },\n    RawBlock { format: String, content: String },\n    MetadataBlock { entries: Vec<(String, String)> },\n    Group { label: Option<String>, heading_level: Option<u8>, heading_text: Option<String> },\n}\n\npub struct TextAnnotation {\n    pub start: u32,\n    pub end: u32,\n    pub kind: AnnotationKind,\n}\n\n// AnnotationKind implements Default (default variant: Bold)\n#[derive(Default)]\npub enum AnnotationKind {\n    #[default]\n    Bold, Italic, Underline, Strikethrough, Code,\n    Subscript, Superscript, Highlight,\n    Link { url: String, title: Option<String> },\n}\n\n// NodeContent implements Default (default: Heading { level: 1, text: String::new() })\npub enum NodeContent { ... }\n```\n\n## Error Types\n\n```rust\npub enum ConversionError {\n    ParseError(String),\n    SanitizationError(String),\n    ConfigError(String),\n    IoError(std::io::Error),\n    Panic(String),\n    InvalidInput(String),          // binary data, invalid UTF-8\n    #[cfg(feature = \"visitor\")]\n    Visitor(String),\n    Other(String),\n}\n\npub type Result<T> = std::result::Result<T, ConversionError>;\n```\n\n## Metadata Types (requires `metadata` feature)\n\n```rust\npub struct HtmlMetadata {\n    pub document: DocumentMetadata,\n    pub headers: Vec<HeaderMetadata>,\n    pub links: Vec<LinkMetadata>,\n    pub images: Vec<ImageMetadata>,\n    pub structured_data: Vec<StructuredData>,\n}\n\npub struct DocumentMetadata {\n    pub title: Option<String>,\n    pub description: Option<String>,\n    pub keywords: Vec<String>,\n    pub author: Option<String>,\n    pub canonical_url: Option<String>,\n    pub base_href: Option<String>,\n    pub language: Option<String>,\n    pub text_direction: Option<TextDirection>,\n    pub open_graph: BTreeMap<String, String>,\n    pub twitter_card: BTreeMap<String, String>,\n    pub meta_tags: BTreeMap<String, String>,\n}\n\npub struct MetadataConfig {\n    pub extract_document: bool,     // default: true\n    pub extract_headers: bool,      // default: true\n    pub extract_links: bool,        // default: true\n    pub extract_images: bool,       // default: true\n    pub extract_structured_data: bool,  // default: true\n    pub max_structured_data_size: usize,\n}\n```\n\n## TableData Types\n\n`TableData` is exported from the crate root (`use html_to_markdown_rs::TableData`).\n\n```rust\npub struct TableData {\n    pub grid: TableGrid,\n    pub markdown: String,\n}\n\npub struct TableGrid {\n    pub rows: usize,\n    pub cols: usize,\n    pub cells: Vec<GridCell>,\n}\n\npub struct GridCell {\n    pub content: String,\n    pub row: usize,\n    pub col: usize,\n    pub row_span: usize,\n    pub col_span: usize,\n    pub is_header: bool,\n}\n```\n"
  },
  {
    "path": "skills/html-to-markdown/references/typescript-api.md",
    "content": "# TypeScript / Node.js API Reference\n\nPackage: `@kreuzberg/html-to-markdown-node`\nThe TypeScript package (`@kreuzberg/html-to-markdown`) re-exports everything from `@kreuzberg/html-to-markdown-node` (the native NAPI-RS binding) and adds file/stream helpers.\n\n## Installation\n\n```bash\nnpm install @kreuzberg/html-to-markdown-node\n# or\npnpm add @kreuzberg/html-to-markdown-node\n```\n\n## Primary Function\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown-node';\n\n// convert() returns a JSON string — always JSON.parse() the result\nconst result = JSON.parse(convert(html, options?));\nconsole.log(result.content);    // Markdown string or null\nconsole.log(result.tables);     // array of table objects\nconsole.log(result.warnings);   // array of warning objects\nconsole.log(result.metadata);   // metadata object or null\n```\n\n**Important:** `convert()` returns a JSON-encoded string, not a parsed object. This is intentional — NAPI-RS serialization of deeply-nested objects is expensive. Always call `JSON.parse()` on the result.\n\n## Function Signatures\n\n### Core (from `@kreuzberg/html-to-markdown-node`)\n\n```typescript\n// Primary conversion — returns JSON string, always JSON.parse() the result\nfunction convert(html: string, options?: JsConversionOptions): string;\n```\n\n### File and Stream Helpers (from `@kreuzberg/html-to-markdown`)\n\n```typescript\nimport {\n    convertFile,\n    convertStream,\n    wrapVisitorCallback,\n    wrapVisitorCallbacks,\n    hasMetadataSupport,\n} from '@kreuzberg/html-to-markdown-node';\nimport type { Readable } from 'node:stream';\n\n// File helpers (async, return JSON string — JSON.parse() the result)\nasync function convertFile(filePath: string, options?: JsConversionOptions | null): Promise<string>;\n\n// Stream helpers (async, return JSON string — JSON.parse() the result)\nasync function convertStream(stream: Readable | AsyncIterable<string | Buffer>, options?: JsConversionOptions | null): Promise<string>;\n```\n\n## Interfaces\n\n### JsConversionOptions\n\nAll fields are optional. Defaults match Rust defaults. Enum values are PascalCase strings (e.g. `'Atx'`, `'Spaces'`).\n\n```typescript\ninterface JsConversionOptions {\n    headingStyle?: 'Atx' | 'Underlined' | 'AtxClosed';\n    listIndentType?: 'Spaces' | 'Tabs';\n    listIndentWidth?: number;\n    bullets?: string;\n    strongEmSymbol?: string;          // '*' or '_'\n    escapeAsterisks?: boolean;\n    escapeUnderscores?: boolean;\n    escapeMisc?: boolean;\n    escapeAscii?: boolean;\n    codeLanguage?: string;\n    autolinks?: boolean;\n    defaultTitle?: boolean;\n    brInTables?: boolean;\n    highlightStyle?: 'DoubleEqual' | 'Html' | 'Bold' | 'None';\n    extractMetadata?: boolean;\n    whitespaceMode?: 'Normalized' | 'Strict';\n    stripNewlines?: boolean;\n    wrap?: boolean;\n    wrapWidth?: number;\n    convertAsInline?: boolean;\n    subSymbol?: string;\n    supSymbol?: string;\n    newlineStyle?: 'Spaces' | 'Backslash';\n    codeBlockStyle?: 'Indented' | 'Backticks' | 'Tildes';\n    keepInlineImagesIn?: string[];\n    preprocessing?: JsPreprocessingOptions;\n    encoding?: string;\n    debug?: boolean;\n    stripTags?: string[];\n    preserveTags?: string[];\n    skipImages?: boolean;\n    outputFormat?: 'Markdown' | 'Djot' | 'Plain';\n}\n```\n\n**Note on enum values:** NAPI-RS `const enum` values are PascalCase strings (e.g. `'Atx'` not `'atx'`, `'Spaces'` not `'spaces'`). Using lowercase will be rejected at runtime.\n\n### JsPreprocessingOptions\n\n```typescript\ninterface JsPreprocessingOptions {\n    enabled?: boolean;\n    preset?: 'minimal' | 'standard' | 'aggressive';\n    removeNavigation?: boolean;\n    removeForms?: boolean;\n}\n```\n\n### JsMetadataConfig\n\nFields use camelCase (matching the NAPI-RS binding):\n\n```typescript\ninterface JsMetadataConfig {\n    extractDocument?: boolean;\n    extractHeaders?: boolean;\n    extractLinks?: boolean;\n    extractImages?: boolean;\n    extractStructuredData?: boolean;\n    maxStructuredDataSize?: number;\n}\n```\n\n### JsInlineImage (in result.images)\n\nInline images are extracted when `extractImages` is enabled in options. The result is in `result.images`:\n\n```typescript\ninterface JsInlineImage {\n    data: Buffer;\n    format: string;\n    filename?: string;\n    description?: string;\n    dimensions?: number[];    // [width, height]\n    source: string;           // \"img_data_uri\" | \"svg_element\"\n    attributes: Record<string, string>;\n}\n```\n\n## ConversionResult (from convert())\n\nThe result of `JSON.parse(convert(html))`:\n\n```typescript\ninterface ConversionResult {\n    content: string | null;     // Markdown text\n    document: object | null;    // structured document tree (null unless includeDocumentStructure enabled)\n    metadata: object | null;    // HtmlMetadata if metadata feature enabled\n    tables: Array<{\n        cells: Array<Array<string>>;    // rows x columns of cell text\n        markdown: string;               // rendered table in target format\n        isHeaderRow: Array<boolean>;    // per-row flag: true if row was inside <thead>\n    }>;\n    warnings: Array<{\n        message: string;\n        kind: string;\n    }>;\n}\n```\n\n**Note on `tables`:** The Node.js binding uses a flat `cells: Array<Array<string>>` structure (no `grid` wrapper), plus `isHeaderRow` for header detection. This differs from the Rust `TableGrid` struct.\n\n## Visitor Pattern\n\nThe visitor is passed as a third argument to `convert()`:\n\n```typescript\nimport { convert } from '@kreuzberg/html-to-markdown-node';\nimport { wrapVisitorCallbacks } from '@kreuzberg/html-to-markdown-node';\n\nconst visitor = wrapVisitorCallbacks({\n    visitElementStart: (ctx) => {\n        // ctx.tagName, ctx.attributes available\n        return { type: 'continue' };\n    },\n    visitText: (ctx, text) => {\n        return { type: 'continue' };\n    },\n});\n\nconst result = convert(html, options, visitor);\n```\n\nVisitor return types: `{ type: 'continue' }` | `{ type: 'skip' }` | `{ type: 'preserve_html' }` | `{ type: 'custom', output: string }` | `{ type: 'error', message: string }`.\n\n## Examples\n\n```typescript\n// Simple conversion\nimport { convert } from '@kreuzberg/html-to-markdown-node';\nconst result = JSON.parse(convert('<h1>Hello</h1>'));\nconsole.log(result.content); // \"# Hello\\n\"\n\n// Metadata extraction — enabled via extractMetadata option, result in result.metadata\nconst result2 = JSON.parse(convert(html, { extractMetadata: true }));\nconsole.log(result2.metadata.document.title);\nconsole.log(result2.metadata.headers.length);\n\n// Tables — always in result.tables\nconst result3 = JSON.parse(convert(html));\nfor (const table of result3.tables) {\n    console.log(table.markdown);\n}\n\n// Inline images — enable extractImages in options\nconst result4 = JSON.parse(convert(html, { extractImages: true, captureSvg: true }));\nfor (const image of result4.images) {\n    console.log(image.format, image.filename);\n}\n\n// File conversion\nimport { convertFile } from '@kreuzberg/html-to-markdown-node';\nconst json = await convertFile('./page.html', { headingStyle: 'Atx' });\nconst fileResult = JSON.parse(json);\nconsole.log(fileResult.content);\n```\n"
  },
  {
    "path": "test_apps/README.md",
    "content": "# test_apps/ - End-to-End Testing Infrastructure\n\nThis directory contains **end-to-end (e2e) tests** for validating published packages from public registries (PyPI, npm, RubyGems, Maven Central, NuGet, Packagist, Hex.pm, pkg.go.dev).\n\n## Purpose\n\nUnlike the main test suites that validate local development builds, `test_apps/` validates that:\n\n1. **Packages publish correctly** to public registries\n2. **Installation works** for end users (`pip install`, `npm install`, etc.)\n3. **APIs function correctly** in real-world usage\n4. **Breaking changes** are caught before users encounter them\n5. **Version synchronization** works across all language packages\n\n## Two-Tier Testing Strategy\n\n### Tier 1: Smoke Tests (Fast)\n\n**Purpose**: Quick validation that packages are installable and functional.\n\n**Characteristics**:\n\n- **Fast**: <30 seconds per language, <5 minutes total\n- **Minimal**: Basic import/require + simple conversion test\n- **Pre-Release Gate**: Runs before publishing to registries\n- **CI Integration**: `task e2e:smoke:all` in publish workflow\n\n**Example** (`python/smoke_test.py`):\n\n```python\ndef test_package_imports():\n    \"\"\"Verify package can be imported.\"\"\"\n    import html_to_markdown\n    assert html_to_markdown is not None\n\ndef test_basic_conversion():\n    \"\"\"Verify basic HTML→Markdown conversion works.\"\"\"\n    from html_to_markdown import convert_html_to_markdown\n    html = \"<p>Hello World</p>\"\n    result = convert_html_to_markdown(html)\n    assert \"Hello World\" in result\n```\n\n### Tier 2: Comprehensive Tests (Thorough)\n\n**Purpose**: Exhaustive validation using shared fixtures across all languages.\n\n**Characteristics**:\n\n- **Thorough**: 1-3 minutes per language\n- **Fixture-Driven**: Shared JSON fixtures ensure language parity\n- **Post-Release Validation**: Runs after publishing (can run locally too)\n- **CI Integration**: `task e2e:test:all` (optional, can be manual)\n\n**Example** (`python/comprehensive_test.py`):\n\n```python\nimport json\nfrom pathlib import Path\nimport pytest\nfrom html_to_markdown import convert_html_to_markdown\n\ndef load_fixtures(filename):\n    \"\"\"Load shared JSON fixtures.\"\"\"\n    fixture_path = Path(__file__).parent.parent / \"fixtures\" / filename\n    with open(fixture_path) as f:\n        return json.load(f)\n\n@pytest.mark.parametrize(\n    \"test_case\",\n    load_fixtures(\"basic-html.json\"),\n    ids=lambda tc: tc[\"name\"]\n)\ndef test_basic_html_conversion(test_case):\n    \"\"\"Test basic HTML conversions against fixtures.\"\"\"\n    result = convert_html_to_markdown(\n        test_case[\"html\"],\n        **test_case.get(\"options\", {})\n    )\n    assert result.strip() == test_case[\"expectedMarkdown\"].strip()\n```\n\n## Directory Structure\n\n```text\ntests/test_apps/\n├── fixtures/                          # Shared test fixtures (JSON)\n│   ├── README.md                     # Fixture format documentation\n│   ├── basic-html.json               # Basic HTML elements (10 cases)\n│   ├── complex-html.json             # Complex structures (future)\n│   ├── edge-cases.json               # Edge cases (future)\n│   ├── metadata-extraction.json      # Metadata features (future)\n│   └── real-world.json               # Real-world samples (future)\n│\n├── python/                           # Python test app (PyPI)\n│   ├── .python-version               # Python version (3.10)\n│   ├── pyproject.toml                # Depends on html-to-markdown from PyPI\n│   ├── uv.lock                       # Locked dependencies\n│   ├── smoke_test.py                 # Fast smoke tests\n│   ├── comprehensive_test.py         # Fixture-driven tests\n│   └── README.md                     # Python-specific notes\n│\n├── node/                             # Node.js test app (npm)\n│   ├── .nvmrc                        # Node version (18+)\n│   ├── package.json                  # Depends on html-to-markdown from npm\n│   ├── pnpm-lock.yaml                # Locked dependencies\n│   ├── smoke.spec.ts                 # Fast smoke tests\n│   ├── comprehensive.spec.ts         # Fixture-driven tests\n│   └── README.md                     # Node-specific notes\n│\n├── ruby/                             # Ruby test app (RubyGems)\n│   ├── .ruby-version                 # Ruby version (3.2+)\n│   ├── Gemfile                       # Depends on html-to-markdown from RubyGems\n│   ├── Gemfile.lock                  # Locked dependencies\n│   ├── smoke_test.rb                 # Fast smoke tests\n│   ├── comprehensive_test.rb         # Fixture-driven tests\n│   └── README.md                     # Ruby-specific notes\n│\n├── go/                               # Go test app (pkg.go.dev)\n│   ├── go.mod                        # Depends on html-to-markdown from pkg.go.dev\n│   ├── go.sum                        # Locked dependencies\n│   ├── main_test.go                  # Smoke + comprehensive tests\n│   ├── fixtures_test.go              # Fixture loading\n│   └── README.md                     # Go-specific notes\n│\n├── java/                             # Java test app (Maven Central)\n│   ├── pom.xml                       # Depends on html-to-markdown from Maven\n│   ├── src/test/java/\n│   │   └── dev/kreuzberg/htmltomarkdown/\n│   │       ├── SmokeTest.java        # Fast smoke tests\n│   │       └── ComprehensiveTest.java # Fixture-driven tests\n│   └── README.md                     # Java-specific notes\n│\n├── csharp/                           # C# test app (NuGet)\n│   ├── TestApp.csproj                # Depends on html-to-markdown from NuGet\n│   ├── SmokeTest.cs                  # Fast smoke tests\n│   ├── ComprehensiveTest.cs          # Fixture-driven tests\n│   ├── Fixtures.cs                   # Fixture loading\n│   └── README.md                     # C#-specific notes\n│\n├── php/                              # PHP test app (Packagist)\n│   ├── composer.json                 # Depends on html-to-markdown from Packagist\n│   ├── composer.lock                 # Locked dependencies\n│   ├── smoke_test.php                # Fast smoke tests\n│   ├── comprehensive_test.php        # Fixture-driven tests\n│   └── README.md                     # PHP-specific notes\n│\n└── elixir/                           # Elixir test app (Hex.pm)\n    ├── mix.exs                       # Depends on html_to_markdown from Hex.pm\n    ├── mix.lock                      # Locked dependencies\n    ├── test/smoke_test.exs           # Fast smoke tests\n    ├── test/comprehensive_test.exs   # Fixture-driven tests\n    └── README.md                     # Elixir-specific notes\n```\n\n## Shared Fixtures Format\n\nAll fixtures follow a consistent JSON schema to ensure language parity.\n\n### Fixture Schema\n\n**File**: `fixtures/basic-html.json` (example)\n\n```json\n[\n  {\n    \"name\": \"Simple paragraph\",\n    \"html\": \"<p>Hello World</p>\",\n    \"expectedMarkdown\": \"Hello World\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Heading level 1\",\n    \"html\": \"<h1>Title</h1>\",\n    \"expectedMarkdown\": \"# Title\",\n    \"options\": {\n      \"headingStyle\": \"Atx\"\n    }\n  },\n  {\n    \"name\": \"Strong emphasis\",\n    \"html\": \"<strong>Bold text</strong>\",\n    \"expectedMarkdown\": \"**Bold text**\",\n    \"options\": {}\n  }\n]\n```\n\n**Schema Fields**:\n\n- `name` (string): Human-readable test case name\n- `html` (string): Input HTML\n- `expectedMarkdown` (string): Expected Markdown output\n- `options` (object): Conversion options (language-specific format)\n\n### Fixture Categories\n\n1. **basic-html.json**: Core HTML elements (p, h1-h6, strong, em, lists, links, images)\n2. **complex-html.json**: Complex structures (nested lists, tables, blockquotes)\n3. **edge-cases.json**: Edge cases (special chars, Unicode, HTML entities, malformed HTML)\n4. **metadata-extraction.json**: Metadata features (title extraction, meta tags)\n5. **real-world.json**: Real-world samples (Wikipedia, Medium, GitHub README)\n\n## Running Tests\n\n### Smoke Tests (Pre-Release)\n\nRun before publishing packages to registries:\n\n```bash\n# All languages (fast: <5 min)\ntask e2e:smoke:all\n\n# Individual languages\ntask e2e:smoke:python\ntask e2e:smoke:node\ntask e2e:smoke:ruby\ntask e2e:smoke:php\ntask e2e:smoke:go\ntask e2e:smoke:java\ntask e2e:smoke:csharp\ntask e2e:smoke:elixir\n```\n\n### Comprehensive Tests (Post-Release)\n\nRun after publishing or during development:\n\n```bash\n# All languages (thorough: 8-24 min)\ntask e2e:test:all\n\n# Individual languages\ntask e2e:test:python\ntask e2e:test:node\ntask e2e:test:ruby\ntask e2e:test:php\ntask e2e:test:go\ntask e2e:test:java\ntask e2e:test:csharp\ntask e2e:test:elixir\n```\n\n### Manual Testing\n\nFor debugging or local validation:\n\n```bash\n# Python\ncd tests/test_apps/python\nuv sync\nuv run pytest smoke_test.py -v\nuv run pytest comprehensive_test.py -v\n\n# Node.js\ncd tests/test_apps/node\npnpm install\npnpm test:smoke\npnpm test:comprehensive\n\n# Ruby\ncd tests/test_apps/ruby\nbundle install\nbundle exec rspec smoke_test.rb\nbundle exec rspec comprehensive_test.rb\n\n# Go\ncd tests/test_apps/go\ngo test -v -run TestSmoke\ngo test -v -run TestComprehensive\n```\n\n## CI/CD Integration\n\n### Pre-Release Gate (Publish Workflow)\n\n**File**: `.github/workflows/publish.yaml`\n\n```yaml\njobs:\n  smoke-tests:\n    name: Smoke Tests - ${{ matrix.language }}\n    needs: prepare\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [python, node, ruby, php, go, java, csharp, elixir]\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install Task\n        uses: arduino/setup-task@v2\n\n      - name: Setup Language\n        # ... language-specific setup\n\n      - name: Run Smoke Tests\n        run: task e2e:smoke:${{ matrix.language }}\n\n  publish:\n    needs: [prepare, smoke-tests]  # Blocks on smoke tests\n    if: success()\n    # ... publish steps\n```\n\n**Behavior**:\n\n- Runs BEFORE publishing packages\n- Blocks release if ANY smoke test fails\n- Fast feedback (<5 min total)\n\n### Post-Release Validation (Optional)\n\n**File**: `.github/workflows/post-release-validation.yaml` (future)\n\n```yaml\non:\n  release:\n    types: [published]\n\njobs:\n  comprehensive-tests:\n    name: Comprehensive Tests - ${{ matrix.language }}\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [python, node, ruby, php, go, java, csharp, elixir]\n    steps:\n      - name: Install Task\n        uses: arduino/setup-task@v2\n\n      - name: Run Comprehensive Tests\n        run: task e2e:test:${{ matrix.language }}\n```\n\n**Behavior**:\n\n- Runs AFTER packages are published\n- Validates real-world installation\n- Does NOT block release (informational)\n\n## Version Management\n\n### Automatic Version Sync\n\nThe `scripts/sync_versions.py` script updates test_apps manifests when versions change:\n\n```python\n# From Cargo.toml (source of truth)\nversion = \"2.18.0\"\n\n# Propagates to:\n# - tests/test_apps/python/pyproject.toml   → html-to-markdown>=2.18.0\n# - tests/test_apps/node/package.json       → html-to-markdown@>=2.18.0\n# - tests/test_apps/ruby/Gemfile            → gem 'html-to-markdown', '>= 2.18.0'\n# - tests/test_apps/go/go.mod               → (version in module path)\n# - tests/test_apps/java/pom.xml            → <version>2.18.0</version>\n# - tests/test_apps/csharp/TestApp.csproj   → <Version>2.18.0</Version>\n# - tests/test_apps/php/composer.json       → \"kreuzberg-dev/html-to-markdown\": \">=2.18.0\"\n# - tests/test_apps/elixir/mix.exs          → {:html_to_markdown, \"~> 2.18.0\"}\n```\n\n**Usage**:\n\n```bash\ntask versions:sync  # Updates all manifests including test_apps\n```\n\n### Version Constraints\n\nAll test_apps use **minimum version constraints** to ensure compatibility:\n\n```json\n// Python (pyproject.toml)\n\"html-to-markdown>=2.18.0\"\n\n// Node (package.json)\n\"html-to-markdown\": \">=2.18.0\"\n\n// Ruby (Gemfile)\ngem 'html-to-markdown', '>= 2.18.0'\n\n// PHP (composer.json)\n\"kreuzberg-dev/html-to-markdown\": \">=2.18.0\"\n```\n\n## Adding a New Test Case\n\n### Step 1: Add to Shared Fixtures\n\n**File**: `fixtures/basic-html.json`\n\n```json\n[\n  {\n    \"name\": \"Code block\",\n    \"html\": \"<pre><code>const x = 42;</code></pre>\",\n    \"expectedMarkdown\": \"```\\nconst x = 42;\\n```\",\n    \"options\": {\n      \"codeBlockStyle\": \"Fenced\"\n    }\n  }\n]\n```\n\n### Step 2: Verify in Comprehensive Tests\n\nComprehensive tests automatically pick up new fixtures (parametrized):\n\n```bash\n# Python\ntask e2e:test:python\n# ✓ test_basic_html_conversion[Code block] PASSED\n\n# Node\ntask e2e:test:node\n# ✓ Code block PASSED\n\n# All languages\ntask e2e:test:all\n# ✓ All 8 languages pass new test case\n```\n\n## Adding a New Language Test App\n\nLet's add **Kotlin** as an example:\n\n### Step 1: Create Directory Structure\n\n```bash\nmkdir -p tests/test_apps/kotlin/src/test/kotlin/dev/kreuzberg/htmltomarkdown\n```\n\n### Step 2: Create Package Manifest\n\n**File**: `tests/test_apps/kotlin/build.gradle.kts`\n\n```kotlin\nplugins {\n    kotlin(\"jvm\") version \"1.9.20\"\n    id(\"org.jetbrains.kotlinx.kover\") version \"0.7.4\"\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation(\"dev.kreuzberg:html-to-markdown:3.4.0\")\n    testImplementation(kotlin(\"test\"))\n    testImplementation(\"org.junit.jupiter:junit-jupiter:5.10.1\")\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\n```\n\n### Step 3: Create Smoke Tests\n\n**File**: `src/test/kotlin/.../SmokeTest.kt`\n\n```kotlin\npackage dev.kreuzberg.htmltomarkdown\n\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertTrue\nimport kotlin.test.assertNotNull\n\nclass SmokeTest {\n    @Test\n    fun `package imports correctly`() {\n        val converter = HtmlToMarkdown()\n        assertNotNull(converter)\n    }\n\n    @Test\n    fun `basic conversion works`() {\n        val converter = HtmlToMarkdown()\n        val html = \"<p>Hello World</p>\"\n        val result = converter.convert(html)\n        assertTrue(result.contains(\"Hello World\"))\n    }\n}\n```\n\n### Step 4: Create Comprehensive Tests\n\n**File**: `src/test/kotlin/.../ComprehensiveTest.kt`\n\n```kotlin\npackage dev.kreuzberg.htmltomarkdown\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\nimport java.nio.file.Files\nimport java.nio.file.Paths\nimport kotlin.test.assertEquals\n\n@Serializable\ndata class TestCase(\n    val name: String,\n    val html: String,\n    val expectedMarkdown: String,\n    val options: Map<String, String> = emptyMap()\n)\n\nclass ComprehensiveTest {\n    private fun loadFixtures(filename: String): List<TestCase> {\n        val path = Paths.get(\"../fixtures/$filename\")\n        val json = Files.readString(path)\n        return Json.decodeFromString(json)\n    }\n\n    @TestFactory\n    fun `basic HTML conversions`() = loadFixtures(\"basic-html.json\").map { tc ->\n        DynamicTest.dynamicTest(tc.name) {\n            val converter = HtmlToMarkdown()\n            val result = converter.convert(tc.html, tc.options)\n            assertEquals(tc.expectedMarkdown.trim(), result.trim())\n        }\n    }\n}\n```\n\n### Step 5: Add to Taskfile\n\n**File**: `Taskfile.yml`\n\n```yaml\ntasks:\n  e2e:smoke:kotlin:\n    desc: \"Smoke test: Kotlin from Maven Central\"\n    dir: tests/test_apps/kotlin\n    cmds:\n      - gradle test --tests SmokeTest\n\n  e2e:test:kotlin:\n    desc: \"Comprehensive test: Kotlin\"\n    dir: tests/test_apps/kotlin\n    cmds:\n      - gradle test\n\n  e2e:smoke:all:\n    cmds:\n      - task: e2e:smoke:python\n      # ... existing languages\n      - task: e2e:smoke:kotlin  # ADD THIS\n\n  e2e:test:all:\n    cmds:\n      - task: e2e:test:python\n      # ... existing languages\n      - task: e2e:test:kotlin  # ADD THIS\n```\n\n### Step 6: Extend Version Sync\n\n**File**: `scripts/sync_versions.py`\n\n```python\ndef update_test_apps_versions(repo_root: Path, version: str) -> None:\n    # ... existing updates\n\n    # Kotlin: build.gradle.kts\n    kotlin_gradle = test_apps / \"kotlin\" / \"build.gradle.kts\"\n    if kotlin_gradle.exists():\n        update_gradle_dependency(kotlin_gradle, \"html-to-markdown\", version)\n```\n\n## Language-Specific Notes\n\n### Python (PyPI)\n\n**Package Manager**: uv (fast pip replacement)\n**Test Framework**: pytest\n**Coverage**: pytest-cov\n\n```bash\ncd tests/test_apps/python\nuv sync\nuv run pytest -v\n```\n\n### Node.js (npm)\n\n**Package Manager**: pnpm\n**Test Framework**: vitest\n**Coverage**: vitest built-in\n\n```bash\ncd tests/test_apps/node\npnpm install\npnpm test\n```\n\n### Ruby (RubyGems)\n\n**Package Manager**: bundler\n**Test Framework**: RSpec\n**Coverage**: simplecov\n\n```bash\ncd tests/test_apps/ruby\nbundle install\nbundle exec rspec\n```\n\n### Go (pkg.go.dev)\n\n**Package Manager**: go modules\n**Test Framework**: testing (stdlib)\n**Coverage**: go test -cover\n\n```bash\ncd tests/test_apps/go\ngo test -v\ngo test -cover\n```\n\n### Java (Maven Central)\n\n**Package Manager**: Maven/Gradle\n**Test Framework**: JUnit 5\n**Coverage**: JaCoCo\n\n```bash\ncd tests/test_apps/java\nmvn test\n```\n\n### C# (NuGet)\n\n**Package Manager**: NuGet\n**Test Framework**: xUnit\n**Coverage**: Coverlet\n\n```bash\ncd tests/test_apps/csharp\ndotnet test\n```\n\n### PHP (Packagist)\n\n**Package Manager**: Composer\n**Test Framework**: PHPUnit\n**Coverage**: PHPUnit built-in\n\n```bash\ncd tests/test_apps/php\ncomposer install\ncomposer test\n```\n\n### Elixir (Hex.pm)\n\n**Package Manager**: Mix\n**Test Framework**: ExUnit\n**Coverage**: excoveralls\n\n```bash\ncd tests/test_apps/elixir\nmix deps.get\nmix test\n```\n\n## Troubleshooting\n\n### Package Not Found\n\n**Problem**: `pip install html-to-markdown` fails with \"No matching distribution found\"\n\n**Solution**: Package hasn't been published yet. Wait for publish workflow to complete.\n\n### Version Mismatch\n\n**Problem**: Tests pass locally but fail in CI with version errors\n\n**Solution**: Run `task versions:sync` to ensure all test_apps manifests use correct version:\n\n```bash\ntask versions:sync\ngit diff tests/test_apps/  # Review changes\n```\n\n### Fixture Loading Fails\n\n**Problem**: `FileNotFoundError: ../fixtures/basic-html.json`\n\n**Solution**: Ensure tests load fixtures relative to `test_apps/` directory:\n\n```python\n# ✅ Correct\nfixture_path = Path(__file__).parent.parent / \"fixtures\" / \"basic-html.json\"\n\n# ❌ Incorrect\nfixture_path = Path(\"../fixtures/basic-html.json\")  # Depends on cwd\n```\n\n### Test Timeout\n\n**Problem**: Comprehensive tests timeout in CI\n\n**Solution**: Reduce fixture count or increase CI timeout:\n\n```yaml\n# .github/workflows/post-release-validation.yaml\n- run: task e2e:test:all\n  timeout-minutes: 30  # Increase from default 10\n```\n\n## Best Practices\n\n1. **Test published packages only** - Don't mount local code\n2. **Use version constraints** - `>=2.18.0` not exact versions\n3. **Share fixtures** - All languages test same inputs/outputs\n4. **Fast smoke tests** - Keep under 30 seconds per language\n5. **Parametrize comprehensive** - Use language-native parametrization\n6. **Clear assertions** - Include test case name in failure messages\n\n## References\n\n- **Fixture Format**: fixtures/README.md\n- **Task Commands**: ../../Taskfile.yml\n- **Version Sync**: ../../scripts/sync_versions.py\n- **CI Workflows**: ../../.github/workflows/publish.yaml\n\n---\n\n**Last Updated**: 2025-12-28\n**Maintainers**: html-to-markdown contributors\n"
  },
  {
    "path": "test_apps/bun/README.md",
    "content": "# Bun Smoke Tests\n\nThis directory contains smoke tests for the html-to-markdown package running in the Bun runtime.\n\n## Purpose\n\nValidates that the NAPI-RS bindings work correctly in Bun's Node-API compatibility layer without requiring separate Bun-specific bindings.\n\n## Running Tests\n\n```bash\nbun install\nbun test\n```\n\n## Test Coverage\n\n- Basic HTML conversion\n- Metadata extraction\n- Options handling\n- Complex HTML structures (lists, links, images)\n- Error handling\n\n## Requirements\n\n- Bun 1.2+\n- @kreuzberg/html-to-markdown package\n\n## Notes\n\nThe existing NAPI-RS `.node` binaries work in Bun without changes due to Bun's 95%+ Node-API compatibility. These tests verify that all core functionality works as expected.\n"
  },
  {
    "path": "test_apps/bun/package.json",
    "content": "{\n\t\"name\": \"html-to-markdown-bun-test-app\",\n\t\"version\": \"3.1.0\",\n\t\"description\": \"Bun runtime smoke tests for html-to-markdown\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"test\": \"bun test\",\n\t\t\"test:smoke\": \"bun test smoke.test.ts\"\n\t},\n\t\"dependencies\": {\n\t\t\"@kreuzberg/html-to-markdown\": \"3.4.0-rc.25\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@types/bun\": \"latest\"\n\t}\n}\n"
  },
  {
    "path": "test_apps/bun/smoke.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\";\nimport { convert } from \"@kreuzberg/html-to-markdown\";\n\ndescribe(\"html-to-markdown Bun smoke tests\", () => {\n  describe(\"Basic conversion\", () => {\n    test(\"converts simple HTML to Markdown\", () => {\n      const html = \"<h1>Hello World</h1>\";\n      const result = convert(html);\n      expect(result.content).toContain(\"# Hello World\");\n      expect(typeof result.content).toBe(\"string\");\n    });\n\n    test(\"converts paragraph to text\", () => {\n      const html = \"<p>This is a test paragraph.</p>\";\n      const result = convert(html);\n      expect(result.content).toContain(\"This is a test paragraph.\");\n    });\n\n    test(\"handles empty input\", () => {\n      const result = convert(\"\");\n      expect(result.content).toBe(\"\");\n    });\n\n    test(\"handles malformed HTML gracefully\", () => {\n      const html = \"<p>Unclosed paragraph\";\n      expect(() => convert(html)).not.toThrow();\n      const result = convert(html);\n      expect(typeof result.content).toBe(\"string\");\n    });\n  });\n\n  describe(\"Result structure\", () => {\n    test(\"returns result with content field\", () => {\n      const html = \"<h1>Test</h1><p>Content</p>\";\n      const result = convert(html);\n      expect(result).toBeDefined();\n      expect(result.content).toBeTruthy();\n    });\n\n    test(\"returns result with metadata field\", () => {\n      const html = \"<h1>Title</h1><h2>Subtitle</h2>\";\n      const result = convert(html);\n      expect(result).toBeDefined();\n      if (result.metadata) {\n        expect(typeof result.metadata).toBe(\"object\");\n      }\n    });\n\n    test(\"returns result with warnings field\", () => {\n      const html = \"<p>Test</p>\";\n      const result = convert(html);\n      expect(result.warnings).toBeDefined();\n    });\n  });\n\n  describe(\"Options handling\", () => {\n    test(\"accepts conversion options\", () => {\n      const html = \"<p>Test</p>\";\n      const options = { hardBreaks: true };\n      expect(() => convert(html, options)).not.toThrow();\n      const result = convert(html, options);\n      expect(typeof result.content).toBe(\"string\");\n    });\n\n    test(\"handles null options\", () => {\n      const html = \"<p>Test</p>\";\n      expect(() => convert(html, null)).not.toThrow();\n    });\n  });\n\n  describe(\"Complex HTML\", () => {\n    test(\"handles lists\", () => {\n      const html = \"<ul><li>Item 1</li><li>Item 2</li></ul>\";\n      const result = convert(html);\n      expect(result.content).toContain(\"Item 1\");\n      expect(result.content).toContain(\"Item 2\");\n    });\n\n    test(\"handles links\", () => {\n      const html = '<a href=\"https://example.com\">Link Text</a>';\n      const result = convert(html);\n      expect(result.content).toContain(\"Link Text\");\n      expect(result.content).toContain(\"https://example.com\");\n    });\n\n    test(\"handles images\", () => {\n      const html = '<img src=\"image.jpg\" alt=\"Test Image\">';\n      const result = convert(html);\n      expect(result.content).toContain(\"Test Image\");\n      expect(result.content).toContain(\"image.jpg\");\n    });\n  });\n});\n"
  },
  {
    "path": "test_apps/c/.gitignore",
    "content": "ffi/\n"
  },
  {
    "path": "test_apps/c/Makefile",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:ccfcc8b8c96f209427e1af93d0028ed90bf6a32674206d4219806de5daee65f7\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nCC = gcc\nFFI_DIR = ffi\n\nifneq ($(wildcard $(FFI_DIR)/include/html_to_markdown.h),)\n    CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include\n    LDFLAGS = -L$(FFI_DIR)/lib -lhtml-to-markdown-ffi -Wl,-rpath,$(FFI_DIR)/lib\nelse ifneq ($(wildcard ../../crates/html-to-markdown-ffi/include/html_to_markdown.h),)\n    CFLAGS = -Wall -Wextra -I. -I../../crates/html-to-markdown-ffi/include\n    LDFLAGS = -L../../target/release -lhtml-to-markdown-ffi -Wl,-rpath,../../target/release\nelse\n    CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags html-to-markdown-ffi 2>/dev/null)\n    LDFLAGS = $(shell pkg-config --libs html-to-markdown-ffi 2>/dev/null)\nendif\n\nSRCS = main.c test_conversion.c test_smoke.c\nTARGET = run_tests\n\n.PHONY: all clean test\n\nall: $(TARGET)\n\n$(TARGET): $(SRCS)\n\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)\n\ntest: $(TARGET)\n\t./$(TARGET)\n\nclean:\n\trm -f $(TARGET)\n"
  },
  {
    "path": "test_apps/c/README.md",
    "content": "# html-to-markdown C FFI Test App\n\nComprehensive test suite for the html-to-markdown C FFI API (html-to-markdown-ffi).\n\n## Quick Start\n\n```bash\n# Build the FFI library first\ncargo build --release -p html-to-markdown-ffi\n\n# Build and run the test suite\nmake test\n```\n\n## Prerequisites\n\n- C11-compatible compiler (gcc or clang)\n- html-to-markdown FFI library compiled: `cargo build --release -p html-to-markdown-ffi`\n\n## Test Coverage\n\nThe test suite (`main.c`) validates 7 sections of the C FFI API:\n\n1. **Library Info** - Version string, error state after success\n2. **Error Code Functions** - All 6 error codes (ok, invalid_utf8, parse, visitor, memory, internal), unknown codes\n3. **Basic Conversion** - Headings, paragraphs, bold, italic, links, empty input, nested HTML, Unicode\n4. **Error Handling** - NULL input, error state propagation, error state clearing, `free_string(NULL)`\n5. **Visitor API** - Result constructors, visitor create/free, convert with visitor, bytes variant\n6. **Profiling API** - Start/stop lifecycle, NULL path, platform availability\n7. **Memory Safety** - 100 repeated conversions, alternating success/failure cycles\n\n## File Structure\n\n```text\ntests/test_apps/c/\n├── main.c              # Comprehensive test suite\n├── Makefile            # Build instructions\n└── README.md           # This file\n```\n\n## Build Options\n\n```bash\n# Debug build\nmake BUILD_MODE=debug\n\n# Use specific compiler\nmake CC=clang\n\n# Custom repo root\nmake HTM_ROOT=/path/to/html-to-markdown\n```\n\n## Expected Output\n\n```text\n================================================================================\nHTML-TO-MARKDOWN C FFI COMPREHENSIVE TEST SUITE\n================================================================================\nLibrary version: 2.26.2\n\n[SECTION 1] Library Info\n--------------------------------------------------------------------------------\n  PASS  html_to_markdown_version() returns \"2.26.2\"\n  ...\n\n================================================================================\nTEST SUMMARY\n================================================================================\nTotal Tests: 50+\n  Passed:  50+\n  Failed:  0\n  Skipped: 0\n\nALL TESTS PASSED\n```\n\nExit codes: `0` = all passed, `1` = failures detected.\n\n## Troubleshooting\n\n### Library not found at runtime\n\n```bash\n# macOS\nexport DYLD_LIBRARY_PATH=/path/to/html-to-markdown/target/release:$DYLD_LIBRARY_PATH\n\n# Linux\nexport LD_LIBRARY_PATH=/path/to/html-to-markdown/target/release:$LD_LIBRARY_PATH\n```\n\n### Header note\n\nThis test app declares FFI functions directly in `main.c` rather than including\nthe generated `html_to_markdown.h` header. The generated header contains cbindgen\nvisitor callback types that use incomplete struct fields, which some C compilers\nreject. The direct declarations are kept in sync with the actual API.\n"
  },
  {
    "path": "test_apps/c/download_ffi.sh",
    "content": "#!/usr/bin/env bash\n# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:2b3adffa3c6c54246f648c2dad57f9bfebaecedab04842782126a8fd4c7f2a30\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nset -euo pipefail\n\nREPO_URL=\"https://github.com/kreuzberg-dev/html-to-markdown\"\nVERSION=\"3.4.0-rc.22\"\nFFI_PKG_NAME=\"html-to-markdown-ffi\"\nFFI_DIR=\"ffi\"\n\n# Detect OS and architecture.\nOS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"\nARCH=\"$(uname -m)\"\n\ncase \"$ARCH\" in\nx86_64 | amd64) ARCH=\"x86_64\" ;;\narm64 | aarch64) ARCH=\"aarch64\" ;;\n*)\n  echo \"Unsupported architecture: $ARCH\" >&2\n  exit 1\n  ;;\nesac\n\ncase \"$OS\" in\nlinux) TRIPLE=\"${ARCH}-unknown-linux-gnu\" ;;\ndarwin) TRIPLE=\"${ARCH}-apple-darwin\" ;;\n*)\n  echo \"Unsupported OS: $OS\" >&2\n  exit 1\n  ;;\nesac\n\nARCHIVE=\"${FFI_PKG_NAME}-${TRIPLE}.tar.gz\"\nURL=\"${REPO_URL}/releases/download/v${VERSION}/${ARCHIVE}\"\n\necho \"Downloading ${ARCHIVE} from v${VERSION}...\"\nmkdir -p \"$FFI_DIR\"\ncurl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"\necho \"FFI library extracted to $FFI_DIR/\"\n"
  },
  {
    "path": "test_apps/c/main.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:56bcdf268ed45cb414f58f420a97b9972030dbdbf059601e2337f2030398265f\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n#include <stdio.h>\n#include \"test_runner.h\"\n\nint main(void) {\n    int passed = 0;\n    int failed = 0;\n\n    /* Category: conversion */\n    printf(\"  Running test_blockquote_multiple_paragraphs...\");\n    test_blockquote_multiple_paragraphs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_nested...\");\n    test_blockquote_nested();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_simple...\");\n    test_blockquote_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_blockquote_with_list...\");\n    test_blockquote_with_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_bold_and_italic...\");\n    test_bold_and_italic();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_bold_strong...\");\n    test_bold_strong();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_block...\");\n    test_code_block();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_block_no_language...\");\n    test_code_block_no_language();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_inline_in_paragraph...\");\n    test_code_inline_in_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_code_with_backticks_in_content...\");\n    test_code_with_backticks_in_content();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_mark_highlight...\");\n    test_emphasis_mark_highlight();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_strikethrough_del...\");\n    test_emphasis_strikethrough_del();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_strikethrough_s...\");\n    test_emphasis_strikethrough_s();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_subscript...\");\n    test_emphasis_subscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_superscript...\");\n    test_emphasis_superscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_emphasis_underline_u...\");\n    test_emphasis_underline_u();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_input_elements...\");\n    test_form_input_elements();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_select_options...\");\n    test_form_select_options();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_form_textarea...\");\n    test_form_textarea();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h1...\");\n    test_heading_h1();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h2...\");\n    test_heading_h2();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h3...\");\n    test_heading_h3();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h4...\");\n    test_heading_h4();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h5...\");\n    test_heading_h5();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_heading_h6...\");\n    test_heading_h6();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_figure_figcaption...\");\n    test_image_figure_figcaption();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_linked...\");\n    test_image_linked();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_no_alt...\");\n    test_image_no_alt();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_simple...\");\n    test_image_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_image_with_title...\");\n    test_image_with_title();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_inline_code...\");\n    test_inline_code();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_italic_em...\");\n    test_italic_em();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_br_tag...\");\n    test_line_break_br_tag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_hr_tag...\");\n    test_line_break_hr_tag();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_line_break_multiple_br...\");\n    test_line_break_multiple_br();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_anchor_fragment...\");\n    test_link_anchor_fragment();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_empty_href...\");\n    test_link_empty_href();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_image_inside...\");\n    test_link_image_inside();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_mailto...\");\n    test_link_mailto();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_simple...\");\n    test_link_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_with_bold_text...\");\n    test_link_with_bold_text();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_link_with_title...\");\n    test_link_with_title();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_definition_dl...\");\n    test_list_definition_dl();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_item_multiple_paragraphs...\");\n    test_list_item_multiple_paragraphs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_mixed_nested...\");\n    test_list_mixed_nested();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_nested_ordered...\");\n    test_list_nested_ordered();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_nested_unordered...\");\n    test_list_nested_unordered();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_list_task_checkboxes...\");\n    test_list_task_checkboxes();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_ordered_list...\");\n    test_ordered_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_multiple...\");\n    test_paragraph_multiple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_nested_divs...\");\n    test_paragraph_nested_divs();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_simple...\");\n    test_paragraph_simple();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_with_inline_formatting...\");\n    test_paragraph_with_inline_formatting();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_paragraph_with_line_breaks...\");\n    test_paragraph_with_line_breaks();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_abbr...\");\n    test_semantic_abbr();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_article...\");\n    test_semantic_article();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_definition_list...\");\n    test_semantic_definition_list();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_details_summary...\");\n    test_semantic_details_summary();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_hr...\");\n    test_semantic_hr();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_mark_highlight...\");\n    test_semantic_mark_highlight();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_section_with_heading...\");\n    test_semantic_section_with_heading();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_semantic_sub_superscript...\");\n    test_semantic_sub_superscript();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_simple_table...\");\n    test_simple_table();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_empty...\");\n    test_table_empty();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_no_thead...\");\n    test_table_no_thead();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_pipe_chars_in_content...\");\n    test_table_pipe_chars_in_content();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_with_alignment...\");\n    test_table_with_alignment();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_table_with_colspan...\");\n    test_table_with_colspan();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_unordered_list...\");\n    test_unordered_list();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    /* Category: smoke */\n    printf(\"  Running test_smoke_empty_string...\");\n    test_smoke_empty_string();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_smoke_simple_heading...\");\n    test_smoke_simple_heading();\n    printf(\" PASSED\\n\");\n    passed++;\n    printf(\"  Running test_smoke_simple_paragraph...\");\n    test_smoke_simple_paragraph();\n    printf(\" PASSED\\n\");\n    passed++;\n\n    printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);\n    return failed > 0 ? 1 : 0;\n}\n"
  },
  {
    "path": "test_apps/c/test_conversion.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:11203c02cca71d9974e1548910e38832ccc08e3eda6dbba94d677c7f40f98e95\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: conversion */\n\n#include <assert.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n\nvoid test_blockquote_multiple_paragraphs(void) {\n    /* Blockquote with multiple paragraphs has each paragraph prefixed */\n    HTMConvert* result = htm_convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"> First paragraph.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"> Second paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_blockquote_nested(void) {\n    /* Nested blockquote produces double-prefixed lines */\n    HTMConvert* result = htm_convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Outer quote.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Inner quote.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_blockquote_simple(void) {\n    /* Simple blockquote */\n    HTMConvert* result = htm_convert(\"<blockquote><p>Quote text</p></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"> Quote text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_blockquote_with_list(void) {\n    /* Blockquote containing a list preserves list items inside quote */\n    HTMConvert* result = htm_convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Quote intro:\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Point one\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Point two\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_bold_and_italic(void) {\n    /* Nested bold and italic */\n    HTMConvert* result = htm_convert(\"<p><strong><em>both</em></strong></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"***both***\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_bold_strong(void) {\n    /* Strong tag converts to bold */\n    HTMConvert* result = htm_convert(\"<p><strong>bold</strong></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"**bold**\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_code_block(void) {\n    /* Code block with language preserves content */\n    HTMConvert* result = htm_convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"print('hello')\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_code_block_no_language(void) {\n    /* Code block without a language class preserves content */\n    HTMConvert* result = htm_convert(\"<pre><code>plain code here</code></pre>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"plain code here\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_code_inline_in_paragraph(void) {\n    /* Inline code element nested inside a paragraph */\n    HTMConvert* result = htm_convert(\"<p>Call the <code>initialize()</code> method first.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"`initialize()`\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_code_with_backticks_in_content(void) {\n    /* Inline code containing backtick characters is properly escaped */\n    HTMConvert* result = htm_convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"backtick\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_mark_highlight(void) {\n    /* mark tag produces highlighted output */\n    HTMConvert* result = htm_convert(\"<p><mark>highlighted</mark></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"highlighted\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_strikethrough_del(void) {\n    /* del tag converts to GFM strikethrough */\n    HTMConvert* result = htm_convert(\"<p><del>deleted text</del></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"~~deleted text~~\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_strikethrough_s(void) {\n    /* s tag converts to GFM strikethrough */\n    HTMConvert* result = htm_convert(\"<p><s>strikethrough</s></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"~~strikethrough~~\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_subscript(void) {\n    /* sub tag content is preserved */\n    HTMConvert* result = htm_convert(\"<p>H<sub>2</sub>O</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"H\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"O\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_superscript(void) {\n    /* sup tag content is preserved */\n    HTMConvert* result = htm_convert(\"<p>x<sup>2</sup></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"x\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_emphasis_underline_u(void) {\n    /* u tag content is preserved in output */\n    HTMConvert* result = htm_convert(\"<p><u>underlined</u></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"underlined\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_form_input_elements(void) {\n    /* Form input elements produce readable output without form mechanics */\n    HTMConvert* result = htm_convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Name\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_form_select_options(void) {\n    /* Select element with options produces readable output */\n    HTMConvert* result = htm_convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Color\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_form_textarea(void) {\n    /* Textarea element produces readable output */\n    HTMConvert* result = htm_convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Message\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h1(void) {\n    /* H1 heading */\n    HTMConvert* result = htm_convert(\"<h1>Heading 1</h1>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"# Heading 1\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h2(void) {\n    /* H2 heading */\n    HTMConvert* result = htm_convert(\"<h2>Heading 2</h2>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"## Heading 2\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h3(void) {\n    /* H3 heading */\n    HTMConvert* result = htm_convert(\"<h3>Heading 3</h3>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"### Heading 3\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h4(void) {\n    /* H4 heading */\n    HTMConvert* result = htm_convert(\"<h4>Heading 4</h4>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"#### Heading 4\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h5(void) {\n    /* H5 heading */\n    HTMConvert* result = htm_convert(\"<h5>Heading 5</h5>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"##### Heading 5\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_heading_h6(void) {\n    /* H6 heading */\n    HTMConvert* result = htm_convert(\"<h6>Heading 6</h6>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"###### Heading 6\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_image_figure_figcaption(void) {\n    /* Figure with figcaption preserves both image and caption */\n    HTMConvert* result = htm_convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"![A sunset](sunset.jpg)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Beautiful sunset over the ocean\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_image_linked(void) {\n    /* Image inside an anchor produces a linked image */\n    HTMConvert* result = htm_convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"![Icon](icon.png)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_image_no_alt(void) {\n    /* Image without alt text produces image markdown */\n    HTMConvert* result = htm_convert(\"<img src=\\\"banner.jpg\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"banner.jpg\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_image_simple(void) {\n    /* Image with alt text */\n    HTMConvert* result = htm_convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"![A photo](photo.jpg)\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_image_with_title(void) {\n    /* Image with title attribute includes title in output */\n    HTMConvert* result = htm_convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"![Sales chart](chart.png\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Q3 Sales\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_inline_code(void) {\n    /* Inline code */\n    HTMConvert* result = htm_convert(\"<p>Use <code>console.log()</code> to debug</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"`console.log()`\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_italic_em(void) {\n    /* Em tag converts to italic */\n    HTMConvert* result = htm_convert(\"<p><em>italic</em></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"*italic*\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_line_break_br_tag(void) {\n    /* Single br tag produces a line break in output */\n    HTMConvert* result = htm_convert(\"<p>First line.<br>Second line.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"First line.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second line.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_line_break_hr_tag(void) {\n    /* hr tag produces a horizontal separator between content */\n    HTMConvert* result = htm_convert(\"<p>Before rule.</p><hr><p>After rule.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Before rule.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"After rule.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_line_break_multiple_br(void) {\n    /* Multiple consecutive br tags in sequence */\n    HTMConvert* result = htm_convert(\"<p>Start.<br><br>End.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Start.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"End.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_anchor_fragment(void) {\n    /* Fragment-only anchor link is preserved */\n    HTMConvert* result = htm_convert(\"<a href=\\\"#section\\\">Jump to section</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"[Jump to section](#section)\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_empty_href(void) {\n    /* Link with empty href produces output with the link text */\n    HTMConvert* result = htm_convert(\"<a href=\\\"\\\">No destination</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"No destination\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_image_inside(void) {\n    /* Image inside a link produces a linked image */\n    HTMConvert* result = htm_convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"![Logo](logo.png)\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_mailto(void) {\n    /* Mailto link is preserved with mailto: scheme */\n    HTMConvert* result = htm_convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"mailto:user@example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_simple(void) {\n    /* Simple link */\n    HTMConvert* result = htm_convert(\"<a href=\\\"https://example.com\\\">Example</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"[Example](https://example.com)\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_with_bold_text(void) {\n    /* Link containing bold text preserves formatting */\n    HTMConvert* result = htm_convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"**Bold link**\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"https://example.com\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_link_with_title(void) {\n    /* Link with title attribute */\n    HTMConvert* result = htm_convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"[Example](https://example.com\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Example Site\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_definition_dl(void) {\n    /* Definition list with dt and dd elements */\n    HTMConvert* result = htm_convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Term One\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Definition of term one.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Term Two\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Definition of term two.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_item_multiple_paragraphs(void) {\n    /* List item containing multiple paragraphs */\n    HTMConvert* result = htm_convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"First paragraph in item.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph in item.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Simple item\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_mixed_nested(void) {\n    /* Mixed list: ordered list nested inside unordered list */\n    HTMConvert* result = htm_convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Item A\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Sub 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Sub 2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Item B\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_nested_ordered(void) {\n    /* Nested ordered list with two levels of depth */\n    HTMConvert* result = htm_convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Step 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 1a\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 1b\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Step 2\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_nested_unordered(void) {\n    /* Nested unordered list with two levels of depth */\n    HTMConvert* result = htm_convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Parent A\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Child A1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Child A2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Parent B\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_list_task_checkboxes(void) {\n    /* Task list with checked and unchecked checkboxes */\n    HTMConvert* result = htm_convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Done task\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Pending task\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_ordered_list(void) {\n    /* Ordered list */\n    HTMConvert* result = htm_convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"1. First\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2. Second\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"3. Third\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_paragraph_multiple(void) {\n    /* Multiple paragraphs are separated by a blank line */\n    HTMConvert* result = htm_convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"First paragraph.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Second paragraph.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_paragraph_nested_divs(void) {\n    /* Text nested inside divs is extracted correctly */\n    HTMConvert* result = htm_convert(\"<div><div><p>Nested text</p></div></div>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Nested text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_paragraph_simple(void) {\n    /* Simple paragraph converts to plain text */\n    HTMConvert* result = htm_convert(\"<p>Hello World</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"Hello World\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_paragraph_with_inline_formatting(void) {\n    /* Paragraph with bold, italic, and a link */\n    HTMConvert* result = htm_convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"**bold**\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"*italic*\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"[link](https://example.com)\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_paragraph_with_line_breaks(void) {\n    /* Paragraph with br tags produces line breaks in output */\n    HTMConvert* result = htm_convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Line one.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line two.\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Line three.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_abbr(void) {\n    /* Abbreviation element text is preserved */\n    HTMConvert* result = htm_convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"WWW\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_article(void) {\n    /* Article element wrapping content preserves inner content */\n    HTMConvert* result = htm_convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Article Title\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Article body.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_definition_list(void) {\n    /* Definition list with term and description */\n    HTMConvert* result = htm_convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"HTML\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"HyperText Markup Language\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"CSS\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Cascading Style Sheets\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_details_summary(void) {\n    /* Details and summary elements produce readable output */\n    HTMConvert* result = htm_convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Click to expand\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_hr(void) {\n    /* Horizontal rule produces a separator in output */\n    HTMConvert* result = htm_convert(\"<p>Above</p><hr><p>Below</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Above\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Below\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_mark_highlight(void) {\n    /* Mark tag produces highlighted output */\n    HTMConvert* result = htm_convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"highlighted text\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_section_with_heading(void) {\n    /* Section element with heading preserves structure */\n    HTMConvert* result = htm_convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Section Heading\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Section content.\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_semantic_sub_superscript(void) {\n    /* Subscript and superscript elements are preserved in output */\n    HTMConvert* result = htm_convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"H\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"O\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"E=mc\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_simple_table(void) {\n    /* Simple table with header */\n    HTMConvert* result = htm_convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"Name\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Age\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Alice\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"30\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"---\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_table_empty(void) {\n    /* Empty table produces no output or minimal output */\n    HTMConvert* result = htm_convert(\"<table></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_table_no_thead(void) {\n    /* Table without thead uses first row as implied header */\n    HTMConvert* result = htm_convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Product\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Price\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Apple\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"1.00\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_table_pipe_chars_in_content(void) {\n    /* Table cells containing pipe characters are escaped in output */\n    HTMConvert* result = htm_convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Expression\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Result\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"true\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_table_with_alignment(void) {\n    /* Table with column alignment attributes */\n    HTMConvert* result = htm_convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Left\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Center\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Right\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"L\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"C\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"R\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"|\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_table_with_colspan(void) {\n    /* Table with colspan attribute in a header cell */\n    HTMConvert* result = htm_convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    assert(strstr(content, \"Full Name\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"John\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"Doe\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_unordered_list(void) {\n    /* Unordered list */\n    HTMConvert* result = htm_convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"- Item 1\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"- Item 2\") != NULL && \"expected to contain substring\");\n    assert(strstr(content, \"- Item 3\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n"
  },
  {
    "path": "test_apps/c/test_runner.h",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2d3592a8c11e14847ac98016bcf0a4332490738df9b404967fc5a167b4e6f97d\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n#ifndef TEST_RUNNER_H\n#define TEST_RUNNER_H\n\n#include <string.h>\n#include <stdlib.h>\n\n/**\n * Compare a string against an expected value, trimming trailing whitespace.\n * Returns 0 if the trimmed actual string equals the expected string.\n */\nstatic inline int str_trim_eq(const char *actual, const char *expected) {\n    if (actual == NULL || expected == NULL) return actual != expected;\n    size_t alen = strlen(actual);\n    while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;\n    size_t elen = strlen(expected);\n    if (alen != elen) return 1;\n    return memcmp(actual, expected, elen);\n}\n\n/**\n * Extract a string value for a given key from a JSON object string.\n * Returns a heap-allocated copy of the value, or NULL if not found.\n * Caller must free() the returned string.\n */\nstatic inline char *alef_json_get_string(const char *json, const char *key) {\n    if (json == NULL || key == NULL) return NULL;\n    /* Build search pattern: \"key\":  */\n    size_t key_len = strlen(key);\n    char *pattern = (char *)malloc(key_len + 5);\n    if (!pattern) return NULL;\n    pattern[0] = '\"';\n    memcpy(pattern + 1, key, key_len);\n    pattern[key_len + 1] = '\"';\n    pattern[key_len + 2] = ':';\n    pattern[key_len + 3] = '\\0';\n    const char *found = strstr(json, pattern);\n    free(pattern);\n    if (!found) return NULL;\n    found += key_len + 3; /* skip past \"key\": */\n    while (*found == ' ' || *found == '\\t') found++;\n    if (*found != '\"') return NULL; /* not a string value */\n    found++; /* skip opening quote */\n    const char *end = found;\n    while (*end && *end != '\"') {\n        if (*end == '\\\\') { end++; if (*end) end++; }\n        else end++;\n    }\n    size_t val_len = (size_t)(end - found);\n    char *result_str = (char *)malloc(val_len + 1);\n    if (!result_str) return NULL;\n    memcpy(result_str, found, val_len);\n    result_str[val_len] = '\\0';\n    return result_str;\n}\n\n/**\n * Count top-level elements in a JSON array string.\n * Returns 0 for empty arrays (\"[]\") or NULL input.\n */\nstatic inline int alef_json_array_count(const char *json) {\n    if (json == NULL) return 0;\n    /* Skip leading whitespace */\n    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;\n    if (*json != '[') return 0;\n    json++;\n    /* Skip whitespace after '[' */\n    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;\n    if (*json == ']') return 0;\n    int count = 1;\n    int depth = 0;\n    int in_string = 0;\n    for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {\n        if (*json == '\\\\' && in_string) { json++; continue; }\n        if (*json == '\"') { in_string = !in_string; continue; }\n        if (in_string) continue;\n        if (*json == '[' || *json == '{') depth++;\n        else if (*json == ']' || *json == '}') depth--;\n        else if (*json == ',' && depth == 0) count++;\n    }\n    return count;\n}\n\n/* Tests for category: conversion */\nvoid test_blockquote_multiple_paragraphs(void);\nvoid test_blockquote_nested(void);\nvoid test_blockquote_simple(void);\nvoid test_blockquote_with_list(void);\nvoid test_bold_and_italic(void);\nvoid test_bold_strong(void);\nvoid test_code_block(void);\nvoid test_code_block_no_language(void);\nvoid test_code_inline_in_paragraph(void);\nvoid test_code_with_backticks_in_content(void);\nvoid test_emphasis_mark_highlight(void);\nvoid test_emphasis_strikethrough_del(void);\nvoid test_emphasis_strikethrough_s(void);\nvoid test_emphasis_subscript(void);\nvoid test_emphasis_superscript(void);\nvoid test_emphasis_underline_u(void);\nvoid test_form_input_elements(void);\nvoid test_form_select_options(void);\nvoid test_form_textarea(void);\nvoid test_heading_h1(void);\nvoid test_heading_h2(void);\nvoid test_heading_h3(void);\nvoid test_heading_h4(void);\nvoid test_heading_h5(void);\nvoid test_heading_h6(void);\nvoid test_image_figure_figcaption(void);\nvoid test_image_linked(void);\nvoid test_image_no_alt(void);\nvoid test_image_simple(void);\nvoid test_image_with_title(void);\nvoid test_inline_code(void);\nvoid test_italic_em(void);\nvoid test_line_break_br_tag(void);\nvoid test_line_break_hr_tag(void);\nvoid test_line_break_multiple_br(void);\nvoid test_link_anchor_fragment(void);\nvoid test_link_empty_href(void);\nvoid test_link_image_inside(void);\nvoid test_link_mailto(void);\nvoid test_link_simple(void);\nvoid test_link_with_bold_text(void);\nvoid test_link_with_title(void);\nvoid test_list_definition_dl(void);\nvoid test_list_item_multiple_paragraphs(void);\nvoid test_list_mixed_nested(void);\nvoid test_list_nested_ordered(void);\nvoid test_list_nested_unordered(void);\nvoid test_list_task_checkboxes(void);\nvoid test_ordered_list(void);\nvoid test_paragraph_multiple(void);\nvoid test_paragraph_nested_divs(void);\nvoid test_paragraph_simple(void);\nvoid test_paragraph_with_inline_formatting(void);\nvoid test_paragraph_with_line_breaks(void);\nvoid test_semantic_abbr(void);\nvoid test_semantic_article(void);\nvoid test_semantic_definition_list(void);\nvoid test_semantic_details_summary(void);\nvoid test_semantic_hr(void);\nvoid test_semantic_mark_highlight(void);\nvoid test_semantic_section_with_heading(void);\nvoid test_semantic_sub_superscript(void);\nvoid test_simple_table(void);\nvoid test_table_empty(void);\nvoid test_table_no_thead(void);\nvoid test_table_pipe_chars_in_content(void);\nvoid test_table_with_alignment(void);\nvoid test_table_with_colspan(void);\nvoid test_unordered_list(void);\n\n/* Tests for category: smoke */\nvoid test_smoke_empty_string(void);\nvoid test_smoke_simple_heading(void);\nvoid test_smoke_simple_paragraph(void);\n\n#endif /* TEST_RUNNER_H */\n"
  },
  {
    "path": "test_apps/c/test_smoke.c",
    "content": "/*\n * This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:3d2ab1ab4f66ddea42d3244060ff1780c316be521d3e5ca94b3c155e1a6131df\n * To regenerate: alef generate\n * To verify freshness: alef verify --exit-code\n * Issues & docs: https://github.com/kreuzberg-dev/alef\n */\n/* E2e tests for category: smoke */\n\n#include <assert.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include \"html_to_markdown.h\"\n#include \"test_runner.h\"\n\nvoid test_smoke_empty_string(void) {\n    /* Empty string produces empty output */\n    HTMConvert* result = htm_convert(\"\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"\") == 0 && \"equals assertion failed\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_smoke_simple_heading(void) {\n    /* H1 heading converts to ATX markdown */\n    HTMConvert* result = htm_convert(\"<h1>Title</h1>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(strstr(content, \"# Title\") != NULL && \"expected to contain substring\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n\nvoid test_smoke_simple_paragraph(void) {\n    /* Simple paragraph converts correctly */\n    HTMConvert* result = htm_convert(\"<p>Hello World</p>\", NULL);\n    assert(result != NULL && \"expected call to succeed\");\n    char* content = htm_convert_content(result);\n    assert(str_trim_eq(content, \"Hello World\") == 0 && \"equals assertion failed\");\n    assert(strlen(content) > 0 && \"expected non-empty value\");\n    htm_free_string(content);\n    htm_convert_free(result);\n}\n"
  },
  {
    "path": "test_apps/csharp/E2eTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.12.0\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"KreuzbergDev.HtmlToMarkdown\" Version=\"3.2.4\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test_apps/csharp/KreuzbergDev.HtmlToMarkdown.E2eTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.5.1\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"KreuzbergDev.HtmlToMarkdown\" Version=\"3.2.0\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test_apps/csharp/README.md",
    "content": "# C# Test App for html-to-markdown\n\nTests the published `KreuzbergDev.HtmlToMarkdown` NuGet package via P/Invoke bindings.\n\n## Overview\n\nThis test application validates that the C# bindings for html-to-markdown work correctly when installed from NuGet. The test suite includes:\n\n- **SmokeTest.cs** - Basic P/Invoke functionality tests\n  - Package loading and type availability\n  - Basic HTML to Markdown conversion\n  - All major HTML element types (headings, lists, links, etc.)\n  - Error handling for edge cases\n  - Null and malformed input handling\n  - 20+ individual test cases\n\n- **ComprehensiveTest.cs** - Fixture-driven comprehensive tests\n  - Conversion accuracy against known test cases\n  - Output type safety and consistency\n  - Batch processing capabilities\n  - Large input handling (1000+ paragraphs)\n  - Deeply nested HTML structures\n  - Unicode and special character support\n  - HTML entity handling\n  - Mixed formatting scenarios\n  - Script and comment removal\n  - Whitespace normalization\n  - Text encoding validation\n  - 20+ test cases\n\n## Package Information\n\n- **Package ID**: `KreuzbergDev.HtmlToMarkdown`\n- **Current Version**: 2.24.1\n- **Source**: Published on NuGet.org (not local path reference)\n- **Type**: C# P/Invoke bindings to Rust core via FFI\n\n## Prerequisites\n\n- .NET 10.0 or higher (uses `net10.0` target framework)\n- NuGet access to nuget.org\n- xUnit test framework (auto-installed via dotnet restore)\n\n## Setup\n\n```bash\n# Restore NuGet packages (installs KreuzbergDev.HtmlToMarkdown 2.24.1 from nuget.org)\ndotnet restore\n\n# Optionally, verify package installation\ndotnet list package\n```\n\n## Run Tests\n\n```bash\n# Run all tests\ndotnet test\n\n# Run only smoke tests (basic functionality)\ndotnet test --filter FullyQualifiedName~SmokeTest\n\n# Run only comprehensive tests (fixture-driven)\ndotnet test --filter FullyQualifiedName~ComprehensiveTest\n\n# Run with verbose output\ndotnet test --verbosity detailed\n\n# Run with detailed logging\ndotnet test --logger \"console;verbosity=detailed\"\n\n# Run specific test by name\ndotnet test --filter \"Name~TestBasicParagraphConversion\"\n```\n\n## Test Coverage\n\n### SmokeTest.cs (20+ tests)\n\n- Package loading and type availability\n- Basic paragraph, heading, and multi-level heading conversion\n- Bold and italic text formatting\n- Unordered and ordered lists\n- Hyperlinks with proper Markdown format\n- Inline code and code blocks\n- Blockquotes and horizontal rules\n- Line breaks\n- Null input and malformed HTML error handling\n\n### ComprehensiveTest.cs (20+ tests)\n\n- Fixture-driven conversion accuracy (basic-html.json)\n- Output type safety (returns non-null string)\n- Conversion consistency (idempotence)\n- Batch processing of multiple documents\n- Edge case handling (empty strings, whitespace)\n- Large HTML input (1000+ paragraphs)\n- Deeply nested HTML structures\n- Unicode character support (Chinese, accented, emoji)\n- HTML entity decoding (&nbsp;, &lt;, &amp;, &quot;)\n- Mixed formatting combinations\n- Script and comment removal from output\n- Whitespace normalization\n- UTF-8 text encoding validation\n\n## Test Fixtures\n\nTest fixtures are located in the shared `tests/test_apps/fixtures/` directory:\n\n- `basic-html.json` - 10 basic HTML element conversion tests\n- `complex-html.json` - Complex structure tests (placeholder - 0 tests)\n- `edge-cases.json` - Edge case handling (placeholder - 0 tests)\n- `metadata-extraction.json` - Metadata extraction (placeholder - 0 tests)\n- `real-world.json` - Real-world HTML samples (placeholder - 0 tests)\n\n## Type Safety\n\nThe C# test app uses:\n\n- **Strict typing**: All variables properly typed\n- **xUnit assertions**: Type-safe assertion framework\n- **Nullable reference types**: `#nullable enable` for safety\n- **Record types**: Immutable test case definitions\n- **Generics**: Type-safe collection handling\n\n## Verifying NuGet Installation\n\nTo verify that the test app is using the published NuGet package (not local paths):\n\n```bash\n# Check project dependencies\ndotnet list package\n\n# Output should show:\n#   KreuzbergDev.HtmlToMarkdown (direct dependency, version 2.24.1)\n#   Microsoft.NET.Test.Sdk (transitive)\n#   xunit (transitive)\n#   Newtonsoft.Json (transitive)\n\n# Inspect package location\ndotnet nuget list source\n\n# View installed package details\nnuget list KreuzbergDev.HtmlToMarkdown -AllVersions\n```\n\n## Troubleshooting\n\n### Package Not Found\n\nIf you get a \"Package not found\" error:\n\n```bash\n# Ensure NuGet sources are configured\ndotnet nuget list source\n\n# Restore with verbose logging\ndotnet restore --verbosity detailed\n```\n\n### Test Fixture Path Issues\n\nIf fixture files are not found:\n\n```bash\n# Verify fixture files exist\nls -la ../fixtures/\n\n# Check working directory\npwd\n```\n\n### P/Invoke Binding Issues\n\nIf P/Invoke binding fails:\n\n- Verify the native library is installed on the system\n- Check that the architecture (x64, arm64) matches\n- Ensure the FFI version matches the NuGet package version\n\n## CI/CD Integration\n\nThis test app can be used in CI pipelines:\n\n```yaml\n# Example GitHub Actions workflow\n- name: Run C# Tests\n  run: |\n    cd tests/test_apps/csharp\n    dotnet restore\n    dotnet test --verbosity detailed --logger \"trx;LogFileName=results.trx\"\n```\n\n## Notes\n\n- This is a **published package test**, not a development test\n- It validates the P/Invoke interface to the Rust FFI library\n- All test fixtures are shared across language bindings for consistency\n- The test app does NOT use local path references to `packages/csharp`\n- Performance tests are not included (see `task bench` for Rust benchmarks)\n"
  },
  {
    "path": "test_apps/csharp/tests/ConversionTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:9cd194887fb7922c3908500c5e46fd6df0ceb50b60bce974c9a1ee5f23c628c3\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: conversion.</summary>\npublic class ConversionTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteMultipleParagraphs()\n    {\n        // Blockquote with multiple paragraphs has each paragraph prefixed\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteNested()\n    {\n        // Nested blockquote produces double-prefixed lines\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteSimple()\n    {\n        // Simple blockquote\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BlockquoteWithList()\n    {\n        // Blockquote containing a list preserves list items inside quote\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BoldAndItalic()\n    {\n        // Nested bold and italic\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_BoldStrong()\n    {\n        // Strong tag converts to bold\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeBlock()\n    {\n        // Code block with language preserves content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeBlockNoLanguage()\n    {\n        // Code block without a language class preserves content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeInlineInParagraph()\n    {\n        // Inline code element nested inside a paragraph\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_CodeWithBackticksInContent()\n    {\n        // Inline code containing backtick characters is properly escaped\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisMarkHighlight()\n    {\n        // mark tag produces highlighted output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisStrikethroughDel()\n    {\n        // del tag converts to GFM strikethrough\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisStrikethroughS()\n    {\n        // s tag converts to GFM strikethrough\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisSubscript()\n    {\n        // sub tag content is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisSuperscript()\n    {\n        // sup tag content is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_EmphasisUnderlineU()\n    {\n        // u tag content is preserved in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormInputElements()\n    {\n        // Form input elements produce readable output without form mechanics\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormSelectOptions()\n    {\n        // Select element with options produces readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_FormTextarea()\n    {\n        // Textarea element produces readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH1()\n    {\n        // H1 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH2()\n    {\n        // H2 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH3()\n    {\n        // H3 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH4()\n    {\n        // H4 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH5()\n    {\n        // H5 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_HeadingH6()\n    {\n        // H6 heading\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageFigureFigcaption()\n    {\n        // Figure with figcaption preserves both image and caption\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageLinked()\n    {\n        // Image inside an anchor produces a linked image\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageNoAlt()\n    {\n        // Image without alt text produces image markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageSimple()\n    {\n        // Image with alt text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ImageWithTitle()\n    {\n        // Image with title attribute includes title in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_InlineCode()\n    {\n        // Inline code\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ItalicEm()\n    {\n        // Em tag converts to italic\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakBrTag()\n    {\n        // Single br tag produces a line break in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakHrTag()\n    {\n        // hr tag produces a horizontal separator between content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LineBreakMultipleBr()\n    {\n        // Multiple consecutive br tags in sequence\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkAnchorFragment()\n    {\n        // Fragment-only anchor link is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkEmptyHref()\n    {\n        // Link with empty href produces output with the link text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkImageInside()\n    {\n        // Image inside a link produces a linked image\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkMailto()\n    {\n        // Mailto link is preserved with mailto: scheme\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkSimple()\n    {\n        // Simple link\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkWithBoldText()\n    {\n        // Link containing bold text preserves formatting\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_LinkWithTitle()\n    {\n        // Link with title attribute\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListDefinitionDl()\n    {\n        // Definition list with dt and dd elements\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListItemMultipleParagraphs()\n    {\n        // List item containing multiple paragraphs\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListMixedNested()\n    {\n        // Mixed list: ordered list nested inside unordered list\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListNestedOrdered()\n    {\n        // Nested ordered list with two levels of depth\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListNestedUnordered()\n    {\n        // Nested unordered list with two levels of depth\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ListTaskCheckboxes()\n    {\n        // Task list with checked and unchecked checkboxes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_OrderedList()\n    {\n        // Ordered list\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphMultiple()\n    {\n        // Multiple paragraphs are separated by a blank line\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphNestedDivs()\n    {\n        // Text nested inside divs is extracted correctly\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphSimple()\n    {\n        // Simple paragraph converts to plain text\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphWithInlineFormatting()\n    {\n        // Paragraph with bold, italic, and a link\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_ParagraphWithLineBreaks()\n    {\n        // Paragraph with br tags produces line breaks in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticAbbr()\n    {\n        // Abbreviation element text is preserved\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticArticle()\n    {\n        // Article element wrapping content preserves inner content\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticDefinitionList()\n    {\n        // Definition list with term and description\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticDetailsSummary()\n    {\n        // Details and summary elements produce readable output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticHr()\n    {\n        // Horizontal rule produces a separator in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticMarkHighlight()\n    {\n        // Mark tag produces highlighted output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticSectionWithHeading()\n    {\n        // Section element with heading preserves structure\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SemanticSubSuperscript()\n    {\n        // Subscript and superscript elements are preserved in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SimpleTable()\n    {\n        // Simple table with header\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableEmpty()\n    {\n        // Empty table produces no output or minimal output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableNoThead()\n    {\n        // Table without thead uses first row as implied header\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TablePipeCharsInContent()\n    {\n        // Table cells containing pipe characters are escaped in output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableWithAlignment()\n    {\n        // Table with column alignment attributes\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_TableWithColspan()\n    {\n        // Table with colspan attribute in a header cell\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_UnorderedList()\n    {\n        // Unordered list\n    }\n}\n"
  },
  {
    "path": "test_apps/csharp/tests/SmokeTests.cs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:46299c362b36f80b21efae328d198a5f68a3355b95a2647ceb683a5f9034e2a5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing Xunit;\nusing HtmlToMarkdownRs;\n\nnamespace Kreuzberg.E2e;\n\n/// <summary>E2e tests for category: smoke.</summary>\npublic class SmokeTests\n{\n    private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeEmptyString()\n    {\n        // Empty string produces empty output\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeSimpleHeading()\n    {\n        // H1 heading converts to ATX markdown\n    }\n\n    [Fact(Skip = \"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function\")]\n    public void Test_SmokeSimpleParagraph()\n    {\n        // Simple paragraph converts correctly\n    }\n}\n"
  },
  {
    "path": "test_apps/elixir/README.md",
    "content": "# Elixir Test App for html-to-markdown\n\nTests the published html-to-markdown package from Hex.pm.\n\n## Setup\n\n```bash\nmix deps.get\n```\n\n## Run Tests\n\n```bash\n# Smoke tests\nmix test test/smoke_test.exs\n\n# Comprehensive tests\nmix test test/comprehensive_test.exs\n\n# All tests\nmix test\n```\n"
  },
  {
    "path": "test_apps/elixir/deps/html_to_markdown/.formatter.exs",
    "content": "[\n  import_deps: [:rustler],\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "test_apps/elixir/deps/html_to_markdown/README.md",
    "content": "# html-to-markdown\n\n<div align=\"center\" style=\"display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;\">\n  <!-- Language Bindings -->\n  <a href=\"https://crates.io/crates/html-to-markdown-rs\">\n    <img src=\"https://img.shields.io/crates/v/html-to-markdown-rs?label=Rust&color=007ec6\" alt=\"Rust\">\n  </a>\n  <a href=\"https://pypi.org/project/html-to-markdown/\">\n    <img src=\"https://img.shields.io/pypi/v/html-to-markdown?label=Python&color=007ec6\" alt=\"Python\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-node\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-node?label=Node.js&color=007ec6\" alt=\"Node.js\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@kreuzberg/html-to-markdown-wasm\">\n    <img src=\"https://img.shields.io/npm/v/@kreuzberg/html-to-markdown-wasm?label=WASM&color=007ec6\" alt=\"WASM\">\n  </a>\n  <a href=\"https://central.sonatype.com/artifact/dev.kreuzberg/html-to-markdown\">\n    <img src=\"https://img.shields.io/maven-central/v/dev.kreuzberg/html-to-markdown?label=Java&color=007ec6\" alt=\"Java\">\n  </a>\n  <a href=\"https://pkg.go.dev/github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\">\n    <img src=\"https://img.shields.io/github/v/tag/kreuzberg-dev/html-to-markdown?label=Go&color=007ec6&filter=v3.2.0\" alt=\"Go\">\n  </a>\n  <a href=\"https://www.nuget.org/packages/KreuzbergDev.HtmlToMarkdown/\">\n    <img src=\"https://img.shields.io/nuget/v/KreuzbergDev.HtmlToMarkdown?label=C%23&color=007ec6\" alt=\"C#\">\n  </a>\n  <a href=\"https://packagist.org/packages/kreuzberg-dev/html-to-markdown\">\n    <img src=\"https://img.shields.io/packagist/v/kreuzberg-dev/html-to-markdown?label=PHP&color=007ec6\" alt=\"PHP\">\n  </a>\n  <a href=\"https://rubygems.org/gems/html-to-markdown\">\n    <img src=\"https://img.shields.io/gem/v/html-to-markdown?label=Ruby&color=007ec6\" alt=\"Ruby\">\n  </a>\n  <a href=\"https://hex.pm/packages/html_to_markdown\">\n    <img src=\"https://img.shields.io/hexpm/v/html_to_markdown?label=Elixir&color=007ec6\" alt=\"Elixir\">\n  </a>\n  <a href=\"https://kreuzberg-dev.r-universe.dev/htmltomarkdown\">\n    <img src=\"https://img.shields.io/cran/v/htmltomarkdown?label=R&color=007ec6\" alt=\"R\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/releases\">\n    <img src=\"https://img.shields.io/badge/C-FFI-007ec6\" alt=\"C\">\n  </a>\n\n  <!-- Project Info -->\n  <a href=\"https://docs.html-to-markdown.kreuzberg.dev\">\n    <img src=\"https://img.shields.io/badge/Docs-kreuzberg.dev-007ec6\" alt=\"Documentation\">\n  </a>\n  <a href=\"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\">\n  </a>\n</div>\n\n<img width=\"1128\" height=\"191\" alt=\"html-to-markdown\" src=\"https://github.com/user-attachments/assets/419fc06c-8313-4324-b159-4b4d3cfce5c0\" />\n\n<div align=\"center\" style=\"margin-top: 20px;\">\n  <a href=\"https://discord.gg/pXxagNK2zN\">\n      <img height=\"22\" src=\"https://img.shields.io/badge/Discord-Join%20our%20community-7289da?logo=discord&logoColor=white\" alt=\"Discord\">\n  </a>\n</div>\n\nElixir bindings for the Rust html-to-markdown engine. The package exposes a fast HTML to Markdown converter implemented with Rustler.\nShip identical Markdown across every runtime while enjoying native performance with Rustler NIF bindings.\n\n\n## Installation\n\n```bash\nAdd {:html_to_markdown, \"~> 3.0\"} to mix.exs deps\n```\n\n\n\nRequires Elixir 1.19+ and OTP 28. Add to your `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:html_to_markdown, \"~> 3.2.0\"}\n  ]\nend\n```\n\n\n\n\n\n\n\n## Performance Snapshot\n\n**Apple M4** · `convert()` · Real Wikipedia documents\n\n| Document | Size | Latency | Throughput |\n|----------|------|---------|------------|\n| Lists (Timeline) | 129KB |  | 321.7 MB/s |\n| Tables (Countries) | 360KB |  | 293.8 MB/s |\n| Medium (Python) | 656KB |  | 281.5 MB/s |\n| Large (Rust) | 567KB |  | 268.7 MB/s |\n| Small (Intro) | 463KB |  | 262.9 MB/s |\n\n\n\n\n## Quick Start\n\nBasic conversion:\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>This is <strong>fast</strong>!</p>\")\nIO.puts(result.content)\n```\n\n\nWith conversion options:\n\n```elixir\nopts = %HtmlToMarkdown.Options{wrap: true, wrap_width: 40}\n{:ok, result} = HtmlToMarkdown.convert(\"<h1>Hello</h1><p>World</p>\", opts)\nIO.puts(result.content)\n```\n\n\n## API Reference\n\n### Core Function\n\n\n**`HtmlToMarkdown.convert(html, options \\\\ nil) :: {:ok, ConversionResult.t()} | {:error, term()}`**\n\nConverts HTML to Markdown. Returns `{:ok, result}` where result is a struct with all results in a single call.\n\n```elixir\n{:ok, result} = HtmlToMarkdown.convert(html)\nresult.content    # Converted Markdown string\nresult.metadata   # Metadata map (when extract_metadata: true)\nresult.tables     # Table data list (when extract_tables: true)\nresult.document   # Document-level info\nresult.images     # Extracted images\nresult.warnings   # Any conversion warnings\n```\n\n\n\n### Options\n\n**`ConversionOptions`** – Key configuration fields:\n\n- `heading_style`: Heading format (`\"underlined\"` | `\"atx\"` | `\"atx_closed\"`) — default: `\"underlined\"`\n- `list_indent_width`: Spaces per indent level — default: `2`\n- `bullets`: Bullet characters cycle — default: `\"*+-\"`\n- `wrap`: Enable text wrapping — default: `false`\n- `wrap_width`: Wrap at column — default: `80`\n- `code_language`: Default fenced code block language — default: none\n- `extract_metadata`: Enable metadata extraction into `result.metadata` — default: `false`\n- `extract_tables`: Enable structured table extraction into `result.tables` — default: `false`\n- `output_format`: Output markup format (`\"markdown\"` | `\"djot\"` | `\"plain\"`) — default: `\"markdown\"`\n\n## Djot Output Format\n\nThe library supports converting HTML to [Djot](https://djot.net/), a lightweight markup language similar to Markdown but with a different syntax for some elements. Set `output_format` to `\"djot\"` to use this format.\n\n### Syntax Differences\n\n| Element | Markdown | Djot |\n|---------|----------|------|\n| Strong | `**text**` | `*text*` |\n| Emphasis | `*text*` | `_text_` |\n| Strikethrough | `~~text~~` | `{-text-}` |\n| Inserted/Added | N/A | `{+text+}` |\n| Highlighted | N/A | `{=text=}` |\n| Subscript | N/A | `~text~` |\n| Superscript | N/A | `^text^` |\n\n### Example Usage\n\n\n\n```elixir\nhtml = \"<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n# Default Markdown output\n{:ok, markdown} = HtmlToMarkdown.convert(html)\n# Result: \"This is **bold** and *italic* text.\"\n\n# Djot output\n{:ok, djot} = HtmlToMarkdown.convert(html, %{output_format: \"djot\"})\n# Result: \"This is *bold* and _italic_ text.\"\n```\n\n\n\nDjot's extended syntax allows you to express more semantic meaning in lightweight text, making it useful for documents that require strikethrough, insertion tracking, or mathematical notation.\n\n## Plain Text Output\n\nSet `output_format` to `\"plain\"` to strip all markup and return only visible text. This bypasses the Markdown conversion pipeline entirely for maximum speed.\n\n\n\n```elixir\nhtml = \"<h1>Title</h1><p>This is <strong>bold</strong> and <em>italic</em> text.</p>\"\n\n{:ok, plain} = HtmlToMarkdown.convert(html, %{output_format: \"plain\"})\n# Result: \"Title\\n\\nThis is bold and italic text.\"\n```\n\n\n\nPlain text mode is useful for search indexing, text extraction, and feeding content to LLMs.\n\n\n\n## Metadata Extraction\n\nThe metadata extraction feature enables comprehensive document analysis during conversion. Extract document properties, headers, links, images, and structured data in a single pass — all via the standard `convert()` function.\n\n**Use Cases:**\n\n- **SEO analysis** – Extract title, description, Open Graph tags, Twitter cards\n- **Table of contents generation** – Build structured outlines from heading hierarchy\n- **Content migration** – Document all external links and resources\n- **Accessibility audits** – Check for images without alt text, empty links, invalid heading hierarchy\n- **Link validation** – Classify and validate anchor, internal, external, email, and phone links\n\n**Zero Overhead When Disabled:** Metadata extraction adds negligible overhead and happens during the HTML parsing pass. Pass `extract_metadata: true` in `ConversionOptions` to enable it; the result is available at `result.metadata`.\n\n### Example: Quick Start\n\n\n\n```elixir\nhtml = \"<h1>Article</h1><img src=\\\"test.jpg\\\" alt=\\\"test\\\">\"\nopts = %HtmlToMarkdown.Options{extract_metadata: true}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\n\nIO.puts(result.content)                           # Converted Markdown\nIO.inspect(result.metadata[\"document\"][\"title\"])  # Document title\nIO.inspect(result.metadata[\"headers\"])            # All h1-h6 elements\nIO.inspect(result.metadata[\"links\"])              # All hyperlinks\nIO.inspect(result.metadata[\"images\"])             # All images with alt text\nIO.inspect(result.metadata[\"structured_data\"])    # JSON-LD, Microdata, RDFa\n```\n\n\n\n\n\n\n## Visitor Pattern\n\nThe visitor pattern enables custom HTML→Markdown conversion logic by providing callbacks for specific HTML elements during traversal. Pass a visitor as the third argument to `convert()`.\n\n**Use Cases:**\n\n- **Custom Markdown dialects** – Convert to Obsidian, Notion, or other flavors\n- **Content filtering** – Remove tracking pixels, ads, or unwanted elements\n- **URL rewriting** – Rewrite CDN URLs, add query parameters, validate links\n- **Accessibility validation** – Check alt text, heading hierarchy, link text\n- **Analytics** – Track element usage, link destinations, image sources\n\n**Supported Visitor Methods:** 40+ callbacks for text, inline elements, links, images, headings, lists, blocks, and tables.\n\n### Example: Quick Start\n\n\n\n```elixir\ndefmodule MyVisitor do\n  use HtmlToMarkdown.Visitor\n\n  @impl true\n  def handle_link(_ctx, href, text, _title) do\n    # Rewrite CDN URLs\n    href = if String.starts_with?(href, \"https://old-cdn.com\") do\n      String.replace(href, \"https://old-cdn.com\", \"https://new-cdn.com\")\n    else\n      href\n    end\n    {:custom, \"[#{text}](#{href})\"}\n  end\n\n  @impl true\n  def handle_image(_ctx, src, _alt, _title) do\n    # Skip tracking pixels\n    if String.contains?(src, \"tracking\"), do: :skip, else: :continue\n  end\nend\n\nhtml = \"<a href=\\\"https://old-cdn.com/file.pdf\\\">Download</a>\"\nopts = %HtmlToMarkdown.Options{visitor: MyVisitor}\n{:ok, result} = HtmlToMarkdown.convert(html, opts)\nresult.content\n```\n\n\n\n\n## Examples\n\n\n## Links\n\n- **GitHub:** [github.com/kreuzberg-dev/html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown)\n\n- **Hex.pm:** [hex.pm/packages/html_to_markdown](https://hex.pm/packages/html_to_markdown)\n\n- **Kreuzberg Ecosystem:** [kreuzberg.dev](https://kreuzberg.dev)\n- **Discord:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up the development environment\n- Running tests locally\n- Submitting pull requests\n- Reporting issues\n\nAll contributions must follow our code quality standards (enforced via pre-commit hooks):\n\n- Proper test coverage (Rust 95%+, language bindings 80%+)\n- Formatting and linting checks\n- Documentation for public APIs\n\n## License\n\nMIT License – see [LICENSE](https://github.com/kreuzberg-dev/html-to-markdown/blob/main/LICENSE).\n\n## Support\n\nIf you find this library useful, consider [sponsoring the project](https://github.com/sponsors/kreuzberg-dev).\n\nHave questions or run into issues? We're here to help:\n\n- **GitHub Issues:** [github.com/kreuzberg-dev/html-to-markdown/issues](https://github.com/kreuzberg-dev/html-to-markdown/issues)\n- **Discussions:** [github.com/kreuzberg-dev/html-to-markdown/discussions](https://github.com/kreuzberg-dev/html-to-markdown/discussions)\n- **Discord Community:** [discord.gg/pXxagNK2zN](https://discord.gg/pXxagNK2zN)\n"
  },
  {
    "path": "test_apps/elixir/deps/html_to_markdown/checksum-Elixir.HtmlToMarkdown.Native.exs",
    "content": "%{\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.16-aarch64-apple-darwin.so.tar.gz\" => \"sha256:e67a546cc9a7d6007d55d45fb44bfd257e4f3bd07827c55e727e61331452da37\",\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.16-aarch64-unknown-linux-gnu.so.tar.gz\" => \"sha256:17ed5ef457a2c6e9d323b4399a4275ed50e4b33107e0a1167332b457087e52a5\",\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.16-x86_64-unknown-linux-gnu.so.tar.gz\" => \"sha256:9fc1d20d12facd2c591662b0a47ef39ceea46b149b53ec3920cee32a555ccacf\",\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.17-aarch64-apple-darwin.so.tar.gz\" => \"sha256:36a62da6c77c0701dfa0cba32b3f38aca9d9be46e6758e87082b4224820b706f\",\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.17-aarch64-unknown-linux-gnu.so.tar.gz\" => \"sha256:041247b5c4327bffd2a34ee995e4565e3c6169c43a20550006a9c934f0fc7dd1\",\n  \"libhtml_to_markdown_nif-v3.2.4-nif-2.17-x86_64-unknown-linux-gnu.so.tar.gz\" => \"sha256:cf9ad809d92ba80fdaa13b4484d4e982ba17ff53260aaad0abdbc49d6bab3f6e\",\n}\n"
  },
  {
    "path": "test_apps/elixir/deps/html_to_markdown/hex_metadata.config",
    "content": "{<<\"links\">>,\n [{<<\"GitHub\">>,<<\"https://github.com/kreuzberg-dev/html-to-markdown\">>}]}.\n{<<\"name\">>,<<\"html_to_markdown\">>}.\n{<<\"version\">>,<<\"3.2.4\">>}.\n{<<\"description\">>,<<\"High-performance HTML to Markdown converter\">>}.\n{<<\"elixir\">>,<<\"~> 1.14\">>}.\n{<<\"app\">>,<<\"html_to_markdown\">>}.\n{<<\"licenses\">>,[<<\"MIT\">>]}.\n{<<\"files\">>,\n [<<\"lib\">>,<<\"lib/html_to_markdown.ex\">>,<<\"lib/html_to_markdown\">>,\n  <<\"lib/html_to_markdown/metadata_config.ex\">>,\n  <<\"lib/html_to_markdown/table_data.ex\">>,\n  <<\"lib/html_to_markdown/text_annotation.ex\">>,\n  <<\"lib/html_to_markdown/html_metadata.ex\">>,\n  <<\"lib/html_to_markdown/document_structure.ex\">>,\n  <<\"lib/html_to_markdown/table_grid.ex\">>,\n  <<\"lib/html_to_markdown/preprocessing_options_update.ex\">>,\n  <<\"lib/html_to_markdown/document_node.ex\">>,\n  <<\"lib/html_to_markdown/conversion_result.ex\">>,\n  <<\"lib/html_to_markdown/conversion_options.ex\">>,\n  <<\"lib/html_to_markdown/header_metadata.ex\">>,\n  <<\"lib/html_to_markdown/image_metadata.ex\">>,\n  <<\"lib/html_to_markdown/preprocessing_options.ex\">>,\n  <<\"lib/html_to_markdown/link_metadata.ex\">>,\n  <<\"lib/html_to_markdown/document_metadata.ex\">>,\n  <<\"lib/html_to_markdown/native.ex\">>,\n  <<\"lib/html_to_markdown/processing_warning.ex\">>,\n  <<\"lib/html_to_markdown/grid_cell.ex\">>,\n  <<\"lib/html_to_markdown/metadata_config_update.ex\">>,\n  <<\"lib/html_to_markdown/structured_data.ex\">>,\n  <<\"lib/html_to_markdown/conversion_options_update.ex\">>,<<\"mix.exs\">>,\n  <<\"README.md\">>,<<\".formatter.exs\">>,\n  <<\"checksum-Elixir.HtmlToMarkdown.Native.exs\">>]}.\n{<<\"requirements\">>,\n [[{<<\"name\">>,<<\"rustler\">>},\n   {<<\"app\">>,<<\"rustler\">>},\n   {<<\"optional\">>,true},\n   {<<\"requirement\">>,<<\"~> 0.34\">>},\n   {<<\"repository\">>,<<\"hexpm\">>}],\n  [{<<\"name\">>,<<\"rustler_precompiled\">>},\n   {<<\"app\">>,<<\"rustler_precompiled\">>},\n   {<<\"optional\">>,false},\n   {<<\"requirement\">>,<<\"~> 0.9\">>},\n   {<<\"repository\">>,<<\"hexpm\">>}]]}.\n{<<\"build_tools\">>,[<<\"mix\">>]}.\n"
  },
  {
    "path": "test_apps/elixir/deps/html_to_markdown/mix.exs",
    "content": "defmodule Html_to_markdown.MixProject do\n  use Mix.Project\n\n  @version \"3.2.4\"\n  @source_url \"https://github.com/kreuzberg-dev/html-to-markdown\"\n\n  def project do\n    [\n      app: :html_to_markdown,\n      version: @version,\n      elixir: \"~> 1.14\",\n      description: \"High-performance HTML to Markdown converter\",\n      package: package(),\n      deps: deps(),\n      source_url: @source_url\n    ]\n  end\n\n  defp package do\n    [\n      licenses: [\"MIT\"],\n      links: %{\"GitHub\" => @source_url},\n      files: ~w(\n        lib\n        mix.exs\n        README.md\n        .formatter.exs\n        checksum-Elixir.HtmlToMarkdown.Native.exs\n      )\n    ]\n  end\n\n  defp deps do\n    [\n      {:rustler, \"~> 0.34\", optional: true, runtime: false},\n      {:rustler_precompiled, \"~> 0.9\"},\n      {:credo, \"~> 1.7\", only: [:dev, :test], runtime: false},\n      {:ex_doc, \"~> 0.40\", only: :dev, runtime: false}\n    ]\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/deps/jason/CHANGELOG.md",
    "content": "# Changelog\n\n## 1.4.4 (26.07.2024)\n\n* Fix warnings on Elixir 1.17 by conditionally compiling Decimal support\n\n## 1.4.3 (29.06.2024)\n\n* Fix derive with _ struct key\n\n## 1.4.2 (29.06.2024)\n\n* Fix compiler warnings for Elixir 1.17\n\n## 1.4.1 (06.07.2023)\n\n* Add limit to decoded integer sizes of 1024 digits. This can be changed\n  with the `decoding_integer_digit_limit` app env config.\n\n## 1.4.0 (12.09.2022)\n\n### Enhancements\n\n* Use the `:erlang.float_to_binary(_, [:short])` function, instead of `io_lib_format.fwrite_g/1`\n  where available (OTP 24.1+). This provides equivalent output with much less memory used\n  and significantly improved performance.\n\n## 1.3.0 (21.12.2021)\n\n### Enhancements\n\n* Add the `Jason.OrderedObject` struct\n* Support decoding objects preserving all the keys with `objects: :ordered_objects` option\n* Support decoding floats to `Decimal` with `floats: :decimals` option\n* Add `~j` and `~J` sigils in module `Jason.Sigil` to support writing JSON literals in code\n\n### Fixes\n\n* Fix error reporting when decoding strings (it was possible to mis-attribute the offending byte)\n* Verify fields given to `@derive`\n\n## 1.2.2 (08.09.2020)\n\n### Enhancements\n\n* Support Decimal 2.0\n\n## 1.2.1 (04.05.2020)\n\n### Security\n\n* Fix `html_safe` escaping in `Jason.encode`\n\n  The `<!--` sequence of characters would not be escaped in `Jason.encode`\n  with`html_escape` mode, which could lead to DoS attacks when used for\n  embedding of arbitrary, user controlled strings into HTML through JSON\n  (e.g. inside of `<script>` tags).\n\n  If you were not using the `html_safe` option, you are not affected.\n\n  Affected versions: < 1.2.1\n  Patched versions: >= 1.2.1\n\n## 1.2.0 (17.03.2020)\n\n### Enhancements\n\n* Add `Jason.Encode.keyword/2`\n  ([cb1f26a](https://github.com/michalmuskala/jason/commit/cb1f26a)).\n\n### Bug fixes\n\n* Fix `Jason.Helpers.json_map/1` value expansion\n  ([70b046a](https://github.com/michalmuskala/jason/commit/70b046a)).\n\n## 1.1.2 (19.10.2018)\n\n### Bug fixes\n\n* correctly handle the `pretty: false` option\n  ([ba318c8](https://github.com/michalmuskala/jason/commit/ba318c8)).\n\n## 1.1.1 (10.07.2018)\n\n### Bug fixes\n\n* correctly handle escape sequences in strings when pretty printing\n  ([794bbe4](https://github.com/michalmuskala/jason/commit/794bbe4)).\n\n## 1.1.0 (02.07.2018)\n\n### Enhancements\n\n* pretty-printing support through `Jason.Formatter` and `pretty: true` option\n  in `Jason.encode/2` ([d758e36](https://github.com/michalmuskala/jason/commit/d758e36)).\n\n### Bug fixes\n\n* silence variable warnings for fields with underscores used during deriving\n  ([88dd85c](https://github.com/michalmuskala/jason/commit/88dd85c)).\n* **potential incompatibility** don't raise `Protocol.UndefinedError` in non-bang functions\n  ([ad0f57b](https://github.com/michalmuskala/jason/commit/ad0f57b)).\n\n## 1.0.1 (02.07.2018)\n\n### Bug fixes\n\n* fix `Jason.Encode.escape` type ([a57b430](https://github.com/michalmuskala/jason/commit/a57b430))\n* multiple documentation improvements\n\n## 1.0.0 (26.01.2018)\n\nNo changes\n\n## 1.0.0-rc.3 (26.01.2018)\n\n### Changes\n\n* update `escape` option of `Jason.encode/2` to take values:\n  `:json | :unicode_safe | :html_safe | :javascript_safe` for consistency. Old values of\n  `:unicode` and `:javascript` are still supported for compatibility with Poison.\n  ([f42dcbd](https://github.com/michalmuskala/jason/commit/f42dcbd))\n\n## 1.0.0-rc.2 (07.01.2018)\n\n### Bug fixes\n\n* add type for `strings` option ([b459ee4](https://github.com/michalmuskala/jason/commit/b459ee4))\n* support iodata in `decode!` ([a1f3456](https://github.com/michalmuskala/jason/commit/a1f3456))\n\n## 1.0.0-rc.1 (22.12.2017)\n\n* Initial release\n"
  },
  {
    "path": "test_apps/elixir/deps/jason/LICENSE",
    "content": "Copyright (c) 2017-present Michał Muskała\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "test_apps/elixir/deps/jason/README.md",
    "content": "# Jason\n\nA blazing fast JSON parser and generator in pure Elixir.\n\nThe parser and generator are at least twice as fast as other Elixir/Erlang libraries\n(most notably `Poison`).\nThe performance is comparable to `jiffy`, which is implemented in C as a NIF.\nJason is usually only twice as slow.\n\nBoth parser and generator fully conform to\n[RFC 8259](https://tools.ietf.org/html/rfc8259) and\n[ECMA 404](http://www.ecma-international.org/publications/standards/Ecma-404.htm)\nstandards. The parser is tested using [JSONTestSuite](https://github.com/nst/JSONTestSuite).\n\n## Installation\n\nThe package can be installed by adding `jason` to your list of dependencies\nin `mix.exs`:\n\n```elixir\ndef deps do\n  [{:jason, \"~> 1.4\"}]\nend\n```\n\n## Basic Usage\n\n``` elixir\niex(1)> Jason.encode!(%{\"age\" => 44, \"name\" => \"Steve Irwin\", \"nationality\" => \"Australian\"})\n\"{\\\"age\\\":44,\\\"name\\\":\\\"Steve Irwin\\\",\\\"nationality\\\":\\\"Australian\\\"}\"\n\niex(2)> Jason.decode!(~s({\"age\":44,\"name\":\"Steve Irwin\",\"nationality\":\"Australian\"}))\n%{\"age\" => 44, \"name\" => \"Steve Irwin\", \"nationality\" => \"Australian\"}\n```\n\nFull documentation can be found at [https://hexdocs.pm/jason](https://hexdocs.pm/jason).\n\n## Use with other libraries\n\n### Postgrex\n\nVersions starting at 0.14.0 use `Jason` by default. For earlier versions, please refer to\n[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#postgrex).\n\n### Ecto\n\nVersions starting at 3.0.0 use `Jason` by default. For earlier versions, please refer to\n[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#ecto).\n\n### Plug (and Phoenix)\n\nPhoenix starting at 1.4.0 uses `Jason` by default. For earlier versions, please refer to\n[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#plug-and-phoenix).\n\n### Absinthe\n\nYou need to pass the `:json_codec` option to `Absinthe.Plug`\n\n```elixir\n# When called directly:\nplug Absinthe.Plug,\n  schema: MyApp.Schema,\n  json_codec: Jason\n\n# When used in phoenix router:\nforward \"/api\",\n  to: Absinthe.Plug,\n  init_opts: [schema: MyApp.Schema, json_codec: Jason]\n```\n\n## Benchmarks\n\nDetailed benchmarks (including memory measurements):\n<https://gist.github.com/michalmuskala/4d64a5a7696ca84ac7c169a0206640d5>\n\nHTML reports for the benchmark (only performance measurements):\n<http://michal.muskala.eu/jason/decode.html> and <http://michal.muskala.eu/jason/encode.html>\n\n### Running\n\nBenchmarks against most popular Elixir & Erlang json libraries can be executed after\ngoing into the `bench/` folder and then executing `mix bench.encode` and `mix bench.decode`.\nA HTML report of the benchmarks (after their execution) can be found in\n`bench/output/encode.html` and `bench/output/decode.html` respectively.\n\n## Differences to Poison\n\nJason has a couple feature differences compared to Poison.\n\n* Jason follows the JSON spec more strictly, for example it does not allow\n  unescaped newline characters in JSON strings - e.g. `\"\\\"\\n\\\"\"` will\n  produce a decoding error.\n* no support for decoding into data structures (the `as:` option).\n* no built-in encoders for `MapSet`, `Range` and `Stream`.\n* no support for encoding arbitrary structs - explicit implementation\n  of the `Jason.Encoder` protocol is always required.\n* different pretty-printing customisation options (default `pretty: true` works the same)\n\n### Encoders\n\nIf you require encoders for any of the unsupported collection types, I suggest\nadding the needed implementations directly to your project:\n\n```elixir\ndefimpl Jason.Encoder, for: [MapSet, Range, Stream] do\n  def encode(struct, opts) do\n    Jason.Encode.list(Enum.to_list(struct), opts)\n  end\nend\n```\n\nIf you need to encode some struct that does not implement the protocol,\nif you own the struct, you can derive the implementation specifying\nwhich fields should be encoded to JSON:\n\n```elixir\n@derive {Jason.Encoder, only: [....]}\ndefstruct # ...\n```\n\nIt is also possible to encode all fields, although this should be\nused carefully to avoid accidentally leaking private information\nwhen new fields are added:\n\n```elixir\n@derive Jason.Encoder\ndefstruct # ...\n```\n\nFinally, if you don't own the struct you want to encode to JSON,\nyou may use `Protocol.derive/3` placed outside of any module:\n\n```elixir\nProtocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])\nProtocol.derive(Jason.Encoder, NameOfTheStruct)\n```\n\n## Injecting an already encoded JSON inside a to-be-encoded structure\n\nIf parts of the to-be-encoded structure are already JSON-encoded, you can\nuse `Jason.Fragment` to mark the parts as already encoded, and avoid a\ndecoding/encoding roundtrip.\n\n```elixir\nalready_encoded_json = Jason.encode!(%{hello: \"world\"})\nJason.encode!(%{foo: Jason.Fragment.new(already_encoded_json)})\n```\n\nThis feature is especially useful if you need to cache a part of the JSON,\nor if it is already provided by another system (e.g. `jsonb_agg` with Postgres).\n\n## License\n\nJason is released under the Apache License 2.0 - see the [LICENSE](LICENSE) file.\n\nSome elements of tests and benchmarks have their origins in the\n[Poison library](https://github.com/devinus/poison) and were initially licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).\n"
  },
  {
    "path": "test_apps/elixir/deps/jason/hex_metadata.config",
    "content": "{<<\"links\">>,[{<<\"GitHub\">>,<<\"https://github.com/michalmuskala/jason\">>}]}.\n{<<\"name\">>,<<\"jason\">>}.\n{<<\"version\">>,<<\"1.4.4\">>}.\n{<<\"description\">>,\n <<\"A blazing fast JSON parser and generator in pure Elixir.\">>}.\n{<<\"elixir\">>,<<\"~> 1.4\">>}.\n{<<\"app\">>,<<\"jason\">>}.\n{<<\"licenses\">>,[<<\"Apache-2.0\">>]}.\n{<<\"requirements\">>,\n [[{<<\"name\">>,<<\"decimal\">>},\n   {<<\"app\">>,<<\"decimal\">>},\n   {<<\"optional\">>,true},\n   {<<\"requirement\">>,<<\"~> 1.0 or ~> 2.0\">>},\n   {<<\"repository\">>,<<\"hexpm\">>}]]}.\n{<<\"files\">>,\n [<<\"lib\">>,<<\"lib/jason.ex\">>,<<\"lib/encoder.ex\">>,<<\"lib/decoder.ex\">>,\n  <<\"lib/ordered_object.ex\">>,<<\"lib/formatter.ex\">>,<<\"lib/encode.ex\">>,\n  <<\"lib/codegen.ex\">>,<<\"lib/helpers.ex\">>,<<\"lib/sigil.ex\">>,\n  <<\"lib/fragment.ex\">>,<<\"mix.exs\">>,<<\"README.md\">>,<<\"LICENSE\">>,\n  <<\"CHANGELOG.md\">>]}.\n{<<\"build_tools\">>,[<<\"mix\">>]}.\n"
  },
  {
    "path": "test_apps/elixir/deps/jason/mix.exs",
    "content": "defmodule Jason.Mixfile do\n  use Mix.Project\n\n  @source_url \"https://github.com/michalmuskala/jason\"\n  @version \"1.4.4\"\n\n  def project() do\n    [\n      app: :jason,\n      version: @version,\n      elixir: \"~> 1.4\",\n      start_permanent: Mix.env() == :prod,\n      consolidate_protocols: Mix.env() != :test,\n      deps: deps(),\n      preferred_cli_env: [docs: :docs],\n      dialyzer: dialyzer(),\n      description: description(),\n      package: package(),\n      docs: docs()\n    ]\n  end\n\n  def application() do\n    [\n      extra_applications: []\n    ]\n  end\n\n  defp deps() do\n    [\n      {:decimal, \"~> 1.0 or ~> 2.0\", optional: true},\n      {:dialyxir, \"~> 1.0\", only: [:dev, :test], runtime: false},\n      {:ex_doc, \">= 0.0.0\", only: :dev, runtime: false},\n    ] ++ maybe_stream_data()\n  end\n\n  defp maybe_stream_data() do\n    if Version.match?(System.version(), \"~> 1.12\") do\n      [{:stream_data, \"~> 1.0\", only: :test}]\n    else\n      []\n    end\n  end\n\n  defp dialyzer() do\n    [\n      plt_add_apps: [:decimal]\n    ]\n  end\n\n  defp description() do\n    \"\"\"\n    A blazing fast JSON parser and generator in pure Elixir.\n    \"\"\"\n  end\n\n  defp package() do\n    [\n      maintainers: [\"Michał Muskała\"],\n      licenses: [\"Apache-2.0\"],\n      links: %{\"GitHub\" => @source_url}\n    ]\n  end\n\n  defp docs() do\n    [\n      main: \"readme\",\n      name: \"Jason\",\n      source_ref: \"v#{@version}\",\n      canonical: \"http://hexdocs.pm/jason\",\n      source_url: @source_url,\n      extras: [\"README.md\", \"CHANGELOG.md\", \"LICENSE\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/README.md",
    "content": "# Rustler\n\n[![Module Version](https://img.shields.io/hexpm/v/rustler.svg)](https://hex.pm/packages/rustler)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/rustler/)\n[![Total Download](https://img.shields.io/hexpm/dt/rustler.svg)](https://hex.pm/packages/rustler)\n[![License](https://img.shields.io/hexpm/l/rustler.svg)](https://github.com/rusterlium/rustler/blob/master/LICENSE)\n[![Last Updated](https://img.shields.io/github/last-commit/rusterlium/rustler.svg)](https://github.com/rusterlium/rustler/commits/master)\n\nThe Mix package for [rustler](https://github.com/rusterlium/rustler), a library to write Erlang Native Implemented Functions (NIFs) in [Rust](https://www.rust-lang.org/) programming language.\n\n## Installation\n\nThis package is available on [Hex.pm](https://hex.pm/packages/rustler). To install it, add `:rustler` to your dependencies:\n\n```elixir\ndef deps do\n  [\n    {:rustler, \"~> 0.37.3\", runtime: false}\n  ]\nend\n```\n\n## Usage\n\n1. Fetch and compile all necessary dependencies:\n\n    ```text\n    $ mix deps.get && mix deps.compile\n    ```\n\n2. Check your installation by showing help from the installed Mix task:\n\n    ```text\n    $ mix help rustler.new\n    ```\n\n3. Generate the boilerplate for a new Rustler project. Follow the instructions\n   to configure your project:\n\n    ```text\n    $ mix rustler.new\n    ```\n\n4. [Load the NIF in your program.](#loading-the-nif).\n\n## Crate configuration\n\nThe Rust crate compilation can be controlled via Mix compile-time configuration in `config/config.exs`.\nSee [configuration options](https://hexdocs.pm/rustler/Rustler.html#module-configuration-options) for more details.\n\n\n## Loading the NIF\n\nLoading a Rustler NIF is done in almost the same way as normal NIFs.\n\nThe actual loading is done by calling `use Rustler, otp_app: :my_app` in the module you want to load the NIF in.\nThis sets up the `@on_load` module hook to load the NIF when the module is first\nloaded.\n\n```elixir\ndefmodule MyProject.MyModule do\n  use Rustler,\n    otp_app: :my_app,\n    crate: :my_crate\n\n  # When loading a NIF module, dummy clauses for all NIF function are required.\n  # NIF dummies usually just error out when called when the NIF is not loaded, as that should never normally happen.\n  def my_native_function(_arg1, _arg2), do: :erlang.nif_error(:nif_not_loaded)\nend\n```\n\nNote that `:crate` is the name in the `[lib]` section of your `Cargo.toml`. The\n`:crate` option is optional if your crate and `otp_app` use the same name.\n\nSee the `Rustler` module for more information.\n\n## Copyright and License\n\nLicensed under either of\n\n- Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)\n- MIT license ([LICENSE-MIT](../LICENSE-MIT) or <http://opensource.org/licenses/MIT>)\n\nat your option.\n\n## Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/hex_metadata.config",
    "content": "{<<\"links\">>,\n [{<<\"Changelog\">>,<<\"https://hexdocs.pm/rustler/changelog.html\">>},\n  {<<\"GitHub\">>,<<\"https://github.com/rusterlium/rustler\">>}]}.\n{<<\"name\">>,<<\"rustler\">>}.\n{<<\"version\">>,<<\"0.37.3\">>}.\n{<<\"description\">>,<<\"Mix compiler and runtime helpers for Rustler.\">>}.\n{<<\"elixir\">>,<<\"~> 1.15\">>}.\n{<<\"app\">>,<<\"rustler\">>}.\n{<<\"files\">>,\n [<<\"lib\">>,<<\"lib/mix\">>,<<\"lib/mix/tasks\">>,\n  <<\"lib/mix/tasks/rustler.new.ex\">>,<<\"lib/rustler.ex\">>,<<\"lib/rustler\">>,\n  <<\"lib/rustler/compiler.ex\">>,<<\"lib/rustler/build_results.ex\">>,\n  <<\"lib/rustler/compiler\">>,<<\"lib/rustler/compiler/config.ex\">>,\n  <<\"lib/rustler/compiler/messages.ex\">>,<<\"lib/rustler/compiler/rustup.ex\">>,\n  <<\"priv\">>,<<\"priv/templates\">>,<<\"priv/templates/basic\">>,\n  <<\"priv/templates/basic/README.md\">>,\n  <<\"priv/templates/basic/Cargo.toml.eex\">>,<<\"priv/templates/basic/src\">>,\n  <<\"priv/templates/basic/src/lib.rs\">>,<<\"priv/templates/root\">>,\n  <<\"priv/templates/root/Cargo.toml.eex\">>,<<\"mix.exs\">>,<<\"README.md\">>]}.\n{<<\"licenses\">>,[<<\"MIT\">>,<<\"Apache-2.0\">>]}.\n{<<\"requirements\">>,\n [[{<<\"name\">>,<<\"jason\">>},\n   {<<\"app\">>,<<\"jason\">>},\n   {<<\"optional\">>,false},\n   {<<\"requirement\">>,<<\"~> 1.0\">>},\n   {<<\"repository\">>,<<\"hexpm\">>}]]}.\n{<<\"build_tools\">>,[<<\"mix\">>]}.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/mix.exs",
    "content": "defmodule Rustler.Mixfile do\n  use Mix.Project\n\n  @source_url \"https://github.com/rusterlium/rustler\"\n  @version \"0.37.3\"\n\n  def project do\n    [\n      app: :rustler,\n      name: \"Rustler\",\n      version: @version,\n      elixir: \"~> 1.15\",\n      build_embedded: Mix.env() == :prod,\n      start_permanent: Mix.env() == :prod,\n      deps: deps(),\n      package: package(),\n      docs: docs()\n    ]\n  end\n\n  def application do\n    [extra_applications: [:logger, :eex]]\n  end\n\n  defp deps do\n    [\n      {:ex_doc, \">= 0.0.0\", only: :dev, runtime: false},\n      {:credo, \"~> 1.0\", only: :dev, runtime: false},\n      {:jason, \"~> 1.0\", runtime: false}\n    ]\n  end\n\n  defp package do\n    [\n      description: \"Mix compiler and runtime helpers for Rustler.\",\n      files: [\"lib\", \"priv\", \"mix.exs\", \"README.md\"],\n      maintainers: [\"hansihe\"],\n      licenses: [\"MIT\", \"Apache-2.0\"],\n      links: %{\n        \"Changelog\" => \"https://hexdocs.pm/rustler/changelog.html\",\n        \"GitHub\" => @source_url\n      }\n    ]\n  end\n\n  defp docs do\n    [\n      extras: [\n        \"../CHANGELOG.md\",\n        \"../UPGRADE.md\",\n        {:\"../LICENSE-APACHE\", [title: \"License (Apache-2.0)\"]},\n        {:\"../LICENSE-MIT\", [title: \"License (MIT)\"]},\n        \"README.md\"\n      ],\n      main: \"readme\",\n      homepage_url: @source_url,\n      source_url: @source_url,\n      source_url_pattern: \"#{@source_url}/blob/rustler-#{@version}/rustler_mix/%{path}#L%{line}\",\n      formatters: [\"html\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/priv/templates/basic/Cargo.toml.eex",
    "content": "[package]\nname = \"<%= library_name %>\"\nversion = \"0.1.0\"\nauthors = []\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nrustler = \"<%= rustler_version %>\"\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/priv/templates/basic/README.md",
    "content": "# NIF for <%= module %>\n\n## To build the NIF module\n\n- Your NIF will now build along with your project.\n\n## To load the NIF\n\n```elixir\ndefmodule <%= module %> do\n  use Rustler, otp_app: :<%= otp_app %>, crate: \"<%= library_name %>\"\n\n  # When your NIF is loaded, it will override this function.\n  def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)\nend\n```\n\n## Examples\n\n[This](https://github.com/rusterlium/NifIo) is a complete example of a NIF written in Rust.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/priv/templates/basic/src/lib.rs",
    "content": "#[rustler::nif]\nfn add(a: i64, b: i64) -> i64 {\n    a + b\n}\n\nrustler::init!(\"<%= native_module %>\");\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler/priv/templates/root/Cargo.toml.eex",
    "content": "[workspace]\nresolver = \"2\"\n\nmembers = [\"native/<%= library_name %>\"]\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.9.0] - 2026-03-26\n\n### Added\n\n- Add support for the `NO_PROXY` environment variable.\n  With this env var, you can filter out hosts to avoid using\n  the configured proxy. Accepts a list of hosts separated\n  by a comma `\",\"`.\n\n- Add support for choosing the IP version to use, therefore supporting IPv6.\n  The environment variable `RUSTLER_PRECOMPILED_IPFAMILY` can be set to:\n  - \"inet\" - for IPv4 only. This is the default option.\n  - \"inet6\" - for IPv6 only.\n  - \"inet6fb4\" - for trying IPv6 first, and then fallback to IPv4.\n\n### Changed\n\n- Drop support for Elixir 1.13 and 1.14. Since those versions are\n  not supported anymore by the core team, they should be avoided.\n\n- Rely on cert stores provided by Erlang/OTP +25.\n\n  This change removes the dependency on `castore` package in favour\n  of loading the cert stores from OTP. In case an older OTP version is\n  in use, a warning is emitted.\n\n  Notice that loading a cert file from the `HEX_CACERTS_PATH` env var\n  is still supported and has precedence over the OTP cert stores.\n\n- Updates to the precompilation guide and the example project, so they\n  are easier to follow and the deps are up-to-date.\n\n## [0.8.4] - 2025-12-08\n\n### Fixed\n\nAdd proxy authentication support for `HTTP_PROXY` and `HTTPS_PROXY`\n\nPreviously, when using proxy environment variables with credentials\n(e.g., <http://user:password@proxy:port>), rustler_precompiled only\nextracted the host and port, ignoring the userinfo field containing\nauthentication credentials. This caused :httpc to fail with 401\nUnauthorized when connecting through authenticated proxies.\n\nThis fix:\n\n- Extracts and parses proxy credentials from the userinfo field\n- Passes credentials to :httpc via the proxy_auth HTTP option\n- Redacts credentials in debug logs to avoid leaking sensitive info\n- Handles edge cases like passwords containing colons and JWT tokens\n\n## [0.8.3] - 2025-07-23\n\n### Added\n\n- Update the list of supported targets from running `rustc --print target-list`\n  with Rust v1.88.0. We kept the legacy targets that would be removed, so we\n  don't have a breaking change. We should remove those in v0.9.\n\n### Fixed\n\n- Fix error handling when trying to create directories for the metadata.\n  It makes working with Nix a little bit easier.\n\n## [0.8.2] - 2024-09-27\n\n### Added\n\n- Add support for overriding the CA file path by setting the `HEX_CACERTS_PATH`\n  env var, which is the same that Mix uses. If unset, it will fallback to\n  `CAStore.file_path/0`.\n\n## [0.8.1] - 2024-09-11\n\n### Fixed\n\n- Fix return of `available_nifs/1`. It was returning only URLs.\n\n- Fix the \"rustler_precompiled.download\" mix task to use the correct\n  function for the download of artifacts.\n\n### Changed\n\n- Print SHA256 of artifacts when downloading them from the mix task.\n\n## [0.8.0] - 2024-08-28\n\n### Added\n\n- Add support for passing headers or a `{module, :function_name}` to `:base_url`.\n  The headers can be passed in the format `{\"URL\", [{\"header_key\", \"header_value\"}]}`.\n  This feature is useful for people that need to perform authentication before downloading\n  the artifacts.\n\n- Add `available_nifs/1` to replace `available_nif_urls/1` that was deprecated.\n  The new function will return a list of tuples in the format: `{\"lib_name\", {\"base_url\", [headers]}}`.\n\n- Add `current_target_nifs/1` to replace `current_target_nif_urls/1` that was deprecated.\n  The new function will return a list of tuples in the format: `{\"lib_name\", {\"base_url\", [headers]}}`.\n\n### Deprecated\n\n- `available_nif_urls/1`\n- `current_target_nif_urls/1`\n\n### Removed\n\n- Remove support for Elixir 1.12.\n\n## [0.7.3] - 2024-08-28\n\n### Fixed\n\n- Add the `alpine` vendor to the list of Linux vendors that we treat as \"unknown\".\n\n## [0.7.2] - 2024-06-26\n\n### Added\n\n- The `RUSTLER_PRECOMPILED_GLOBAL_CACHE_PATH` environment variable was added to enable\n  an easy way to configure a directory to fetch the artifacts before reaching the internet.\n  This is useful to make more predictable where the cache files will be located, thus\n  enabling users to use that directory as a repository for the artifacts.\n  This env var will affect all packages using this library.\n\n- Add the `RUSTLER_PRECOMPILED_FORCE_BUILD_ALL` env var to force the build of all packages\n  using RustlerPrecompiled. Be aware that `Rustler` will be required if this is set\n  to `1` or `true`. There is a new application env that has precedence over the env var,\n  which is the `:force_build_all`.\n\n### Changed\n\n- Automatically run the \"app.config\" mix task in the \"rustler_precompiled.download\" mix task.\n\n  This will trigger the project compilation and configuration, but will not start the app.\n  It's necessary to check if the module passed to the task exists or not.\n\n  You can deactivate this behaviour by passing the flag `--no-config` to that mix task.\n  The mix task \"rustler_precompiled.download\" is used in the step of publishing a package.\n\n- Stop throwing an error in case the metadata cannot be written.\n  This is because metadata is only necessary in the process of publishing the package,\n  and users may be running with \"write\" permission restrictions.\n  We now print a warning instead.\n\n### Fixed\n\n- Restrict the change of vendor to \"unknown\" only if the target is a Linux system.\n\n## [0.7.1] - 2023-11-30\n\n### Fixed\n\n- Fix the URL for variants on download.\n\n## [0.7.0] - 2023-09-22\n\n### Added\n\n- Add `:max_retries` option, to control how many times we should try\n  to download a NIF artifact. By default it is going to try 3 times.\n  To disable this feature, use the value `0`.\n\n- Add support for variants. This is a feature that enables building\n  for the same target with multiple configurations. It can support\n  different features or OS dependencies. The selection is done in\n  compile time.\n\n### Changed\n\n- Change default list of NIF versions to only include the version `2.15`.\n  This is because most of the users won't need to activate newer versions,\n  unless they use features from those versions.\n\n  This is going to simplify and speed up the release process for most of the\n  projects.\n\n## [0.6.3] - 2023-08-28\n\n### Fixed\n\n- Make sure `:nif_versions` option is respected.\n\n  This is a small bug fix that was blocking the usage of a more\n  restrict list of \"NIF versions\". For example, if my system is\n  using NIF version 2.16, but I want to be compatible with only\n  version 2.15, this was being ignored, since the algorithm for\n  finding compatible versions was not taking into account this\n  restriction.\n\n## [0.6.2] - 2023-07-05\n\n### Added\n\n- Add support for FreeBSD as a target.\n\n### Changed\n\n- Remove `:crypto` from the extra applications list, because `:ssl` already includes it.\n\n- Update the guide to mention the new way to select a NIF version in Rustler >= 0.29.\n\n## [0.6.1] - 2023-02-16\n\n### Changed\n\n- Depend on `:ssl` instead of `:public_key` application. Since `:public_key` is started\n  with `:ssl`, this shouldn't break. This change is needed in order to support the upcoming\n  Elixir 1.15.\n\n## [0.6.0] - 2023-01-27\n\n### Added\n\n- Add support for configuring the NIF versions that the project supports.\n  This can be done with the `:nif_versions` config.\n\n- Add support for `castore` version `~> 1.0`.\n\n### Changed\n\n- Add `aarch64-unknown-linux-musl` and `riscv64gc-unknown-linux-gnu` as default targets.\n  The first one is common for people running Linux containers that were built with Musl on Apple computers.\n  The second one is becoming popular for tiny computers, normally running Nerves.\n\n  The adoption of these targets can increase a little bit the compilation time, but\n  can affect a great number of users.\n\n  For package maintainers: please remember to add these targets to your CI workflow.\n  See an example workflow at: <https://github.com/philss/rustler_precompilation_example/blob/main/.github/workflows/release.yml>\n\n- Change the depth of SSL peer verification to \"3\". This should be more compatible with servers.\n\n- Remove version \"2.14\" from the default NIF versions. Like the change of default targets,\n  this should only have effect in the moment of release of a new package version.\n  Remember to update your workflow file.\n\n## [0.5.5] - 2022-12-10\n\n### Fixed\n\n- Add support for Suse Linux targets. This is a fix to the plataform resolution. Thanks [@fabriziosestito](https://github.com/fabriziosestito).\n- Fix validation of HTTP proxy. This makes the validation similar to the HTTPS proxy. Thanks [@w0rd-driven](https://github.com/w0rd-driven).\n- Map `riscv64` to `riscv64gc` to match Rust naming. Thanks [@fhunleth](https://github.com/fhunleth).\n\n## [0.5.4] - 2022-11-05\n\n### Fixed\n\n- Fix building metadata when \"force build\" is enabled and the target is not available.\n\n## [0.5.3] - 2022-10-19\n\n### Fixed\n\n- Always write the metadata file in compilation time, so mix tasks can work smoothly.\n\n## [0.5.2] - 2022-10-03\n\n### Fixed\n\n- Fix the `target/0` function to use default targets a default argument. This makes the example\n  in the docs work again. Thanks [@jackalcooper](https://github.com/jackalcooper).\n- Only use proxy if it is valid. Thanks [@josevalim](https://github.com/josevalim).\n- Fix the support for PCs running RedHat Linux. Thanks [@Benjamin-Philip](https://github.com/Benjamin-Philip).\n- Improve some points in the docs. Thanks [@whatyouhide](https://github.com/whatyouhide) and [@fabriziosestito](https://github.com/fabriziosestito).\n\n## [0.5.1] - 2022-05-24\n\n### Fixed\n\n- Fix available targets naming to include the NIF version in the name. It was removed accidentally.\n  Thanks [@adriankumpf](https://github.com/adriankumpf).\n\n## [0.5.0] - 2022-05-24\n\n### Added\n\n- Now it's possible to configure the targets list, based in the [Rust's Plataform Support](https://doc.rust-lang.org/stable/rustc/platform-support.html)\n  list. You can run `rustc --print target-list` to get the full list.\n  Thanks [@adriankumpf](https://github.com/adriankumpf).\n\n### Changed\n\n- The precompilation guide was improved with instructions and suggestions for the `files` key at\n  the project config.\n  Thanks [@nbw](https://github.com/nbw).\n- Now we raise with a different error if the NIF artifact cannot be written when downloading to create\n  the checksum file.\n\n## [0.4.1] - 2022-04-28\n\n### Fixed\n\n- Fix `__using__` macro for when Rustler is not loaded.\n\n## [0.4.0] - 2022-04-28\n\n### Changed\n\n- Make Rustler an optional dependency. This makes installation faster for most of the users.\n\n## [0.3.0] - 2022-03-26\n\n### Added\n\n- Add the possibility to skip the download of unavailable NIFs when generating the\n  checksum file - thanks [@fahchen](https://github.com/fahchen)\n\n## [0.2.0] - 2022-02-18\n\n### Fixed\n\n- Fix validation of URL in order to be compatible with Elixir ~> 1.11.\n  The previous implementation was restricted to Elixir ~> 1.13.\n\n### Added\n\n- Add `:force_build` option that fallback to `Rustler`. It passes all options\n  except the ones used by `RustlerPrecompiled` down to `Rustler`.\n  This option will be by default `false`, but if the project is using a pre-release,\n  then it will always be set to `true`.\n  With this change the project starts depending on Rustler.\n\n### Changed\n\n- Relax dependencies to the minor versions.\n\n## [0.1.0] - 2022-02-16\n\n### Added\n\n- Add basic features to download and use the precompiled NIFs in a safe way.\n\n[Unreleased]: https://github.com/philss/rustler_precompiled/compare/v0.9.0...HEAD\n[0.9.0]: https://github.com/philss/rustler_precompiled/compare/v0.8.4...v0.9.0\n[0.8.4]: https://github.com/philss/rustler_precompiled/compare/v0.8.3...v0.8.4\n[0.8.3]: https://github.com/philss/rustler_precompiled/compare/v0.8.2...v0.8.3\n[0.8.2]: https://github.com/philss/rustler_precompiled/compare/v0.8.1...v0.8.2\n[0.8.1]: https://github.com/philss/rustler_precompiled/compare/v0.8.0...v0.8.1\n[0.8.0]: https://github.com/philss/rustler_precompiled/compare/v0.7.3...v0.8.0\n[0.7.3]: https://github.com/philss/rustler_precompiled/compare/v0.7.2...v0.7.3\n[0.7.2]: https://github.com/philss/rustler_precompiled/compare/v0.7.1...v0.7.2\n[0.7.1]: https://github.com/philss/rustler_precompiled/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/philss/rustler_precompiled/compare/v0.6.3...v0.7.0\n[0.6.3]: https://github.com/philss/rustler_precompiled/compare/v0.6.2...v0.6.3\n[0.6.2]: https://github.com/philss/rustler_precompiled/compare/v0.6.1...v0.6.2\n[0.6.1]: https://github.com/philss/rustler_precompiled/compare/v0.6.0...v0.6.1\n[0.6.0]: https://github.com/philss/rustler_precompiled/compare/v0.5.5...v0.6.0\n[0.5.5]: https://github.com/philss/rustler_precompiled/compare/v0.5.4...v0.5.5\n[0.5.4]: https://github.com/philss/rustler_precompiled/compare/v0.5.3...v0.5.4\n[0.5.3]: https://github.com/philss/rustler_precompiled/compare/v0.5.2...v0.5.3\n[0.5.2]: https://github.com/philss/rustler_precompiled/compare/v0.5.1...v0.5.2\n[0.5.1]: https://github.com/philss/rustler_precompiled/compare/v0.5.0...v0.5.1\n[0.5.0]: https://github.com/philss/rustler_precompiled/compare/v0.4.1...v0.5.0\n[0.4.1]: https://github.com/philss/rustler_precompiled/compare/v0.4.0...v0.4.1\n[0.4.0]: https://github.com/philss/rustler_precompiled/compare/v0.3.0...v0.4.0\n[0.3.0]: https://github.com/philss/rustler_precompiled/compare/v0.2.0...v0.3.0\n[0.2.0]: https://github.com/philss/rustler_precompiled/compare/v0.1.0...v0.2.0\n[0.1.0]: https://github.com/philss/rustler_precompiled/releases/tag/v0.1.0\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/PRECOMPILATION_GUIDE.md",
    "content": "# Precompilation guide\n\nRustler provides an easy way write safer NIFs for our OTP applications.\nRustler Precompiled makes the usage of NIFs created with Rustler easier,\nso people don't need to have the Rust toolchain installed in order to use their projects.\n\nWhen users install your package that is using this library, they will see Rustler Precompiled\ndownloading the precompiled artifact alongside with its SHA256 representing the fingerprint\nof that file.\n\nThe precompilation happens in a CI server, always in a transparent way, and\nthe Hex package published should always include a checksum file to ensure\nthe NIFs stays the same, therefore mitigating supply chain attacks.\n\nIn this guide I will show you how to prepare your project to use Rustler Precompiled.\n\n## Prepare for the build\n\nMost of the work is done in the CI server. In this example we are going to use GitHub Actions.\n\nThe GH Actions service has the benefit of hosting artifacts for releases and make them\npublic available. This is important, because it's where the library is going to download\nthe artifacts from.\n\n### Configure Github Actions\n\nIn order for the workflow to succeed, some \"write permissions\" will need to be enabled for the\nrepository.\nThe best way to do that is by using the `permissions:` key in your workflow file. This is effectively\nchanging the permissions of the `GITHUB_TOKEN` used in the workflow (or for an individual job).\n\nIn your job section, add the following:\n\n```yaml\npermissions:\n  # For creating a new release.\n  contents: write\n  # The following are needed for the \"actions/attest\" GH Action.\n  id-token: write\n  attestations: write\n  artifact-metadata: write\n```\n\nThe permissions related to the \"actions/attest\" GitHub Action are optional if you don't plan to use attestations,\nbut they enhance the security a little bit.\nRead the [Artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations)\ndocs for more details.\n\n### Configure Targets\n\nUsually we want to build for the most popular targets and the minimum NIF version supported.\n\nNIF versions are more stable than OTP versions because they usually change only after two major\nreleases of Erlang/OTP. But older versions are compatible with newer versions if they have the same MAJOR\nnumber. For example, the NIF `2.15` is compatible with `2.16` and `2.17`. So you only need to\ncompile for `2.15` if you want to support these versions.\n\nIn case a new feature from the newer versions is needed, then you can build for both versions as well.\nSee [the trobleshooting](TROUBLESHOOTING.md) document to find how to do that.\n\nFor this guide our targets will be the following:\n\n- OS: Linux, Windows, macOS\n- Architectures: `x86_64`, `aarch64` (ARM 64 bits)\n- NIF version: `2.15` - this is the default for Rustler since `v0.29`.\n\nIn summary the build matrix looks like this:\n\n```yaml\nmatrix:\n  nif: [\"2.15\"]\n  job:\n    - { target: aarch64-unknown-linux-gnu   , os: ubuntu-22.04 , use-cross: true }\n    - { target: aarch64-apple-darwin        , os: macos-15      }\n    - { target: x86_64-apple-darwin         , os: macos-15-intel }\n    - { target: x86_64-unknown-linux-gnu    , os: ubuntu-22.04  }\n    - { target: x86_64-unknown-linux-musl   , os: ubuntu-22.04 , use-cross: true }\n    - { target: x86_64-pc-windows-gnu       , os: windows-2022  }\n    - { target: x86_64-pc-windows-msvc      , os: windows-2022  }\n```\n\nA complete workflow example can be found in the [`rustler_precompilation_example`](https://github.com/philss/rustler_precompilation_example/blob/main/.github/workflows/release.yml) project.\nYou can copy that file and modify it with your desired inputs.\n\nThat workflow is using a GitHub Action especially made for our goal: [philss/rustler-precompiled-action](https://github.com/philss/rustler-precompiled-action).\nThe GitHub Action will deal with the installation of `cross` and the build of the project, naming the files in the correct format.\n\nSome targets are only supported by later versions of `cross`. For those, you might want to\ninstall `cross` directly from GitHub. You can see an example in [this\npipeline](https://github.com/kloeckner-i/mail_parser/blob/f4af5083aec73a47f0e41a202ba46a91f60602cf/.github/workflows/release.yml#L101-L105).\n\n## Additional configuration before build\n\nIn our build we are going to cross compile our crate project (the Rust code for our NIF) using\na variety of targets, as we saw in the previous section. For this to work we need to guide the Rust\ncompiler in some cases by providing additional configuration in the `.cargo/config.toml` file of our project.\n\nHere is an example of that file:\n\n```toml\n# This is needed for \"musl\". See https://github.com/rust-lang/rust/issues/59302\n[target.x86_64-unknown-linux-musl]\nrustflags = [\n  \"-C\", \"target-feature=-crt-static\"\n]\n\n# Provides a small build size for the \"release\" profile, but takes more time to build.\n[profile.release]\nlto = true\n```\n\nFor more common configuration needed for other targets, see the [troubleshooting document](TROUBLESHOOTING.md).\n\nIn addition to that, we also need a tool called [`cross`](https://github.com/rust-embedded/cross) that\nmakes the build easier for some targets (the ones using `use-cross: true` in our example).\nThis tool will be installed automatically by the `rustler-precompiled-action`.\n\n## The Rustler module\n\nWe need to tell `RustlerPrecompiled` where to find our NIF files, and we need to tell which version to use.\n\n```elixir\ndefmodule RustlerPrecompilationExample.Native do\n  version = Mix.Project.config()[:version]\n\n  use RustlerPrecompiled,\n    otp_app: :rustler_precompilation_example,\n    crate: \"example\",\n    base_url:\n      \"https://github.com/philss/rustler_precompilation_example/releases/download/v#{version}\",\n    force_build: System.get_env(\"RUSTLER_PRECOMPILATION_EXAMPLE_BUILD\") in [\"1\", \"true\"],\n    version: version\n\n  # When your NIF is loaded, it will override this function.\n  def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)\nend\n```\n\nThis example was also extracted from the [`rustler_precompilation_example`](https://github.com/philss/rustler_precompilation_example/blob/main/lib/rustler_precompilation_example/native.ex) project.\nRustlerPrecompiled will try to figure out the target and download the correct file for us. This will happen in compile\ntime only.\n\nOptionally it's possible to force the compilation by setting an env var, like the example suggests.\n\nIt's also possible to force the build by using a pre release version, like `0.1.0-dev`.\nThe only requirement to force the build is to have Rustler declared as a dependency as well:\n\n`{:rustler, \">= 0.0.0\", optional: true}`\n\n## The release flow\n\n### Generating a checksum file\n\nWhen you need to release a Hex package using precompiled NIFs, you first need to\nbuild the release in the CI, wait for all artifacts to be available and then generate\nthe **checksum file** that is **MANDATORY** for your package to work.\n\nThis checksum file is generated by running the following command after the build is complete:\n\n    $ mix rustler_precompiled.download YourRustlerModule --all --print\n\nWith the module I used for this guide, the command would be:\n\n    $ mix rustler_precompiled.download RustlerPrecompilationExample.Native --all --print\n\nThe file generated will be named `checksum-Elixir.RustlerPrecompilationExample.Native.exs` and\nit's **extremely important that you include this file in your Hex package** (by updating the `files:`\nfield in your `mix.exs`). Otherwise your package **won't work**. Your `files:` key at your\npackage configuration will look like this:\n\n```elixir\ndefp package do\n  [\n    files: [\n      \"lib\",\n      \"native/my_nif/.cargo\",\n      \"native/my_nif/src\",\n      \"native/my_nif/Cargo*\",\n      \"checksum-*.exs\",\n      \"mix.exs\"\n    ],\n    # ...\n  ]\nend\n```\n\nNote: you don't need to track the checksum file in your version control system (git or other).\nAnother thing is that you want to make sure that the \"target\" directory is not released with\nthe Hex package. To ensure that, you can remove the directory before releasing: `rm -rf native/my_nif/target`.\n\nFor an example, refer to the `mix.exs` file of the [rustler precompilation example](https://github.com/philss/rustler_precompilation_example/blob/main/mix.exs)\nor elixir-nx's [explorer](https://github.com/elixir-nx/explorer/blob/723eea63204e43bc9238d2488fd355f17a1e13f2/mix.exs#L65-L72) library.\n\nTip: use the `mix hex.build --unpack` command to confirm which files are being included (and if the package looks good before publishing).\n\n### Recommended flow\n\nTo recap, the suggested flow is the following:\n\n1. release a new tag\n2. push the code to your repository with the new tag: `git push origin main --tags`\n3. wait for all NIFs to be built\n4. run the `mix rustler_precompiled.download` task (with the flag `--all`)\n5. release the package to Hex.pm (make sure your release includes the correct files).\n\n## Conclusion\n\nThe ability to use precompiled NIFs written in Rust can increase the adoption of some packages,\nbecause people won't need to have Rust installed. But this comes with some drawbacks and more\nresponsibilities to the maintainers, so use this feature carefully.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/README.md",
    "content": "# Rustler Precompiled\n\n[![CI](https://github.com/philss/rustler_precompiled/actions/workflows/ci.yml/badge.svg)](https://github.com/philss/rustler_precompiled/actions/workflows/ci.yml)\n![Hex.pm](https://img.shields.io/hexpm/v/rustler_precompiled)\n\nThis project aims to make the usage of precompiled NIFs easier\nfor Elixir projects using [Rustler](https://github.com/rusterlium/rustler).\n\nRead the [Precompilation guide](https://hexdocs.pm/rustler_precompiled/precompilation_guide.html) and\ncheck the [documentation](https://hexdocs.pm/rustler_precompiled) for further details.\n\n## Installation\n\nThe package can be installed by adding `rustler_precompiled` to your\nlist of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:rustler_precompiled, \"~> 0.9\"}\n  ]\nend\n```\n\nYou would normally keep Rustler listed, but as an optional dependency because it's only used in development.\n\n## License\n\nCopyright 2022 Philip Sampaio\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/TROUBLESHOOTING.md",
    "content": "# Troubleshooting\n\nSome problems are related to specific targets that need additional configuration or dependencies.\nIn this document you can find the way to fix those problems.\n\n## Supporting \"musl\" ABI targets\n\nThis type of targets are common for people running on systems like the Alpine Linux.\nThe main difference is that you need to disable \"static linking\". This can be done\nby changing the configuration of your target (or targets) in the `.cargo/cargo.toml` file.\n\nFor example, if the idea is to support `x86_64-unknown-linux-musl`, you need to add\nthe following:\n\n```toml\n# This is needed for \"musl\". See https://github.com/rust-lang/rust/issues/59302\n[target.x86_64-unknown-linux-musl]\nrustflags = [\n  \"-C\", \"target-feature=-crt-static\"\n]\n```\n\nIt's common to run containers of Linux in machines that are using the Arch64 architecture\n(like the M family from Apple). So you will probably want to repeat the same configuration\nfor the `aarch64-unknown-linux-musl` target.\n\n## Using different NIF versions\n\nBy default Rustler builds for the NIF version 2.15, which aims Erlang/OTP 22 and above.\nIf your project needs something that was released in more recent NIF versions, you can configure\nthe enabled cargo features for Rustler by adding features to your project - that enable the desired\nNIF versions.\n\nYour \"Cargo.toml\" would look like this:\n\n```toml\n[dependencies]\nrustler = { version = \"0.37\", default-features = false }\n\n# And then, your features.\n[features]\ndefault = [\"nif_version_2_15\"]\nnif_version_2_15 = [\"rustler/nif_version_2_15\"]\nnif_version_2_16 = [\"rustler/nif_version_2_16\"]\nnif_version_2_17 = [\"rustler/nif_version_2_17\"]\n```\n\nIn your code, you would use these features - like `nif_version_2_16` - to control how your\ncode is going to be compiled. You can hide some features behind these \"cargo features\".\n\nIf the ideia is to depend on one version, let's say \"2.16\", you can declare your rustler dependency like this:\n\n```toml\n[dependencies]\nrustler = { version = \"0.37\", default-features = false, features = [\"nif_version_2_16\"] }\n```\n\nThe available NIF versions are the following:\n\n* `2.14` - for OTP 21 and above.\n* `2.15` - for OTP 22 and above.\n* `2.16` - for OTP 24 and above.\n* `2.17` - for OTP 26 and above.\n\nAnd the default NIF version activated by Rustler versions is the following:\n\n* Rustler `~> v0.29` - NIF `2.15`\n\nThe [`rustler-precompiled-action`](https://github.com/philss/rustler-precompiled-action) would\nonly require that you specify the NIF version, so it would active the respective feature.\n\nNote that it's important to follow the format of `nif_version_MAJOR_MINOR` in order to make that\nGitHub Action work automatically.\nYou can also specify the `cargo-args` in that GitHub Action usage, as described in the docs.\n\n## Link to libatomic for 32-bit builds\n\nIf you are targeting 32 bits platforms like ARM, you may need to add a configuration to \"link libatomic\".\nMore details can be found in this issue: [#53](https://github.com/philss/rustler_precompiled/issues/53).\n\nModify your `.cargo/config.yml` file for the targets affected with something like the following:\n\n```toml\n# Libatomic is needed for 32 bits ARM.\n# See: https://github.com/philss/rustler_precompiled/issues/53\n[target.arm-unknown-linux-gnueabihf]\nrustflags = [\n  \"-l\", \"dylib=atomic\"\n]\n```\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/hex_metadata.config",
    "content": "{<<\"links\">>,\n [{<<\"GitHub\">>,<<\"https://github.com/philss/rustler_precompiled\">>}]}.\n{<<\"name\">>,<<\"rustler_precompiled\">>}.\n{<<\"version\">>,<<\"0.9.0\">>}.\n{<<\"description\">>,\n <<\"Make the usage of precompiled NIFs easier for projects using Rustler\">>}.\n{<<\"elixir\">>,<<\"~> 1.15\">>}.\n{<<\"app\">>,<<\"rustler_precompiled\">>}.\n{<<\"licenses\">>,[<<\"Apache-2.0\">>]}.\n{<<\"files\">>,\n [<<\"lib\">>,<<\"lib/mix\">>,<<\"lib/mix/tasks\">>,\n  <<\"lib/mix/tasks/rustler_precompiled.download.ex\">>,\n  <<\"lib/rustler_precompiled\">>,<<\"lib/rustler_precompiled/config\">>,\n  <<\"lib/rustler_precompiled/config/available_targets.ex\">>,\n  <<\"lib/rustler_precompiled/config.ex\">>,<<\"lib/rustler_precompiled.ex\">>,\n  <<\"mix.exs\">>,<<\"README.md\">>,<<\"CHANGELOG.md\">>,\n  <<\"PRECOMPILATION_GUIDE.md\">>,<<\"TROUBLESHOOTING.md\">>]}.\n{<<\"requirements\">>,\n [[{<<\"name\">>,<<\"rustler\">>},\n   {<<\"app\">>,<<\"rustler\">>},\n   {<<\"optional\">>,true},\n   {<<\"requirement\">>,<<\"~> 0.23\">>},\n   {<<\"repository\">>,<<\"hexpm\">>}]]}.\n{<<\"build_tools\">>,[<<\"mix\">>]}.\n"
  },
  {
    "path": "test_apps/elixir/deps/rustler_precompiled/mix.exs",
    "content": "defmodule RustlerPrecompiled.MixProject do\n  use Mix.Project\n\n  @version \"0.9.0\"\n  @repo \"https://github.com/philss/rustler_precompiled\"\n\n  def project do\n    [\n      app: :rustler_precompiled,\n      version: @version,\n      elixir: \"~> 1.15\",\n      start_permanent: Mix.env() == :prod,\n      description: \"Make the usage of precompiled NIFs easier for projects using Rustler\",\n      package: package(),\n      test_ignore_filters: [&String.starts_with?(&1, \"test/fixtures\")],\n      docs: docs(),\n      deps: deps()\n    ]\n  end\n\n  def application do\n    [\n      extra_applications: [:logger, :inets, :ssl]\n    ]\n  end\n\n  defp docs do\n    [\n      main: \"RustlerPrecompiled\",\n      extras: [\"PRECOMPILATION_GUIDE.md\", \"CHANGELOG.md\", \"TROUBLESHOOTING.md\"],\n      source_url: @repo,\n      source_ref: \"v#{@version}\"\n    ]\n  end\n\n  defp deps do\n    [\n      {:rustler, \"~> 0.23\", optional: true},\n      {:ex_doc, \"~> 0.27\", only: :dev},\n      {:bypass, \"~> 2.1\", only: :test}\n    ]\n  end\n\n  defp package do\n    %{\n      licenses: [\"Apache-2.0\"],\n      maintainers: [\"Philip Sampaio\"],\n      files: ~w(lib mix.exs README.md CHANGELOG.md PRECOMPILATION_GUIDE.md TROUBLESHOOTING.md),\n      links: %{\"GitHub\" => @repo}\n    }\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/deps/toml/LICENSE",
    "content": "                                  Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "test_apps/elixir/deps/toml/README.md",
    "content": "# TOML for Elixir\n\n[![Main](https://github.com/bitwalker/toml-elixir/workflows/elixir/badge.svg?branch=main)](https://github.com/bitwalker/toml-elixir/actions?query=workflow%3A%22elixir%22+branch%3Amain)\n[![Hex.pm Version](https://img.shields.io/hexpm/v/toml.svg?style=flat)](https://hex.pm/packages/toml)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg?style=flat)](https://hexdocs.pm/toml)\n[![Total Download](https://img.shields.io/hexpm/dt/toml.svg?style=flat)](https://hex.pm/packages/toml)\n[![Last Updated](https://img.shields.io/github/last-commit/bitwalker/toml-elixir.svg?style=flat)](https://github.com/bitwalker/toml-elixir/commits/main)\n\nThis is a TOML library for Elixir projects.\n\nIt is compliant with version 1.0 of the [official TOML specification](https://github.com/toml-lang/toml). You can find a\nbrief overview of the feature set below, but you are encouraged to read the full spec at the link above (it is short and easy to read!).\n\n## Features\n\n- Decode from string, file, or stream\n- Fully compliant with the latest version of the TOML spec\n- Is tested against [toml-test](https://github.com/BurntSushi/toml-test), a test\n  suite for spec-compliant TOML encoders/decoders, used by implementations in\n  multiple languages. The test suite has been integrated into this project to be\n  run under Mix so that we get better error information and so it can run as\n  part of the test suite.\n- Decoder produces a map with values using the appropriate Elixir data types for\n  representation\n- Supports extension via value transformers (see `Toml.Transform` docs for details)\n- Supports use as a configuration provider in Distillery 2.x+ (use TOML\n  files for configuration!)\n- Decoder is written by hand to take advantage of various optimizations.\n- Library passes Dialyzer checks\n\n## Comparison To Other Libraries\n\nI compared `toml` to four other libraries:\n\n- `toml_elixir`\n- `tomlex`\n- `jerry`\n- `etoml`\n\nOf these four, none correctly implement the 0.5.0 specification. Either they are\ntargeting older versions of the spec (in `etoml`, it is built against pre-0.1),\nare not fully implemented (i.e. don't support all features), or have bugs which\nprevent them from properly parsing a 0.5.0 example file (the\n`test/fixtures/example.toml` file in this repository).\n\nIf you are looking for a TOML library, at present `toml` is the only one which\nfull implements the spec and correctly decodes `example.toml`.\n\n## Installation\n\nThis library is available on Hex as `:toml`, and can be added to your deps like so:\n\n```elixir\ndef deps do\n  [\n    {:toml, \"~> 0.7\"}\n  ]\nend\n```\n\nNOTE: You can determine the latest version on Hex by running `mix hex.info toml`.\n\n## Type Conversions\n\nIn case you are curious how TOML types are translated to Elixir types, the\nfollowing table provides the conversions.\n\n**NOTE:** The various possible representations of each type, such as\nhex/octal/binary integers, quoted/literal strings, etc., are considered to be\nthe same base type (e.g. integer and string respectively in the examples given).\n\n| TOML | Elixir |\n|-------|-------|\n| String | String.t (binary) |\n| Integer | integer |\n| inf | :infinity |\n| +inf | :infinity |\n| -inf | :negative_infinity |\n| nan | :nan |\n| +nan | :nan |\n| -nan | :negative_nan |\n| Boolean | boolean |\n| Offset Date-Time | DateTime.t |\n| Local Date-Time | NaiveDateTime.t |\n| Local Date | Date.t |\n| Local Time | Time.t |\n| Array | list |\n| Table | map |\n| Table Array | list(map) |\n\n## Implementation-specific Behaviors\n\nCertain features of TOML have implementation-specific behavior:\n\n- `-inf`, `inf`, and `+inf` are all valid infinity values in TOML.\n  In Erlang/Elixir, these don't have exact representations. Instead, by convention,\n  `:infinity` is used for positive infinity, as atoms are always larger than integers\n  when using comparison operators, so `:infinity > <any integer>` will always be true.\n  However, negative infinity cannot be represented, as numbers are always considered smaller\n  than every other type in term comparisons. Instead, we represent it with `:negative_infinity`,\n  so that the type information is not lost, but you must be careful to deal with it specifically\n  in comparisons/sorting/etc.\n- `-nan`, `nan`, and `+nan` are all valid NaN (not a number) values in TOML. In Erlang/Elixir,\n  NaN is traditionally represented with `:nan`, but there is no representation for negative NaN,\n  and no API actually produces `:nan`, instead invalid numbers typically raise errors, in the typical\n  spirit of \"let it crash\" in the face of errors. For purposes of preserving type information though,\n  we use the `:nan` convention, and `:negative_nan` for -NaN. You will need to take care to deal with\n  these values manually if the values need to be preserved.\n- The maximum precision of times in the various time types is microseconds (i.e. precision to six decimal places),\n  if you provide higher precision values (i.e. nanoseconds), the extra precision will be lost.\n- Hex, octal, and binary numbers are converted to integers, so serializing those values after decoding\n  them from a TOML document will be in their decimal representation.\n\n## Example Usage\n\nThe following is a brief overview of how to use this library. First, let's take\na look at an example TOML file, as borrowed from the [TOML\nhomepage](https://github.com/toml-lang/toml):\n\n``` toml\n# This is a TOML document.\n\ntitle = \"TOML Example\"\n\n[owner]\nname = \"Tom Preston-Werner\"\ndob = 1979-05-27T07:32:00-08:00 # First class dates\n\n[database]\nserver = \"192.168.1.1\"\nports = [ 8001, 8001, 8002 ]\nconnection_max = 5000\nenabled = true\n\n[servers]\n\n  # Indentation (tabs and/or spaces) is allowed but not required\n  [servers.alpha]\n  ip = \"10.0.0.1\"\n  dc = \"eqdc10\"\n\n  [servers.beta]\n  ip = \"10.0.0.2\"\n  dc = \"eqdc10\"\n\n[clients]\ndata = [ [\"gamma\", \"delta\"], [1, 2] ]\n\n# Line breaks are OK when inside arrays\nhosts = [\n  \"alpha\",\n  \"omega\"\n]\n```\n\n### Parsing\n\n```elixir\niex> input = \"\"\"\n[database]\nserver = \"192.168.1.1\"\n\"\"\"\n...> {:ok, %{\"database\" => %{\"server\" => \"192.168.1.1\"}}} = Toml.decode(input)\n...> {:ok, %{database: %{server: \"192.168.1.1\"}}} = Toml.decode(input, keys: :atoms)\n...> stream = File.stream!(\"example.toml\")\n...> {:ok, %{\"database\" => %{\"server\" => \"192.168.1.1\"}}} = Toml.decode_stream(stream)\n...> {:ok, %{\"database\" => %{\"server\" => \"192.168.1.1\"}}} = Toml.decode_file(\"example.toml\")\n...> invalid = \"\"\"\n[invalid]\na = 1 b = 2\n\"\"\"\n...> {:error, {:invalid_toml, reason}} = Toml.decode(invalid); IO.puts(reason)\nexpected '\\n', but got 'b' in nofile on line 2:\n\n    a = 1 b = 2\n         ^\n\n:ok\n```\n\n### Transforms\n\nSupport for extending value conversions is provided by the `Toml.Transform`\nbehavior. An example is shown below:\n\nGiven the following TOML document:\n\n``` toml\n[servers.alpha]\nip = \"192.168.1.1\"\nports = [8080, 8081]\n\n[servers.beta]\nip = \"192.168.1.2\"\nports = [8082, 8083]\n```\n\nAnd the following modules:\n\n``` elixir\ndefmodule Server do\n  defstruct [:name, :ip, :ports]\nend\n\ndefmodule IPStringToCharlist do\n  use Toml.Transform\n\n  def transform(:ip, v) when is_binary(v) do\n    String.to_charlist(v)\n  end\n  def transform(_k, v), do: v\nend\n\ndefmodule CharlistToIP do\n  use Toml.Transform\n\n  def transform(:ip, v) when is_list(v) do\n    case :inet.parse_ipv4_address(v) do\n      {:ok, address} ->\n        address\n      {:error, reason} ->\n        {:error, {:invalid_ip_address, reason}}\n    end\n  end\n  def transform(:ip, v), do: {:error, {:invalid_ip_address, v}}\n  def transform(_k, v), do: v\nend\n\ndefmodule ServerMapToList do\n  use Toml.Transform\n\n  def transform(:servers, v) when is_map(v) do\n    for {name, server} <- v, do: struct(Server, Map.put(server, :name, name))\n  end\n  def transform(_k, v), do: v\nend\n```\n\nYou can convert the TOML document to a more strongly-typed version using the\nabove transforms like so:\n\n```elixir\niex> transforms = [IPStringToCharlist, CharlistToIP, ServerMapToList]\n...> {:ok, result} = Toml.decode(\"example.toml\", keys: :atoms, transforms: transforms)\n%{servers: [%Server{name: :alpha, ip: {192,168,1,1}}, ports: [8080, 8081] | _]}\n```\n\nThe transforms given here are intended to show how they can be composed: they\nare applied in the order provided, and the document is transformed using a\ndepth-first, bottom-up traversal. Put another way, you transform the leaves of\nthe tree before the branches; as shown in the example above, this means the\n`:ip` key is converted to an address tuple before the `:servers` key is\ntransformed into a list of `Server` structs.\n\n## Using with Elixir Releases (1.9+)\n\nTo use this library as a configuration provider in Elixir, use the following\nexample of how one might use it in their release configuration, and tailor it\nto your needs:\n\n```elixir\nconfig_providers: [\n  {Toml.Provider, [\n    path: {:system, \"XDG_CONFIG_DIR\", \"myapp.toml\"},\n    transforms: [...]\n  ]}\n]\n```\n\nSee the \"Using as a Config Provider\" section for more info.\n\n## Using with Distillery\n\nLike the above, use the following example as a guideline for how you use this\nin your own release configuration (i.e. in `rel/config.exs`):\n\n``` elixir\nrelease :myapp do\n  # ...snip...\n  set config_providers: [\n    {Toml.Provider, [path: \"${XDG_CONFIG_DIR}/myapp.toml\", transforms: [...]]}\n  ]\nend\n```\n\n## Using as a Config Provider\n\nThe usages described above will result in `Toml.Provider` being invoked during boot, at which point it\nwill evaluate the given path and read the TOML file it finds. If one is not\nfound, or is not accessible, the provider will raise an error, and the boot\nsequence will terminate unsuccessfully. If it succeeds, it persists settings in\nthe file to the application environment (i.e. you access it via\n`Application.get_env/2`).\n\nYou can pass the same options in the arguments list for `Toml.Provider` as you\ncan to `Toml.decode/2`, but `:path` is required, and `:keys` only supports\n`:atoms` and `:atoms!` values.\n\nThe config provider expects a certain format to the TOML file, namely that keys\nat the root of the document correspond to applications which need to be configured.\nIf it encounters keys at the root of the document which are not tables, they are ignored.\n\n``` toml\n# This is an example of something that would be ignored\ntitle = \"My config file\"\n\n# We're expecting something like this:\n[myapp]\nkey = \"value\"\n\n# To use a bit of Phoenix config, you translate to TOML like so:\n[myapp.\"MyApp.Endpoint\"]\ncache_static_manifest = \"priv/static/cache_manifest.json\"\n\n[myapp.\"MyApp.Endpoint\".http]\nport = \"4000\"\n\n[myapp.\"MyApp.Endpoint\".force_ssl]\nhsts = true\n\n# Or logger..\n[logger]\nlevel = \"info\"\n\n[logger.console]\nformat = \"[$level] $message \\n\"\n```\n\n## Roadmap\n\n- [x] Add benchmarking suite\n- [x] Provide options for converting keys to atom, similar to Jason/Poison/etc.\n- [ ] Optimize lexer to always send offsets to decoder, rather than only in some cases\n- [ ] Try to find pathological TOML files to test\n\n## License\n\nThis project is licensed Apache 2.0, see the `LICENSE` file in this repo for details.\n"
  },
  {
    "path": "test_apps/elixir/deps/toml/hex_metadata.config",
    "content": "{<<\"app\">>,<<\"toml\">>}.\n{<<\"build_tools\">>,[<<\"mix\">>]}.\n{<<\"description\">>,<<\"An implementation of TOML for Elixir projects\">>}.\n{<<\"elixir\">>,<<\"~> 1.9\">>}.\n{<<\"files\">>,\n [<<\"lib\">>,<<\"lib/lexer\">>,<<\"lib/lexer/guards.ex\">>,\n  <<\"lib/lexer/string.ex\">>,<<\"lib/decoder.ex\">>,<<\"lib/error.ex\">>,\n  <<\"lib/provider.ex\">>,<<\"lib/toml.ex\">>,<<\"lib/builder.ex\">>,\n  <<\"lib/transform.ex\">>,<<\"lib/document.ex\">>,<<\"lib/lexer.ex\">>,\n  <<\"mix.exs\">>,<<\"README.md\">>,<<\"LICENSE\">>]}.\n{<<\"licenses\">>,[<<\"Apache-2.0\">>]}.\n{<<\"links\">>,[{<<\"GitHub\">>,<<\"https://github.com/bitwalker/toml-elixir\">>}]}.\n{<<\"name\">>,<<\"toml\">>}.\n{<<\"requirements\">>,[]}.\n{<<\"version\">>,<<\"0.7.0\">>}.\n"
  },
  {
    "path": "test_apps/elixir/deps/toml/mix.exs",
    "content": "defmodule Toml.MixProject do\n  use Mix.Project\n\n  @version \"0.7.0\"\n  @source_url \"https://github.com/bitwalker/toml-elixir\"\n\n  def project do\n    [\n      app: :toml,\n      version: @version,\n      elixir: \"~> 1.9\",\n      start_permanent: Mix.env() == :prod,\n      consolidate_protocols: Mix.env() != :test,\n      description: \"An implementation of TOML for Elixir projects\",\n      package: package(),\n      deps: deps(),\n      aliases: aliases(Mix.env()),\n      preferred_cli_env: [\n        bench: :bench,\n        \"bench.decoder\": :bench,\n        \"bench.lexer\": :bench,\n        docs: :docs,\n        \"hex.publish\": :docs,\n        coveralls: :test,\n        \"coveralls.html\": :test,\n        \"coveralls.details\": :test\n      ],\n      dialyzer: dialyzer(),\n      docs: docs(),\n      elixirc_paths: elixirc_paths(Mix.env()),\n      escript: escript(Mix.env()),\n      test_coverage: [tool: ExCoveralls]\n    ]\n  end\n\n  # Run \"mix help compile.app\" to learn about applications.\n  def application do\n    [\n      extra_applications: []\n    ]\n  end\n\n  # Run \"mix help deps\" to learn about dependencies.\n  defp deps do\n    [\n      {:ex_doc, \">= 0.0.0\", only: [:docs]},\n      {:dialyxir, \"~> 1.0\", only: [:dev, :test], runtime: false},\n      {:benchee, \"~> 1.0\", only: [:bench]},\n      {:benchee_html, \"~> 1.0\", only: [:bench]},\n      {:jason, \"~> 1.0\", only: [:test, :bench]},\n      {:excoveralls, \"~> 0.9\", only: [:test]}\n      # For benchmarking, though none of these libraries work at this point\n      # {:tomlex, \"~> 0.0.5\", only: [:bench]},\n      # {:toml_elixir, \"~> 2.0.1\", only: [:bench]},\n      # {:jerry, \"~> 0.1.4\", only: [:bench]},\n      # {:etoml, \"~> 0.1.0\", only: [:bench]},\n    ]\n  end\n\n  defp package do\n    [\n      files: [\"lib\", \"mix.exs\", \"README.md\", \"LICENSE\"],\n      maintainers: [\"Paul Schoenfelder\"],\n      licenses: [\"Apache-2.0\"],\n      links: %{\"GitHub\" => @source_url}\n    ]\n  end\n\n  defp docs do\n    [\n      main: \"readme\",\n      source_url: @source_url,\n      source_ref: @version,\n      extras: [\n        LICENSE: [title: \"License\"],\n        \"README.md\": [title: \"Overview\"]\n      ],\n      formatters: [\"html\"]\n    ]\n  end\n\n  defp escript(:test) do\n    [\n      main_module: Toml.CLI,\n      name: :toml,\n      path: Path.join([__DIR__, \"bin\", \"toml\"])\n    ]\n  end\n\n  defp escript(_), do: nil\n\n  defp aliases(_env) do\n    [\n      \"compile-check\": [\n        \"compile --warnings-as-errors\",\n        \"format --check-formatted --dry-run\",\n        \"dialyzer --format dialyxir\"\n      ],\n      clean: [\"clean\", &clean/1],\n      bench: [\"bench.decoder\", \"bench.lexer\"],\n      \"bench.decoder\": [\"run bench/bench.decoder.exs\"],\n      \"bench.lexer\": [\"run bench/bench.lexer.exs\"]\n    ]\n  end\n\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(:bench), do: [\"lib\", \"bench/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  defp dialyzer do\n    [\n      ignore_warnings: \"dialyzer.ignore\",\n      flags: [:error_handling, :underspecs],\n      plt_core_path: System.get_env(\"PLT_PATH\") || System.get_env(\"MIX_HOME\")\n    ]\n  end\n\n  defp clean(_args) do\n    toml = Path.join([__DIR__, \"bin\", \"toml\"])\n\n    if File.exists?(toml) do\n      _ = File.rm(toml)\n    end\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/mix.exs",
    "content": "defmodule E2eElixir.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :e2e_elixir,\n      version: \"0.1.0\",\n      elixir: \"~> 1.14\",\n      deps: deps()\n    ]\n  end\n\n  defp deps do\n    [\n    ]\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/test/conversion_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:4d6ada8afa230fe381c1d77b8058f30b2d9e47bc726df619407d3257de429793\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: conversion\ndefmodule E2e.ConversionTest do\n  use ExUnit.Case, async: true\n\n  describe \"blockquote_multiple_paragraphs\" do\n    test \"Blockquote with multiple paragraphs has each paragraph prefixed\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\")\n      assert String.contains?(to_string(result.content), \"> First paragraph.\")\n      assert String.contains?(to_string(result.content), \"> Second paragraph.\")\n    end\n  end\n\n  describe \"blockquote_nested\" do\n    test \"Nested blockquote produces double-prefixed lines\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Outer quote.\")\n      assert String.contains?(to_string(result.content), \"Inner quote.\")\n    end\n  end\n\n  describe \"blockquote_simple\" do\n    test \"Simple blockquote\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<blockquote><p>Quote text</p></blockquote>\")\n      assert String.contains?(to_string(result.content), \"> Quote text\")\n    end\n  end\n\n  describe \"blockquote_with_list\" do\n    test \"Blockquote containing a list preserves list items inside quote\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Quote intro:\")\n      assert String.contains?(to_string(result.content), \"Point one\")\n      assert String.contains?(to_string(result.content), \"Point two\")\n    end\n  end\n\n  describe \"bold_and_italic\" do\n    test \"Nested bold and italic\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><strong><em>both</em></strong></p>\")\n      assert String.contains?(to_string(result.content), \"***both***\")\n    end\n  end\n\n  describe \"bold_strong\" do\n    test \"Strong tag converts to bold\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><strong>bold</strong></p>\")\n      assert String.contains?(to_string(result.content), \"**bold**\")\n    end\n  end\n\n  describe \"code_block\" do\n    test \"Code block with language preserves content\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"print('hello')\")\n    end\n  end\n\n  describe \"code_block_no_language\" do\n    test \"Code block without a language class preserves content\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<pre><code>plain code here</code></pre>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"plain code here\")\n    end\n  end\n\n  describe \"code_inline_in_paragraph\" do\n    test \"Inline code element nested inside a paragraph\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Call the <code>initialize()</code> method first.</p>\")\n      assert String.contains?(to_string(result.content), \"`initialize()`\")\n    end\n  end\n\n  describe \"code_with_backticks_in_content\" do\n    test \"Inline code containing backtick characters is properly escaped\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"backtick\")\n    end\n  end\n\n  describe \"emphasis_mark_highlight\" do\n    test \"mark tag produces highlighted output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><mark>highlighted</mark></p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"highlighted\")\n    end\n  end\n\n  describe \"emphasis_strikethrough_del\" do\n    test \"del tag converts to GFM strikethrough\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><del>deleted text</del></p>\")\n      assert String.contains?(to_string(result.content), \"~~deleted text~~\")\n    end\n  end\n\n  describe \"emphasis_strikethrough_s\" do\n    test \"s tag converts to GFM strikethrough\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><s>strikethrough</s></p>\")\n      assert String.contains?(to_string(result.content), \"~~strikethrough~~\")\n    end\n  end\n\n  describe \"emphasis_subscript\" do\n    test \"sub tag content is preserved\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>H<sub>2</sub>O</p>\")\n      assert String.contains?(to_string(result.content), \"H\")\n      assert String.contains?(to_string(result.content), \"2\")\n      assert String.contains?(to_string(result.content), \"O\")\n    end\n  end\n\n  describe \"emphasis_superscript\" do\n    test \"sup tag content is preserved\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>x<sup>2</sup></p>\")\n      assert String.contains?(to_string(result.content), \"x\")\n      assert String.contains?(to_string(result.content), \"2\")\n    end\n  end\n\n  describe \"emphasis_underline_u\" do\n    test \"u tag content is preserved in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><u>underlined</u></p>\")\n      assert String.contains?(to_string(result.content), \"underlined\")\n    end\n  end\n\n  describe \"form_input_elements\" do\n    test \"Form input elements produce readable output without form mechanics\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Name\")\n    end\n  end\n\n  describe \"form_select_options\" do\n    test \"Select element with options produces readable output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Color\")\n    end\n  end\n\n  describe \"form_textarea\" do\n    test \"Textarea element produces readable output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Message\")\n    end\n  end\n\n  describe \"heading_h1\" do\n    test \"H1 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h1>Heading 1</h1>\")\n      assert String.trim(result.content) == \"\\# Heading 1\"\n    end\n  end\n\n  describe \"heading_h2\" do\n    test \"H2 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h2>Heading 2</h2>\")\n      assert String.trim(result.content) == \"\\#\\# Heading 2\"\n    end\n  end\n\n  describe \"heading_h3\" do\n    test \"H3 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h3>Heading 3</h3>\")\n      assert String.trim(result.content) == \"\\#\\#\\# Heading 3\"\n    end\n  end\n\n  describe \"heading_h4\" do\n    test \"H4 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h4>Heading 4</h4>\")\n      assert String.trim(result.content) == \"\\#\\#\\#\\# Heading 4\"\n    end\n  end\n\n  describe \"heading_h5\" do\n    test \"H5 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h5>Heading 5</h5>\")\n      assert String.trim(result.content) == \"\\#\\#\\#\\#\\# Heading 5\"\n    end\n  end\n\n  describe \"heading_h6\" do\n    test \"H6 heading\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h6>Heading 6</h6>\")\n      assert String.trim(result.content) == \"\\#\\#\\#\\#\\#\\# Heading 6\"\n    end\n  end\n\n  describe \"image_figure_figcaption\" do\n    test \"Figure with figcaption preserves both image and caption\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\")\n      assert String.contains?(to_string(result.content), \"![A sunset](sunset.jpg)\")\n      assert String.contains?(to_string(result.content), \"Beautiful sunset over the ocean\")\n    end\n  end\n\n  describe \"image_linked\" do\n    test \"Image inside an anchor produces a linked image\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\")\n      assert String.contains?(to_string(result.content), \"![Icon](icon.png)\")\n      assert String.contains?(to_string(result.content), \"https://example.com\")\n    end\n  end\n\n  describe \"image_no_alt\" do\n    test \"Image without alt text produces image markdown\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<img src=\\\"banner.jpg\\\">\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"banner.jpg\")\n    end\n  end\n\n  describe \"image_simple\" do\n    test \"Image with alt text\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\")\n      assert String.contains?(to_string(result.content), \"![A photo](photo.jpg)\")\n    end\n  end\n\n  describe \"image_with_title\" do\n    test \"Image with title attribute includes title in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\")\n      assert String.contains?(to_string(result.content), \"![Sales chart](chart.png\")\n      assert String.contains?(to_string(result.content), \"Q3 Sales\")\n    end\n  end\n\n  describe \"inline_code\" do\n    test \"Inline code\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Use <code>console.log()</code> to debug</p>\")\n      assert String.contains?(to_string(result.content), \"`console.log()`\")\n    end\n  end\n\n  describe \"italic_em\" do\n    test \"Em tag converts to italic\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p><em>italic</em></p>\")\n      assert String.contains?(to_string(result.content), \"*italic*\")\n    end\n  end\n\n  describe \"line_break_br_tag\" do\n    test \"Single br tag produces a line break in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>First line.<br>Second line.</p>\")\n      assert String.contains?(to_string(result.content), \"First line.\")\n      assert String.contains?(to_string(result.content), \"Second line.\")\n    end\n  end\n\n  describe \"line_break_hr_tag\" do\n    test \"hr tag produces a horizontal separator between content\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Before rule.</p><hr><p>After rule.</p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Before rule.\")\n      assert String.contains?(to_string(result.content), \"After rule.\")\n    end\n  end\n\n  describe \"line_break_multiple_br\" do\n    test \"Multiple consecutive br tags in sequence\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Start.<br><br>End.</p>\")\n      assert String.contains?(to_string(result.content), \"Start.\")\n      assert String.contains?(to_string(result.content), \"End.\")\n    end\n  end\n\n  describe \"link_anchor_fragment\" do\n    test \"Fragment-only anchor link is preserved\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"\\#section\\\">Jump to section</a>\")\n      assert String.contains?(to_string(result.content), \"[Jump to section](\\#section)\")\n    end\n  end\n\n  describe \"link_empty_href\" do\n    test \"Link with empty href produces output with the link text\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"\\\">No destination</a>\")\n      assert String.contains?(to_string(result.content), \"No destination\")\n    end\n  end\n\n  describe \"link_image_inside\" do\n    test \"Image inside a link produces a linked image\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\")\n      assert String.contains?(to_string(result.content), \"![Logo](logo.png)\")\n      assert String.contains?(to_string(result.content), \"https://example.com\")\n    end\n  end\n\n  describe \"link_mailto\" do\n    test \"Mailto link is preserved with mailto: scheme\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\")\n      assert String.contains?(to_string(result.content), \"mailto:user@example.com\")\n    end\n  end\n\n  describe \"link_simple\" do\n    test \"Simple link\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"https://example.com\\\">Example</a>\")\n      assert String.contains?(to_string(result.content), \"[Example](https://example.com)\")\n    end\n  end\n\n  describe \"link_with_bold_text\" do\n    test \"Link containing bold text preserves formatting\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\")\n      assert String.contains?(to_string(result.content), \"**Bold link**\")\n      assert String.contains?(to_string(result.content), \"https://example.com\")\n    end\n  end\n\n  describe \"link_with_title\" do\n    test \"Link with title attribute\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\")\n      assert String.contains?(to_string(result.content), \"[Example](https://example.com\")\n      assert String.contains?(to_string(result.content), \"Example Site\")\n    end\n  end\n\n  describe \"list_definition_dl\" do\n    test \"Definition list with dt and dd elements\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\")\n      assert String.contains?(to_string(result.content), \"Term One\")\n      assert String.contains?(to_string(result.content), \"Definition of term one.\")\n      assert String.contains?(to_string(result.content), \"Term Two\")\n      assert String.contains?(to_string(result.content), \"Definition of term two.\")\n    end\n  end\n\n  describe \"list_item_multiple_paragraphs\" do\n    test \"List item containing multiple paragraphs\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\")\n      assert String.contains?(to_string(result.content), \"First paragraph in item.\")\n      assert String.contains?(to_string(result.content), \"Second paragraph in item.\")\n      assert String.contains?(to_string(result.content), \"Simple item\")\n    end\n  end\n\n  describe \"list_mixed_nested\" do\n    test \"Mixed list: ordered list nested inside unordered list\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\")\n      assert String.contains?(to_string(result.content), \"Item A\")\n      assert String.contains?(to_string(result.content), \"Sub 1\")\n      assert String.contains?(to_string(result.content), \"Sub 2\")\n      assert String.contains?(to_string(result.content), \"Item B\")\n    end\n  end\n\n  describe \"list_nested_ordered\" do\n    test \"Nested ordered list with two levels of depth\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\")\n      assert String.contains?(to_string(result.content), \"Step 1\")\n      assert String.contains?(to_string(result.content), \"Step 1a\")\n      assert String.contains?(to_string(result.content), \"Step 1b\")\n      assert String.contains?(to_string(result.content), \"Step 2\")\n    end\n  end\n\n  describe \"list_nested_unordered\" do\n    test \"Nested unordered list with two levels of depth\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\")\n      assert String.contains?(to_string(result.content), \"Parent A\")\n      assert String.contains?(to_string(result.content), \"Child A1\")\n      assert String.contains?(to_string(result.content), \"Child A2\")\n      assert String.contains?(to_string(result.content), \"Parent B\")\n    end\n  end\n\n  describe \"list_task_checkboxes\" do\n    test \"Task list with checked and unchecked checkboxes\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Done task\")\n      assert String.contains?(to_string(result.content), \"Pending task\")\n    end\n  end\n\n  describe \"ordered_list\" do\n    test \"Ordered list\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\")\n      assert String.contains?(to_string(result.content), \"1. First\")\n      assert String.contains?(to_string(result.content), \"2. Second\")\n      assert String.contains?(to_string(result.content), \"3. Third\")\n    end\n  end\n\n  describe \"paragraph_multiple\" do\n    test \"Multiple paragraphs are separated by a blank line\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\")\n      assert String.contains?(to_string(result.content), \"First paragraph.\")\n      assert String.contains?(to_string(result.content), \"Second paragraph.\")\n    end\n  end\n\n  describe \"paragraph_nested_divs\" do\n    test \"Text nested inside divs is extracted correctly\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<div><div><p>Nested text</p></div></div>\")\n      assert String.contains?(to_string(result.content), \"Nested text\")\n    end\n  end\n\n  describe \"paragraph_simple\" do\n    test \"Simple paragraph converts to plain text\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Hello World</p>\")\n      assert String.trim(result.content) == \"Hello World\"\n    end\n  end\n\n  describe \"paragraph_with_inline_formatting\" do\n    test \"Paragraph with bold, italic, and a link\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\")\n      assert String.contains?(to_string(result.content), \"**bold**\")\n      assert String.contains?(to_string(result.content), \"*italic*\")\n      assert String.contains?(to_string(result.content), \"[link](https://example.com)\")\n    end\n  end\n\n  describe \"paragraph_with_line_breaks\" do\n    test \"Paragraph with br tags produces line breaks in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Line one.\")\n      assert String.contains?(to_string(result.content), \"Line two.\")\n      assert String.contains?(to_string(result.content), \"Line three.\")\n    end\n  end\n\n  describe \"semantic_abbr\" do\n    test \"Abbreviation element text is preserved\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\")\n      assert String.contains?(to_string(result.content), \"WWW\")\n    end\n  end\n\n  describe \"semantic_article\" do\n    test \"Article element wrapping content preserves inner content\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\")\n      assert String.contains?(to_string(result.content), \"Article Title\")\n      assert String.contains?(to_string(result.content), \"Article body.\")\n    end\n  end\n\n  describe \"semantic_definition_list\" do\n    test \"Definition list with term and description\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\")\n      assert String.contains?(to_string(result.content), \"HTML\")\n      assert String.contains?(to_string(result.content), \"HyperText Markup Language\")\n      assert String.contains?(to_string(result.content), \"CSS\")\n      assert String.contains?(to_string(result.content), \"Cascading Style Sheets\")\n    end\n  end\n\n  describe \"semantic_details_summary\" do\n    test \"Details and summary elements produce readable output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Click to expand\")\n    end\n  end\n\n  describe \"semantic_hr\" do\n    test \"Horizontal rule produces a separator in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Above</p><hr><p>Below</p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Above\")\n      assert String.contains?(to_string(result.content), \"Below\")\n    end\n  end\n\n  describe \"semantic_mark_highlight\" do\n    test \"Mark tag produces highlighted output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"highlighted text\")\n    end\n  end\n\n  describe \"semantic_section_with_heading\" do\n    test \"Section element with heading preserves structure\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\")\n      assert String.contains?(to_string(result.content), \"Section Heading\")\n      assert String.contains?(to_string(result.content), \"Section content.\")\n    end\n  end\n\n  describe \"semantic_sub_superscript\" do\n    test \"Subscript and superscript elements are preserved in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"H\")\n      assert String.contains?(to_string(result.content), \"2\")\n      assert String.contains?(to_string(result.content), \"O\")\n      assert String.contains?(to_string(result.content), \"E=mc\")\n    end\n  end\n\n  describe \"simple_table\" do\n    test \"Simple table with header\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\")\n      assert String.contains?(to_string(result.content), \"Name\")\n      assert String.contains?(to_string(result.content), \"Age\")\n      assert String.contains?(to_string(result.content), \"Alice\")\n      assert String.contains?(to_string(result.content), \"30\")\n      assert String.contains?(to_string(result.content), \"|\")\n      assert String.contains?(to_string(result.content), \"---\")\n    end\n  end\n\n  describe \"table_empty\" do\n    test \"Empty table produces no output or minimal output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table></table>\")\n      assert String.trim(result.content) == \"\"\n    end\n  end\n\n  describe \"table_no_thead\" do\n    test \"Table without thead uses first row as implied header\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Product\")\n      assert String.contains?(to_string(result.content), \"Price\")\n      assert String.contains?(to_string(result.content), \"Apple\")\n      assert String.contains?(to_string(result.content), \"1.00\")\n      assert String.contains?(to_string(result.content), \"|\")\n    end\n  end\n\n  describe \"table_pipe_chars_in_content\" do\n    test \"Table cells containing pipe characters are escaped in output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Expression\")\n      assert String.contains?(to_string(result.content), \"Result\")\n      assert String.contains?(to_string(result.content), \"true\")\n    end\n  end\n\n  describe \"table_with_alignment\" do\n    test \"Table with column alignment attributes\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Left\")\n      assert String.contains?(to_string(result.content), \"Center\")\n      assert String.contains?(to_string(result.content), \"Right\")\n      assert String.contains?(to_string(result.content), \"L\")\n      assert String.contains?(to_string(result.content), \"C\")\n      assert String.contains?(to_string(result.content), \"R\")\n      assert String.contains?(to_string(result.content), \"|\")\n    end\n  end\n\n  describe \"table_with_colspan\" do\n    test \"Table with colspan attribute in a header cell\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\")\n      assert result.content != \"\"\n      assert String.contains?(to_string(result.content), \"Full Name\")\n      assert String.contains?(to_string(result.content), \"John\")\n      assert String.contains?(to_string(result.content), \"Doe\")\n    end\n  end\n\n  describe \"unordered_list\" do\n    test \"Unordered list\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\")\n      assert String.contains?(to_string(result.content), \"- Item 1\")\n      assert String.contains?(to_string(result.content), \"- Item 2\")\n      assert String.contains?(to_string(result.content), \"- Item 3\")\n    end\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/test/smoke_test.exs",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:d7a4ed35e6c4ee789ddf485d0dffedd820466a9ce035debd3d3829f2cb939301\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: smoke\ndefmodule E2e.SmokeTest do\n  use ExUnit.Case, async: true\n\n  describe \"smoke_empty_string\" do\n    test \"Empty string produces empty output\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"\")\n      assert String.trim(result.content) == \"\"\n    end\n  end\n\n  describe \"smoke_simple_heading\" do\n    test \"H1 heading converts to ATX markdown\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<h1>Title</h1>\")\n      assert String.contains?(to_string(result.content), \"\\# Title\")\n    end\n  end\n\n  describe \"smoke_simple_paragraph\" do\n    test \"Simple paragraph converts correctly\" do\n      {:ok, result} = HtmlToMarkdownRs.convert(\"<p>Hello World</p>\")\n      assert String.trim(result.content) == \"Hello World\"\n      assert result.content != \"\"\n    end\n  end\nend\n"
  },
  {
    "path": "test_apps/elixir/test/test_helper.exs",
    "content": "ExUnit.start()\n"
  },
  {
    "path": "test_apps/fixtures/README.md",
    "content": "# Test Fixtures\n\nShared test fixtures for testing published html-to-markdown packages across all language bindings.\n\n## Format\n\nEach fixture file contains a JSON array of test cases:\n\n```json\n[\n  {\n    \"name\": \"Test case name\",\n    \"html\": \"<p>Input HTML</p>\",\n    \"expectedMarkdown\": \"Expected markdown output\",\n    \"options\": {}\n  }\n]\n```\n\n## Files\n\n- **basic-html.json** - 100 basic HTML elements (headings, paragraphs, lists, etc.)\n- **complex-html.json** - 50 complex structures (nested lists, tables, etc.)\n- **edge-cases.json** - 30 edge cases (special chars, Unicode, entities)\n- **metadata-extraction.json** - 20 metadata extraction tests\n- **real-world.json** - 10 real-world HTML samples\n"
  },
  {
    "path": "test_apps/fixtures/basic-html.json",
    "content": "[\n  {\n    \"name\": \"Simple paragraph\",\n    \"html\": \"<p>Hello World</p>\",\n    \"expectedMarkdown\": \"Hello World\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Heading level 1\",\n    \"html\": \"<h1>Title</h1>\",\n    \"expectedMarkdown\": \"# Title\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Heading with paragraph\",\n    \"html\": \"<h1>Title</h1><p>Content</p>\",\n    \"expectedMarkdown\": \"# Title\\n\\nContent\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Strong text\",\n    \"html\": \"<p>This is <strong>bold</strong> text</p>\",\n    \"expectedMarkdown\": \"This is **bold** text\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Emphasis text\",\n    \"html\": \"<p>This is <em>italic</em> text</p>\",\n    \"expectedMarkdown\": \"This is *italic* text\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Unordered list\",\n    \"html\": \"<ul><li>Item 1</li><li>Item 2</li></ul>\",\n    \"expectedMarkdown\": \"- Item 1\\n- Item 2\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Ordered list\",\n    \"html\": \"<ol><li>First</li><li>Second</li></ol>\",\n    \"expectedMarkdown\": \"1. First\\n2. Second\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Link\",\n    \"html\": \"<a href=\\\"https://example.com\\\">Example</a>\",\n    \"expectedMarkdown\": \"[Example](https://example.com)\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Code inline\",\n    \"html\": \"<code>console.log('hello')</code>\",\n    \"expectedMarkdown\": \"`console.log('hello')`\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Blockquote\",\n    \"html\": \"<blockquote>Quote</blockquote>\",\n    \"expectedMarkdown\": \"> Quote\",\n    \"options\": {}\n  }\n]\n"
  },
  {
    "path": "test_apps/fixtures/complex-html.json",
    "content": "[\n  {\n    \"name\": \"Nested lists\",\n    \"html\": \"<ul><li>Item 1<ul><li>Nested 1</li><li>Nested 2</li></ul></li><li>Item 2</li></ul>\",\n    \"expectedMarkdown\": \"- Item 1\\n  - Nested 1\\n  - Nested 2\\n- Item 2\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Mixed content with heading and list\",\n    \"html\": \"<h2>Section</h2><p>Introduction</p><ol><li>First</li><li>Second</li></ol><p>Conclusion</p>\",\n    \"expectedMarkdown\": \"## Section\\n\\nIntroduction\\n\\n1. First\\n2. Second\\n\\n\\nConclusion\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Complex formatting\",\n    \"html\": \"<p><strong>Bold</strong> and <em>italic</em> and <code>code</code></p>\",\n    \"expectedMarkdown\": \"**Bold** and *italic* and `code`\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Nested emphasis\",\n    \"html\": \"<p><strong><em>Bold italic</em></strong></p>\",\n    \"expectedMarkdown\": \"***Bold italic***\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Multiple paragraphs\",\n    \"html\": \"<p>First paragraph</p><p>Second paragraph</p><p>Third paragraph</p>\",\n    \"expectedMarkdown\": \"First paragraph\\n\\nSecond paragraph\\n\\nThird paragraph\",\n    \"options\": {}\n  }\n]\n"
  },
  {
    "path": "test_apps/fixtures/edge-cases.json",
    "content": "[\n  {\n    \"name\": \"HTML entities\",\n    \"html\": \"<p>&nbsp;&lt;&gt;&amp;</p>\",\n    \"expectedMarkdown\": \"<>&\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Unicode characters\",\n    \"html\": \"<p>Hello 世界 مرحبا שלום</p>\",\n    \"expectedMarkdown\": \"Hello 世界 مرحبا שלום\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Special markdown characters\",\n    \"html\": \"<p>Test * _ [ ] ( ) # + - . !</p>\",\n    \"expectedMarkdown\": \"Test * _ [ ] ( ) # + - . !\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Empty elements\",\n    \"html\": \"<p></p><div></div><span></span>\",\n    \"expectedMarkdown\": \"\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Whitespace preservation\",\n    \"html\": \"<p>Text   with   spaces</p>\",\n    \"expectedMarkdown\": \"Text with spaces\",\n    \"options\": {}\n  }\n]\n"
  },
  {
    "path": "test_apps/fixtures/metadata-extraction.json",
    "content": "[\n  {\n    \"name\": \"Document with title\",\n    \"html\": \"<html><head><title>Page Title</title></head><body><p>Content</p></body></html>\",\n    \"expectedMarkdown\": \"Content\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Document with meta description\",\n    \"html\": \"<html><head><meta name=\\\"description\\\" content=\\\"Page description\\\"></head><body><p>Content</p></body></html>\",\n    \"expectedMarkdown\": \"Content\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Document with language\",\n    \"html\": \"<html lang=\\\"fr\\\"><body><p>Contenu</p></body></html>\",\n    \"expectedMarkdown\": \"Contenu\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Multiple headers extraction\",\n    \"html\": \"<h1>Main Title</h1><h2>Subtitle</h2><h3>Sub-subtitle</h3>\",\n    \"expectedMarkdown\": \"# Main Title\\n\\n## Subtitle\\n\\n### Sub-subtitle\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Multiple links\",\n    \"html\": \"<a href=\\\"/page1\\\">Link 1</a> and <a href=\\\"https://example.com\\\">Link 2</a>\",\n    \"expectedMarkdown\": \"[Link 1](/page1) and [Link 2](https://example.com)\",\n    \"options\": {}\n  }\n]\n"
  },
  {
    "path": "test_apps/fixtures/real-world.json",
    "content": "[\n  {\n    \"name\": \"Blog article excerpt\",\n    \"html\": \"<article><h1>Article Title</h1><p class=\\\"author\\\">By John Doe</p><p>First paragraph with <strong>important</strong> content.</p><p>Second paragraph with a <a href=\\\"#reference\\\">reference</a>.</p></article>\",\n    \"expectedMarkdown\": \"# Article Title\\n\\nBy John Doe\\n\\nFirst paragraph with **important** content.\\n\\nSecond paragraph with a [reference](#reference).\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Documentation with code\",\n    \"html\": \"<h2>Usage</h2><p>To use the library:</p><code>npm install html-to-markdown</code><p>Then import and use.</p>\",\n    \"expectedMarkdown\": \"## Usage\\n\\nTo use the library:\\n\\n`npm install html-to-markdown`\\n\\nThen import and use.\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"FAQ with definitions\",\n    \"html\": \"<dl><dt>What is this?</dt><dd>An explanation</dd><dt>How to use?</dt><dd>Instructions here</dd></dl>\",\n    \"expectedMarkdown\": \"What is this?\\nAn explanation\\n\\nHow to use?\\nInstructions here\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Social media card content\",\n    \"html\": \"<div><h3>Interesting Title</h3><p>Short description about the content</p><footer>Posted on Jan 29, 2025</footer></div>\",\n    \"expectedMarkdown\": \"### Interesting Title\\n\\nShort description about the content\\n\\nPosted on Jan 29, 2025\",\n    \"options\": {}\n  },\n  {\n    \"name\": \"Product description\",\n    \"html\": \"<section><h2>Product Name</h2><p><strong>Price:</strong> $99</p><p><strong>Features:</strong></p><ul><li>Feature 1</li><li>Feature 2</li><li>Feature 3</li></ul></section>\",\n    \"expectedMarkdown\": \"## Product Name\\n\\n**Price:** $99\\n\\n**Features:**\\n\\n- Feature 1\\n- Feature 2\\n- Feature 3\",\n    \"options\": {}\n  }\n]\n"
  },
  {
    "path": "test_apps/go/README.md",
    "content": "# Go Test App for html-to-markdown\n\nComprehensive test suite for the html-to-markdown Go module. Tests the FFI bindings and validates that all major features work correctly, including basic HTML-to-Markdown conversion, metadata extraction, error handling, and memory safety.\n\n## Overview\n\nThis test app validates:\n\n- **Basic HTML conversion** via FFI bindings\n- **Metadata extraction** (title, description, headers, links, images)\n- **Complex HTML structures** (nested lists, tables, code blocks, etc.)\n- **Error handling** and edge cases\n- **Memory safety** through repeated conversions\n- **Large document handling** and performance\n- **Consistency** across multiple conversions\n- **Unicode and special characters** support\n- **FFI version information** access\n\nTotal: 51 test cases across 19 test functions.\n\n## Setup\n\n```bash\n# Install Go 1.25+\ngo mod download\n```\n\n## Run Tests\n\n```bash\n# All tests with verbose output\ngo test -v\n\n# Run specific test function\ngo test -v -run TestVersion\n\n# Run smoke tests only\ngo test -v -run Smoke\n\n# Run feature tests only\ngo test -v -run Feature\n\n# Run metadata tests only\ngo test -v -run Metadata\n\n# Run with short timeout (will fail if not immediate)\ngo test -v -short\n\n# Show test coverage\ngo test -cover\n```\n\n## Test Structure\n\n### Smoke Tests (`smoke_test.go`)\n\n- Package imports work correctly\n- Basic HTML to Markdown conversion\n- Heading conversion validation\n- Empty input handling\n\n### Comprehensive Tests (`comprehensive_test.go`)\n\n- Fixture-driven tests loading from JSON\n- Basic HTML conversions (paragraphs, headings, links, etc.)\n\n### Feature Tests (`feature_test.go`)\n\n- **API Tests**: Version information, MustConvert panic behavior\n- **Conversion Tests**: Complex HTML structures (nested lists, tables, code blocks, images, etc.)\n- **Error Handling**: Valid HTML, malformed HTML, empty strings, whitespace\n- **Metadata Extraction**: Headers, links, images, document metadata\n- **Memory Safety**: Repeated conversions to ensure no memory leaks\n- **Large Documents**: Handling HTML with 100+ elements\n- **Consistency**: Multiple conversions produce identical output\n- **Special Characters**: HTML entities, Unicode, emoji, special symbols\n- **Regression Tests**: Content preservation across conversions\n\n## Test Coverage by Feature\n\n| Feature | Tests | Coverage |\n|---------|-------|----------|\n| Basic Conversion | 15 | Basic paragraphs, headings, lists, formatting |\n| Metadata Extraction | 6 | Headers, links, images, document metadata |\n| Error Handling | 4 | Valid/malformed HTML, empty input |\n| Complex HTML | 8 | Nested structures, tables, code blocks |\n| Special Characters | 4 | Entities, Unicode, emoji, symbols |\n| Memory & Performance | 3 | Repeated conversions, large documents, consistency |\n| API Features | 3 | Version, MustConvert, sequential operations |\n| Regression | 4 | Content preservation, mixed formats |\n\n## Module Dependencies\n\n- Go 1.25+\n- `github.com/kreuzberg-dev/html-to-markdown/packages/go/v2` (v2.24.1)\n\nThe `go.mod` file uses a local `replace` directive for development. This can be removed and the module can be published to pkg.go.dev for external consumption.\n\n## Expected Output\n\nAll 51 tests should pass:\n\n```text\nok  \tgithub.com/kreuzberg-dev/html-to-markdown-test-app\t0.3s\n```\n\n## Notes\n\n- Tests use the FFI bindings (cgo) to call the Rust core\n- All conversions are performed synchronously (Go's HTML parsing is blocking-compatible)\n- Memory is automatically managed by the FFI layer\n- Tests are deterministic and do not depend on external resources\n"
  },
  {
    "path": "test_apps/go/conversion_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:7d3a4826f8c280d19036d6dfad0c22e5e24f23ad63c4f562993d67e2256830aa\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: conversion\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc Test_BlockquoteMultipleParagraphs(t *testing.T) {\n\t// Blockquote with multiple paragraphs has each paragraph prefixed\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_BlockquoteNested(t *testing.T) {\n\t// Nested blockquote produces double-prefixed lines\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_BlockquoteSimple(t *testing.T) {\n\t// Simple blockquote\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_BlockquoteWithList(t *testing.T) {\n\t// Blockquote containing a list preserves list items inside quote\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_BoldAndItalic(t *testing.T) {\n\t// Nested bold and italic\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_BoldStrong(t *testing.T) {\n\t// Strong tag converts to bold\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_CodeBlock(t *testing.T) {\n\t// Code block with language preserves content\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_CodeBlockNoLanguage(t *testing.T) {\n\t// Code block without a language class preserves content\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_CodeInlineInParagraph(t *testing.T) {\n\t// Inline code element nested inside a paragraph\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_CodeWithBackticksInContent(t *testing.T) {\n\t// Inline code containing backtick characters is properly escaped\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisMarkHighlight(t *testing.T) {\n\t// mark tag produces highlighted output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisStrikethroughDel(t *testing.T) {\n\t// del tag converts to GFM strikethrough\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisStrikethroughS(t *testing.T) {\n\t// s tag converts to GFM strikethrough\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisSubscript(t *testing.T) {\n\t// sub tag content is preserved\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisSuperscript(t *testing.T) {\n\t// sup tag content is preserved\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_EmphasisUnderlineU(t *testing.T) {\n\t// u tag content is preserved in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_FormInputElements(t *testing.T) {\n\t// Form input elements produce readable output without form mechanics\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_FormSelectOptions(t *testing.T) {\n\t// Select element with options produces readable output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_FormTextarea(t *testing.T) {\n\t// Textarea element produces readable output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH1(t *testing.T) {\n\t// H1 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH2(t *testing.T) {\n\t// H2 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH3(t *testing.T) {\n\t// H3 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH4(t *testing.T) {\n\t// H4 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH5(t *testing.T) {\n\t// H5 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_HeadingH6(t *testing.T) {\n\t// H6 heading\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ImageFigureFigcaption(t *testing.T) {\n\t// Figure with figcaption preserves both image and caption\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ImageLinked(t *testing.T) {\n\t// Image inside an anchor produces a linked image\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ImageNoAlt(t *testing.T) {\n\t// Image without alt text produces image markdown\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ImageSimple(t *testing.T) {\n\t// Image with alt text\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ImageWithTitle(t *testing.T) {\n\t// Image with title attribute includes title in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_InlineCode(t *testing.T) {\n\t// Inline code\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ItalicEm(t *testing.T) {\n\t// Em tag converts to italic\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LineBreakBrTag(t *testing.T) {\n\t// Single br tag produces a line break in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LineBreakHrTag(t *testing.T) {\n\t// hr tag produces a horizontal separator between content\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LineBreakMultipleBr(t *testing.T) {\n\t// Multiple consecutive br tags in sequence\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkAnchorFragment(t *testing.T) {\n\t// Fragment-only anchor link is preserved\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkEmptyHref(t *testing.T) {\n\t// Link with empty href produces output with the link text\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkImageInside(t *testing.T) {\n\t// Image inside a link produces a linked image\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkMailto(t *testing.T) {\n\t// Mailto link is preserved with mailto: scheme\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkSimple(t *testing.T) {\n\t// Simple link\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkWithBoldText(t *testing.T) {\n\t// Link containing bold text preserves formatting\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_LinkWithTitle(t *testing.T) {\n\t// Link with title attribute\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListDefinitionDl(t *testing.T) {\n\t// Definition list with dt and dd elements\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListItemMultipleParagraphs(t *testing.T) {\n\t// List item containing multiple paragraphs\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListMixedNested(t *testing.T) {\n\t// Mixed list: ordered list nested inside unordered list\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListNestedOrdered(t *testing.T) {\n\t// Nested ordered list with two levels of depth\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListNestedUnordered(t *testing.T) {\n\t// Nested unordered list with two levels of depth\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ListTaskCheckboxes(t *testing.T) {\n\t// Task list with checked and unchecked checkboxes\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_OrderedList(t *testing.T) {\n\t// Ordered list\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ParagraphMultiple(t *testing.T) {\n\t// Multiple paragraphs are separated by a blank line\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ParagraphNestedDivs(t *testing.T) {\n\t// Text nested inside divs is extracted correctly\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ParagraphSimple(t *testing.T) {\n\t// Simple paragraph converts to plain text\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ParagraphWithInlineFormatting(t *testing.T) {\n\t// Paragraph with bold, italic, and a link\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_ParagraphWithLineBreaks(t *testing.T) {\n\t// Paragraph with br tags produces line breaks in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticAbbr(t *testing.T) {\n\t// Abbreviation element text is preserved\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticArticle(t *testing.T) {\n\t// Article element wrapping content preserves inner content\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticDefinitionList(t *testing.T) {\n\t// Definition list with term and description\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticDetailsSummary(t *testing.T) {\n\t// Details and summary elements produce readable output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticHr(t *testing.T) {\n\t// Horizontal rule produces a separator in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticMarkHighlight(t *testing.T) {\n\t// Mark tag produces highlighted output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticSectionWithHeading(t *testing.T) {\n\t// Section element with heading preserves structure\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SemanticSubSuperscript(t *testing.T) {\n\t// Subscript and superscript elements are preserved in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SimpleTable(t *testing.T) {\n\t// Simple table with header\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_TableEmpty(t *testing.T) {\n\t// Empty table produces no output or minimal output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_TableNoThead(t *testing.T) {\n\t// Table without thead uses first row as implied header\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_TablePipeCharsInContent(t *testing.T) {\n\t// Table cells containing pipe characters are escaped in output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_TableWithAlignment(t *testing.T) {\n\t// Table with column alignment attributes\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_TableWithColspan(t *testing.T) {\n\t// Table with colspan attribute in a header cell\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_UnorderedList(t *testing.T) {\n\t// Unordered list\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n"
  },
  {
    "path": "test_apps/go/go.mod",
    "content": "module e2e_go\n\ngo 1.26\n\nrequire (\n\tgithub.com/kreuzberg-dev/html-to-markdown/packages/go/v3 v3.2.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n"
  },
  {
    "path": "test_apps/go/go.sum",
    "content": ""
  },
  {
    "path": "test_apps/go/run_tests.sh",
    "content": "#!/bin/bash\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\necho \"Installing FFI library from remote...\"\ngo generate github.com/kreuzberg-dev/html-to-markdown/packages/go/v3/htmltomarkdown\n\necho \"Running tests...\"\ngo test -v -count=1 ./...\n"
  },
  {
    "path": "test_apps/go/smoke_test.go",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:800f62f44a4609cf2ac43b6e9624a685124fd040cde2c03260c045e1c1dce00b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\n// E2e tests for category: smoke\npackage e2e_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc Test_SmokeEmptyString(t *testing.T) {\n\t// Empty string produces empty output\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SmokeSimpleHeading(t *testing.T) {\n\t// H1 heading converts to ATX markdown\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n\nfunc Test_SmokeSimpleParagraph(t *testing.T) {\n\t// Simple paragraph converts correctly\n\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")\n}\n"
  },
  {
    "path": "test_apps/java/.mvn/wrapper/maven-wrapper.properties",
    "content": "wrapperVersion=3.3.4\ndistributionType=only-script\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip\n"
  },
  {
    "path": "test_apps/java/README.md",
    "content": "# Java Test App for html-to-markdown\n\nIntegration tests for the published `html-to-markdown` Java package from Maven Central.\n\nThis test app validates that the package is correctly published and accessible via Maven Central, with comprehensive test coverage for core conversion, error handling, and type safety. The v3 API uses a single `convert()` function.\n\n## Prerequisites\n\n- Java 25+ (with preview features enabled)\n- Maven 3.8+\n- Rust toolchain (1.85+) - for FFI library building\n- cargo 1.85+ - Rust package manager\n\n## Setup\n\n### Option 1: Using Published Maven Central Package (Recommended for Production)\n\nOnce the `html-to-markdown` package is published to Maven Central:\n\n```bash\nmvn clean install\n```\n\nThis will download the package from Maven Central.\n\n### Option 2: Development Build from Local Workspace\n\nTo test against the current workspace build:\n\n```bash\n# Step 1: Build and install the Java package locally\ncd ../../packages/java\nmvn clean install -DskipTests -Dskip.rust.ffi=false\n\n# Step 2: Build the test app\ncd ../../tests/test_apps/java\nmvn clean install\n```\n\nThe first command will:\n\n1. Build the Rust FFI library via cargo\n2. Create native bindings for JNI/FFI\n3. Package the library for local Maven repository\n4. Install it in your local Maven repository (~/.m2/repository)\n\nThe second command will then use that local package to build the test app.\n\n## Run Tests\n\n### All Tests\n\n```bash\nmvn test\n```\n\n### Individual Test Suites\n\n```bash\n# Smoke tests - basic functionality validation\nmvn test -Dtest=SmokeTest\n\n# Comprehensive tests - fixture-based validation and feature coverage\nmvn test -Dtest=ComprehensiveTest\n\n# Error handling tests - edge cases and error conditions\nmvn test -Dtest=ErrorHandlingTest\n```\n\n### Run Specific Test Methods\n\n```bash\nmvn test -Dtest=SmokeTest#testBasicConversion\n```\n\n## Test Coverage\n\n### SmokeTest (14 tests)\n\n- Package loading and class availability\n- Basic HTML to Markdown conversion\n- Heading, list, link, code, and blockquote conversion\n- Library version retrieval\n- Null input handling\n- Combined element conversion\n\n### ComprehensiveTest (13 tests)\n\n- Fixture-based test validation (basic-html.json)\n- Complex HTML formatting with mixed elements\n- Metadata extraction with document metadata\n- Custom visitor implementation\n- Batch conversion operations\n- ConversionOptions type safety\n- FFI functionality verification\n- Table conversion\n- Code block conversion\n- HTML attribute preservation\n\n### ErrorHandlingTest (13 tests)\n\n- Null input validation\n- Empty and empty element handling\n- Malformed HTML resilience\n- HTML entity and special character handling\n- Unicode character support\n- Large input processing\n- Deep HTML nesting\n- Mixed content handling\n- HTML comment handling\n- Script and style tag handling\n- Data URI security\n- Visitor error cases\n\n### TypeSafetyTest (13 tests)\n\n- ConversionOptions type validation\n- Builder pattern type safety\n- OutputFormat enum type safety\n- Conversion return type validation\n- MetadataExtraction type validation\n- Visitor interface type safety\n- VisitResult type hierarchy\n- ConversionException exception type validation\n- Generic method handling\n- Heading style parameter validation\n- NodeContext type safety\n- Version string type safety\n- Metadata component type validation\n\n### MetadataExtractionTest (18 tests)\n\n- Basic document metadata extraction\n- Markdown content extraction with metadata\n- Multiple header extraction\n- Link extraction\n- Image extraction\n- Complex document extraction with mixed content\n- Nested HTML structure extraction\n- Special content types (blockquotes, code, tables)\n- Empty and minimal document extraction\n- Meta tags extraction (title, description, keywords)\n- Open Graph metadata extraction\n- Twitter Card metadata extraction\n- Large document extraction\n- Null input validation\n\n### VisitorFunctionalityTest (14 tests)\n\n- Basic visitor implementation\n- Element skipping functionality\n- Conditional element handling\n- Multiple callback implementations\n- Empty visitor with default behavior\n- Complex HTML structure processing\n- Heading level discrimination\n- Image filtering\n- Code block handling\n- Null input validation\n- Mixed content processing\n- Multiple conversions with same visitor instance\n\n## Total Test Count: 85 tests\n\n## Package Information\n\n- **GroupId**: `dev.kreuzberg`\n- **ArtifactId**: `html-to-markdown`\n- **Version**: `2.24.1`\n- **Repository**: Maven Central\n\n## Dependencies\n\n- **junit-jupiter**: 5.10.2 (Testing framework)\n- **jackson-databind**: 2.17.0 (JSON fixture parsing)\n- **html-to-markdown**: 2.24.1 (Native Java FFI bindings)\n\n## Features Tested\n\n### Core Conversion\n\n- Basic HTML elements (paragraphs, headings, lists, links, code, blockquotes)\n- Text formatting (bold, italic, strikethrough)\n- Nested structures and complex layouts\n\n### Error Handling\n\n- Null input validation with proper exceptions\n- Malformed HTML resilience\n- Unicode and special character support\n- Large input processing\n- Deep nesting handling\n\n### Type Safety\n\n- Strong Java typing with generics\n- Builder pattern implementation\n- Enum type validation\n- Exception hierarchy\n\n### Metadata Extraction\n\n- Document title, description, author extraction\n- Header hierarchy extraction\n- Link and image metadata\n- Open Graph and Twitter Card support\n\n### Advanced Features\n\n- Custom visitor pattern for element interception\n- Element skipping and filtering\n- Conditional processing based on attributes\n- Multiple conversion formats\n\n## Performance Notes\n\n- All tests use the published Maven Central package (not local path dependencies)\n- Tests validate JNI/FFI functionality through the Panama FFI API\n- Tests are independent and can run in parallel\n- No build steps for native code (uses pre-built binaries in Maven package)\n\n## Troubleshooting\n\n### \"Failed to find html-to-markdown\"\n\nEnsure Maven Central is accessible and that your Maven repositories are configured correctly.\n\n### \"Cannot enable preview features\"\n\nMake sure Java 25+ is installed and the maven-compiler-plugin is configured with `--enable-preview`.\n\n### \"UnsatisfiedLinkError\"\n\nThis test app does not build the FFI library - it uses the published package which includes pre-built binaries. Ensure the html-to-markdown Maven package is properly downloaded.\n"
  },
  {
    "path": "test_apps/java/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.3.4\n#\n# Optional ENV vars\n# -----------------\n#   JAVA_HOME - location of a JDK home dir, required when download maven via java source\n#   MVNW_REPOURL - repo url base for downloading maven distribution\n#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\n#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output\n# ----------------------------------------------------------------------------\n\nset -euf\n[ \"${MVNW_VERBOSE-}\" != debug ] || set -x\n\n# OS specific support.\nnative_path() { printf %s\\\\n \"$1\"; }\ncase \"$(uname)\" in\nCYGWIN* | MINGW*)\n  [ -z \"${JAVA_HOME-}\" ] || JAVA_HOME=\"$(cygpath --unix \"$JAVA_HOME\")\"\n  native_path() { cygpath --path --windows \"$1\"; }\n  ;;\nesac\n\n# set JAVACMD and JAVACCMD\nset_java_home() {\n  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched\n  if [ -n \"${JAVA_HOME-}\" ]; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n      JAVACCMD=\"$JAVA_HOME/jre/sh/javac\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n      JAVACCMD=\"$JAVA_HOME/bin/javac\"\n\n      if [ ! -x \"$JAVACMD\" ] || [ ! -x \"$JAVACCMD\" ]; then\n        echo \"The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run.\" >&2\n        echo \"JAVA_HOME is set to \\\"$JAVA_HOME\\\", but \\\"\\$JAVA_HOME/bin/java\\\" or \\\"\\$JAVA_HOME/bin/javac\\\" does not exist.\" >&2\n        return 1\n      fi\n    fi\n  else\n    JAVACMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v java\n    )\" || :\n    JAVACCMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v javac\n    )\" || :\n\n    if [ ! -x \"${JAVACMD-}\" ] || [ ! -x \"${JAVACCMD-}\" ]; then\n      echo \"The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run.\" >&2\n      return 1\n    fi\n  fi\n}\n\n# hash string like Java String::hashCode\nhash_string() {\n  str=\"${1:-}\" h=0\n  while [ -n \"$str\" ]; do\n    char=\"${str%\"${str#?}\"}\"\n    h=$(((h * 31 + $(LC_CTYPE=C printf %d \"'$char\")) % 4294967296))\n    str=\"${str#?}\"\n  done\n  printf %x\\\\n $h\n}\n\nverbose() { :; }\n[ \"${MVNW_VERBOSE-}\" != true ] || verbose() { printf %s\\\\n \"${1-}\"; }\n\ndie() {\n  printf %s\\\\n \"$1\" >&2\n  exit 1\n}\n\ntrim() {\n  # MWRAPPER-139:\n  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.\n  #   Needed for removing poorly interpreted newline sequences when running in more\n  #   exotic environments such as mingw bash on Windows.\n  printf \"%s\" \"${1}\" | tr -d '[:space:]'\n}\n\nscriptDir=\"$(dirname \"$0\")\"\nscriptName=\"$(basename \"$0\")\"\n\n# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties\nwhile IFS=\"=\" read -r key value; do\n  case \"${key-}\" in\n  distributionUrl) distributionUrl=$(trim \"${value-}\") ;;\n  distributionSha256Sum) distributionSha256Sum=$(trim \"${value-}\") ;;\n  esac\ndone <\"$scriptDir/.mvn/wrapper/maven-wrapper.properties\"\n[ -n \"${distributionUrl-}\" ] || die \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\n\ncase \"${distributionUrl##*/}\" in\nmaven-mvnd-*bin.*)\n  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/\n  case \"${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)\" in\n  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;\n  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;\n  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;\n  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;\n  *)\n    echo \"Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version\" >&2\n    distributionPlatform=linux-amd64\n    ;;\n  esac\n  distributionUrl=\"${distributionUrl%-bin.*}-$distributionPlatform.zip\"\n  ;;\nmaven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;\n*) MVN_CMD=\"mvn${scriptName#mvnw}\" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;\nesac\n\n# apply MVNW_REPOURL and calculate MAVEN_HOME\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\n[ -z \"${MVNW_REPOURL-}\" ] || distributionUrl=\"$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*\"$_MVNW_REPO_PATTERN\"}\"\ndistributionUrlName=\"${distributionUrl##*/}\"\ndistributionUrlNameMain=\"${distributionUrlName%.*}\"\ndistributionUrlNameMain=\"${distributionUrlNameMain%-bin}\"\nMAVEN_USER_HOME=\"${MAVEN_USER_HOME:-${HOME}/.m2}\"\nMAVEN_HOME=\"${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string \"$distributionUrl\")\"\n\nexec_maven() {\n  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :\n  exec \"$MAVEN_HOME/bin/$MVN_CMD\" \"$@\" || die \"cannot exec $MAVEN_HOME/bin/$MVN_CMD\"\n}\n\nif [ -d \"$MAVEN_HOME\" ]; then\n  verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\n  exec_maven \"$@\"\nfi\n\ncase \"${distributionUrl-}\" in\n*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;\n*) die \"distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'\" ;;\nesac\n\n# prepare tmp dir\nif TMP_DOWNLOAD_DIR=\"$(mktemp -d)\" && [ -d \"$TMP_DOWNLOAD_DIR\" ]; then\n  clean() { rm -rf -- \"$TMP_DOWNLOAD_DIR\"; }\n  trap clean HUP INT TERM EXIT\nelse\n  die \"cannot create temp dir\"\nfi\n\nmkdir -p -- \"${MAVEN_HOME%/*}\"\n\n# Download and Install Apache Maven\nverbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\nverbose \"Downloading from: $distributionUrl\"\nverbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\n\n# select .zip or .tar.gz\nif ! command -v unzip >/dev/null; then\n  distributionUrl=\"${distributionUrl%.zip}.tar.gz\"\n  distributionUrlName=\"${distributionUrl##*/}\"\nfi\n\n# verbose opt\n__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''\n[ \"${MVNW_VERBOSE-}\" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v\n\n# normalize http auth\ncase \"${MVNW_PASSWORD:+has-password}\" in\n'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nhas-password) [ -n \"${MVNW_USERNAME-}\" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nesac\n\nif [ -z \"${MVNW_USERNAME-}\" ] && command -v wget >/dev/null; then\n  verbose \"Found wget ... using wget\"\n  wget ${__MVNW_QUIET_WGET:+\"$__MVNW_QUIET_WGET\"} \"$distributionUrl\" -O \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" || die \"wget: Failed to fetch $distributionUrl\"\nelif [ -z \"${MVNW_USERNAME-}\" ] && command -v curl >/dev/null; then\n  verbose \"Found curl ... using curl\"\n  curl ${__MVNW_QUIET_CURL:+\"$__MVNW_QUIET_CURL\"} -f -L -o \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" \"$distributionUrl\" || die \"curl: Failed to fetch $distributionUrl\"\nelif set_java_home; then\n  verbose \"Falling back to use Java to download\"\n  javaSource=\"$TMP_DOWNLOAD_DIR/Downloader.java\"\n  targetZip=\"$TMP_DOWNLOAD_DIR/$distributionUrlName\"\n  cat >\"$javaSource\" <<-END\n\t\tpublic class Downloader extends java.net.Authenticator\n\t\t{\n\t\t  protected java.net.PasswordAuthentication getPasswordAuthentication()\n\t\t  {\n\t\t    return new java.net.PasswordAuthentication( System.getenv( \"MVNW_USERNAME\" ), System.getenv( \"MVNW_PASSWORD\" ).toCharArray() );\n\t\t  }\n\t\t  public static void main( String[] args ) throws Exception\n\t\t  {\n\t\t    setDefault( new Downloader() );\n\t\t    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );\n\t\t  }\n\t\t}\n\tEND\n  # For Cygwin/MinGW, switch paths to Windows format before running javac and java\n  verbose \" - Compiling Downloader.java ...\"\n  \"$(native_path \"$JAVACCMD\")\" \"$(native_path \"$javaSource\")\" || die \"Failed to compile Downloader.java\"\n  verbose \" - Running Downloader.java ...\"\n  \"$(native_path \"$JAVACMD\")\" -cp \"$(native_path \"$TMP_DOWNLOAD_DIR\")\" Downloader \"$distributionUrl\" \"$(native_path \"$targetZip\")\"\nfi\n\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\nif [ -n \"${distributionSha256Sum-}\" ]; then\n  distributionSha256Result=false\n  if [ \"$MVN_CMD\" = mvnd.sh ]; then\n    echo \"Checksum validation is not supported for maven-mvnd.\" >&2\n    echo \"Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  elif command -v sha256sum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | sha256sum -c - >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  elif command -v shasum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | shasum -a 256 -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\" >&2\n    echo \"Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  fi\n  if [ $distributionSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified distributionSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\n# unzip and move\nif command -v unzip >/dev/null; then\n  unzip ${__MVNW_QUIET_UNZIP:+\"$__MVNW_QUIET_UNZIP\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -d \"$TMP_DOWNLOAD_DIR\" || die \"failed to unzip\"\nelse\n  tar xzf${__MVNW_QUIET_TAR:+\"$__MVNW_QUIET_TAR\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -C \"$TMP_DOWNLOAD_DIR\" || die \"failed to untar\"\nfi\n\n# Find the actual extracted directory name (handles snapshots where filename != directory name)\nactualDistributionDir=\"\"\n\n# First try the expected directory name (for regular distributions)\nif [ -d \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" ]; then\n  if [ -f \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD\" ]; then\n    actualDistributionDir=\"$distributionUrlNameMain\"\n  fi\nfi\n\n# If not found, search for any directory with the Maven executable (for snapshots)\nif [ -z \"$actualDistributionDir\" ]; then\n  # enable globbing to iterate over items\n  set +f\n  for dir in \"$TMP_DOWNLOAD_DIR\"/*; do\n    if [ -d \"$dir\" ]; then\n      if [ -f \"$dir/bin/$MVN_CMD\" ]; then\n        actualDistributionDir=\"$(basename \"$dir\")\"\n        break\n      fi\n    fi\n  done\n  set -f\nfi\n\nif [ -z \"$actualDistributionDir\" ]; then\n  verbose \"Contents of $TMP_DOWNLOAD_DIR:\"\n  verbose \"$(ls -la \"$TMP_DOWNLOAD_DIR\")\"\n  die \"Could not find Maven distribution directory in extracted archive\"\nfi\n\nverbose \"Found extracted Maven distribution directory: $actualDistributionDir\"\nprintf %s\\\\n \"$distributionUrl\" >\"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url\"\nmv -- \"$TMP_DOWNLOAD_DIR/$actualDistributionDir\" \"$MAVEN_HOME\" || [ -d \"$MAVEN_HOME\" ] || die \"fail to move MAVEN_HOME\"\n\nclean || :\nexec_maven \"$@\"\n"
  },
  {
    "path": "test_apps/java/mvnw.cmd",
    "content": "<# : batch portion\r\n@REM ----------------------------------------------------------------------------\r\n@REM Licensed to the Apache Software Foundation (ASF) under one\r\n@REM or more contributor license agreements.  See the NOTICE file\r\n@REM distributed with this work for additional information\r\n@REM regarding copyright ownership.  The ASF licenses this file\r\n@REM to you under the Apache License, Version 2.0 (the\r\n@REM \"License\"); you may not use this file except in compliance\r\n@REM with the License.  You may obtain a copy of the License at\r\n@REM\r\n@REM    http://www.apache.org/licenses/LICENSE-2.0\r\n@REM\r\n@REM Unless required by applicable law or agreed to in writing,\r\n@REM software distributed under the License is distributed on an\r\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n@REM KIND, either express or implied.  See the License for the\r\n@REM specific language governing permissions and limitations\r\n@REM under the License.\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@REM ----------------------------------------------------------------------------\r\n@REM Apache Maven Wrapper startup batch script, version 3.3.4\r\n@REM\r\n@REM Optional ENV vars\r\n@REM   MVNW_REPOURL - repo url base for downloading maven distribution\r\n@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\r\n@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@IF \"%__MVNW_ARG0_NAME__%\"==\"\" (SET __MVNW_ARG0_NAME__=%~nx0)\r\n@SET __MVNW_CMD__=\r\n@SET __MVNW_ERROR__=\r\n@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%\r\n@SET PSModulePath=\r\n@FOR /F \"usebackq tokens=1* delims==\" %%A IN (`powershell -noprofile \"& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}\"`) DO @(\r\n  IF \"%%A\"==\"MVN_CMD\" (set __MVNW_CMD__=%%B) ELSE IF \"%%B\"==\"\" (echo %%A) ELSE (echo %%A=%%B)\r\n)\r\n@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%\r\n@SET __MVNW_PSMODULEP_SAVE=\r\n@SET __MVNW_ARG0_NAME__=\r\n@SET MVNW_USERNAME=\r\n@SET MVNW_PASSWORD=\r\n@IF NOT \"%__MVNW_CMD__%\"==\"\" (\"%__MVNW_CMD__%\" %*)\r\n@echo Cannot start maven from wrapper >&2 && exit /b 1\r\n@GOTO :EOF\r\n: end batch / begin powershell #>\r\n\r\n$ErrorActionPreference = \"Stop\"\r\nif ($env:MVNW_VERBOSE -eq \"true\") {\r\n  $VerbosePreference = \"Continue\"\r\n}\r\n\r\n# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties\r\n$distributionUrl = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionUrl\r\nif (!$distributionUrl) {\r\n  Write-Error \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\r\n}\r\n\r\nswitch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {\r\n  \"maven-mvnd-*\" {\r\n    $USE_MVND = $true\r\n    $distributionUrl = $distributionUrl -replace '-bin\\.[^.]*$',\"-windows-amd64.zip\"\r\n    $MVN_CMD = \"mvnd.cmd\"\r\n    break\r\n  }\r\n  default {\r\n    $USE_MVND = $false\r\n    $MVN_CMD = $script -replace '^mvnw','mvn'\r\n    break\r\n  }\r\n}\r\n\r\n# apply MVNW_REPOURL and calculate MAVEN_HOME\r\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\r\nif ($env:MVNW_REPOURL) {\r\n  $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { \"/org/apache/maven/\" } else { \"/maven/mvnd/\" }\r\n  $distributionUrl = \"$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace \"^.*$MVNW_REPO_PATTERN\",'')\"\r\n}\r\n$distributionUrlName = $distributionUrl -replace '^.*/',''\r\n$distributionUrlNameMain = $distributionUrlName -replace '\\.[^.]*$','' -replace '-bin$',''\r\n\r\n$MAVEN_M2_PATH = \"$HOME/.m2\"\r\nif ($env:MAVEN_USER_HOME) {\r\n  $MAVEN_M2_PATH = \"$env:MAVEN_USER_HOME\"\r\n}\r\n\r\nif (-not (Test-Path -Path $MAVEN_M2_PATH)) {\r\n    New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null\r\n}\r\n\r\n$MAVEN_WRAPPER_DISTS = $null\r\nif ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {\r\n  $MAVEN_WRAPPER_DISTS = \"$MAVEN_M2_PATH/wrapper/dists\"\r\n} else {\r\n  $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + \"/wrapper/dists\"\r\n}\r\n\r\n$MAVEN_HOME_PARENT = \"$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain\"\r\n$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString(\"x2\")}) -join ''\r\n$MAVEN_HOME = \"$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME\"\r\n\r\nif (Test-Path -Path \"$MAVEN_HOME\" -PathType Container) {\r\n  Write-Verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\r\n  Write-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n  exit $?\r\n}\r\n\r\nif (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {\r\n  Write-Error \"distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl\"\r\n}\r\n\r\n# prepare tmp dir\r\n$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile\r\n$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path \"$TMP_DOWNLOAD_DIR_HOLDER.dir\"\r\n$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null\r\ntrap {\r\n  if ($TMP_DOWNLOAD_DIR.Exists) {\r\n    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n    catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n  }\r\n}\r\n\r\nNew-Item -Itemtype Directory -Path \"$MAVEN_HOME_PARENT\" -Force | Out-Null\r\n\r\n# Download and Install Apache Maven\r\nWrite-Verbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\r\nWrite-Verbose \"Downloading from: $distributionUrl\"\r\nWrite-Verbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\r\n\r\n$webclient = New-Object System.Net.WebClient\r\nif ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {\r\n  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)\r\n}\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n$webclient.DownloadFile($distributionUrl, \"$TMP_DOWNLOAD_DIR/$distributionUrlName\") | Out-Null\r\n\r\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\r\n$distributionSha256Sum = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionSha256Sum\r\nif ($distributionSha256Sum) {\r\n  if ($USE_MVND) {\r\n    Write-Error \"Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\"\r\n  }\r\n  Import-Module $PSHOME\\Modules\\Microsoft.PowerShell.Utility -Function Get-FileHash\r\n  if ((Get-FileHash \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {\r\n    Write-Error \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property.\"\r\n  }\r\n}\r\n\r\n# unzip and move\r\nExpand-Archive \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -DestinationPath \"$TMP_DOWNLOAD_DIR\" | Out-Null\r\n\r\n# Find the actual extracted directory name (handles snapshots where filename != directory name)\r\n$actualDistributionDir = \"\"\r\n\r\n# First try the expected directory name (for regular distributions)\r\n$expectedPath = Join-Path \"$TMP_DOWNLOAD_DIR\" \"$distributionUrlNameMain\"\r\n$expectedMvnPath = Join-Path \"$expectedPath\" \"bin/$MVN_CMD\"\r\nif ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {\r\n  $actualDistributionDir = $distributionUrlNameMain\r\n}\r\n\r\n# If not found, search for any directory with the Maven executable (for snapshots)\r\nif (!$actualDistributionDir) {\r\n  Get-ChildItem -Path \"$TMP_DOWNLOAD_DIR\" -Directory | ForEach-Object {\r\n    $testPath = Join-Path $_.FullName \"bin/$MVN_CMD\"\r\n    if (Test-Path -Path $testPath -PathType Leaf) {\r\n      $actualDistributionDir = $_.Name\r\n    }\r\n  }\r\n}\r\n\r\nif (!$actualDistributionDir) {\r\n  Write-Error \"Could not find Maven distribution directory in extracted archive\"\r\n}\r\n\r\nWrite-Verbose \"Found extracted Maven distribution directory: $actualDistributionDir\"\r\nRename-Item -Path \"$TMP_DOWNLOAD_DIR/$actualDistributionDir\" -NewName $MAVEN_HOME_NAME | Out-Null\r\ntry {\r\n  Move-Item -Path \"$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME\" -Destination $MAVEN_HOME_PARENT | Out-Null\r\n} catch {\r\n  if (! (Test-Path -Path \"$MAVEN_HOME\" -PathType Container)) {\r\n    Write-Error \"fail to move MAVEN_HOME\"\r\n  }\r\n} finally {\r\n  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n  catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n}\r\n\r\nWrite-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n"
  },
  {
    "path": "test_apps/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>dev.kreuzberg.htmltomarkdown</groupId>\n    <artifactId>html-to-markdown-e2e-java</artifactId>\n    <version>0.1.0</version>\n\n    <properties>\n        <maven.compiler.source>25</maven.compiler.source>\n        <maven.compiler.target>25</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <junit.version>6.0.3</junit.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>dev.kreuzberg</groupId>\n            <artifactId>html-to-markdown</artifactId>\n            <version>3.4.0-rc.22</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <version>2.18.2</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <version>2.18.2</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <version>3.6.1</version>\n                <executions>\n                    <execution>\n                        <id>add-test-source</id>\n                        <phase>generate-test-sources</phase>\n                        <goals>\n                            <goal>add-test-source</goal>\n                        </goals>\n                        <configuration>\n                            <sources>\n                                <source>src/test/java</source>\n                            </sources>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>3.5.2</version>\n                <configuration>\n                    <argLine>--enable-preview --enable-native-access=ALL-UNNAMED -Djava.library.path=../../target/release</argLine>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "test_apps/java/src/test/java/dev/kreuzberg/e2e/ConversionTest.java",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\npackage dev.kreuzberg.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\n\n/** E2e tests for category: conversion. */\nclass ConversionTest {\n    @Test\n    void testBlockquoteMultipleParagraphs() throws Exception {\n        // Blockquote with multiple paragraphs has each paragraph prefixed\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\");\n        assertTrue(result.content().orElse(\"\").contains(\"> First paragraph.\"), \"expected to contain: \" + \"> First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"> Second paragraph.\"), \"expected to contain: \" + \"> Second paragraph.\");\n    }\n\n    @Test\n    void testBlockquoteNested() throws Exception {\n        // Nested blockquote produces double-prefixed lines\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Outer quote.\"), \"expected to contain: \" + \"Outer quote.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Inner quote.\"), \"expected to contain: \" + \"Inner quote.\");\n    }\n\n    @Test\n    void testBlockquoteSimple() throws Exception {\n        // Simple blockquote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote text</p></blockquote>\");\n        assertTrue(result.content().orElse(\"\").contains(\"> Quote text\"), \"expected to contain: \" + \"> Quote text\");\n    }\n\n    @Test\n    void testBlockquoteWithList() throws Exception {\n        // Blockquote containing a list preserves list items inside quote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Quote intro:\"), \"expected to contain: \" + \"Quote intro:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point one\"), \"expected to contain: \" + \"Point one\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point two\"), \"expected to contain: \" + \"Point two\");\n    }\n\n    @Test\n    void testBoldAndItalic() throws Exception {\n        // Nested bold and italic\n        var result = HtmlToMarkdown.convert(\"<p><strong><em>both</em></strong></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"***both***\"), \"expected to contain: \" + \"***both***\");\n    }\n\n    @Test\n    void testBoldStrong() throws Exception {\n        // Strong tag converts to bold\n        var result = HtmlToMarkdown.convert(\"<p><strong>bold</strong></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n    }\n\n    @Test\n    void testCodeBlock() throws Exception {\n        // Code block with language preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"print('hello')\"), \"expected to contain: \" + \"print('hello')\");\n    }\n\n    @Test\n    void testCodeBlockNoLanguage() throws Exception {\n        // Code block without a language class preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code>plain code here</code></pre>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"plain code here\"), \"expected to contain: \" + \"plain code here\");\n    }\n\n    @Test\n    void testCodeInlineInParagraph() throws Exception {\n        // Inline code element nested inside a paragraph\n        var result = HtmlToMarkdown.convert(\"<p>Call the <code>initialize()</code> method first.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"`initialize()`\"), \"expected to contain: \" + \"`initialize()`\");\n    }\n\n    @Test\n    void testCodeWithBackticksInContent() throws Exception {\n        // Inline code containing backtick characters is properly escaped\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"backtick\"), \"expected to contain: \" + \"backtick\");\n    }\n\n    @Test\n    void testEmphasisMarkHighlight() throws Exception {\n        // mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p><mark>highlighted</mark></p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted\"), \"expected to contain: \" + \"highlighted\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughDel() throws Exception {\n        // del tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><del>deleted text</del></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"~~deleted text~~\"), \"expected to contain: \" + \"~~deleted text~~\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughS() throws Exception {\n        // s tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><s>strikethrough</s></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"~~strikethrough~~\"), \"expected to contain: \" + \"~~strikethrough~~\");\n    }\n\n    @Test\n    void testEmphasisSubscript() throws Exception {\n        // sub tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n    }\n\n    @Test\n    void testEmphasisSuperscript() throws Exception {\n        // sup tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>x<sup>2</sup></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"x\"), \"expected to contain: \" + \"x\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n    }\n\n    @Test\n    void testEmphasisUnderlineU() throws Exception {\n        // u tag content is preserved in output\n        var result = HtmlToMarkdown.convert(\"<p><u>underlined</u></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"underlined\"), \"expected to contain: \" + \"underlined\");\n    }\n\n    @Test\n    void testFormInputElements() throws Exception {\n        // Form input elements produce readable output without form mechanics\n        var result = HtmlToMarkdown.convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n    }\n\n    @Test\n    void testFormSelectOptions() throws Exception {\n        // Select element with options produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Color\"), \"expected to contain: \" + \"Color\");\n    }\n\n    @Test\n    void testFormTextarea() throws Exception {\n        // Textarea element produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Message\"), \"expected to contain: \" + \"Message\");\n    }\n\n    @Test\n    void testHeadingH1() throws Exception {\n        // H1 heading\n        var result = HtmlToMarkdown.convert(\"<h1>Heading 1</h1>\");\n        assertEquals(\"# Heading 1\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH2() throws Exception {\n        // H2 heading\n        var result = HtmlToMarkdown.convert(\"<h2>Heading 2</h2>\");\n        assertEquals(\"## Heading 2\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH3() throws Exception {\n        // H3 heading\n        var result = HtmlToMarkdown.convert(\"<h3>Heading 3</h3>\");\n        assertEquals(\"### Heading 3\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH4() throws Exception {\n        // H4 heading\n        var result = HtmlToMarkdown.convert(\"<h4>Heading 4</h4>\");\n        assertEquals(\"#### Heading 4\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH5() throws Exception {\n        // H5 heading\n        var result = HtmlToMarkdown.convert(\"<h5>Heading 5</h5>\");\n        assertEquals(\"##### Heading 5\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH6() throws Exception {\n        // H6 heading\n        var result = HtmlToMarkdown.convert(\"<h6>Heading 6</h6>\");\n        assertEquals(\"###### Heading 6\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testImageFigureFigcaption() throws Exception {\n        // Figure with figcaption preserves both image and caption\n        var result = HtmlToMarkdown.convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\");\n        assertTrue(result.content().orElse(\"\").contains(\"![A sunset](sunset.jpg)\"), \"expected to contain: \" + \"![A sunset](sunset.jpg)\");\n        assertTrue(result.content().orElse(\"\").contains(\"Beautiful sunset over the ocean\"), \"expected to contain: \" + \"Beautiful sunset over the ocean\");\n    }\n\n    @Test\n    void testImageLinked() throws Exception {\n        // Image inside an anchor produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"![Icon](icon.png)\"), \"expected to contain: \" + \"![Icon](icon.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testImageNoAlt() throws Exception {\n        // Image without alt text produces image markdown\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"banner.jpg\\\">\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"banner.jpg\"), \"expected to contain: \" + \"banner.jpg\");\n    }\n\n    @Test\n    void testImageSimple() throws Exception {\n        // Image with alt text\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\");\n        assertTrue(result.content().orElse(\"\").contains(\"![A photo](photo.jpg)\"), \"expected to contain: \" + \"![A photo](photo.jpg)\");\n    }\n\n    @Test\n    void testImageWithTitle() throws Exception {\n        // Image with title attribute includes title in output\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\");\n        assertTrue(result.content().orElse(\"\").contains(\"![Sales chart](chart.png\"), \"expected to contain: \" + \"![Sales chart](chart.png\");\n        assertTrue(result.content().orElse(\"\").contains(\"Q3 Sales\"), \"expected to contain: \" + \"Q3 Sales\");\n    }\n\n    @Test\n    void testInlineCode() throws Exception {\n        // Inline code\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>console.log()</code> to debug</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"`console.log()`\"), \"expected to contain: \" + \"`console.log()`\");\n    }\n\n    @Test\n    void testItalicEm() throws Exception {\n        // Em tag converts to italic\n        var result = HtmlToMarkdown.convert(\"<p><em>italic</em></p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n    }\n\n    @Test\n    void testLineBreakBrTag() throws Exception {\n        // Single br tag produces a line break in output\n        var result = HtmlToMarkdown.convert(\"<p>First line.<br>Second line.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"First line.\"), \"expected to contain: \" + \"First line.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second line.\"), \"expected to contain: \" + \"Second line.\");\n    }\n\n    @Test\n    void testLineBreakHrTag() throws Exception {\n        // hr tag produces a horizontal separator between content\n        var result = HtmlToMarkdown.convert(\"<p>Before rule.</p><hr><p>After rule.</p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Before rule.\"), \"expected to contain: \" + \"Before rule.\");\n        assertTrue(result.content().orElse(\"\").contains(\"After rule.\"), \"expected to contain: \" + \"After rule.\");\n    }\n\n    @Test\n    void testLineBreakMultipleBr() throws Exception {\n        // Multiple consecutive br tags in sequence\n        var result = HtmlToMarkdown.convert(\"<p>Start.<br><br>End.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Start.\"), \"expected to contain: \" + \"Start.\");\n        assertTrue(result.content().orElse(\"\").contains(\"End.\"), \"expected to contain: \" + \"End.\");\n    }\n\n    @Test\n    void testLinkAnchorFragment() throws Exception {\n        // Fragment-only anchor link is preserved\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"#section\\\">Jump to section</a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"[Jump to section](#section)\"), \"expected to contain: \" + \"[Jump to section](#section)\");\n    }\n\n    @Test\n    void testLinkEmptyHref() throws Exception {\n        // Link with empty href produces output with the link text\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"\\\">No destination</a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"No destination\"), \"expected to contain: \" + \"No destination\");\n    }\n\n    @Test\n    void testLinkImageInside() throws Exception {\n        // Image inside a link produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"![Logo](logo.png)\"), \"expected to contain: \" + \"![Logo](logo.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkMailto() throws Exception {\n        // Mailto link is preserved with mailto: scheme\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"mailto:user@example.com\"), \"expected to contain: \" + \"mailto:user@example.com\");\n    }\n\n    @Test\n    void testLinkSimple() throws Exception {\n        // Simple link\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\">Example</a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com)\"), \"expected to contain: \" + \"[Example](https://example.com)\");\n    }\n\n    @Test\n    void testLinkWithBoldText() throws Exception {\n        // Link containing bold text preserves formatting\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"**Bold link**\"), \"expected to contain: \" + \"**Bold link**\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkWithTitle() throws Exception {\n        // Link with title attribute\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\");\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com\"), \"expected to contain: \" + \"[Example](https://example.com\");\n        assertTrue(result.content().orElse(\"\").contains(\"Example Site\"), \"expected to contain: \" + \"Example Site\");\n    }\n\n    @Test\n    void testListDefinitionDl() throws Exception {\n        // Definition list with dt and dd elements\n        var result = HtmlToMarkdown.convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Term One\"), \"expected to contain: \" + \"Term One\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term one.\"), \"expected to contain: \" + \"Definition of term one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Term Two\"), \"expected to contain: \" + \"Term Two\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term two.\"), \"expected to contain: \" + \"Definition of term two.\");\n    }\n\n    @Test\n    void testListItemMultipleParagraphs() throws Exception {\n        // List item containing multiple paragraphs\n        var result = HtmlToMarkdown.convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\");\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph in item.\"), \"expected to contain: \" + \"First paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph in item.\"), \"expected to contain: \" + \"Second paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Simple item\"), \"expected to contain: \" + \"Simple item\");\n    }\n\n    @Test\n    void testListMixedNested() throws Exception {\n        // Mixed list: ordered list nested inside unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Item A\"), \"expected to contain: \" + \"Item A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 1\"), \"expected to contain: \" + \"Sub 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 2\"), \"expected to contain: \" + \"Sub 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Item B\"), \"expected to contain: \" + \"Item B\");\n    }\n\n    @Test\n    void testListNestedOrdered() throws Exception {\n        // Nested ordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1\"), \"expected to contain: \" + \"Step 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1a\"), \"expected to contain: \" + \"Step 1a\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1b\"), \"expected to contain: \" + \"Step 1b\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 2\"), \"expected to contain: \" + \"Step 2\");\n    }\n\n    @Test\n    void testListNestedUnordered() throws Exception {\n        // Nested unordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Parent A\"), \"expected to contain: \" + \"Parent A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A1\"), \"expected to contain: \" + \"Child A1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A2\"), \"expected to contain: \" + \"Child A2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Parent B\"), \"expected to contain: \" + \"Parent B\");\n    }\n\n    @Test\n    void testListTaskCheckboxes() throws Exception {\n        // Task list with checked and unchecked checkboxes\n        var result = HtmlToMarkdown.convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Done task\"), \"expected to contain: \" + \"Done task\");\n        assertTrue(result.content().orElse(\"\").contains(\"Pending task\"), \"expected to contain: \" + \"Pending task\");\n    }\n\n    @Test\n    void testOrderedList() throws Exception {\n        // Ordered list\n        var result = HtmlToMarkdown.convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\");\n        assertTrue(result.content().orElse(\"\").contains(\"1. First\"), \"expected to contain: \" + \"1. First\");\n        assertTrue(result.content().orElse(\"\").contains(\"2. Second\"), \"expected to contain: \" + \"2. Second\");\n        assertTrue(result.content().orElse(\"\").contains(\"3. Third\"), \"expected to contain: \" + \"3. Third\");\n    }\n\n    @Test\n    void testParagraphMultiple() throws Exception {\n        // Multiple paragraphs are separated by a blank line\n        var result = HtmlToMarkdown.convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph.\"), \"expected to contain: \" + \"First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph.\"), \"expected to contain: \" + \"Second paragraph.\");\n    }\n\n    @Test\n    void testParagraphNestedDivs() throws Exception {\n        // Text nested inside divs is extracted correctly\n        var result = HtmlToMarkdown.convert(\"<div><div><p>Nested text</p></div></div>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Nested text\"), \"expected to contain: \" + \"Nested text\");\n    }\n\n    @Test\n    void testParagraphSimple() throws Exception {\n        // Simple paragraph converts to plain text\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\");\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testParagraphWithInlineFormatting() throws Exception {\n        // Paragraph with bold, italic, and a link\n        var result = HtmlToMarkdown.convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n        assertTrue(result.content().orElse(\"\").contains(\"[link](https://example.com)\"), \"expected to contain: \" + \"[link](https://example.com)\");\n    }\n\n    @Test\n    void testParagraphWithLineBreaks() throws Exception {\n        // Paragraph with br tags produces line breaks in output\n        var result = HtmlToMarkdown.convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line one.\"), \"expected to contain: \" + \"Line one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line two.\"), \"expected to contain: \" + \"Line two.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line three.\"), \"expected to contain: \" + \"Line three.\");\n    }\n\n    @Test\n    void testSemanticAbbr() throws Exception {\n        // Abbreviation element text is preserved\n        var result = HtmlToMarkdown.convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\");\n        assertTrue(result.content().orElse(\"\").contains(\"WWW\"), \"expected to contain: \" + \"WWW\");\n    }\n\n    @Test\n    void testSemanticArticle() throws Exception {\n        // Article element wrapping content preserves inner content\n        var result = HtmlToMarkdown.convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Article Title\"), \"expected to contain: \" + \"Article Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Article body.\"), \"expected to contain: \" + \"Article body.\");\n    }\n\n    @Test\n    void testSemanticDefinitionList() throws Exception {\n        // Definition list with term and description\n        var result = HtmlToMarkdown.convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\");\n        assertTrue(result.content().orElse(\"\").contains(\"HTML\"), \"expected to contain: \" + \"HTML\");\n        assertTrue(result.content().orElse(\"\").contains(\"HyperText Markup Language\"), \"expected to contain: \" + \"HyperText Markup Language\");\n        assertTrue(result.content().orElse(\"\").contains(\"CSS\"), \"expected to contain: \" + \"CSS\");\n        assertTrue(result.content().orElse(\"\").contains(\"Cascading Style Sheets\"), \"expected to contain: \" + \"Cascading Style Sheets\");\n    }\n\n    @Test\n    void testSemanticDetailsSummary() throws Exception {\n        // Details and summary elements produce readable output\n        var result = HtmlToMarkdown.convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Click to expand\"), \"expected to contain: \" + \"Click to expand\");\n    }\n\n    @Test\n    void testSemanticHr() throws Exception {\n        // Horizontal rule produces a separator in output\n        var result = HtmlToMarkdown.convert(\"<p>Above</p><hr><p>Below</p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Above\"), \"expected to contain: \" + \"Above\");\n        assertTrue(result.content().orElse(\"\").contains(\"Below\"), \"expected to contain: \" + \"Below\");\n    }\n\n    @Test\n    void testSemanticMarkHighlight() throws Exception {\n        // Mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted text\"), \"expected to contain: \" + \"highlighted text\");\n    }\n\n    @Test\n    void testSemanticSectionWithHeading() throws Exception {\n        // Section element with heading preserves structure\n        var result = HtmlToMarkdown.convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section Heading\"), \"expected to contain: \" + \"Section Heading\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section content.\"), \"expected to contain: \" + \"Section content.\");\n    }\n\n    @Test\n    void testSemanticSubSuperscript() throws Exception {\n        // Subscript and superscript elements are preserved in output\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n        assertTrue(result.content().orElse(\"\").contains(\"E=mc\"), \"expected to contain: \" + \"E=mc\");\n    }\n\n    @Test\n    void testSimpleTable() throws Exception {\n        // Simple table with header\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\");\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"Age\"), \"expected to contain: \" + \"Age\");\n        assertTrue(result.content().orElse(\"\").contains(\"Alice\"), \"expected to contain: \" + \"Alice\");\n        assertTrue(result.content().orElse(\"\").contains(\"30\"), \"expected to contain: \" + \"30\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n        assertTrue(result.content().orElse(\"\").contains(\"---\"), \"expected to contain: \" + \"---\");\n    }\n\n    @Test\n    void testTableEmpty() throws Exception {\n        // Empty table produces no output or minimal output\n        var result = HtmlToMarkdown.convert(\"<table></table>\");\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testTableNoThead() throws Exception {\n        // Table without thead uses first row as implied header\n        var result = HtmlToMarkdown.convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Product\"), \"expected to contain: \" + \"Product\");\n        assertTrue(result.content().orElse(\"\").contains(\"Price\"), \"expected to contain: \" + \"Price\");\n        assertTrue(result.content().orElse(\"\").contains(\"Apple\"), \"expected to contain: \" + \"Apple\");\n        assertTrue(result.content().orElse(\"\").contains(\"1.00\"), \"expected to contain: \" + \"1.00\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTablePipeCharsInContent() throws Exception {\n        // Table cells containing pipe characters are escaped in output\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Expression\"), \"expected to contain: \" + \"Expression\");\n        assertTrue(result.content().orElse(\"\").contains(\"Result\"), \"expected to contain: \" + \"Result\");\n        assertTrue(result.content().orElse(\"\").contains(\"true\"), \"expected to contain: \" + \"true\");\n    }\n\n    @Test\n    void testTableWithAlignment() throws Exception {\n        // Table with column alignment attributes\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Left\"), \"expected to contain: \" + \"Left\");\n        assertTrue(result.content().orElse(\"\").contains(\"Center\"), \"expected to contain: \" + \"Center\");\n        assertTrue(result.content().orElse(\"\").contains(\"Right\"), \"expected to contain: \" + \"Right\");\n        assertTrue(result.content().orElse(\"\").contains(\"L\"), \"expected to contain: \" + \"L\");\n        assertTrue(result.content().orElse(\"\").contains(\"C\"), \"expected to contain: \" + \"C\");\n        assertTrue(result.content().orElse(\"\").contains(\"R\"), \"expected to contain: \" + \"R\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTableWithColspan() throws Exception {\n        // Table with colspan attribute in a header cell\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\");\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Full Name\"), \"expected to contain: \" + \"Full Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"John\"), \"expected to contain: \" + \"John\");\n        assertTrue(result.content().orElse(\"\").contains(\"Doe\"), \"expected to contain: \" + \"Doe\");\n    }\n\n    @Test\n    void testUnorderedList() throws Exception {\n        // Unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 1\"), \"expected to contain: \" + \"- Item 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 2\"), \"expected to contain: \" + \"- Item 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 3\"), \"expected to contain: \" + \"- Item 3\");\n    }\n\n}\n"
  },
  {
    "path": "test_apps/java/src/test/java/dev/kreuzberg/e2e/SmokeTest.java",
    "content": "// This file is auto-generated by alef. DO NOT EDIT.\npackage dev.kreuzberg.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\n\n/** E2e tests for category: smoke. */\nclass SmokeTest {\n    @Test\n    void testSmokeEmptyString() throws Exception {\n        // Empty string produces empty output\n        var result = HtmlToMarkdown.convert(\"\");\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testSmokeSimpleHeading() throws Exception {\n        // H1 heading converts to ATX markdown\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1>\");\n        assertTrue(result.content().orElse(\"\").contains(\"# Title\"), \"expected to contain: \" + \"# Title\");\n    }\n\n    @Test\n    void testSmokeSimpleParagraph() throws Exception {\n        // Simple paragraph converts correctly\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\");\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n        assertFalse(result.content().orElse(\"\").isEmpty(), \"expected non-empty value\");\n    }\n\n}\n"
  },
  {
    "path": "test_apps/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/ConversionTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6fbde233307e2baf76c8aae650b18807793f24d0e0c1a2312db9ce08a84eaa66\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: conversion. */\nclass ConversionTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testBlockquoteMultipleParagraphs() throws Exception {\n        // Blockquote with multiple paragraphs has each paragraph prefixed\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"> First paragraph.\"), \"expected to contain: \" + \"> First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"> Second paragraph.\"), \"expected to contain: \" + \"> Second paragraph.\");\n    }\n\n    @Test\n    void testBlockquoteNested() throws Exception {\n        // Nested blockquote produces double-prefixed lines\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Outer quote.\"), \"expected to contain: \" + \"Outer quote.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Inner quote.\"), \"expected to contain: \" + \"Inner quote.\");\n    }\n\n    @Test\n    void testBlockquoteSimple() throws Exception {\n        // Simple blockquote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote text</p></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"> Quote text\"), \"expected to contain: \" + \"> Quote text\");\n    }\n\n    @Test\n    void testBlockquoteWithList() throws Exception {\n        // Blockquote containing a list preserves list items inside quote\n        var result = HtmlToMarkdown.convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Quote intro:\"), \"expected to contain: \" + \"Quote intro:\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point one\"), \"expected to contain: \" + \"Point one\");\n        assertTrue(result.content().orElse(\"\").contains(\"Point two\"), \"expected to contain: \" + \"Point two\");\n    }\n\n    @Test\n    void testBoldAndItalic() throws Exception {\n        // Nested bold and italic\n        var result = HtmlToMarkdown.convert(\"<p><strong><em>both</em></strong></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"***both***\"), \"expected to contain: \" + \"***both***\");\n    }\n\n    @Test\n    void testBoldStrong() throws Exception {\n        // Strong tag converts to bold\n        var result = HtmlToMarkdown.convert(\"<p><strong>bold</strong></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n    }\n\n    @Test\n    void testCodeBlock() throws Exception {\n        // Code block with language preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"print('hello')\"), \"expected to contain: \" + \"print('hello')\");\n    }\n\n    @Test\n    void testCodeBlockNoLanguage() throws Exception {\n        // Code block without a language class preserves content\n        var result = HtmlToMarkdown.convert(\"<pre><code>plain code here</code></pre>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"plain code here\"), \"expected to contain: \" + \"plain code here\");\n    }\n\n    @Test\n    void testCodeInlineInParagraph() throws Exception {\n        // Inline code element nested inside a paragraph\n        var result = HtmlToMarkdown.convert(\"<p>Call the <code>initialize()</code> method first.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"`initialize()`\"), \"expected to contain: \" + \"`initialize()`\");\n    }\n\n    @Test\n    void testCodeWithBackticksInContent() throws Exception {\n        // Inline code containing backtick characters is properly escaped\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"backtick\"), \"expected to contain: \" + \"backtick\");\n    }\n\n    @Test\n    void testEmphasisMarkHighlight() throws Exception {\n        // mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p><mark>highlighted</mark></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted\"), \"expected to contain: \" + \"highlighted\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughDel() throws Exception {\n        // del tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><del>deleted text</del></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"~~deleted text~~\"), \"expected to contain: \" + \"~~deleted text~~\");\n    }\n\n    @Test\n    void testEmphasisStrikethroughS() throws Exception {\n        // s tag converts to GFM strikethrough\n        var result = HtmlToMarkdown.convert(\"<p><s>strikethrough</s></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"~~strikethrough~~\"), \"expected to contain: \" + \"~~strikethrough~~\");\n    }\n\n    @Test\n    void testEmphasisSubscript() throws Exception {\n        // sub tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n    }\n\n    @Test\n    void testEmphasisSuperscript() throws Exception {\n        // sup tag content is preserved\n        var result = HtmlToMarkdown.convert(\"<p>x<sup>2</sup></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"x\"), \"expected to contain: \" + \"x\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n    }\n\n    @Test\n    void testEmphasisUnderlineU() throws Exception {\n        // u tag content is preserved in output\n        var result = HtmlToMarkdown.convert(\"<p><u>underlined</u></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"underlined\"), \"expected to contain: \" + \"underlined\");\n    }\n\n    @Test\n    void testFormInputElements() throws Exception {\n        // Form input elements produce readable output without form mechanics\n        var result = HtmlToMarkdown.convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n    }\n\n    @Test\n    void testFormSelectOptions() throws Exception {\n        // Select element with options produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Color\"), \"expected to contain: \" + \"Color\");\n    }\n\n    @Test\n    void testFormTextarea() throws Exception {\n        // Textarea element produces readable output\n        var result = HtmlToMarkdown.convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Message\"), \"expected to contain: \" + \"Message\");\n    }\n\n    @Test\n    void testHeadingH1() throws Exception {\n        // H1 heading\n        var result = HtmlToMarkdown.convert(\"<h1>Heading 1</h1>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"# Heading 1\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH2() throws Exception {\n        // H2 heading\n        var result = HtmlToMarkdown.convert(\"<h2>Heading 2</h2>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"## Heading 2\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH3() throws Exception {\n        // H3 heading\n        var result = HtmlToMarkdown.convert(\"<h3>Heading 3</h3>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"### Heading 3\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH4() throws Exception {\n        // H4 heading\n        var result = HtmlToMarkdown.convert(\"<h4>Heading 4</h4>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"#### Heading 4\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH5() throws Exception {\n        // H5 heading\n        var result = HtmlToMarkdown.convert(\"<h5>Heading 5</h5>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"##### Heading 5\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testHeadingH6() throws Exception {\n        // H6 heading\n        var result = HtmlToMarkdown.convert(\"<h6>Heading 6</h6>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"###### Heading 6\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testImageFigureFigcaption() throws Exception {\n        // Figure with figcaption preserves both image and caption\n        var result = HtmlToMarkdown.convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![A sunset](sunset.jpg)\"), \"expected to contain: \" + \"![A sunset](sunset.jpg)\");\n        assertTrue(result.content().orElse(\"\").contains(\"Beautiful sunset over the ocean\"), \"expected to contain: \" + \"Beautiful sunset over the ocean\");\n    }\n\n    @Test\n    void testImageLinked() throws Exception {\n        // Image inside an anchor produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Icon](icon.png)\"), \"expected to contain: \" + \"![Icon](icon.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testImageNoAlt() throws Exception {\n        // Image without alt text produces image markdown\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"banner.jpg\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"banner.jpg\"), \"expected to contain: \" + \"banner.jpg\");\n    }\n\n    @Test\n    void testImageSimple() throws Exception {\n        // Image with alt text\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![A photo](photo.jpg)\"), \"expected to contain: \" + \"![A photo](photo.jpg)\");\n    }\n\n    @Test\n    void testImageWithTitle() throws Exception {\n        // Image with title attribute includes title in output\n        var result = HtmlToMarkdown.convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Sales chart](chart.png\"), \"expected to contain: \" + \"![Sales chart](chart.png\");\n        assertTrue(result.content().orElse(\"\").contains(\"Q3 Sales\"), \"expected to contain: \" + \"Q3 Sales\");\n    }\n\n    @Test\n    void testInlineCode() throws Exception {\n        // Inline code\n        var result = HtmlToMarkdown.convert(\"<p>Use <code>console.log()</code> to debug</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"`console.log()`\"), \"expected to contain: \" + \"`console.log()`\");\n    }\n\n    @Test\n    void testItalicEm() throws Exception {\n        // Em tag converts to italic\n        var result = HtmlToMarkdown.convert(\"<p><em>italic</em></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n    }\n\n    @Test\n    void testLineBreakBrTag() throws Exception {\n        // Single br tag produces a line break in output\n        var result = HtmlToMarkdown.convert(\"<p>First line.<br>Second line.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First line.\"), \"expected to contain: \" + \"First line.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second line.\"), \"expected to contain: \" + \"Second line.\");\n    }\n\n    @Test\n    void testLineBreakHrTag() throws Exception {\n        // hr tag produces a horizontal separator between content\n        var result = HtmlToMarkdown.convert(\"<p>Before rule.</p><hr><p>After rule.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Before rule.\"), \"expected to contain: \" + \"Before rule.\");\n        assertTrue(result.content().orElse(\"\").contains(\"After rule.\"), \"expected to contain: \" + \"After rule.\");\n    }\n\n    @Test\n    void testLineBreakMultipleBr() throws Exception {\n        // Multiple consecutive br tags in sequence\n        var result = HtmlToMarkdown.convert(\"<p>Start.<br><br>End.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Start.\"), \"expected to contain: \" + \"Start.\");\n        assertTrue(result.content().orElse(\"\").contains(\"End.\"), \"expected to contain: \" + \"End.\");\n    }\n\n    @Test\n    void testLinkAnchorFragment() throws Exception {\n        // Fragment-only anchor link is preserved\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"#section\\\">Jump to section</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Jump to section](#section)\"), \"expected to contain: \" + \"[Jump to section](#section)\");\n    }\n\n    @Test\n    void testLinkEmptyHref() throws Exception {\n        // Link with empty href produces output with the link text\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"\\\">No destination</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"No destination\"), \"expected to contain: \" + \"No destination\");\n    }\n\n    @Test\n    void testLinkImageInside() throws Exception {\n        // Image inside a link produces a linked image\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"![Logo](logo.png)\"), \"expected to contain: \" + \"![Logo](logo.png)\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkMailto() throws Exception {\n        // Mailto link is preserved with mailto: scheme\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"mailto:user@example.com\"), \"expected to contain: \" + \"mailto:user@example.com\");\n    }\n\n    @Test\n    void testLinkSimple() throws Exception {\n        // Simple link\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\">Example</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com)\"), \"expected to contain: \" + \"[Example](https://example.com)\");\n    }\n\n    @Test\n    void testLinkWithBoldText() throws Exception {\n        // Link containing bold text preserves formatting\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**Bold link**\"), \"expected to contain: \" + \"**Bold link**\");\n        assertTrue(result.content().orElse(\"\").contains(\"https://example.com\"), \"expected to contain: \" + \"https://example.com\");\n    }\n\n    @Test\n    void testLinkWithTitle() throws Exception {\n        // Link with title attribute\n        var result = HtmlToMarkdown.convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"[Example](https://example.com\"), \"expected to contain: \" + \"[Example](https://example.com\");\n        assertTrue(result.content().orElse(\"\").contains(\"Example Site\"), \"expected to contain: \" + \"Example Site\");\n    }\n\n    @Test\n    void testListDefinitionDl() throws Exception {\n        // Definition list with dt and dd elements\n        var result = HtmlToMarkdown.convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Term One\"), \"expected to contain: \" + \"Term One\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term one.\"), \"expected to contain: \" + \"Definition of term one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Term Two\"), \"expected to contain: \" + \"Term Two\");\n        assertTrue(result.content().orElse(\"\").contains(\"Definition of term two.\"), \"expected to contain: \" + \"Definition of term two.\");\n    }\n\n    @Test\n    void testListItemMultipleParagraphs() throws Exception {\n        // List item containing multiple paragraphs\n        var result = HtmlToMarkdown.convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph in item.\"), \"expected to contain: \" + \"First paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph in item.\"), \"expected to contain: \" + \"Second paragraph in item.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Simple item\"), \"expected to contain: \" + \"Simple item\");\n    }\n\n    @Test\n    void testListMixedNested() throws Exception {\n        // Mixed list: ordered list nested inside unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Item A\"), \"expected to contain: \" + \"Item A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 1\"), \"expected to contain: \" + \"Sub 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Sub 2\"), \"expected to contain: \" + \"Sub 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Item B\"), \"expected to contain: \" + \"Item B\");\n    }\n\n    @Test\n    void testListNestedOrdered() throws Exception {\n        // Nested ordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1\"), \"expected to contain: \" + \"Step 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1a\"), \"expected to contain: \" + \"Step 1a\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 1b\"), \"expected to contain: \" + \"Step 1b\");\n        assertTrue(result.content().orElse(\"\").contains(\"Step 2\"), \"expected to contain: \" + \"Step 2\");\n    }\n\n    @Test\n    void testListNestedUnordered() throws Exception {\n        // Nested unordered list with two levels of depth\n        var result = HtmlToMarkdown.convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Parent A\"), \"expected to contain: \" + \"Parent A\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A1\"), \"expected to contain: \" + \"Child A1\");\n        assertTrue(result.content().orElse(\"\").contains(\"Child A2\"), \"expected to contain: \" + \"Child A2\");\n        assertTrue(result.content().orElse(\"\").contains(\"Parent B\"), \"expected to contain: \" + \"Parent B\");\n    }\n\n    @Test\n    void testListTaskCheckboxes() throws Exception {\n        // Task list with checked and unchecked checkboxes\n        var result = HtmlToMarkdown.convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Done task\"), \"expected to contain: \" + \"Done task\");\n        assertTrue(result.content().orElse(\"\").contains(\"Pending task\"), \"expected to contain: \" + \"Pending task\");\n    }\n\n    @Test\n    void testOrderedList() throws Exception {\n        // Ordered list\n        var result = HtmlToMarkdown.convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"1. First\"), \"expected to contain: \" + \"1. First\");\n        assertTrue(result.content().orElse(\"\").contains(\"2. Second\"), \"expected to contain: \" + \"2. Second\");\n        assertTrue(result.content().orElse(\"\").contains(\"3. Third\"), \"expected to contain: \" + \"3. Third\");\n    }\n\n    @Test\n    void testParagraphMultiple() throws Exception {\n        // Multiple paragraphs are separated by a blank line\n        var result = HtmlToMarkdown.convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"First paragraph.\"), \"expected to contain: \" + \"First paragraph.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Second paragraph.\"), \"expected to contain: \" + \"Second paragraph.\");\n    }\n\n    @Test\n    void testParagraphNestedDivs() throws Exception {\n        // Text nested inside divs is extracted correctly\n        var result = HtmlToMarkdown.convert(\"<div><div><p>Nested text</p></div></div>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Nested text\"), \"expected to contain: \" + \"Nested text\");\n    }\n\n    @Test\n    void testParagraphSimple() throws Exception {\n        // Simple paragraph converts to plain text\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testParagraphWithInlineFormatting() throws Exception {\n        // Paragraph with bold, italic, and a link\n        var result = HtmlToMarkdown.convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"**bold**\"), \"expected to contain: \" + \"**bold**\");\n        assertTrue(result.content().orElse(\"\").contains(\"*italic*\"), \"expected to contain: \" + \"*italic*\");\n        assertTrue(result.content().orElse(\"\").contains(\"[link](https://example.com)\"), \"expected to contain: \" + \"[link](https://example.com)\");\n    }\n\n    @Test\n    void testParagraphWithLineBreaks() throws Exception {\n        // Paragraph with br tags produces line breaks in output\n        var result = HtmlToMarkdown.convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line one.\"), \"expected to contain: \" + \"Line one.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line two.\"), \"expected to contain: \" + \"Line two.\");\n        assertTrue(result.content().orElse(\"\").contains(\"Line three.\"), \"expected to contain: \" + \"Line three.\");\n    }\n\n    @Test\n    void testSemanticAbbr() throws Exception {\n        // Abbreviation element text is preserved\n        var result = HtmlToMarkdown.convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"WWW\"), \"expected to contain: \" + \"WWW\");\n    }\n\n    @Test\n    void testSemanticArticle() throws Exception {\n        // Article element wrapping content preserves inner content\n        var result = HtmlToMarkdown.convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Article Title\"), \"expected to contain: \" + \"Article Title\");\n        assertTrue(result.content().orElse(\"\").contains(\"Article body.\"), \"expected to contain: \" + \"Article body.\");\n    }\n\n    @Test\n    void testSemanticDefinitionList() throws Exception {\n        // Definition list with term and description\n        var result = HtmlToMarkdown.convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"HTML\"), \"expected to contain: \" + \"HTML\");\n        assertTrue(result.content().orElse(\"\").contains(\"HyperText Markup Language\"), \"expected to contain: \" + \"HyperText Markup Language\");\n        assertTrue(result.content().orElse(\"\").contains(\"CSS\"), \"expected to contain: \" + \"CSS\");\n        assertTrue(result.content().orElse(\"\").contains(\"Cascading Style Sheets\"), \"expected to contain: \" + \"Cascading Style Sheets\");\n    }\n\n    @Test\n    void testSemanticDetailsSummary() throws Exception {\n        // Details and summary elements produce readable output\n        var result = HtmlToMarkdown.convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Click to expand\"), \"expected to contain: \" + \"Click to expand\");\n    }\n\n    @Test\n    void testSemanticHr() throws Exception {\n        // Horizontal rule produces a separator in output\n        var result = HtmlToMarkdown.convert(\"<p>Above</p><hr><p>Below</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Above\"), \"expected to contain: \" + \"Above\");\n        assertTrue(result.content().orElse(\"\").contains(\"Below\"), \"expected to contain: \" + \"Below\");\n    }\n\n    @Test\n    void testSemanticMarkHighlight() throws Exception {\n        // Mark tag produces highlighted output\n        var result = HtmlToMarkdown.convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"highlighted text\"), \"expected to contain: \" + \"highlighted text\");\n    }\n\n    @Test\n    void testSemanticSectionWithHeading() throws Exception {\n        // Section element with heading preserves structure\n        var result = HtmlToMarkdown.convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Section Heading\"), \"expected to contain: \" + \"Section Heading\");\n        assertTrue(result.content().orElse(\"\").contains(\"Section content.\"), \"expected to contain: \" + \"Section content.\");\n    }\n\n    @Test\n    void testSemanticSubSuperscript() throws Exception {\n        // Subscript and superscript elements are preserved in output\n        var result = HtmlToMarkdown.convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"H\"), \"expected to contain: \" + \"H\");\n        assertTrue(result.content().orElse(\"\").contains(\"2\"), \"expected to contain: \" + \"2\");\n        assertTrue(result.content().orElse(\"\").contains(\"O\"), \"expected to contain: \" + \"O\");\n        assertTrue(result.content().orElse(\"\").contains(\"E=mc\"), \"expected to contain: \" + \"E=mc\");\n    }\n\n    @Test\n    void testSimpleTable() throws Exception {\n        // Simple table with header\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"Name\"), \"expected to contain: \" + \"Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"Age\"), \"expected to contain: \" + \"Age\");\n        assertTrue(result.content().orElse(\"\").contains(\"Alice\"), \"expected to contain: \" + \"Alice\");\n        assertTrue(result.content().orElse(\"\").contains(\"30\"), \"expected to contain: \" + \"30\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n        assertTrue(result.content().orElse(\"\").contains(\"---\"), \"expected to contain: \" + \"---\");\n    }\n\n    @Test\n    void testTableEmpty() throws Exception {\n        // Empty table produces no output or minimal output\n        var result = HtmlToMarkdown.convert(\"<table></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testTableNoThead() throws Exception {\n        // Table without thead uses first row as implied header\n        var result = HtmlToMarkdown.convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Product\"), \"expected to contain: \" + \"Product\");\n        assertTrue(result.content().orElse(\"\").contains(\"Price\"), \"expected to contain: \" + \"Price\");\n        assertTrue(result.content().orElse(\"\").contains(\"Apple\"), \"expected to contain: \" + \"Apple\");\n        assertTrue(result.content().orElse(\"\").contains(\"1.00\"), \"expected to contain: \" + \"1.00\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTablePipeCharsInContent() throws Exception {\n        // Table cells containing pipe characters are escaped in output\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Expression\"), \"expected to contain: \" + \"Expression\");\n        assertTrue(result.content().orElse(\"\").contains(\"Result\"), \"expected to contain: \" + \"Result\");\n        assertTrue(result.content().orElse(\"\").contains(\"true\"), \"expected to contain: \" + \"true\");\n    }\n\n    @Test\n    void testTableWithAlignment() throws Exception {\n        // Table with column alignment attributes\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Left\"), \"expected to contain: \" + \"Left\");\n        assertTrue(result.content().orElse(\"\").contains(\"Center\"), \"expected to contain: \" + \"Center\");\n        assertTrue(result.content().orElse(\"\").contains(\"Right\"), \"expected to contain: \" + \"Right\");\n        assertTrue(result.content().orElse(\"\").contains(\"L\"), \"expected to contain: \" + \"L\");\n        assertTrue(result.content().orElse(\"\").contains(\"C\"), \"expected to contain: \" + \"C\");\n        assertTrue(result.content().orElse(\"\").contains(\"R\"), \"expected to contain: \" + \"R\");\n        assertTrue(result.content().orElse(\"\").contains(\"|\"), \"expected to contain: \" + \"|\");\n    }\n\n    @Test\n    void testTableWithColspan() throws Exception {\n        // Table with colspan attribute in a header cell\n        var result = HtmlToMarkdown.convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n        assertTrue(result.content().orElse(\"\").contains(\"Full Name\"), \"expected to contain: \" + \"Full Name\");\n        assertTrue(result.content().orElse(\"\").contains(\"John\"), \"expected to contain: \" + \"John\");\n        assertTrue(result.content().orElse(\"\").contains(\"Doe\"), \"expected to contain: \" + \"Doe\");\n    }\n\n    @Test\n    void testUnorderedList() throws Exception {\n        // Unordered list\n        var result = HtmlToMarkdown.convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 1\"), \"expected to contain: \" + \"- Item 1\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 2\"), \"expected to contain: \" + \"- Item 2\");\n        assertTrue(result.content().orElse(\"\").contains(\"- Item 3\"), \"expected to contain: \" + \"- Item 3\");\n    }\n\n}\n"
  },
  {
    "path": "test_apps/java/src/test/java/dev/kreuzberg/htmltomarkdown/e2e/SmokeTest.java",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:d0fa5aea3e17864bfc7d21f39a5b43d772d01aef0da613a11e8e506f49e7998a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\npackage dev.kreuzberg.htmltomarkdown.e2e;\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport dev.kreuzberg.htmltomarkdown.ConversionOptions;\n\n/** E2e tests for category: smoke. */\nclass SmokeTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new Jdk8Module());\n    @Test\n    void testSmokeEmptyString() throws Exception {\n        // Empty string produces empty output\n        var result = HtmlToMarkdown.convert(\"\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"\", result.content().orElse(\"\").trim());\n    }\n\n    @Test\n    void testSmokeSimpleHeading() throws Exception {\n        // H1 heading converts to ATX markdown\n        var result = HtmlToMarkdown.convert(\"<h1>Title</h1>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertTrue(result.content().orElse(\"\").contains(\"# Title\"), \"expected to contain: \" + \"# Title\");\n    }\n\n    @Test\n    void testSmokeSimpleParagraph() throws Exception {\n        // Simple paragraph converts correctly\n        var result = HtmlToMarkdown.convert(\"<p>Hello World</p>\", MAPPER.readValue(\"{}\", ConversionOptions.class));\n        assertEquals(\"Hello World\", result.content().orElse(\"\").trim());\n        assertFalse(result.content().isEmpty(), \"expected non-empty value\");\n    }\n\n}\n"
  },
  {
    "path": "test_apps/node/.nvmrc",
    "content": "18\n"
  },
  {
    "path": "test_apps/node/README.md",
    "content": "# Node.js Test App for html-to-markdown\n\nTests the published html-to-markdown package from npm.\n\n## Setup\n\n```bash\npnpm install\n```\n\n## Run Tests\n\n```bash\n# Smoke tests (fast)\npnpm test:smoke\n\n# Comprehensive tests\npnpm test:comprehensive\n\n# All tests\npnpm test\n```\n"
  },
  {
    "path": "test_apps/node/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-e2e-typescript\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"@kreuzberg/html-to-markdown\": \"3.4.0-rc.25\",\n    \"vitest\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "test_apps/node/tests/conversion.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:812a7d4d52960f4167567f4f53e4efa1d710a15474be1b229f3bd3125339941a\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from 'vitest';\nimport { convert } from '@kreuzberg/html-to-markdown';\n\ndescribe('conversion', () => {\n  it('blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed', () => {\n    const result = convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\");\n    expect(result.content).toContain(\"> First paragraph.\");\n    expect(result.content).toContain(\"> Second paragraph.\");\n  });\n\n  it('blockquote_nested: Nested blockquote produces double-prefixed lines', () => {\n    const result = convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Outer quote.\");\n    expect(result.content).toContain(\"Inner quote.\");\n  });\n\n  it('blockquote_simple: Simple blockquote', () => {\n    const result = convert(\"<blockquote><p>Quote text</p></blockquote>\");\n    expect(result.content).toContain(\"> Quote text\");\n  });\n\n  it('blockquote_with_list: Blockquote containing a list preserves list items inside quote', () => {\n    const result = convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Quote intro:\");\n    expect(result.content).toContain(\"Point one\");\n    expect(result.content).toContain(\"Point two\");\n  });\n\n  it('bold_and_italic: Nested bold and italic', () => {\n    const result = convert(\"<p><strong><em>both</em></strong></p>\");\n    expect(result.content).toContain(\"***both***\");\n  });\n\n  it('bold_strong: Strong tag converts to bold', () => {\n    const result = convert(\"<p><strong>bold</strong></p>\");\n    expect(result.content).toContain(\"**bold**\");\n  });\n\n  it('code_block: Code block with language preserves content', () => {\n    const result = convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"print('hello')\");\n  });\n\n  it('code_block_no_language: Code block without a language class preserves content', () => {\n    const result = convert(\"<pre><code>plain code here</code></pre>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"plain code here\");\n  });\n\n  it('code_inline_in_paragraph: Inline code element nested inside a paragraph', () => {\n    const result = convert(\"<p>Call the <code>initialize()</code> method first.</p>\");\n    expect(result.content).toContain(\"`initialize()`\");\n  });\n\n  it('code_with_backticks_in_content: Inline code containing backtick characters is properly escaped', () => {\n    const result = convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"backtick\");\n  });\n\n  it('emphasis_mark_highlight: mark tag produces highlighted output', () => {\n    const result = convert(\"<p><mark>highlighted</mark></p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted\");\n  });\n\n  it('emphasis_strikethrough_del: del tag converts to GFM strikethrough', () => {\n    const result = convert(\"<p><del>deleted text</del></p>\");\n    expect(result.content).toContain(\"~~deleted text~~\");\n  });\n\n  it('emphasis_strikethrough_s: s tag converts to GFM strikethrough', () => {\n    const result = convert(\"<p><s>strikethrough</s></p>\");\n    expect(result.content).toContain(\"~~strikethrough~~\");\n  });\n\n  it('emphasis_subscript: sub tag content is preserved', () => {\n    const result = convert(\"<p>H<sub>2</sub>O</p>\");\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n  });\n\n  it('emphasis_superscript: sup tag content is preserved', () => {\n    const result = convert(\"<p>x<sup>2</sup></p>\");\n    expect(result.content).toContain(\"x\");\n    expect(result.content).toContain(\"2\");\n  });\n\n  it('emphasis_underline_u: u tag content is preserved in output', () => {\n    const result = convert(\"<p><u>underlined</u></p>\");\n    expect(result.content).toContain(\"underlined\");\n  });\n\n  it('form_input_elements: Form input elements produce readable output without form mechanics', () => {\n    const result = convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Name\");\n  });\n\n  it('form_select_options: Select element with options produces readable output', () => {\n    const result = convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Color\");\n  });\n\n  it('form_textarea: Textarea element produces readable output', () => {\n    const result = convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Message\");\n  });\n\n  it('heading_h1: H1 heading', () => {\n    const result = convert(\"<h1>Heading 1</h1>\");\n    expect((result.content ?? \"\").trim()).toBe(\"# Heading 1\");\n  });\n\n  it('heading_h2: H2 heading', () => {\n    const result = convert(\"<h2>Heading 2</h2>\");\n    expect((result.content ?? \"\").trim()).toBe(\"## Heading 2\");\n  });\n\n  it('heading_h3: H3 heading', () => {\n    const result = convert(\"<h3>Heading 3</h3>\");\n    expect((result.content ?? \"\").trim()).toBe(\"### Heading 3\");\n  });\n\n  it('heading_h4: H4 heading', () => {\n    const result = convert(\"<h4>Heading 4</h4>\");\n    expect((result.content ?? \"\").trim()).toBe(\"#### Heading 4\");\n  });\n\n  it('heading_h5: H5 heading', () => {\n    const result = convert(\"<h5>Heading 5</h5>\");\n    expect((result.content ?? \"\").trim()).toBe(\"##### Heading 5\");\n  });\n\n  it('heading_h6: H6 heading', () => {\n    const result = convert(\"<h6>Heading 6</h6>\");\n    expect((result.content ?? \"\").trim()).toBe(\"###### Heading 6\");\n  });\n\n  it('image_figure_figcaption: Figure with figcaption preserves both image and caption', () => {\n    const result = convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\");\n    expect(result.content).toContain(\"![A sunset](sunset.jpg)\");\n    expect(result.content).toContain(\"Beautiful sunset over the ocean\");\n  });\n\n  it('image_linked: Image inside an anchor produces a linked image', () => {\n    const result = convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\");\n    expect(result.content).toContain(\"![Icon](icon.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it('image_no_alt: Image without alt text produces image markdown', () => {\n    const result = convert(\"<img src=\\\"banner.jpg\\\">\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"banner.jpg\");\n  });\n\n  it('image_simple: Image with alt text', () => {\n    const result = convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\");\n    expect(result.content).toContain(\"![A photo](photo.jpg)\");\n  });\n\n  it('image_with_title: Image with title attribute includes title in output', () => {\n    const result = convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\");\n    expect(result.content).toContain(\"![Sales chart](chart.png\");\n    expect(result.content).toContain(\"Q3 Sales\");\n  });\n\n  it('inline_code: Inline code', () => {\n    const result = convert(\"<p>Use <code>console.log()</code> to debug</p>\");\n    expect(result.content).toContain(\"`console.log()`\");\n  });\n\n  it('italic_em: Em tag converts to italic', () => {\n    const result = convert(\"<p><em>italic</em></p>\");\n    expect(result.content).toContain(\"*italic*\");\n  });\n\n  it('line_break_br_tag: Single br tag produces a line break in output', () => {\n    const result = convert(\"<p>First line.<br>Second line.</p>\");\n    expect(result.content).toContain(\"First line.\");\n    expect(result.content).toContain(\"Second line.\");\n  });\n\n  it('line_break_hr_tag: hr tag produces a horizontal separator between content', () => {\n    const result = convert(\"<p>Before rule.</p><hr><p>After rule.</p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Before rule.\");\n    expect(result.content).toContain(\"After rule.\");\n  });\n\n  it('line_break_multiple_br: Multiple consecutive br tags in sequence', () => {\n    const result = convert(\"<p>Start.<br><br>End.</p>\");\n    expect(result.content).toContain(\"Start.\");\n    expect(result.content).toContain(\"End.\");\n  });\n\n  it('link_anchor_fragment: Fragment-only anchor link is preserved', () => {\n    const result = convert(\"<a href=\\\"#section\\\">Jump to section</a>\");\n    expect(result.content).toContain(\"[Jump to section](#section)\");\n  });\n\n  it('link_empty_href: Link with empty href produces output with the link text', () => {\n    const result = convert(\"<a href=\\\"\\\">No destination</a>\");\n    expect(result.content).toContain(\"No destination\");\n  });\n\n  it('link_image_inside: Image inside a link produces a linked image', () => {\n    const result = convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\");\n    expect(result.content).toContain(\"![Logo](logo.png)\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it('link_mailto: Mailto link is preserved with mailto: scheme', () => {\n    const result = convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\");\n    expect(result.content).toContain(\"mailto:user@example.com\");\n  });\n\n  it('link_simple: Simple link', () => {\n    const result = convert(\"<a href=\\\"https://example.com\\\">Example</a>\");\n    expect(result.content).toContain(\"[Example](https://example.com)\");\n  });\n\n  it('link_with_bold_text: Link containing bold text preserves formatting', () => {\n    const result = convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\");\n    expect(result.content).toContain(\"**Bold link**\");\n    expect(result.content).toContain(\"https://example.com\");\n  });\n\n  it('link_with_title: Link with title attribute', () => {\n    const result = convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\");\n    expect(result.content).toContain(\"[Example](https://example.com\");\n    expect(result.content).toContain(\"Example Site\");\n  });\n\n  it('list_definition_dl: Definition list with dt and dd elements', () => {\n    const result = convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\");\n    expect(result.content).toContain(\"Term One\");\n    expect(result.content).toContain(\"Definition of term one.\");\n    expect(result.content).toContain(\"Term Two\");\n    expect(result.content).toContain(\"Definition of term two.\");\n  });\n\n  it('list_item_multiple_paragraphs: List item containing multiple paragraphs', () => {\n    const result = convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\");\n    expect(result.content).toContain(\"First paragraph in item.\");\n    expect(result.content).toContain(\"Second paragraph in item.\");\n    expect(result.content).toContain(\"Simple item\");\n  });\n\n  it('list_mixed_nested: Mixed list: ordered list nested inside unordered list', () => {\n    const result = convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\");\n    expect(result.content).toContain(\"Item A\");\n    expect(result.content).toContain(\"Sub 1\");\n    expect(result.content).toContain(\"Sub 2\");\n    expect(result.content).toContain(\"Item B\");\n  });\n\n  it('list_nested_ordered: Nested ordered list with two levels of depth', () => {\n    const result = convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\");\n    expect(result.content).toContain(\"Step 1\");\n    expect(result.content).toContain(\"Step 1a\");\n    expect(result.content).toContain(\"Step 1b\");\n    expect(result.content).toContain(\"Step 2\");\n  });\n\n  it('list_nested_unordered: Nested unordered list with two levels of depth', () => {\n    const result = convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\");\n    expect(result.content).toContain(\"Parent A\");\n    expect(result.content).toContain(\"Child A1\");\n    expect(result.content).toContain(\"Child A2\");\n    expect(result.content).toContain(\"Parent B\");\n  });\n\n  it('list_task_checkboxes: Task list with checked and unchecked checkboxes', () => {\n    const result = convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Done task\");\n    expect(result.content).toContain(\"Pending task\");\n  });\n\n  it('ordered_list: Ordered list', () => {\n    const result = convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\");\n    expect(result.content).toContain(\"1. First\");\n    expect(result.content).toContain(\"2. Second\");\n    expect(result.content).toContain(\"3. Third\");\n  });\n\n  it('paragraph_multiple: Multiple paragraphs are separated by a blank line', () => {\n    const result = convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\");\n    expect(result.content).toContain(\"First paragraph.\");\n    expect(result.content).toContain(\"Second paragraph.\");\n  });\n\n  it('paragraph_nested_divs: Text nested inside divs is extracted correctly', () => {\n    const result = convert(\"<div><div><p>Nested text</p></div></div>\");\n    expect(result.content).toContain(\"Nested text\");\n  });\n\n  it('paragraph_simple: Simple paragraph converts to plain text', () => {\n    const result = convert(\"<p>Hello World</p>\");\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n  });\n\n  it('paragraph_with_inline_formatting: Paragraph with bold, italic, and a link', () => {\n    const result = convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\");\n    expect(result.content).toContain(\"**bold**\");\n    expect(result.content).toContain(\"*italic*\");\n    expect(result.content).toContain(\"[link](https://example.com)\");\n  });\n\n  it('paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output', () => {\n    const result = convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Line one.\");\n    expect(result.content).toContain(\"Line two.\");\n    expect(result.content).toContain(\"Line three.\");\n  });\n\n  it('semantic_abbr: Abbreviation element text is preserved', () => {\n    const result = convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\");\n    expect(result.content).toContain(\"WWW\");\n  });\n\n  it('semantic_article: Article element wrapping content preserves inner content', () => {\n    const result = convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\");\n    expect(result.content).toContain(\"Article Title\");\n    expect(result.content).toContain(\"Article body.\");\n  });\n\n  it('semantic_definition_list: Definition list with term and description', () => {\n    const result = convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\");\n    expect(result.content).toContain(\"HTML\");\n    expect(result.content).toContain(\"HyperText Markup Language\");\n    expect(result.content).toContain(\"CSS\");\n    expect(result.content).toContain(\"Cascading Style Sheets\");\n  });\n\n  it('semantic_details_summary: Details and summary elements produce readable output', () => {\n    const result = convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Click to expand\");\n  });\n\n  it('semantic_hr: Horizontal rule produces a separator in output', () => {\n    const result = convert(\"<p>Above</p><hr><p>Below</p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Above\");\n    expect(result.content).toContain(\"Below\");\n  });\n\n  it('semantic_mark_highlight: Mark tag produces highlighted output', () => {\n    const result = convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"highlighted text\");\n  });\n\n  it('semantic_section_with_heading: Section element with heading preserves structure', () => {\n    const result = convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\");\n    expect(result.content).toContain(\"Section Heading\");\n    expect(result.content).toContain(\"Section content.\");\n  });\n\n  it('semantic_sub_superscript: Subscript and superscript elements are preserved in output', () => {\n    const result = convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"H\");\n    expect(result.content).toContain(\"2\");\n    expect(result.content).toContain(\"O\");\n    expect(result.content).toContain(\"E=mc\");\n  });\n\n  it('simple_table: Simple table with header', () => {\n    const result = convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\");\n    expect(result.content).toContain(\"Name\");\n    expect(result.content).toContain(\"Age\");\n    expect(result.content).toContain(\"Alice\");\n    expect(result.content).toContain(\"30\");\n    expect(result.content).toContain(\"|\");\n    expect(result.content).toContain(\"---\");\n  });\n\n  it('table_empty: Empty table produces no output or minimal output', () => {\n    const result = convert(\"<table></table>\");\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it('table_no_thead: Table without thead uses first row as implied header', () => {\n    const result = convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Product\");\n    expect(result.content).toContain(\"Price\");\n    expect(result.content).toContain(\"Apple\");\n    expect(result.content).toContain(\"1.00\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it('table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output', () => {\n    const result = convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Expression\");\n    expect(result.content).toContain(\"Result\");\n    expect(result.content).toContain(\"true\");\n  });\n\n  it('table_with_alignment: Table with column alignment attributes', () => {\n    const result = convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Left\");\n    expect(result.content).toContain(\"Center\");\n    expect(result.content).toContain(\"Right\");\n    expect(result.content).toContain(\"L\");\n    expect(result.content).toContain(\"C\");\n    expect(result.content).toContain(\"R\");\n    expect(result.content).toContain(\"|\");\n  });\n\n  it('table_with_colspan: Table with colspan attribute in a header cell', () => {\n    const result = convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n    expect(result.content).toContain(\"Full Name\");\n    expect(result.content).toContain(\"John\");\n    expect(result.content).toContain(\"Doe\");\n  });\n\n  it('unordered_list: Unordered list', () => {\n    const result = convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\");\n    expect(result.content).toContain(\"- Item 1\");\n    expect(result.content).toContain(\"- Item 2\");\n    expect(result.content).toContain(\"- Item 3\");\n  });\n});\n"
  },
  {
    "path": "test_apps/node/tests/smoke.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:a87ddf643d5ba6e75c909026ae3a3806177525dedd926325b1496a0547f06a40\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from 'vitest';\nimport { convert } from '@kreuzberg/html-to-markdown';\n\ndescribe('smoke', () => {\n  it('smoke_empty_string: Empty string produces empty output', () => {\n    const result = convert(\"\");\n    expect((result.content ?? \"\").trim()).toBe(\"\");\n  });\n\n  it('smoke_simple_heading: H1 heading converts to ATX markdown', () => {\n    const result = convert(\"<h1>Title</h1>\");\n    expect(result.content).toContain(\"# Title\");\n  });\n\n  it('smoke_simple_paragraph: Simple paragraph converts correctly', () => {\n    const result = convert(\"<p>Hello World</p>\");\n    expect((result.content ?? \"\").trim()).toBe(\"Hello World\");\n    expect((result.content ?? \"\").length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "test_apps/node/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"strictNullChecks\": false,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"tests/**/*.ts\", \"vitest.config.ts\"]\n}\n"
  },
  {
    "path": "test_apps/node/vitest.config.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:1aa468bb58d7041a551803bf8aaf23fe3ce8a3772b6f8a8df881569c1dcc7c50\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    include: ['tests/**/*.test.ts'],\n  },\n});\n"
  },
  {
    "path": "test_apps/php/README.md",
    "content": "# PHP Test App for html-to-markdown\n\nComprehensive test suite validating the published `kreuzberg-dev/html-to-markdown` PHP package from Packagist.\n\nThis test app verifies:\n\n- **Package installation** from Packagist registry (not local path dependency)\n- **Core conversion functions** - basic HTML to Markdown conversion\n- **Metadata extraction** - document properties, headers, links, images\n- **Inline image extraction** - embedding images during conversion\n- **Visitor pattern** - extensible conversion interface\n- **Type safety** - PHPStan level 9 static analysis\n- **Error handling** - graceful error handling and edge cases\n- **Fixture-based tests** - Parametrized tests with comprehensive fixtures\n\n## Requirements\n\n- PHP 8.2+\n- Composer\n- html_to_markdown native extension (installed via PIE/binary download)\n\n## Setup\n\n```bash\n# Install dependencies (includes phpunit and phpstan)\ncomposer install\n\n# The test app will download and test the published package from Packagist\n# (not a local path dependency)\n```\n\n## Run Tests\n\n```bash\n# Run all tests\ncomposer test\n\n# Run with verbose output\nvendor/bin/phpunit --verbose\n\n# Run specific test class\nvendor/bin/phpunit SmokeTest.php\nvendor/bin/phpunit ComprehensiveTest.php\n```\n\n## Static Analysis\n\n```bash\n# Type check with PHPStan level 9\ncomposer lint\n\n# Or directly\nvendor/bin/phpstan analyse SmokeTest.php ComprehensiveTest.php\n```\n\n## Test Coverage\n\n### SmokeTest.php\n\n- Package loads (extension_loaded check)\n- Basic conversion with function interface\n- Basic conversion with extension interface\n- Heading, paragraph, list, link, code, blockquote conversion\n- Empty input handling\n\n### ComprehensiveTest.php\n\n- **Fixture-based tests**: Parametrized tests from JSON fixtures\n  - basic-html.json (10+ basic element tests)\n  - complex-html.json (5+ complex structure tests)\n  - edge-cases.json (5+ edge case tests)\n  - metadata-extraction.json (5+ metadata tests)\n  - real-world.json (5+ real-world HTML samples)\n\n- **Metadata extraction tests**:\n  - Document metadata (title, description, language, author)\n  - Header extraction with levels and ids\n  - Link extraction (external, internal, email)\n  - Image extraction with alt text\n  - Open Graph and Twitter Card tags\n  - Selective metadata extraction configuration\n\n- **Conversion features**:\n  - ConversionOptions class usage\n  - Options as array\n  - Inline image conversion\n  - Complex HTML with multiple elements\n  - Multiple heading levels\n\n- **Error handling**:\n  - UTF-8 handling\n  - Null options handling\n  - Invalid input resilience\n\n## Features Tested\n\n### Core Conversion\n\n- ✅ Headings (h1-h6)\n- ✅ Paragraphs\n- ✅ Bold/Strong\n- ✅ Italic/Emphasis\n- ✅ Code (inline and blocks)\n- ✅ Links (external, internal, email)\n- ✅ Images\n- ✅ Lists (ordered and unordered)\n- ✅ Nested lists\n- ✅ Blockquotes\n- ✅ Definitions lists\n- ✅ Mixed formatting\n\n### Metadata Extraction\n\n- ✅ Document metadata (title, description, keywords, author, language)\n- ✅ Header structure with levels and ids\n- ✅ Link types and attributes\n- ✅ Image sources and alt text\n- ✅ Open Graph tags\n- ✅ Twitter Card tags\n- ✅ Structured data (JSON-LD, Microdata)\n\n### Configuration\n\n- ✅ ConversionOptions class\n- ✅ Array-based options\n- ✅ Selective metadata extraction\n- ✅ Heading style configuration\n- ✅ List indentation configuration\n\n### Type Safety\n\n- ✅ PHPStan level 9 validation\n- ✅ Type stubs for native functions\n- ✅ Return type verification\n- ✅ Parameter type checking\n\n## Gaps in Test Coverage\n\n### Current Coverage\n\n- Basic HTML conversion: 10+ test cases from fixtures\n- Metadata extraction: 5+ test cases\n- Error handling: Basic UTF-8, null options\n- Type safety: PHPStan level 9\n\n### Known Gaps\n\n- **HTML5 semantic elements**: Further testing of `<article>`, `<aside>`, `<section>`, `<nav>`\n- **Complex tables**: Table to Markdown conversion\n- **Horizontal rules**: `<hr>` conversion\n- **Strikethrough**: `<del>` and `<strike>` elements\n- **Inline HTML preservation**: `<span>` and `<div>` handling\n- **Script/Style filtering**: Ensuring `<script>` and `<style>` are properly removed\n- **Performance tests**: Benchmarking large document conversion\n- **Memory tests**: Memory usage profiling\n- **Visitor pattern direct testing**: Custom visitor implementation examples\n- **Stream processing**: Handling very large HTML documents\n- **Concurrency**: Thread-safety validation (if applicable)\n\n## Contributing\n\nWhen adding new tests:\n\n1. Use parametrized fixtures for HTML conversion tests\n2. Add test cases to appropriate JSON fixture file\n3. Maintain PHPStan level 9 compliance\n4. Document expected behavior in test methods\n5. Update this README with new features tested\n"
  },
  {
    "path": "test_apps/php/bootstrap.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6a96c167064c052ad8b951536c56550a5c22ea7d559e3dd2f2e90e7d3c6a0d09\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\n// Load the e2e project autoloader (PHPUnit, test helpers).\nrequire_once __DIR__ . '/vendor/autoload.php';\n\n// Load the PHP binding package classes via its Composer autoloader.\n// The package's autoloader is separate from the e2e project's autoloader\n// since the php-ext type prevents direct composer path dependency.\n$pkgAutoloader = __DIR__ . '/../../packages/php/vendor/autoload.php';\nif (file_exists($pkgAutoloader)) {\n    require_once $pkgAutoloader;\n}\n"
  },
  {
    "path": "test_apps/php/composer.json",
    "content": "{\n  \"name\": \"kreuzberg-dev/e2e-php\",\n  \"description\": \"E2e tests for PHP bindings\",\n  \"type\": \"project\",\n  \"require\": {\n    \"kreuzberg-dev/html-to-markdown-rs\": \">=3.2.0\"\n  },\n  \"require-dev\": {\n    \"phpunit/phpunit\": \"^13.1\",\n    \"guzzlehttp/guzzle\": \"^7.0\"\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"Html\\\\To\\\\Markdown\\\\Rs\\\\E2e\\\\\": \"tests/\"\n    }\n  }\n}\n"
  },
  {
    "path": "test_apps/php/phpstan.neon",
    "content": "parameters:\n  level: 9\n  paths:\n    - SmokeTest.php\n    - ComprehensiveTest.php\n  treatMissingClosureParamTypeAsAny: false\n"
  },
  {
    "path": "test_apps/php/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/13.1/phpunit.xsd\"\n         bootstrap=\"bootstrap.php\"\n         colors=\"true\"\n         failOnRisky=\"true\"\n         failOnWarning=\"true\">\n    <testsuites>\n        <testsuite name=\"e2e\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "test_apps/php/tests/ConversionTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:9bf8e07fdcaaec0e6fe1796dc597e4c24454cad8cad70f8857c9bf180400c949\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: conversion. */\nfinal class ConversionTest extends TestCase\n{\n    /** Blockquote with multiple paragraphs has each paragraph prefixed */\n    public function test_blockquote_multiple_paragraphs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\");\n        $this->assertStringContainsString(\"> First paragraph.\", $result);\n        $this->assertStringContainsString(\"> Second paragraph.\", $result);\n    }\n\n    /** Nested blockquote produces double-prefixed lines */\n    public function test_blockquote_nested(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Outer quote.\", $result);\n        $this->assertStringContainsString(\"Inner quote.\", $result);\n    }\n\n    /** Simple blockquote */\n    public function test_blockquote_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Quote text</p></blockquote>\");\n        $this->assertStringContainsString(\"> Quote text\", $result);\n    }\n\n    /** Blockquote containing a list preserves list items inside quote */\n    public function test_blockquote_with_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Quote intro:\", $result);\n        $this->assertStringContainsString(\"Point one\", $result);\n        $this->assertStringContainsString(\"Point two\", $result);\n    }\n\n    /** Nested bold and italic */\n    public function test_bold_and_italic(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><strong><em>both</em></strong></p>\");\n        $this->assertStringContainsString(\"***both***\", $result);\n    }\n\n    /** Strong tag converts to bold */\n    public function test_bold_strong(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><strong>bold</strong></p>\");\n        $this->assertStringContainsString(\"**bold**\", $result);\n    }\n\n    /** Code block with language preserves content */\n    public function test_code_block(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"print('hello')\", $result);\n    }\n\n    /** Code block without a language class preserves content */\n    public function test_code_block_no_language(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<pre><code>plain code here</code></pre>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"plain code here\", $result);\n    }\n\n    /** Inline code element nested inside a paragraph */\n    public function test_code_inline_in_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Call the <code>initialize()</code> method first.</p>\");\n        $this->assertStringContainsString(\"`initialize()`\", $result);\n    }\n\n    /** Inline code containing backtick characters is properly escaped */\n    public function test_code_with_backticks_in_content(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use <code>`backtick` here</code> carefully.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"backtick\", $result);\n    }\n\n    /** mark tag produces highlighted output */\n    public function test_emphasis_mark_highlight(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><mark>highlighted</mark></p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"highlighted\", $result);\n    }\n\n    /** del tag converts to GFM strikethrough */\n    public function test_emphasis_strikethrough_del(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><del>deleted text</del></p>\");\n        $this->assertStringContainsString(\"~~deleted text~~\", $result);\n    }\n\n    /** s tag converts to GFM strikethrough */\n    public function test_emphasis_strikethrough_s(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><s>strikethrough</s></p>\");\n        $this->assertStringContainsString(\"~~strikethrough~~\", $result);\n    }\n\n    /** sub tag content is preserved */\n    public function test_emphasis_subscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O</p>\");\n        $this->assertStringContainsString(\"H\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n        $this->assertStringContainsString(\"O\", $result);\n    }\n\n    /** sup tag content is preserved */\n    public function test_emphasis_superscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>x<sup>2</sup></p>\");\n        $this->assertStringContainsString(\"x\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n    }\n\n    /** u tag content is preserved in output */\n    public function test_emphasis_underline_u(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><u>underlined</u></p>\");\n        $this->assertStringContainsString(\"underlined\", $result);\n    }\n\n    /** Form input elements produce readable output without form mechanics */\n    public function test_form_input_elements(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Name\", $result);\n    }\n\n    /** Select element with options produces readable output */\n    public function test_form_select_options(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Color\", $result);\n    }\n\n    /** Textarea element produces readable output */\n    public function test_form_textarea(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<form><label>Message:</label><textarea>Default text content</textarea></form>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Message\", $result);\n    }\n\n    /** H1 heading */\n    public function test_heading_h1(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Heading 1</h1>\");\n        $this->assertEquals(\"# Heading 1\", trim($result));\n    }\n\n    /** H2 heading */\n    public function test_heading_h2(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h2>Heading 2</h2>\");\n        $this->assertEquals(\"## Heading 2\", trim($result));\n    }\n\n    /** H3 heading */\n    public function test_heading_h3(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h3>Heading 3</h3>\");\n        $this->assertEquals(\"### Heading 3\", trim($result));\n    }\n\n    /** H4 heading */\n    public function test_heading_h4(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h4>Heading 4</h4>\");\n        $this->assertEquals(\"#### Heading 4\", trim($result));\n    }\n\n    /** H5 heading */\n    public function test_heading_h5(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h5>Heading 5</h5>\");\n        $this->assertEquals(\"##### Heading 5\", trim($result));\n    }\n\n    /** H6 heading */\n    public function test_heading_h6(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h6>Heading 6</h6>\");\n        $this->assertEquals(\"###### Heading 6\", trim($result));\n    }\n\n    /** Figure with figcaption preserves both image and caption */\n    public function test_image_figure_figcaption(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\");\n        $this->assertStringContainsString(\"![A sunset](sunset.jpg)\", $result);\n        $this->assertStringContainsString(\"Beautiful sunset over the ocean\", $result);\n    }\n\n    /** Image inside an anchor produces a linked image */\n    public function test_image_linked(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\");\n        $this->assertStringContainsString(\"![Icon](icon.png)\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Image without alt text produces image markdown */\n    public function test_image_no_alt(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"banner.jpg\\\">\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"banner.jpg\", $result);\n    }\n\n    /** Image with alt text */\n    public function test_image_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\");\n        $this->assertStringContainsString(\"![A photo](photo.jpg)\", $result);\n    }\n\n    /** Image with title attribute includes title in output */\n    public function test_image_with_title(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\");\n        $this->assertStringContainsString(\"![Sales chart](chart.png\", $result);\n        $this->assertStringContainsString(\"Q3 Sales\", $result);\n    }\n\n    /** Inline code */\n    public function test_inline_code(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Use <code>console.log()</code> to debug</p>\");\n        $this->assertStringContainsString(\"`console.log()`\", $result);\n    }\n\n    /** Em tag converts to italic */\n    public function test_italic_em(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p><em>italic</em></p>\");\n        $this->assertStringContainsString(\"*italic*\", $result);\n    }\n\n    /** Single br tag produces a line break in output */\n    public function test_line_break_br_tag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First line.<br>Second line.</p>\");\n        $this->assertStringContainsString(\"First line.\", $result);\n        $this->assertStringContainsString(\"Second line.\", $result);\n    }\n\n    /** hr tag produces a horizontal separator between content */\n    public function test_line_break_hr_tag(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Before rule.</p><hr><p>After rule.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Before rule.\", $result);\n        $this->assertStringContainsString(\"After rule.\", $result);\n    }\n\n    /** Multiple consecutive br tags in sequence */\n    public function test_line_break_multiple_br(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Start.<br><br>End.</p>\");\n        $this->assertStringContainsString(\"Start.\", $result);\n        $this->assertStringContainsString(\"End.\", $result);\n    }\n\n    /** Fragment-only anchor link is preserved */\n    public function test_link_anchor_fragment(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"#section\\\">Jump to section</a>\");\n        $this->assertStringContainsString(\"[Jump to section](#section)\", $result);\n    }\n\n    /** Link with empty href produces output with the link text */\n    public function test_link_empty_href(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"\\\">No destination</a>\");\n        $this->assertStringContainsString(\"No destination\", $result);\n    }\n\n    /** Image inside a link produces a linked image */\n    public function test_link_image_inside(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\");\n        $this->assertStringContainsString(\"![Logo](logo.png)\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Mailto link is preserved with mailto: scheme */\n    public function test_link_mailto(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"mailto:user@example.com\\\">Email us</a>\");\n        $this->assertStringContainsString(\"mailto:user@example.com\", $result);\n    }\n\n    /** Simple link */\n    public function test_link_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\">Example</a>\");\n        $this->assertStringContainsString(\"[Example](https://example.com)\", $result);\n    }\n\n    /** Link containing bold text preserves formatting */\n    public function test_link_with_bold_text(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\");\n        $this->assertStringContainsString(\"**Bold link**\", $result);\n        $this->assertStringContainsString(\"https://example.com\", $result);\n    }\n\n    /** Link with title attribute */\n    public function test_link_with_title(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\");\n        $this->assertStringContainsString(\"[Example](https://example.com\", $result);\n        $this->assertStringContainsString(\"Example Site\", $result);\n    }\n\n    /** Definition list with dt and dd elements */\n    public function test_list_definition_dl(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\");\n        $this->assertStringContainsString(\"Term One\", $result);\n        $this->assertStringContainsString(\"Definition of term one.\", $result);\n        $this->assertStringContainsString(\"Term Two\", $result);\n        $this->assertStringContainsString(\"Definition of term two.\", $result);\n    }\n\n    /** List item containing multiple paragraphs */\n    public function test_list_item_multiple_paragraphs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\");\n        $this->assertStringContainsString(\"First paragraph in item.\", $result);\n        $this->assertStringContainsString(\"Second paragraph in item.\", $result);\n        $this->assertStringContainsString(\"Simple item\", $result);\n    }\n\n    /** Mixed list: ordered list nested inside unordered list */\n    public function test_list_mixed_nested(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\");\n        $this->assertStringContainsString(\"Item A\", $result);\n        $this->assertStringContainsString(\"Sub 1\", $result);\n        $this->assertStringContainsString(\"Sub 2\", $result);\n        $this->assertStringContainsString(\"Item B\", $result);\n    }\n\n    /** Nested ordered list with two levels of depth */\n    public function test_list_nested_ordered(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\");\n        $this->assertStringContainsString(\"Step 1\", $result);\n        $this->assertStringContainsString(\"Step 1a\", $result);\n        $this->assertStringContainsString(\"Step 1b\", $result);\n        $this->assertStringContainsString(\"Step 2\", $result);\n    }\n\n    /** Nested unordered list with two levels of depth */\n    public function test_list_nested_unordered(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\");\n        $this->assertStringContainsString(\"Parent A\", $result);\n        $this->assertStringContainsString(\"Child A1\", $result);\n        $this->assertStringContainsString(\"Child A2\", $result);\n        $this->assertStringContainsString(\"Parent B\", $result);\n    }\n\n    /** Task list with checked and unchecked checkboxes */\n    public function test_list_task_checkboxes(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Done task\", $result);\n        $this->assertStringContainsString(\"Pending task\", $result);\n    }\n\n    /** Ordered list */\n    public function test_ordered_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ol><li>First</li><li>Second</li><li>Third</li></ol>\");\n        $this->assertStringContainsString(\"1. First\", $result);\n        $this->assertStringContainsString(\"2. Second\", $result);\n        $this->assertStringContainsString(\"3. Third\", $result);\n    }\n\n    /** Multiple paragraphs are separated by a blank line */\n    public function test_paragraph_multiple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>First paragraph.</p><p>Second paragraph.</p>\");\n        $this->assertStringContainsString(\"First paragraph.\", $result);\n        $this->assertStringContainsString(\"Second paragraph.\", $result);\n    }\n\n    /** Text nested inside divs is extracted correctly */\n    public function test_paragraph_nested_divs(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<div><div><p>Nested text</p></div></div>\");\n        $this->assertStringContainsString(\"Nested text\", $result);\n    }\n\n    /** Simple paragraph converts to plain text */\n    public function test_paragraph_simple(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello World</p>\");\n        $this->assertEquals(\"Hello World\", trim($result));\n    }\n\n    /** Paragraph with bold, italic, and a link */\n    public function test_paragraph_with_inline_formatting(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\");\n        $this->assertStringContainsString(\"**bold**\", $result);\n        $this->assertStringContainsString(\"*italic*\", $result);\n        $this->assertStringContainsString(\"[link](https://example.com)\", $result);\n    }\n\n    /** Paragraph with br tags produces line breaks in output */\n    public function test_paragraph_with_line_breaks(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Line one.<br>Line two.<br>Line three.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Line one.\", $result);\n        $this->assertStringContainsString(\"Line two.\", $result);\n        $this->assertStringContainsString(\"Line three.\", $result);\n    }\n\n    /** Abbreviation element text is preserved */\n    public function test_semantic_abbr(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\");\n        $this->assertStringContainsString(\"WWW\", $result);\n    }\n\n    /** Article element wrapping content preserves inner content */\n    public function test_semantic_article(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<article><h2>Article Title</h2><p>Article body.</p></article>\");\n        $this->assertStringContainsString(\"Article Title\", $result);\n        $this->assertStringContainsString(\"Article body.\", $result);\n    }\n\n    /** Definition list with term and description */\n    public function test_semantic_definition_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\");\n        $this->assertStringContainsString(\"HTML\", $result);\n        $this->assertStringContainsString(\"HyperText Markup Language\", $result);\n        $this->assertStringContainsString(\"CSS\", $result);\n        $this->assertStringContainsString(\"Cascading Style Sheets\", $result);\n    }\n\n    /** Details and summary elements produce readable output */\n    public function test_semantic_details_summary(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Click to expand\", $result);\n    }\n\n    /** Horizontal rule produces a separator in output */\n    public function test_semantic_hr(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Above</p><hr><p>Below</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Above\", $result);\n        $this->assertStringContainsString(\"Below\", $result);\n    }\n\n    /** Mark tag produces highlighted output */\n    public function test_semantic_mark_highlight(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>This is <mark>highlighted text</mark> in a sentence.</p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"highlighted text\", $result);\n    }\n\n    /** Section element with heading preserves structure */\n    public function test_semantic_section_with_heading(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<section><h3>Section Heading</h3><p>Section content.</p></section>\");\n        $this->assertStringContainsString(\"Section Heading\", $result);\n        $this->assertStringContainsString(\"Section content.\", $result);\n    }\n\n    /** Subscript and superscript elements are preserved in output */\n    public function test_semantic_sub_superscript(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"H\", $result);\n        $this->assertStringContainsString(\"2\", $result);\n        $this->assertStringContainsString(\"O\", $result);\n        $this->assertStringContainsString(\"E=mc\", $result);\n    }\n\n    /** Simple table with header */\n    public function test_simple_table(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\");\n        $this->assertStringContainsString(\"Name\", $result);\n        $this->assertStringContainsString(\"Age\", $result);\n        $this->assertStringContainsString(\"Alice\", $result);\n        $this->assertStringContainsString(\"30\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n        $this->assertStringContainsString(\"---\", $result);\n    }\n\n    /** Empty table produces no output or minimal output */\n    public function test_table_empty(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table></table>\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** Table without thead uses first row as implied header */\n    public function test_table_no_thead(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Product\", $result);\n        $this->assertStringContainsString(\"Price\", $result);\n        $this->assertStringContainsString(\"Apple\", $result);\n        $this->assertStringContainsString(\"1.00\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n    }\n\n    /** Table cells containing pipe characters are escaped in output */\n    public function test_table_pipe_chars_in_content(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Expression\", $result);\n        $this->assertStringContainsString(\"Result\", $result);\n        $this->assertStringContainsString(\"true\", $result);\n    }\n\n    /** Table with column alignment attributes */\n    public function test_table_with_alignment(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Left\", $result);\n        $this->assertStringContainsString(\"Center\", $result);\n        $this->assertStringContainsString(\"Right\", $result);\n        $this->assertStringContainsString(\"L\", $result);\n        $this->assertStringContainsString(\"C\", $result);\n        $this->assertStringContainsString(\"R\", $result);\n        $this->assertStringContainsString(\"|\", $result);\n    }\n\n    /** Table with colspan attribute in a header cell */\n    public function test_table_with_colspan(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\");\n        $this->assertNotEmpty($result);\n        $this->assertStringContainsString(\"Full Name\", $result);\n        $this->assertStringContainsString(\"John\", $result);\n        $this->assertStringContainsString(\"Doe\", $result);\n    }\n\n    /** Unordered list */\n    public function test_unordered_list(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\");\n        $this->assertStringContainsString(\"- Item 1\", $result);\n        $this->assertStringContainsString(\"- Item 2\", $result);\n        $this->assertStringContainsString(\"- Item 3\", $result);\n    }\n}\n"
  },
  {
    "path": "test_apps/php/tests/SmokeTest.php",
    "content": "<?php\n// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:c6baa68827ad378cbc575417d4f482e91e487f6b950245a3fe4162aaef25b95b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n\ndeclare(strict_types=1);\n\nnamespace Kreuzberg\\E2e;\n\nuse PHPUnit\\Framework\\TestCase;\nuse HtmlToMarkdown\\HtmlToMarkdown;\n\n/** E2e tests for category: smoke. */\nfinal class SmokeTest extends TestCase\n{\n    /** Empty string produces empty output */\n    public function test_smoke_empty_string(): void\n    {\n        $result = HtmlToMarkdown::convert(\"\");\n        $this->assertEquals(\"\", trim($result));\n    }\n\n    /** H1 heading converts to ATX markdown */\n    public function test_smoke_simple_heading(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<h1>Title</h1>\");\n        $this->assertStringContainsString(\"# Title\", $result);\n    }\n\n    /** Simple paragraph converts correctly */\n    public function test_smoke_simple_paragraph(): void\n    {\n        $result = HtmlToMarkdown::convert(\"<p>Hello World</p>\");\n        $this->assertEquals(\"Hello World\", trim($result));\n        $this->assertNotEmpty($result);\n    }\n}\n"
  },
  {
    "path": "test_apps/php-ext/README.md",
    "content": "# PHP Extension Test App for html-to-markdown\n\nComprehensive test suite validating the native `html_to_markdown` PHP extension built with ext-php-rs.\n\nThis test app exercises the **raw extension functions** directly (e.g. `html_to_markdown_convert()`), without the Composer wrapper package. For tests of the Composer package (`kreuzberg-dev/html-to-markdown`), see `tests/test_apps/php/`.\n\n## What This Test Suite Covers\n\n### Test Organization\n\nThe test suite (`main.php`) is organized into 8 sections:\n\n1. **Extension Loading & Function Availability** - Verifies the extension is loaded and all exported functions exist\n2. **Basic Conversion** - Tests `html_to_markdown_convert()` with headings, paragraphs, lists, links, code, blockquotes, images\n3. **Conversion with Options** - Tests options arrays (heading_style, code_block_style, autolinks, skip_images, strip_tags)\n4. **Inline Image Extraction** - Tests `html_to_markdown_convert_with_inline_images()` return structure and base64 image handling\n5. **Metadata Extraction** - Tests `html_to_markdown_convert_with_metadata()` for document, headers, links, images, Open Graph, structured data\n6. **Multiple Heading Levels** - Tests h1-h6 conversion and metadata extraction\n7. **Complex HTML Structures** - Tests nested lists, tables, mixed formatting, special characters, unicode\n8. **Error Handling** - Tests malformed HTML, deeply nested HTML, script/style stripping, invalid options\n\n## Requirements\n\n- PHP 8.2+\n- The `html_to_markdown` native extension (compiled from `crates/html-to-markdown-php`)\n\nNo Composer dependencies are required. This test app is self-contained.\n\n## Building the Extension\n\n```bash\n# From the repository root\ncargo build --release -p html-to-markdown-php\n```\n\n## Running Tests\n\n```bash\n# Using the test runner script (auto-detects extension location)\nbash run_tests.sh\n\n# Or directly with PHP (if extension is in php.ini)\nphp main.php\n\n# Or directly with PHP (loading extension via -d flag)\nphp -d extension=/path/to/libhtml_to_markdown_php.dylib main.php   # macOS\nphp -d extension=/path/to/libhtml_to_markdown_php.so main.php      # Linux\n```\n\n## Expected Output\n\n```text\n========================================================================\n  TEST SUMMARY\n========================================================================\n  Total:   50+\n  Passed:  50+\n  Failed:  0\n  Skipped: 0\n\n  ALL TESTS PASSED\n========================================================================\n```\n\n## Extension Functions Tested\n\n```php\n// Basic conversion\nhtml_to_markdown_convert(string $html, ?array $options = null): string\n\n// Conversion with inline image extraction\nhtml_to_markdown_convert_with_inline_images(\n    string $html,\n    ?array $options = null,\n    ?array $imageConfig = null\n): array  // ['markdown' => string, 'inline_images' => array, 'warnings' => array]\n\n// Conversion with metadata extraction\nhtml_to_markdown_convert_with_metadata(\n    string $html,\n    ?array $options = null,\n    ?array $metadataConfig = null\n): array  // ['markdown' => string, 'metadata' => array]\n```\n\n### Conversion Options\n\nOptions are passed as associative arrays:\n\n```php\n$options = [\n    'heading_style' => 'atx',          // 'atx', 'atx_closed', 'underlined'\n    'code_block_style' => 'backticks', // 'backticks', 'tildes', 'indented'\n    'escape_asterisks' => true,\n    'autolinks' => true,\n    'skip_images' => false,\n    'strip_tags' => ['nav', 'footer'],\n    // ... and many more (see packages/php-ext/README.md)\n];\n```\n\n## Differences from php/ Test App\n\n| Aspect | `php/` (Composer package) | `php-ext/` (Native extension) |\n|--------|--------------------------|-------------------------------|\n| Dependency | `kreuzberg-dev/html-to-markdown` via Composer | Raw `.so`/`.dylib` extension |\n| Functions | Namespaced: `HtmlToMarkdown\\convert()` | Global: `html_to_markdown_convert()` |\n| Test framework | PHPUnit | Self-contained test runner |\n| Types | Typed objects (ConversionOptions, etc.) | Associative arrays |\n| Requires | Composer install | Cargo build only |\n"
  },
  {
    "path": "test_apps/php-ext/main.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * html-to-markdown PHP Extension (php-ext) - Comprehensive Test Suite\n *\n * Tests the native PHP extension built with ext-php-rs directly, without\n * the Composer wrapper package. This validates the raw extension function:\n *\n * - Extension loading and function availability\n * - html_to_markdown_convert() - conversion returning markdown string\n * - html_to_markdown_convert() with options - conversion options support\n * - Error handling for invalid inputs\n *\n * Requires PHP 8.2+ with the html_to_markdown extension loaded.\n */\n\n// ---------------------------------------------------------------------------\n// Test runner infrastructure\n// ---------------------------------------------------------------------------\n\nfinal class TestRunner\n{\n    private int $passed = 0;\n    private int $failed = 0;\n    private int $skipped = 0;\n    private int $total = 0;\n\n    public function section(string $name): void\n    {\n        echo \"\\n\";\n        echo str_repeat('=', 72) . \"\\n\";\n        echo \"  {$name}\\n\";\n        echo str_repeat('=', 72) . \"\\n\";\n    }\n\n    public function test(string $description, callable $fn): void\n    {\n        $this->total++;\n        try {\n            $fn();\n            $this->passed++;\n            echo \"  PASS  {$description}\\n\";\n        } catch (SkipException $e) {\n            $this->skipped++;\n            echo \"  SKIP  {$description} ({$e->getMessage()})\\n\";\n        } catch (\\Throwable $e) {\n            $this->failed++;\n            $errorMsg = $e->getMessage();\n            $file = basename($e->getFile());\n            $line = $e->getLine();\n            echo \"  FAIL  {$description}\\n\";\n            echo \"        Error: {$errorMsg} ({$file}:{$line})\\n\";\n        }\n    }\n\n    public function summary(): int\n    {\n        echo \"\\n\";\n        echo str_repeat('=', 72) . \"\\n\";\n        echo \"  TEST SUMMARY\\n\";\n        echo str_repeat('=', 72) . \"\\n\";\n        echo \"  Total:   {$this->total}\\n\";\n        echo \"  Passed:  {$this->passed}\\n\";\n        echo \"  Failed:  {$this->failed}\\n\";\n        echo \"  Skipped: {$this->skipped}\\n\";\n        echo \"\\n\";\n\n        if ($this->failed === 0) {\n            echo \"  ALL TESTS PASSED\\n\";\n        } else {\n            echo \"  SOME TESTS FAILED\\n\";\n        }\n\n        echo str_repeat('=', 72) . \"\\n\";\n\n        return $this->failed > 0 ? 1 : 0;\n    }\n}\n\nfinal class SkipException extends \\RuntimeException\n{\n}\n\nfunction skip(string $reason): never\n{\n    throw new SkipException($reason);\n}\n\nfunction assert_true(bool $value, string $message = 'Expected true'): void\n{\n    if (!$value) {\n        throw new \\RuntimeException(\"Assertion failed: {$message}\");\n    }\n}\n\nfunction assert_false(bool $value, string $message = 'Expected false'): void\n{\n    if ($value) {\n        throw new \\RuntimeException(\"Assertion failed: {$message}\");\n    }\n}\n\nfunction assert_equals(mixed $expected, mixed $actual, string $message = ''): void\n{\n    if ($expected !== $actual) {\n        $expectedStr = var_export($expected, true);\n        $actualStr = var_export($actual, true);\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\n            \"Assertion failed: {$msg}expected {$expectedStr}, got {$actualStr}\"\n        );\n    }\n}\n\nfunction assert_not_empty(mixed $value, string $message = 'Expected non-empty value'): void\n{\n    if (empty($value)) {\n        throw new \\RuntimeException(\"Assertion failed: {$message}\");\n    }\n}\n\nfunction assert_string_contains(string $needle, string $haystack, string $message = ''): void\n{\n    if (!str_contains($haystack, $needle)) {\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\n            \"Assertion failed: {$msg}string does not contain '{$needle}'\"\n        );\n    }\n}\n\nfunction assert_string_not_contains(string $needle, string $haystack, string $message = ''): void\n{\n    if (str_contains($haystack, $needle)) {\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\n            \"Assertion failed: {$msg}string should not contain '{$needle}'\"\n        );\n    }\n}\n\nfunction assert_array_key(string $key, array $array, string $message = ''): void\n{\n    if (!array_key_exists($key, $array)) {\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\"Assertion failed: {$msg}key '{$key}' not found in array\");\n    }\n}\n\nfunction assert_greater_than(int|float $expected, int|float $actual, string $message = ''): void\n{\n    if ($actual <= $expected) {\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\n            \"Assertion failed: {$msg}expected value > {$expected}, got {$actual}\"\n        );\n    }\n}\n\nfunction assert_throws(string $exceptionClass, callable $fn, string $message = ''): void\n{\n    try {\n        $fn();\n        $msg = $message !== '' ? \"{$message}: \" : '';\n        throw new \\RuntimeException(\n            \"Assertion failed: {$msg}expected {$exceptionClass} to be thrown\"\n        );\n    } catch (\\Throwable $e) {\n        if (!($e instanceof $exceptionClass)) {\n            $actual = get_class($e);\n            $msg = $message !== '' ? \"{$message}: \" : '';\n            throw new \\RuntimeException(\n                \"Assertion failed: {$msg}expected {$exceptionClass}, got {$actual}: {$e->getMessage()}\"\n            );\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Run tests\n// ---------------------------------------------------------------------------\n\n$runner = new TestRunner();\n\necho str_repeat('=', 72) . \"\\n\";\necho \"  html-to-markdown PHP Extension Test Suite\\n\";\necho str_repeat('=', 72) . \"\\n\";\necho \"  PHP Version:      \" . PHP_VERSION . \"\\n\";\necho \"  Extension Loaded: \" . (extension_loaded('html_to_markdown') ? 'Yes' : 'No') . \"\\n\";\n\n// =========================================================================\n// Section 1: Extension Loading & Function Availability\n// =========================================================================\n\n$runner->section('1. Extension Loading & Function Availability');\n\n$runner->test('html_to_markdown extension is loaded', function (): void {\n    assert_true(\n        extension_loaded('html_to_markdown'),\n        'html_to_markdown extension must be loaded'\n    );\n});\n\n$runner->test('html_to_markdown_convert function exists', function (): void {\n    assert_true(\n        function_exists('html_to_markdown_convert'),\n        'html_to_markdown_convert should be available'\n    );\n});\n\n// =========================================================================\n// Section 2: Basic Conversion (html_to_markdown_convert)\n// =========================================================================\n\n$runner->section('2. Basic Conversion');\n\n$runner->test('convert returns string', function (): void {\n    $result = html_to_markdown_convert('<p>Hello World</p>');\n    assert_true(is_string($result), 'result should be a string');\n});\n\n$runner->test('convert simple paragraph', function (): void {\n    $result = html_to_markdown_convert('<p>Hello World</p>');\n    assert_true(is_string($result), 'content should be a string');\n    assert_string_contains('Hello World', $result);\n});\n\n$runner->test('convert empty string', function (): void {\n    $result = html_to_markdown_convert('');\n    assert_true(is_string($result), 'result should be a string');\n    assert_equals('', $result, 'empty input should produce empty content');\n});\n\n$runner->test('convert heading h1', function (): void {\n    $result = html_to_markdown_convert('<h1>Title</h1>');\n    assert_string_contains('Title', $result);\n});\n\n$runner->test('convert heading h2', function (): void {\n    $result = html_to_markdown_convert('<h2>Subtitle</h2>');\n    assert_string_contains('Subtitle', $result);\n});\n\n$runner->test('convert bold text', function (): void {\n    $result = html_to_markdown_convert('<p>Hello <strong>Bold</strong> text</p>');\n    assert_string_contains('**Bold**', $result);\n});\n\n$runner->test('convert italic text', function (): void {\n    $result = html_to_markdown_convert('<p>Hello <em>Italic</em> text</p>');\n    assert_string_contains('*Italic*', $result);\n});\n\n$runner->test('convert unordered list', function (): void {\n    $result = html_to_markdown_convert('<ul><li>Item 1</li><li>Item 2</li></ul>');\n    assert_string_contains('Item 1', $result);\n    assert_string_contains('Item 2', $result);\n});\n\n$runner->test('convert ordered list', function (): void {\n    $result = html_to_markdown_convert('<ol><li>First</li><li>Second</li></ol>');\n    assert_string_contains('First', $result);\n    assert_string_contains('Second', $result);\n});\n\n$runner->test('convert link', function (): void {\n    $result = html_to_markdown_convert('<a href=\"https://example.com\">Example</a>');\n    assert_string_contains('Example', $result);\n    assert_string_contains('https://example.com', $result);\n});\n\n$runner->test('convert inline code', function (): void {\n    $result = html_to_markdown_convert('<code>console.log()</code>');\n    assert_string_contains('console.log()', $result);\n});\n\n$runner->test('convert code block', function (): void {\n    $result = html_to_markdown_convert('<pre><code>function hello() {}</code></pre>');\n    assert_string_contains('function hello() {}', $result);\n});\n\n$runner->test('convert blockquote', function (): void {\n    $result = html_to_markdown_convert('<blockquote>Quote text</blockquote>');\n    assert_string_contains('Quote text', $result);\n});\n\n$runner->test('convert image', function (): void {\n    $result = html_to_markdown_convert('<img src=\"https://example.com/img.png\" alt=\"Alt text\">');\n    assert_string_contains('Alt text', $result);\n    assert_string_contains('https://example.com/img.png', $result);\n});\n\n$runner->test('convert horizontal rule', function (): void {\n    $result = html_to_markdown_convert('<p>Above</p><hr><p>Below</p>');\n    assert_string_contains('Above', $result);\n    assert_string_contains('Below', $result);\n});\n\n$runner->test('convert nested HTML', function (): void {\n    $html = '<div><h1>Header</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em></p><ul><li>Item</li></ul></div>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Header', $result);\n    assert_string_contains('**bold**', $result);\n    assert_string_contains('*italic*', $result);\n    assert_string_contains('Item', $result);\n});\n\n$runner->test('convert with null options', function (): void {\n    $result = html_to_markdown_convert('<p>Test</p>', null);\n    assert_string_contains('Test', $result);\n});\n\n// =========================================================================\n// Section 3: Conversion with Options\n// =========================================================================\n\n$runner->section('3. Conversion with Options');\n\n$runner->test('convert with heading_style atx option', function (): void {\n    $result = html_to_markdown_convert('<h1>Title</h1>', ['heading_style' => 'atx']);\n    assert_true(is_string($result), 'result should be a string');\n    assert_string_contains('Title', $result);\n});\n\n$runner->test('convert with heading_style atx_closed option', function (): void {\n    $result = html_to_markdown_convert('<h1>Title</h1>', ['heading_style' => 'atx_closed']);\n    assert_true(is_string($result), 'result should be a string');\n    assert_string_contains('Title', $result);\n});\n\n$runner->test('convert with code_block_style backticks', function (): void {\n    $result = html_to_markdown_convert(\n        '<pre><code>code</code></pre>',\n        ['code_block_style' => 'backticks']\n    );\n    assert_string_contains('code', $result);\n});\n\n$runner->test('convert with escape_asterisks false', function (): void {\n    $result = html_to_markdown_convert('<p>Hello World</p>', ['escape_asterisks' => false]);\n    assert_string_contains('Hello World', $result);\n});\n\n$runner->test('convert with empty options array', function (): void {\n    $result = html_to_markdown_convert('<p>Test</p>', []);\n    assert_string_contains('Test', $result);\n});\n\n$runner->test('convert with autolinks option', function (): void {\n    $result = html_to_markdown_convert(\n        '<a href=\"https://example.com\">https://example.com</a>',\n        ['autolinks' => true]\n    );\n    assert_string_contains('example.com', $result);\n});\n\n$runner->test('convert with skip_images option', function (): void {\n    $html = '<p>Text <img src=\"image.png\" alt=\"pic\"> more text</p>';\n    $result = html_to_markdown_convert($html, ['skip_images' => true]);\n    assert_string_contains('Text', $result);\n    assert_string_contains('more text', $result);\n    assert_string_not_contains('![pic]', $result, 'Image markdown should be removed');\n    assert_string_not_contains('image.png', $result, 'Image URL should be removed');\n});\n\n$runner->test('convert with strip_tags option', function (): void {\n    $html = '<div><nav>Navigation</nav><p>Content</p></div>';\n    $result = html_to_markdown_convert($html, ['strip_tags' => ['nav']]);\n    assert_string_contains('Content', $result);\n});\n\n// =========================================================================\n// Section 4: Result Structure\n// =========================================================================\n\n$runner->section('4. Result Structure');\n\n$runner->test('result is a string', function (): void {\n    $html = '<p>Hello World</p>';\n    $result = html_to_markdown_convert($html);\n    assert_true(is_string($result), 'result should be a string');\n    assert_string_contains('Hello World', $result);\n});\n\n$runner->test('result from full HTML document is a string', function (): void {\n    $html = <<<'HTML'\n<html lang=\"en\">\n    <head>\n        <title>Test Article</title>\n        <meta name=\"description\" content=\"A test description\">\n    </head>\n    <body>\n        <h1>Main Title</h1>\n        <p>Content</p>\n    </body>\n</html>\nHTML;\n\n    $result = html_to_markdown_convert($html);\n    assert_true(is_string($result), 'result should be a string');\n    assert_string_contains('Main Title', $result);\n});\n\n// =========================================================================\n// Section 5: Multiple Heading Levels\n// =========================================================================\n\n$runner->section('5. Multiple Heading Levels');\n\n$runner->test('convert all heading levels h1-h6', function (): void {\n    $html = '<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('H1', $result);\n    assert_string_contains('H2', $result);\n    assert_string_contains('H3', $result);\n    assert_string_contains('H4', $result);\n    assert_string_contains('H5', $result);\n    assert_string_contains('H6', $result);\n});\n\n// =========================================================================\n// Section 6: Complex HTML Structures\n// =========================================================================\n\n$runner->section('6. Complex HTML Structures');\n\n$runner->test('convert nested lists', function (): void {\n    $html = '<ul><li>Parent<ul><li>Child 1</li><li>Child 2</li></ul></li></ul>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Parent', $result);\n    assert_string_contains('Child 1', $result);\n    assert_string_contains('Child 2', $result);\n});\n\n$runner->test('convert table', function (): void {\n    $html = '<table><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td>A</td><td>1</td></tr></tbody></table>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Name', $result);\n    assert_string_contains('Value', $result);\n    assert_string_contains('A', $result);\n});\n\n$runner->test('convert mixed inline formatting', function (): void {\n    $html = '<p>Text with <strong>bold</strong>, <em>italic</em>, and <code>code</code></p>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('**bold**', $result);\n    assert_string_contains('*italic*', $result);\n    assert_string_contains('`code`', $result);\n});\n\n$runner->test('convert complex document structure', function (): void {\n    $html = <<<'HTML'\n<div>\n    <h1>Header</h1>\n    <p>Paragraph with <strong>bold</strong> and <em>italic</em></p>\n    <ul>\n        <li>Item 1</li>\n        <li>Item 2</li>\n    </ul>\n    <blockquote>Quote</blockquote>\n    <pre><code>code snippet</code></pre>\n</div>\nHTML;\n\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Header', $result);\n    assert_string_contains('**bold**', $result);\n    assert_string_contains('*italic*', $result);\n    assert_string_contains('Item 1', $result);\n    assert_string_contains('Item 2', $result);\n    assert_string_contains('Quote', $result);\n    assert_string_contains('code snippet', $result);\n});\n\n$runner->test('convert HTML with special characters', function (): void {\n    $html = '<p>Ampersand &amp; less-than &lt; greater-than &gt;</p>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Ampersand', $result);\n});\n\n$runner->test('convert HTML with unicode content', function (): void {\n    $html = '<p>Unicode: cafe, naive, resume</p>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Unicode', $result);\n    assert_string_contains('cafe', $result);\n});\n\n// =========================================================================\n// Section 7: Error Handling\n// =========================================================================\n\n$runner->section('7. Error Handling');\n\n$runner->test('convert handles malformed HTML gracefully', function (): void {\n    $html = '<p>Unclosed paragraph<div>And a div</p></div>';\n    $result = html_to_markdown_convert($html);\n    assert_true(is_string($result), 'should still return a string');\n});\n\n$runner->test('convert handles deeply nested HTML', function (): void {\n    $depth = 50;\n    $html = str_repeat('<div>', $depth) . 'Content' . str_repeat('</div>', $depth);\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Content', $result);\n});\n\n$runner->test('convert handles HTML with only whitespace', function (): void {\n    $result = html_to_markdown_convert('   ');\n    assert_true(is_string($result), 'should return a string');\n});\n\n$runner->test('convert handles script tags (should be stripped)', function (): void {\n    $html = '<p>Text</p><script>alert(\"xss\")</script><p>More</p>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Text', $result);\n    assert_string_contains('More', $result);\n});\n\n$runner->test('convert handles style tags (should be stripped)', function (): void {\n    $html = '<style>body { color: red; }</style><p>Visible</p>';\n    $result = html_to_markdown_convert($html);\n    assert_string_contains('Visible', $result);\n});\n\n$runner->test('convert with invalid heading_style throws exception', function (): void {\n    assert_throws(\n        \\Exception::class,\n        static fn () => html_to_markdown_convert('<h1>Title</h1>', ['heading_style' => 'invalid']),\n        'invalid heading_style'\n    );\n});\n\n// =========================================================================\n// Print summary\n// =========================================================================\n\nexit($runner->summary());\n"
  },
  {
    "path": "test_apps/php-ext/run_tests.sh",
    "content": "#!/bin/bash\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../..\" && pwd)\"\n\n# Set library paths for the Rust extension\nexport DYLD_LIBRARY_PATH=\"$REPO_ROOT/target/release:${DYLD_LIBRARY_PATH:-}\"\nexport LD_LIBRARY_PATH=\"$REPO_ROOT/target/release:${LD_LIBRARY_PATH:-}\"\n\n# Determine extension loading arguments\nPHP_EXT_ARGS=\"\"\nif ! php -m 2>/dev/null | grep -q html_to_markdown; then\n  # Try to locate the compiled extension\n  # macOS produces .dylib, Linux produces .so\n  CANDIDATES=(\n    \"$REPO_ROOT/target/release/libhtml_to_markdown_php.dylib\"\n    \"$REPO_ROOT/target/release/deps/libhtml_to_markdown_php.dylib\"\n    \"$REPO_ROOT/target/release/libhtml_to_markdown_php.so\"\n    \"$REPO_ROOT/target/release/deps/libhtml_to_markdown_php.so\"\n  )\n\n  EXT_PATH=\"\"\n  for candidate in \"${CANDIDATES[@]}\"; do\n    if [ -f \"$candidate\" ]; then\n      EXT_PATH=\"$candidate\"\n      break\n    fi\n  done\n\n  if [ -n \"$EXT_PATH\" ]; then\n    PHP_EXT_ARGS=\"-d extension=$EXT_PATH\"\n    echo \"Loading extension from: $EXT_PATH\"\n  else\n    echo \"Warning: html_to_markdown extension not found. Build it first:\"\n    echo \"  cargo build --release -p html-to-markdown-php\"\n    echo \"\"\n    echo \"Searched locations:\"\n    for candidate in \"${CANDIDATES[@]}\"; do\n      echo \"  $candidate\"\n    done\n    exit 1\n  fi\nfi\n\necho \"Running html-to-markdown PHP extension test suite...\"\necho \"\"\n\n# shellcheck disable=SC2086\nphp $PHP_EXT_ARGS \"$SCRIPT_DIR/main.php\"\n"
  },
  {
    "path": "test_apps/python/.python-version",
    "content": "3.10\n"
  },
  {
    "path": "test_apps/python/README.md",
    "content": "# Python Test App for html-to-markdown\n\nTests the published html-to-markdown package from PyPI.\n\n## Setup\n\n```bash\nuv sync\n```\n\n## Run Tests\n\n```bash\n# Smoke tests (fast)\nuv run pytest smoke_test.py -v\n\n# Comprehensive tests\nuv run pytest comprehensive_test.py -v\n\n# All tests\nuv run pytest -v\n```\n"
  },
  {
    "path": "test_apps/python/__init__.py",
    "content": ""
  },
  {
    "path": "test_apps/python/conftest.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:fce3aac894dd4a952ead1fa0eb15d4ca6c772bd7fef724352bbb799383af256f\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"Pytest configuration for e2e tests.\"\"\"\n# Ensure the package is importable.\n# The html_to_markdown package is expected to be installed in the current environment.\n"
  },
  {
    "path": "test_apps/python/pyproject.toml",
    "content": "[build-system]\nbuild-backend = \"setuptools.build_meta\"\nrequires = [ \"setuptools>=68\", \"wheel\" ]\n\n[project]\nname = \"html-to-markdown-e2e-tests\"\nversion = \"0.0.0\"\ndescription = \"End-to-end tests\"\nrequires-python = \">=3.10\"\nclassifiers = [\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n]\ndependencies = [ \"html-to-markdown>=3.2\", \"pytest>=7.4\", \"pytest-asyncio>=0.23\", \"pytest-timeout>=2.1\" ]\n\n[tool.setuptools]\npackages = []\n\n[tool.ruff]\nlint.ignore = [ \"PLR2004\" ]\nlint.per-file-ignores.\"tests/**\" = [ \"B017\", \"PT011\", \"S101\", \"S108\" ]\n\n[tool.pytest]\nini_options.asyncio_mode = \"auto\"\nini_options.testpaths = [ \"tests\" ]\nini_options.python_files = \"test_*.py\"\nini_options.python_functions = \"test_*\"\nini_options.addopts = \"-v --strict-markers --tb=short\"\nini_options.timeout = 300\n"
  },
  {
    "path": "test_apps/python/tests/__init__.py",
    "content": ""
  },
  {
    "path": "test_apps/python/tests/test_conversion.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:db983bd9630a89a6bfe1e95625ac422496c3a403322b866a5706e4295f1f954e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: conversion.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_blockquote_multiple_paragraphs() -> None:\n    \"\"\"Blockquote with multiple paragraphs has each paragraph prefixed.\"\"\"\n    html = \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"> First paragraph.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"> Second paragraph.\" in result.content  # noqa: S101\n\n\ndef test_blockquote_nested() -> None:\n    \"\"\"Nested blockquote produces double-prefixed lines.\"\"\"\n    html = \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Outer quote.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Inner quote.\" in result.content  # noqa: S101\n\n\ndef test_blockquote_simple() -> None:\n    \"\"\"Simple blockquote.\"\"\"\n    html = \"<blockquote><p>Quote text</p></blockquote>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"> Quote text\" in result.content  # noqa: S101\n\n\ndef test_blockquote_with_list() -> None:\n    \"\"\"Blockquote containing a list preserves list items inside quote.\"\"\"\n    html = \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Quote intro:\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Point one\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Point two\" in result.content  # noqa: S101\n\n\ndef test_bold_and_italic() -> None:\n    \"\"\"Nested bold and italic.\"\"\"\n    html = \"<p><strong><em>both</em></strong></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"***both***\" in result.content  # noqa: S101\n\n\ndef test_bold_strong() -> None:\n    \"\"\"Strong tag converts to bold.\"\"\"\n    html = \"<p><strong>bold</strong></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**bold**\" in result.content  # noqa: S101\n\n\ndef test_code_block() -> None:\n    \"\"\"Code block with language preserves content.\"\"\"\n    html = \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"print('hello')\" in result.content  # noqa: S101\n\n\ndef test_code_block_no_language() -> None:\n    \"\"\"Code block without a language class preserves content.\"\"\"\n    html = \"<pre><code>plain code here</code></pre>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"plain code here\" in result.content  # noqa: S101\n\n\ndef test_code_inline_in_paragraph() -> None:\n    \"\"\"Inline code element nested inside a paragraph.\"\"\"\n    html = \"<p>Call the <code>initialize()</code> method first.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"`initialize()`\" in result.content  # noqa: S101\n\n\ndef test_code_with_backticks_in_content() -> None:\n    \"\"\"Inline code containing backtick characters is properly escaped.\"\"\"\n    html = \"<p>Use <code>`backtick` here</code> carefully.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"backtick\" in result.content  # noqa: S101\n\n\ndef test_emphasis_mark_highlight() -> None:\n    \"\"\"mark tag produces highlighted output.\"\"\"\n    html = \"<p><mark>highlighted</mark></p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"highlighted\" in result.content  # noqa: S101\n\n\ndef test_emphasis_strikethrough_del() -> None:\n    \"\"\"del tag converts to GFM strikethrough.\"\"\"\n    html = \"<p><del>deleted text</del></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"~~deleted text~~\" in result.content  # noqa: S101\n\n\ndef test_emphasis_strikethrough_s() -> None:\n    \"\"\"s tag converts to GFM strikethrough.\"\"\"\n    html = \"<p><s>strikethrough</s></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"~~strikethrough~~\" in result.content  # noqa: S101\n\n\ndef test_emphasis_subscript() -> None:\n    \"\"\"sub tag content is preserved.\"\"\"\n    html = \"<p>H<sub>2</sub>O</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"H\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"O\" in result.content  # noqa: S101\n\n\ndef test_emphasis_superscript() -> None:\n    \"\"\"sup tag content is preserved.\"\"\"\n    html = \"<p>x<sup>2</sup></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"x\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n\n\ndef test_emphasis_underline_u() -> None:\n    \"\"\"u tag content is preserved in output.\"\"\"\n    html = \"<p><u>underlined</u></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"underlined\" in result.content  # noqa: S101\n\n\ndef test_form_input_elements() -> None:\n    \"\"\"Form input elements produce readable output without form mechanics.\"\"\"\n    html = '<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Name\" in result.content  # noqa: S101\n\n\ndef test_form_select_options() -> None:\n    \"\"\"Select element with options produces readable output.\"\"\"\n    html = '<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Color\" in result.content  # noqa: S101\n\n\ndef test_form_textarea() -> None:\n    \"\"\"Textarea element produces readable output.\"\"\"\n    html = (\n        \"<form><label>Message:</label><textarea>Default text content</textarea></form>\"\n    )\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Message\" in result.content  # noqa: S101\n\n\ndef test_heading_h1() -> None:\n    \"\"\"H1 heading.\"\"\"\n    html = \"<h1>Heading 1</h1>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"# Heading 1\"  # noqa: S101\n\n\ndef test_heading_h2() -> None:\n    \"\"\"H2 heading.\"\"\"\n    html = \"<h2>Heading 2</h2>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"## Heading 2\"  # noqa: S101\n\n\ndef test_heading_h3() -> None:\n    \"\"\"H3 heading.\"\"\"\n    html = \"<h3>Heading 3</h3>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"### Heading 3\"  # noqa: S101\n\n\ndef test_heading_h4() -> None:\n    \"\"\"H4 heading.\"\"\"\n    html = \"<h4>Heading 4</h4>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"#### Heading 4\"  # noqa: S101\n\n\ndef test_heading_h5() -> None:\n    \"\"\"H5 heading.\"\"\"\n    html = \"<h5>Heading 5</h5>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"##### Heading 5\"  # noqa: S101\n\n\ndef test_heading_h6() -> None:\n    \"\"\"H6 heading.\"\"\"\n    html = \"<h6>Heading 6</h6>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"###### Heading 6\"  # noqa: S101\n\n\ndef test_image_figure_figcaption() -> None:\n    \"\"\"Figure with figcaption preserves both image and caption.\"\"\"\n    html = '<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![A sunset](sunset.jpg)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Beautiful sunset over the ocean\" in result.content  # noqa: S101\n\n\ndef test_image_linked() -> None:\n    \"\"\"Image inside an anchor produces a linked image.\"\"\"\n    html = '<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Icon](icon.png)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_image_no_alt() -> None:\n    \"\"\"Image without alt text produces image markdown.\"\"\"\n    html = '<img src=\"banner.jpg\">'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"banner.jpg\" in result.content  # noqa: S101\n\n\ndef test_image_simple() -> None:\n    \"\"\"Image with alt text.\"\"\"\n    html = '<img src=\"photo.jpg\" alt=\"A photo\">'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![A photo](photo.jpg)\" in result.content  # noqa: S101\n\n\ndef test_image_with_title() -> None:\n    \"\"\"Image with title attribute includes title in output.\"\"\"\n    html = '<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Sales chart](chart.png\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Q3 Sales\" in result.content  # noqa: S101\n\n\ndef test_inline_code() -> None:\n    \"\"\"Inline code.\"\"\"\n    html = \"<p>Use <code>console.log()</code> to debug</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"`console.log()`\" in result.content  # noqa: S101\n\n\ndef test_italic_em() -> None:\n    \"\"\"Em tag converts to italic.\"\"\"\n    html = \"<p><em>italic</em></p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"*italic*\" in result.content  # noqa: S101\n\n\ndef test_line_break_br_tag() -> None:\n    \"\"\"Single br tag produces a line break in output.\"\"\"\n    html = \"<p>First line.<br>Second line.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First line.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second line.\" in result.content  # noqa: S101\n\n\ndef test_line_break_hr_tag() -> None:\n    \"\"\"hr tag produces a horizontal separator between content.\"\"\"\n    html = \"<p>Before rule.</p><hr><p>After rule.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Before rule.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"After rule.\" in result.content  # noqa: S101\n\n\ndef test_line_break_multiple_br() -> None:\n    \"\"\"Multiple consecutive br tags in sequence.\"\"\"\n    html = \"<p>Start.<br><br>End.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Start.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"End.\" in result.content  # noqa: S101\n\n\ndef test_link_anchor_fragment() -> None:\n    \"\"\"Fragment-only anchor link is preserved.\"\"\"\n    html = '<a href=\"#section\">Jump to section</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Jump to section](#section)\" in result.content  # noqa: S101\n\n\ndef test_link_empty_href() -> None:\n    \"\"\"Link with empty href produces output with the link text.\"\"\"\n    html = '<a href=\"\">No destination</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"No destination\" in result.content  # noqa: S101\n\n\ndef test_link_image_inside() -> None:\n    \"\"\"Image inside a link produces a linked image.\"\"\"\n    html = '<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"![Logo](logo.png)\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_link_mailto() -> None:\n    \"\"\"Mailto link is preserved with mailto: scheme.\"\"\"\n    html = '<a href=\"mailto:user@example.com\">Email us</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"mailto:user@example.com\" in result.content  # noqa: S101\n\n\ndef test_link_simple() -> None:\n    \"\"\"Simple link.\"\"\"\n    html = '<a href=\"https://example.com\">Example</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Example](https://example.com)\" in result.content  # noqa: S101\n\n\ndef test_link_with_bold_text() -> None:\n    \"\"\"Link containing bold text preserves formatting.\"\"\"\n    html = '<a href=\"https://example.com\"><strong>Bold link</strong></a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**Bold link**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"https://example.com\" in result.content  # noqa: S101\n\n\ndef test_link_with_title() -> None:\n    \"\"\"Link with title attribute.\"\"\"\n    html = '<a href=\"https://example.com\" title=\"Example Site\">Example</a>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"[Example](https://example.com\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Example Site\" in result.content  # noqa: S101\n\n\ndef test_list_definition_dl() -> None:\n    \"\"\"Definition list with dt and dd elements.\"\"\"\n    html = \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Term One\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Definition of term one.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Term Two\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Definition of term two.\" in result.content  # noqa: S101\n\n\ndef test_list_item_multiple_paragraphs() -> None:\n    \"\"\"List item containing multiple paragraphs.\"\"\"\n    html = \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph in item.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph in item.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Simple item\" in result.content  # noqa: S101\n\n\ndef test_list_mixed_nested() -> None:\n    \"\"\"Mixed list: ordered list nested inside unordered list.\"\"\"\n    html = (\n        \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\"\n    )\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Item A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Sub 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Sub 2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Item B\" in result.content  # noqa: S101\n\n\ndef test_list_nested_ordered() -> None:\n    \"\"\"Nested ordered list with two levels of depth.\"\"\"\n    html = \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1a\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 1b\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Step 2\" in result.content  # noqa: S101\n\n\ndef test_list_nested_unordered() -> None:\n    \"\"\"Nested unordered list with two levels of depth.\"\"\"\n    html = \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Parent A\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Child A1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Child A2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Parent B\" in result.content  # noqa: S101\n\n\ndef test_list_task_checkboxes() -> None:\n    \"\"\"Task list with checked and unchecked checkboxes.\"\"\"\n    html = '<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Done task\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Pending task\" in result.content  # noqa: S101\n\n\ndef test_ordered_list() -> None:\n    \"\"\"Ordered list.\"\"\"\n    html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"1. First\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2. Second\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"3. Third\" in result.content  # noqa: S101\n\n\ndef test_paragraph_multiple() -> None:\n    \"\"\"Multiple paragraphs are separated by a blank line.\"\"\"\n    html = \"<p>First paragraph.</p><p>Second paragraph.</p>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"First paragraph.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Second paragraph.\" in result.content  # noqa: S101\n\n\ndef test_paragraph_nested_divs() -> None:\n    \"\"\"Text nested inside divs is extracted correctly.\"\"\"\n    html = \"<div><div><p>Nested text</p></div></div>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Nested text\" in result.content  # noqa: S101\n\n\ndef test_paragraph_simple() -> None:\n    \"\"\"Simple paragraph converts to plain text.\"\"\"\n    html = \"<p>Hello World</p>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"Hello World\"  # noqa: S101\n\n\ndef test_paragraph_with_inline_formatting() -> None:\n    \"\"\"Paragraph with bold, italic, and a link.\"\"\"\n    html = '<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"**bold**\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"*italic*\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"[link](https://example.com)\" in result.content  # noqa: S101\n\n\ndef test_paragraph_with_line_breaks() -> None:\n    \"\"\"Paragraph with br tags produces line breaks in output.\"\"\"\n    html = \"<p>Line one.<br>Line two.<br>Line three.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line one.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line two.\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Line three.\" in result.content  # noqa: S101\n\n\ndef test_semantic_abbr() -> None:\n    \"\"\"Abbreviation element text is preserved.\"\"\"\n    html = '<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>'\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"WWW\" in result.content  # noqa: S101\n\n\ndef test_semantic_article() -> None:\n    \"\"\"Article element wrapping content preserves inner content.\"\"\"\n    html = \"<article><h2>Article Title</h2><p>Article body.</p></article>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Article Title\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Article body.\" in result.content  # noqa: S101\n\n\ndef test_semantic_definition_list() -> None:\n    \"\"\"Definition list with term and description.\"\"\"\n    html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"HTML\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"HyperText Markup Language\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"CSS\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Cascading Style Sheets\" in result.content  # noqa: S101\n\n\ndef test_semantic_details_summary() -> None:\n    \"\"\"Details and summary elements produce readable output.\"\"\"\n    html = \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Click to expand\" in result.content  # noqa: S101\n\n\ndef test_semantic_hr() -> None:\n    \"\"\"Horizontal rule produces a separator in output.\"\"\"\n    html = \"<p>Above</p><hr><p>Below</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Above\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Below\" in result.content  # noqa: S101\n\n\ndef test_semantic_mark_highlight() -> None:\n    \"\"\"Mark tag produces highlighted output.\"\"\"\n    html = \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"highlighted text\" in result.content  # noqa: S101\n\n\ndef test_semantic_section_with_heading() -> None:\n    \"\"\"Section element with heading preserves structure.\"\"\"\n    html = \"<section><h3>Section Heading</h3><p>Section content.</p></section>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Section Heading\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Section content.\" in result.content  # noqa: S101\n\n\ndef test_semantic_sub_superscript() -> None:\n    \"\"\"Subscript and superscript elements are preserved in output.\"\"\"\n    html = \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"H\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"O\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"E=mc\" in result.content  # noqa: S101\n\n\ndef test_simple_table() -> None:\n    \"\"\"Simple table with header.\"\"\"\n    html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"Name\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Age\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Alice\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"30\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"---\" in result.content  # noqa: S101\n\n\ndef test_table_empty() -> None:\n    \"\"\"Empty table produces no output or minimal output.\"\"\"\n    html = \"<table></table>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_table_no_thead() -> None:\n    \"\"\"Table without thead uses first row as implied header.\"\"\"\n    html = \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Product\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Price\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Apple\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"1.00\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n\n\ndef test_table_pipe_chars_in_content() -> None:\n    \"\"\"Table cells containing pipe characters are escaped in output.\"\"\"\n    html = \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\"\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Expression\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Result\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"true\" in result.content  # noqa: S101\n\n\ndef test_table_with_alignment() -> None:\n    \"\"\"Table with column alignment attributes.\"\"\"\n    html = '<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Left\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Center\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Right\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"L\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"C\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"R\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"|\" in result.content  # noqa: S101\n\n\ndef test_table_with_colspan() -> None:\n    \"\"\"Table with colspan attribute in a header cell.\"\"\"\n    html = '<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>'\n    result = convert(html=html)\n    assert result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Full Name\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"John\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"Doe\" in result.content  # noqa: S101\n\n\ndef test_unordered_list() -> None:\n    \"\"\"Unordered list.\"\"\"\n    html = \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 1\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 2\" in result.content  # noqa: S101\n    assert result.content is not None  # noqa: S101\n    assert \"- Item 3\" in result.content  # noqa: S101\n"
  },
  {
    "path": "test_apps/python/tests/test_smoke.py",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:a0b762c33d7b7853dc41a06d63ae8f9af52146e7f09acb831c7655769cbb0616\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\"\"\"E2e tests for category: smoke.\"\"\"\n\nfrom html_to_markdown import convert\n\n\ndef test_smoke_empty_string() -> None:\n    \"\"\"Empty string produces empty output.\"\"\"\n    html = \"\"\n    result = convert(html=html)\n    assert result.content.strip() == \"\"  # noqa: S101\n\n\ndef test_smoke_simple_heading() -> None:\n    \"\"\"H1 heading converts to ATX markdown.\"\"\"\n    html = \"<h1>Title</h1>\"\n    result = convert(html=html)\n    assert result.content is not None  # noqa: S101\n    assert \"# Title\" in result.content  # noqa: S101\n\n\ndef test_smoke_simple_paragraph() -> None:\n    \"\"\"Simple paragraph converts correctly.\"\"\"\n    html = \"<p>Hello World</p>\"\n    result = convert(html=html)\n    assert result.content.strip() == \"Hello World\"  # noqa: S101\n    assert result.content  # noqa: S101\n"
  },
  {
    "path": "test_apps/r/DESCRIPTION",
    "content": "Package: e2e.r\nTitle: E2E Tests for htmltomarkdown\nVersion: 0.1.0\nDescription: End-to-end test suite.\nImports: htmltomarkdown (3.2.0)\nSuggests: testthat (>= 3.0.0)\nConfig/testthat/edition: 3\n"
  },
  {
    "path": "test_apps/r/run_tests.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:b404865c05b21c265e1b809a049b2d899b7e6f82a9f58efecfb60fb382714e4e\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\nlibrary(testthat)\n# Package loaded via library() from CRAN install.\n\ntest_dir(\"tests\")\n"
  },
  {
    "path": "test_apps/r/tests/test_conversion.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:bff44762a4a4bccbe86adcf3bb62c4d9365bdd223ad06ced1838f02f0cfb5233\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: conversion\n\ntest_that(\"blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed\", {\n  result <- convert(html = \"<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>\")\n  expect_true(grepl(\"> First paragraph.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"> Second paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_nested: Nested blockquote produces double-prefixed lines\", {\n  result <- convert(html = \"<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Outer quote.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Inner quote.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_simple: Simple blockquote\", {\n  result <- convert(html = \"<blockquote><p>Quote text</p></blockquote>\")\n  expect_true(grepl(\"> Quote text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"blockquote_with_list: Blockquote containing a list preserves list items inside quote\", {\n  result <- convert(html = \"<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Quote intro:\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Point one\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Point two\", result$content, fixed = TRUE))\n})\n\ntest_that(\"bold_and_italic: Nested bold and italic\", {\n  result <- convert(html = \"<p><strong><em>both</em></strong></p>\")\n  expect_true(grepl(\"***both***\", result$content, fixed = TRUE))\n})\n\ntest_that(\"bold_strong: Strong tag converts to bold\", {\n  result <- convert(html = \"<p><strong>bold</strong></p>\")\n  expect_true(grepl(\"**bold**\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_block: Code block with language preserves content\", {\n  result <- convert(html = \"<pre><code class=\\\"language-python\\\">print('hello')</code></pre>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"print('hello')\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_block_no_language: Code block without a language class preserves content\", {\n  result <- convert(html = \"<pre><code>plain code here</code></pre>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"plain code here\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_inline_in_paragraph: Inline code element nested inside a paragraph\", {\n  result <- convert(html = \"<p>Call the <code>initialize()</code> method first.</p>\")\n  expect_true(grepl(\"`initialize()`\", result$content, fixed = TRUE))\n})\n\ntest_that(\"code_with_backticks_in_content: Inline code containing backtick characters is properly escaped\", {\n  result <- convert(html = \"<p>Use <code>`backtick` here</code> carefully.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"backtick\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_mark_highlight: mark tag produces highlighted output\", {\n  result <- convert(html = \"<p><mark>highlighted</mark></p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"highlighted\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_strikethrough_del: del tag converts to GFM strikethrough\", {\n  result <- convert(html = \"<p><del>deleted text</del></p>\")\n  expect_true(grepl(\"~~deleted text~~\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_strikethrough_s: s tag converts to GFM strikethrough\", {\n  result <- convert(html = \"<p><s>strikethrough</s></p>\")\n  expect_true(grepl(\"~~strikethrough~~\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_subscript: sub tag content is preserved\", {\n  result <- convert(html = \"<p>H<sub>2</sub>O</p>\")\n  expect_true(grepl(\"H\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"O\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_superscript: sup tag content is preserved\", {\n  result <- convert(html = \"<p>x<sup>2</sup></p>\")\n  expect_true(grepl(\"x\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n})\n\ntest_that(\"emphasis_underline_u: u tag content is preserved in output\", {\n  result <- convert(html = \"<p><u>underlined</u></p>\")\n  expect_true(grepl(\"underlined\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_input_elements: Form input elements produce readable output without form mechanics\", {\n  result <- convert(html = \"<form><label for=\\\"name\\\">Name:</label><input type=\\\"text\\\" id=\\\"name\\\" placeholder=\\\"Enter name\\\"></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Name\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_select_options: Select element with options produces readable output\", {\n  result <- convert(html = \"<form><label>Color:</label><select><option value=\\\"red\\\">Red</option><option value=\\\"blue\\\" selected>Blue</option><option value=\\\"green\\\">Green</option></select></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Color\", result$content, fixed = TRUE))\n})\n\ntest_that(\"form_textarea: Textarea element produces readable output\", {\n  result <- convert(html = \"<form><label>Message:</label><textarea>Default text content</textarea></form>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Message\", result$content, fixed = TRUE))\n})\n\ntest_that(\"heading_h1: H1 heading\", {\n  result <- convert(html = \"<h1>Heading 1</h1>\")\n  expect_equal(trimws(result$content), \"# Heading 1\")\n})\n\ntest_that(\"heading_h2: H2 heading\", {\n  result <- convert(html = \"<h2>Heading 2</h2>\")\n  expect_equal(trimws(result$content), \"## Heading 2\")\n})\n\ntest_that(\"heading_h3: H3 heading\", {\n  result <- convert(html = \"<h3>Heading 3</h3>\")\n  expect_equal(trimws(result$content), \"### Heading 3\")\n})\n\ntest_that(\"heading_h4: H4 heading\", {\n  result <- convert(html = \"<h4>Heading 4</h4>\")\n  expect_equal(trimws(result$content), \"#### Heading 4\")\n})\n\ntest_that(\"heading_h5: H5 heading\", {\n  result <- convert(html = \"<h5>Heading 5</h5>\")\n  expect_equal(trimws(result$content), \"##### Heading 5\")\n})\n\ntest_that(\"heading_h6: H6 heading\", {\n  result <- convert(html = \"<h6>Heading 6</h6>\")\n  expect_equal(trimws(result$content), \"###### Heading 6\")\n})\n\ntest_that(\"image_figure_figcaption: Figure with figcaption preserves both image and caption\", {\n  result <- convert(html = \"<figure><img src=\\\"sunset.jpg\\\" alt=\\\"A sunset\\\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>\")\n  expect_true(grepl(\"![A sunset](sunset.jpg)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Beautiful sunset over the ocean\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_linked: Image inside an anchor produces a linked image\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><img src=\\\"icon.png\\\" alt=\\\"Icon\\\"></a>\")\n  expect_true(grepl(\"![Icon](icon.png)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_no_alt: Image without alt text produces image markdown\", {\n  result <- convert(html = \"<img src=\\\"banner.jpg\\\">\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"banner.jpg\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_simple: Image with alt text\", {\n  result <- convert(html = \"<img src=\\\"photo.jpg\\\" alt=\\\"A photo\\\">\")\n  expect_true(grepl(\"![A photo](photo.jpg)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"image_with_title: Image with title attribute includes title in output\", {\n  result <- convert(html = \"<img src=\\\"chart.png\\\" alt=\\\"Sales chart\\\" title=\\\"Q3 Sales\\\">\")\n  expect_true(grepl(\"![Sales chart](chart.png\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Q3 Sales\", result$content, fixed = TRUE))\n})\n\ntest_that(\"inline_code: Inline code\", {\n  result <- convert(html = \"<p>Use <code>console.log()</code> to debug</p>\")\n  expect_true(grepl(\"`console.log()`\", result$content, fixed = TRUE))\n})\n\ntest_that(\"italic_em: Em tag converts to italic\", {\n  result <- convert(html = \"<p><em>italic</em></p>\")\n  expect_true(grepl(\"*italic*\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_br_tag: Single br tag produces a line break in output\", {\n  result <- convert(html = \"<p>First line.<br>Second line.</p>\")\n  expect_true(grepl(\"First line.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second line.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_hr_tag: hr tag produces a horizontal separator between content\", {\n  result <- convert(html = \"<p>Before rule.</p><hr><p>After rule.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Before rule.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"After rule.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"line_break_multiple_br: Multiple consecutive br tags in sequence\", {\n  result <- convert(html = \"<p>Start.<br><br>End.</p>\")\n  expect_true(grepl(\"Start.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"End.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_anchor_fragment: Fragment-only anchor link is preserved\", {\n  result <- convert(html = \"<a href=\\\"#section\\\">Jump to section</a>\")\n  expect_true(grepl(\"[Jump to section](#section)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_empty_href: Link with empty href produces output with the link text\", {\n  result <- convert(html = \"<a href=\\\"\\\">No destination</a>\")\n  expect_true(grepl(\"No destination\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_image_inside: Image inside a link produces a linked image\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><img src=\\\"logo.png\\\" alt=\\\"Logo\\\"></a>\")\n  expect_true(grepl(\"![Logo](logo.png)\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_mailto: Mailto link is preserved with mailto: scheme\", {\n  result <- convert(html = \"<a href=\\\"mailto:user@example.com\\\">Email us</a>\")\n  expect_true(grepl(\"mailto:user@example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_simple: Simple link\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\">Example</a>\")\n  expect_true(grepl(\"[Example](https://example.com)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_with_bold_text: Link containing bold text preserves formatting\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\"><strong>Bold link</strong></a>\")\n  expect_true(grepl(\"**Bold link**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"https://example.com\", result$content, fixed = TRUE))\n})\n\ntest_that(\"link_with_title: Link with title attribute\", {\n  result <- convert(html = \"<a href=\\\"https://example.com\\\" title=\\\"Example Site\\\">Example</a>\")\n  expect_true(grepl(\"[Example](https://example.com\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Example Site\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_definition_dl: Definition list with dt and dd elements\", {\n  result <- convert(html = \"<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>\")\n  expect_true(grepl(\"Term One\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Definition of term one.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Term Two\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Definition of term two.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_item_multiple_paragraphs: List item containing multiple paragraphs\", {\n  result <- convert(html = \"<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>\")\n  expect_true(grepl(\"First paragraph in item.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph in item.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Simple item\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_mixed_nested: Mixed list: ordered list nested inside unordered list\", {\n  result <- convert(html = \"<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>\")\n  expect_true(grepl(\"Item A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Sub 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Sub 2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Item B\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_nested_ordered: Nested ordered list with two levels of depth\", {\n  result <- convert(html = \"<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>\")\n  expect_true(grepl(\"Step 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 1a\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 1b\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Step 2\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_nested_unordered: Nested unordered list with two levels of depth\", {\n  result <- convert(html = \"<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>\")\n  expect_true(grepl(\"Parent A\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Child A1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Child A2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Parent B\", result$content, fixed = TRUE))\n})\n\ntest_that(\"list_task_checkboxes: Task list with checked and unchecked checkboxes\", {\n  result <- convert(html = \"<ul><li><input type=\\\"checkbox\\\" checked> Done task</li><li><input type=\\\"checkbox\\\"> Pending task</li></ul>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Done task\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Pending task\", result$content, fixed = TRUE))\n})\n\ntest_that(\"ordered_list: Ordered list\", {\n  result <- convert(html = \"<ol><li>First</li><li>Second</li><li>Third</li></ol>\")\n  expect_true(grepl(\"1. First\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2. Second\", result$content, fixed = TRUE))\n  expect_true(grepl(\"3. Third\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_multiple: Multiple paragraphs are separated by a blank line\", {\n  result <- convert(html = \"<p>First paragraph.</p><p>Second paragraph.</p>\")\n  expect_true(grepl(\"First paragraph.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Second paragraph.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_nested_divs: Text nested inside divs is extracted correctly\", {\n  result <- convert(html = \"<div><div><p>Nested text</p></div></div>\")\n  expect_true(grepl(\"Nested text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_simple: Simple paragraph converts to plain text\", {\n  result <- convert(html = \"<p>Hello World</p>\")\n  expect_equal(trimws(result$content), \"Hello World\")\n})\n\ntest_that(\"paragraph_with_inline_formatting: Paragraph with bold, italic, and a link\", {\n  result <- convert(html = \"<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\\\"https://example.com\\\">link</a>.</p>\")\n  expect_true(grepl(\"**bold**\", result$content, fixed = TRUE))\n  expect_true(grepl(\"*italic*\", result$content, fixed = TRUE))\n  expect_true(grepl(\"[link](https://example.com)\", result$content, fixed = TRUE))\n})\n\ntest_that(\"paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output\", {\n  result <- convert(html = \"<p>Line one.<br>Line two.<br>Line three.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Line one.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line two.\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Line three.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_abbr: Abbreviation element text is preserved\", {\n  result <- convert(html = \"<p>The <abbr title=\\\"World Wide Web\\\">WWW</abbr> is global.</p>\")\n  expect_true(grepl(\"WWW\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_article: Article element wrapping content preserves inner content\", {\n  result <- convert(html = \"<article><h2>Article Title</h2><p>Article body.</p></article>\")\n  expect_true(grepl(\"Article Title\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Article body.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_definition_list: Definition list with term and description\", {\n  result <- convert(html = \"<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>\")\n  expect_true(grepl(\"HTML\", result$content, fixed = TRUE))\n  expect_true(grepl(\"HyperText Markup Language\", result$content, fixed = TRUE))\n  expect_true(grepl(\"CSS\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Cascading Style Sheets\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_details_summary: Details and summary elements produce readable output\", {\n  result <- convert(html = \"<details><summary>Click to expand</summary><p>Hidden content here.</p></details>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Click to expand\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_hr: Horizontal rule produces a separator in output\", {\n  result <- convert(html = \"<p>Above</p><hr><p>Below</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Above\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Below\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_mark_highlight: Mark tag produces highlighted output\", {\n  result <- convert(html = \"<p>This is <mark>highlighted text</mark> in a sentence.</p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"highlighted text\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_section_with_heading: Section element with heading preserves structure\", {\n  result <- convert(html = \"<section><h3>Section Heading</h3><p>Section content.</p></section>\")\n  expect_true(grepl(\"Section Heading\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Section content.\", result$content, fixed = TRUE))\n})\n\ntest_that(\"semantic_sub_superscript: Subscript and superscript elements are preserved in output\", {\n  result <- convert(html = \"<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"H\", result$content, fixed = TRUE))\n  expect_true(grepl(\"2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"O\", result$content, fixed = TRUE))\n  expect_true(grepl(\"E=mc\", result$content, fixed = TRUE))\n})\n\ntest_that(\"simple_table: Simple table with header\", {\n  result <- convert(html = \"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>\")\n  expect_true(grepl(\"Name\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Age\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Alice\", result$content, fixed = TRUE))\n  expect_true(grepl(\"30\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n  expect_true(grepl(\"---\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_empty: Empty table produces no output or minimal output\", {\n  result <- convert(html = \"<table></table>\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"table_no_thead: Table without thead uses first row as implied header\", {\n  result <- convert(html = \"<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Product\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Price\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Apple\", result$content, fixed = TRUE))\n  expect_true(grepl(\"1.00\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output\", {\n  result <- convert(html = \"<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Expression\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Result\", result$content, fixed = TRUE))\n  expect_true(grepl(\"true\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_with_alignment: Table with column alignment attributes\", {\n  result <- convert(html = \"<table><thead><tr><th align=\\\"left\\\">Left</th><th align=\\\"center\\\">Center</th><th align=\\\"right\\\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Left\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Center\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Right\", result$content, fixed = TRUE))\n  expect_true(grepl(\"L\", result$content, fixed = TRUE))\n  expect_true(grepl(\"C\", result$content, fixed = TRUE))\n  expect_true(grepl(\"R\", result$content, fixed = TRUE))\n  expect_true(grepl(\"|\", result$content, fixed = TRUE))\n})\n\ntest_that(\"table_with_colspan: Table with colspan attribute in a header cell\", {\n  result <- convert(html = \"<table><thead><tr><th colspan=\\\"2\\\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n  expect_true(grepl(\"Full Name\", result$content, fixed = TRUE))\n  expect_true(grepl(\"John\", result$content, fixed = TRUE))\n  expect_true(grepl(\"Doe\", result$content, fixed = TRUE))\n})\n\ntest_that(\"unordered_list: Unordered list\", {\n  result <- convert(html = \"<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>\")\n  expect_true(grepl(\"- Item 1\", result$content, fixed = TRUE))\n  expect_true(grepl(\"- Item 2\", result$content, fixed = TRUE))\n  expect_true(grepl(\"- Item 3\", result$content, fixed = TRUE))\n})\n"
  },
  {
    "path": "test_apps/r/tests/test_smoke.R",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:8f73869ef0818c58d5a01f1f3a83ce0f7c04fec41aa97b3e8364c9d7663937d4\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# E2e tests for category: smoke\n\ntest_that(\"smoke_empty_string: Empty string produces empty output\", {\n  result <- convert(html = \"\")\n  expect_equal(trimws(result$content), \"\")\n})\n\ntest_that(\"smoke_simple_heading: H1 heading converts to ATX markdown\", {\n  result <- convert(html = \"<h1>Title</h1>\")\n  expect_true(grepl(\"# Title\", result$content, fixed = TRUE))\n})\n\ntest_that(\"smoke_simple_paragraph: Simple paragraph converts correctly\", {\n  result <- convert(html = \"<p>Hello World</p>\")\n  expect_equal(trimws(result$content), \"Hello World\")\n  expect_true(if (is.character(result$content)) nchar(result$content) > 0 else length(result$content) > 0)\n})\n"
  },
  {
    "path": "test_apps/ruby/.bundle/config",
    "content": "---\nBUNDLE_BUILD__HTML___TO___MARKDOWN: \"--with-cflags=-Wno-error=unused-command-line-argument-hard-error-in-future\"\n"
  },
  {
    "path": "test_apps/ruby/.rubocop.yaml",
    "content": "# Generated by alef e2e — do not edit.\nAllCops:\n  NewCops: enable\n  TargetRubyVersion: 3.2\n  SuggestExtensions: false\n\nplugins:\n  - rubocop-rspec\n\n# --- Justified suppressions for generated test code ---\n\n# Generated tests are verbose by nature (setup + multiple assertions).\nMetrics/BlockLength:\n  Enabled: false\nMetrics/MethodLength:\n  Enabled: false\nLayout/LineLength:\n  Enabled: false\n\n# Generated tests use multiple assertions per example for thorough verification.\nRSpec/MultipleExpectations:\n  Enabled: false\nRSpec/ExampleLength:\n  Enabled: false\n\n# Generated tests describe categories as strings, not classes.\nRSpec/DescribeClass:\n  Enabled: false\n\n# Fixture-driven tests may produce identical assertion bodies for different inputs.\nRSpec/RepeatedExample:\n  Enabled: false\n\n# Error-handling tests use bare raise_error (exception type not known at generation time).\nRSpec/UnspecifiedException:\n  Enabled: false\n"
  },
  {
    "path": "test_apps/ruby/.ruby-version",
    "content": "3.2.0\n"
  },
  {
    "path": "test_apps/ruby/Gemfile",
    "content": "# frozen_string_literal: true\n\nsource 'https://rubygems.org'\n\ngem 'html-to-markdown', '~> 3.2'\ngem 'rspec', '~> 3.13'\ngem 'rubocop', '~> 1.86'\ngem 'rubocop-rspec', '~> 3.9'\ngem 'faraday', '~> 2.0'\n"
  },
  {
    "path": "test_apps/ruby/README.md",
    "content": "# Ruby Test App for html-to-markdown\n\nTests the published html-to-markdown gem from RubyGems (v3).\n\nThis test app validates that the html-to-markdown gem is properly installed from RubyGems (not a local path dependency) and that the v3 `convert()` API works correctly.\n\n## Features Tested\n\n### Basic HTML Conversion\n\n- Paragraphs, headings (h1-h6)\n- Text styling (bold, italic, strikethrough)\n- Lists (ordered, unordered, nested)\n- Links and blockquotes\n- Code blocks and inline code\n- Comprehensive fixture-based tests\n\n### Conversion Options\n\n- Heading styles (atx, atx_closed, underlined)\n- List indentation customization\n- Code block formatting (fenced, indented)\n- Text wrapping and column width\n\n### Error Handling\n\n- Empty HTML input\n- Malformed HTML recovery\n- Large document processing (100KB+)\n- Special character escaping (XSS prevention)\n- Unicode and emoji support\n\n## Setup\n\nRequires Ruby 3.2+ (see .ruby-version)\n\n```bash\nbundle install\n```\n\n## Run Tests\n\n```bash\n# Smoke tests (quick validation)\nbundle exec rspec smoke_test.rb\n\n# Comprehensive tests (full feature coverage)\nbundle exec rspec comprehensive_test.rb\n\n# All tests\nbundle exec rspec\n\n# Run with verbose output\nbundle exec rspec -v\n\n# Run specific test\nbundle exec rspec smoke_test.rb -e \"can load the gem\"\n```\n\n## v3 API\n\nThe v3 API has a single function: `HtmlToMarkdown.convert(html, options)`. All previous `convert_with_*` methods have been removed.\n"
  },
  {
    "path": "test_apps/ruby/spec/conversion_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:50daa7de7425e4c81d33a83686c3cf104751853e54bc01386441f8783415608f\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown_rs'\nrequire 'json'\n\nRSpec.describe 'conversion' do\n  it 'blockquote_multiple_paragraphs: Blockquote with multiple paragraphs has each paragraph prefixed' do\n    result = HtmlToMarkdownRs.convert('<blockquote><p>First paragraph.</p><p>Second paragraph.</p></blockquote>')\n    expect(result.to_s).to include('> First paragraph.')\n    expect(result.to_s).to include('> Second paragraph.')\n  end\n\n  it 'blockquote_nested: Nested blockquote produces double-prefixed lines' do\n    result = HtmlToMarkdownRs.convert('<blockquote><p>Outer quote.</p><blockquote><p>Inner quote.</p></blockquote></blockquote>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Outer quote.')\n    expect(result.to_s).to include('Inner quote.')\n  end\n\n  it 'blockquote_simple: Simple blockquote' do\n    result = HtmlToMarkdownRs.convert('<blockquote><p>Quote text</p></blockquote>')\n    expect(result.to_s).to include('> Quote text')\n  end\n\n  it 'blockquote_with_list: Blockquote containing a list preserves list items inside quote' do\n    result = HtmlToMarkdownRs.convert('<blockquote><p>Quote intro:</p><ul><li>Point one</li><li>Point two</li></ul></blockquote>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Quote intro:')\n    expect(result.to_s).to include('Point one')\n    expect(result.to_s).to include('Point two')\n  end\n\n  it 'bold_and_italic: Nested bold and italic' do\n    result = HtmlToMarkdownRs.convert('<p><strong><em>both</em></strong></p>')\n    expect(result.to_s).to include('***both***')\n  end\n\n  it 'bold_strong: Strong tag converts to bold' do\n    result = HtmlToMarkdownRs.convert('<p><strong>bold</strong></p>')\n    expect(result.to_s).to include('**bold**')\n  end\n\n  it 'code_block: Code block with language preserves content' do\n    result = HtmlToMarkdownRs.convert('<pre><code class=\"language-python\">print(\\'hello\\')</code></pre>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('print(\\'hello\\')')\n  end\n\n  it 'code_block_no_language: Code block without a language class preserves content' do\n    result = HtmlToMarkdownRs.convert('<pre><code>plain code here</code></pre>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('plain code here')\n  end\n\n  it 'code_inline_in_paragraph: Inline code element nested inside a paragraph' do\n    result = HtmlToMarkdownRs.convert('<p>Call the <code>initialize()</code> method first.</p>')\n    expect(result.to_s).to include('`initialize()`')\n  end\n\n  it 'code_with_backticks_in_content: Inline code containing backtick characters is properly escaped' do\n    result = HtmlToMarkdownRs.convert('<p>Use <code>`backtick` here</code> carefully.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('backtick')\n  end\n\n  it 'emphasis_mark_highlight: mark tag produces highlighted output' do\n    result = HtmlToMarkdownRs.convert('<p><mark>highlighted</mark></p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('highlighted')\n  end\n\n  it 'emphasis_strikethrough_del: del tag converts to GFM strikethrough' do\n    result = HtmlToMarkdownRs.convert('<p><del>deleted text</del></p>')\n    expect(result.to_s).to include('~~deleted text~~')\n  end\n\n  it 'emphasis_strikethrough_s: s tag converts to GFM strikethrough' do\n    result = HtmlToMarkdownRs.convert('<p><s>strikethrough</s></p>')\n    expect(result.to_s).to include('~~strikethrough~~')\n  end\n\n  it 'emphasis_subscript: sub tag content is preserved' do\n    result = HtmlToMarkdownRs.convert('<p>H<sub>2</sub>O</p>')\n    expect(result.to_s).to include('H')\n    expect(result.to_s).to include('2')\n    expect(result.to_s).to include('O')\n  end\n\n  it 'emphasis_superscript: sup tag content is preserved' do\n    result = HtmlToMarkdownRs.convert('<p>x<sup>2</sup></p>')\n    expect(result.to_s).to include('x')\n    expect(result.to_s).to include('2')\n  end\n\n  it 'emphasis_underline_u: u tag content is preserved in output' do\n    result = HtmlToMarkdownRs.convert('<p><u>underlined</u></p>')\n    expect(result.to_s).to include('underlined')\n  end\n\n  it 'form_input_elements: Form input elements produce readable output without form mechanics' do\n    result = HtmlToMarkdownRs.convert('<form><label for=\"name\">Name:</label><input type=\"text\" id=\"name\" placeholder=\"Enter name\"></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Name')\n  end\n\n  it 'form_select_options: Select element with options produces readable output' do\n    result = HtmlToMarkdownRs.convert('<form><label>Color:</label><select><option value=\"red\">Red</option><option value=\"blue\" selected>Blue</option><option value=\"green\">Green</option></select></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Color')\n  end\n\n  it 'form_textarea: Textarea element produces readable output' do\n    result = HtmlToMarkdownRs.convert('<form><label>Message:</label><textarea>Default text content</textarea></form>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Message')\n  end\n\n  it 'heading_h1: H1 heading' do\n    result = HtmlToMarkdownRs.convert('<h1>Heading 1</h1>')\n    expect(result.strip).to eq('# Heading 1')\n  end\n\n  it 'heading_h2: H2 heading' do\n    result = HtmlToMarkdownRs.convert('<h2>Heading 2</h2>')\n    expect(result.strip).to eq('## Heading 2')\n  end\n\n  it 'heading_h3: H3 heading' do\n    result = HtmlToMarkdownRs.convert('<h3>Heading 3</h3>')\n    expect(result.strip).to eq('### Heading 3')\n  end\n\n  it 'heading_h4: H4 heading' do\n    result = HtmlToMarkdownRs.convert('<h4>Heading 4</h4>')\n    expect(result.strip).to eq('#### Heading 4')\n  end\n\n  it 'heading_h5: H5 heading' do\n    result = HtmlToMarkdownRs.convert('<h5>Heading 5</h5>')\n    expect(result.strip).to eq('##### Heading 5')\n  end\n\n  it 'heading_h6: H6 heading' do\n    result = HtmlToMarkdownRs.convert('<h6>Heading 6</h6>')\n    expect(result.strip).to eq('###### Heading 6')\n  end\n\n  it 'image_figure_figcaption: Figure with figcaption preserves both image and caption' do\n    result = HtmlToMarkdownRs.convert('<figure><img src=\"sunset.jpg\" alt=\"A sunset\"><figcaption>Beautiful sunset over the ocean</figcaption></figure>')\n    expect(result.to_s).to include('![A sunset](sunset.jpg)')\n    expect(result.to_s).to include('Beautiful sunset over the ocean')\n  end\n\n  it 'image_linked: Image inside an anchor produces a linked image' do\n    result = HtmlToMarkdownRs.convert('<a href=\"https://example.com\"><img src=\"icon.png\" alt=\"Icon\"></a>')\n    expect(result.to_s).to include('![Icon](icon.png)')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'image_no_alt: Image without alt text produces image markdown' do\n    result = HtmlToMarkdownRs.convert('<img src=\"banner.jpg\">')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('banner.jpg')\n  end\n\n  it 'image_simple: Image with alt text' do\n    result = HtmlToMarkdownRs.convert('<img src=\"photo.jpg\" alt=\"A photo\">')\n    expect(result.to_s).to include('![A photo](photo.jpg)')\n  end\n\n  it 'image_with_title: Image with title attribute includes title in output' do\n    result = HtmlToMarkdownRs.convert('<img src=\"chart.png\" alt=\"Sales chart\" title=\"Q3 Sales\">')\n    expect(result.to_s).to include('![Sales chart](chart.png')\n    expect(result.to_s).to include('Q3 Sales')\n  end\n\n  it 'inline_code: Inline code' do\n    result = HtmlToMarkdownRs.convert('<p>Use <code>console.log()</code> to debug</p>')\n    expect(result.to_s).to include('`console.log()`')\n  end\n\n  it 'italic_em: Em tag converts to italic' do\n    result = HtmlToMarkdownRs.convert('<p><em>italic</em></p>')\n    expect(result.to_s).to include('*italic*')\n  end\n\n  it 'line_break_br_tag: Single br tag produces a line break in output' do\n    result = HtmlToMarkdownRs.convert('<p>First line.<br>Second line.</p>')\n    expect(result.to_s).to include('First line.')\n    expect(result.to_s).to include('Second line.')\n  end\n\n  it 'line_break_hr_tag: hr tag produces a horizontal separator between content' do\n    result = HtmlToMarkdownRs.convert('<p>Before rule.</p><hr><p>After rule.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Before rule.')\n    expect(result.to_s).to include('After rule.')\n  end\n\n  it 'line_break_multiple_br: Multiple consecutive br tags in sequence' do\n    result = HtmlToMarkdownRs.convert('<p>Start.<br><br>End.</p>')\n    expect(result.to_s).to include('Start.')\n    expect(result.to_s).to include('End.')\n  end\n\n  it 'link_anchor_fragment: Fragment-only anchor link is preserved' do\n    result = HtmlToMarkdownRs.convert('<a href=\"#section\">Jump to section</a>')\n    expect(result.to_s).to include('[Jump to section](#section)')\n  end\n\n  it 'link_empty_href: Link with empty href produces output with the link text' do\n    result = HtmlToMarkdownRs.convert('<a href=\"\">No destination</a>')\n    expect(result.to_s).to include('No destination')\n  end\n\n  it 'link_image_inside: Image inside a link produces a linked image' do\n    result = HtmlToMarkdownRs.convert('<a href=\"https://example.com\"><img src=\"logo.png\" alt=\"Logo\"></a>')\n    expect(result.to_s).to include('![Logo](logo.png)')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'link_mailto: Mailto link is preserved with mailto: scheme' do\n    result = HtmlToMarkdownRs.convert('<a href=\"mailto:user@example.com\">Email us</a>')\n    expect(result.to_s).to include('mailto:user@example.com')\n  end\n\n  it 'link_simple: Simple link' do\n    result = HtmlToMarkdownRs.convert('<a href=\"https://example.com\">Example</a>')\n    expect(result.to_s).to include('[Example](https://example.com)')\n  end\n\n  it 'link_with_bold_text: Link containing bold text preserves formatting' do\n    result = HtmlToMarkdownRs.convert('<a href=\"https://example.com\"><strong>Bold link</strong></a>')\n    expect(result.to_s).to include('**Bold link**')\n    expect(result.to_s).to include('https://example.com')\n  end\n\n  it 'link_with_title: Link with title attribute' do\n    result = HtmlToMarkdownRs.convert('<a href=\"https://example.com\" title=\"Example Site\">Example</a>')\n    expect(result.to_s).to include('[Example](https://example.com')\n    expect(result.to_s).to include('Example Site')\n  end\n\n  it 'list_definition_dl: Definition list with dt and dd elements' do\n    result = HtmlToMarkdownRs.convert('<dl><dt>Term One</dt><dd>Definition of term one.</dd><dt>Term Two</dt><dd>Definition of term two.</dd></dl>')\n    expect(result.to_s).to include('Term One')\n    expect(result.to_s).to include('Definition of term one.')\n    expect(result.to_s).to include('Term Two')\n    expect(result.to_s).to include('Definition of term two.')\n  end\n\n  it 'list_item_multiple_paragraphs: List item containing multiple paragraphs' do\n    result = HtmlToMarkdownRs.convert('<ul><li><p>First paragraph in item.</p><p>Second paragraph in item.</p></li><li>Simple item</li></ul>')\n    expect(result.to_s).to include('First paragraph in item.')\n    expect(result.to_s).to include('Second paragraph in item.')\n    expect(result.to_s).to include('Simple item')\n  end\n\n  it 'list_mixed_nested: Mixed list: ordered list nested inside unordered list' do\n    result = HtmlToMarkdownRs.convert('<ul><li>Item A<ol><li>Sub 1</li><li>Sub 2</li></ol></li><li>Item B</li></ul>')\n    expect(result.to_s).to include('Item A')\n    expect(result.to_s).to include('Sub 1')\n    expect(result.to_s).to include('Sub 2')\n    expect(result.to_s).to include('Item B')\n  end\n\n  it 'list_nested_ordered: Nested ordered list with two levels of depth' do\n    result = HtmlToMarkdownRs.convert('<ol><li>Step 1<ol><li>Step 1a</li><li>Step 1b</li></ol></li><li>Step 2</li></ol>')\n    expect(result.to_s).to include('Step 1')\n    expect(result.to_s).to include('Step 1a')\n    expect(result.to_s).to include('Step 1b')\n    expect(result.to_s).to include('Step 2')\n  end\n\n  it 'list_nested_unordered: Nested unordered list with two levels of depth' do\n    result = HtmlToMarkdownRs.convert('<ul><li>Parent A<ul><li>Child A1</li><li>Child A2</li></ul></li><li>Parent B</li></ul>')\n    expect(result.to_s).to include('Parent A')\n    expect(result.to_s).to include('Child A1')\n    expect(result.to_s).to include('Child A2')\n    expect(result.to_s).to include('Parent B')\n  end\n\n  it 'list_task_checkboxes: Task list with checked and unchecked checkboxes' do\n    result = HtmlToMarkdownRs.convert('<ul><li><input type=\"checkbox\" checked> Done task</li><li><input type=\"checkbox\"> Pending task</li></ul>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Done task')\n    expect(result.to_s).to include('Pending task')\n  end\n\n  it 'ordered_list: Ordered list' do\n    result = HtmlToMarkdownRs.convert('<ol><li>First</li><li>Second</li><li>Third</li></ol>')\n    expect(result.to_s).to include('1. First')\n    expect(result.to_s).to include('2. Second')\n    expect(result.to_s).to include('3. Third')\n  end\n\n  it 'paragraph_multiple: Multiple paragraphs are separated by a blank line' do\n    result = HtmlToMarkdownRs.convert('<p>First paragraph.</p><p>Second paragraph.</p>')\n    expect(result.to_s).to include('First paragraph.')\n    expect(result.to_s).to include('Second paragraph.')\n  end\n\n  it 'paragraph_nested_divs: Text nested inside divs is extracted correctly' do\n    result = HtmlToMarkdownRs.convert('<div><div><p>Nested text</p></div></div>')\n    expect(result.to_s).to include('Nested text')\n  end\n\n  it 'paragraph_simple: Simple paragraph converts to plain text' do\n    result = HtmlToMarkdownRs.convert('<p>Hello World</p>')\n    expect(result.strip).to eq('Hello World')\n  end\n\n  it 'paragraph_with_inline_formatting: Paragraph with bold, italic, and a link' do\n    result = HtmlToMarkdownRs.convert('<p>This has <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>')\n    expect(result.to_s).to include('**bold**')\n    expect(result.to_s).to include('*italic*')\n    expect(result.to_s).to include('[link](https://example.com)')\n  end\n\n  it 'paragraph_with_line_breaks: Paragraph with br tags produces line breaks in output' do\n    result = HtmlToMarkdownRs.convert('<p>Line one.<br>Line two.<br>Line three.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Line one.')\n    expect(result.to_s).to include('Line two.')\n    expect(result.to_s).to include('Line three.')\n  end\n\n  it 'semantic_abbr: Abbreviation element text is preserved' do\n    result = HtmlToMarkdownRs.convert('<p>The <abbr title=\"World Wide Web\">WWW</abbr> is global.</p>')\n    expect(result.to_s).to include('WWW')\n  end\n\n  it 'semantic_article: Article element wrapping content preserves inner content' do\n    result = HtmlToMarkdownRs.convert('<article><h2>Article Title</h2><p>Article body.</p></article>')\n    expect(result.to_s).to include('Article Title')\n    expect(result.to_s).to include('Article body.')\n  end\n\n  it 'semantic_definition_list: Definition list with term and description' do\n    result = HtmlToMarkdownRs.convert('<dl><dt>HTML</dt><dd>HyperText Markup Language</dd><dt>CSS</dt><dd>Cascading Style Sheets</dd></dl>')\n    expect(result.to_s).to include('HTML')\n    expect(result.to_s).to include('HyperText Markup Language')\n    expect(result.to_s).to include('CSS')\n    expect(result.to_s).to include('Cascading Style Sheets')\n  end\n\n  it 'semantic_details_summary: Details and summary elements produce readable output' do\n    result = HtmlToMarkdownRs.convert('<details><summary>Click to expand</summary><p>Hidden content here.</p></details>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Click to expand')\n  end\n\n  it 'semantic_hr: Horizontal rule produces a separator in output' do\n    result = HtmlToMarkdownRs.convert('<p>Above</p><hr><p>Below</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Above')\n    expect(result.to_s).to include('Below')\n  end\n\n  it 'semantic_mark_highlight: Mark tag produces highlighted output' do\n    result = HtmlToMarkdownRs.convert('<p>This is <mark>highlighted text</mark> in a sentence.</p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('highlighted text')\n  end\n\n  it 'semantic_section_with_heading: Section element with heading preserves structure' do\n    result = HtmlToMarkdownRs.convert('<section><h3>Section Heading</h3><p>Section content.</p></section>')\n    expect(result.to_s).to include('Section Heading')\n    expect(result.to_s).to include('Section content.')\n  end\n\n  it 'semantic_sub_superscript: Subscript and superscript elements are preserved in output' do\n    result = HtmlToMarkdownRs.convert('<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('H')\n    expect(result.to_s).to include('2')\n    expect(result.to_s).to include('O')\n    expect(result.to_s).to include('E=mc')\n  end\n\n  it 'simple_table: Simple table with header' do\n    result = HtmlToMarkdownRs.convert('<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Alice</td><td>30</td></tr></tbody></table>')\n    expect(result.to_s).to include('Name')\n    expect(result.to_s).to include('Age')\n    expect(result.to_s).to include('Alice')\n    expect(result.to_s).to include('30')\n    expect(result.to_s).to include('|')\n    expect(result.to_s).to include('---')\n  end\n\n  it 'table_empty: Empty table produces no output or minimal output' do\n    result = HtmlToMarkdownRs.convert('<table></table>')\n    expect(result.strip).to eq('')\n  end\n\n  it 'table_no_thead: Table without thead uses first row as implied header' do\n    result = HtmlToMarkdownRs.convert('<table><tr><td>Product</td><td>Price</td></tr><tr><td>Apple</td><td>1.00</td></tr></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Product')\n    expect(result.to_s).to include('Price')\n    expect(result.to_s).to include('Apple')\n    expect(result.to_s).to include('1.00')\n    expect(result.to_s).to include('|')\n  end\n\n  it 'table_pipe_chars_in_content: Table cells containing pipe characters are escaped in output' do\n    result = HtmlToMarkdownRs.convert('<table><thead><tr><th>Expression</th><th>Result</th></tr></thead><tbody><tr><td>a | b</td><td>true</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Expression')\n    expect(result.to_s).to include('Result')\n    expect(result.to_s).to include('true')\n  end\n\n  it 'table_with_alignment: Table with column alignment attributes' do\n    result = HtmlToMarkdownRs.convert('<table><thead><tr><th align=\"left\">Left</th><th align=\"center\">Center</th><th align=\"right\">Right</th></tr></thead><tbody><tr><td>L</td><td>C</td><td>R</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Left')\n    expect(result.to_s).to include('Center')\n    expect(result.to_s).to include('Right')\n    expect(result.to_s).to include('L')\n    expect(result.to_s).to include('C')\n    expect(result.to_s).to include('R')\n    expect(result.to_s).to include('|')\n  end\n\n  it 'table_with_colspan: Table with colspan attribute in a header cell' do\n    result = HtmlToMarkdownRs.convert('<table><thead><tr><th colspan=\"2\">Full Name</th></tr></thead><tbody><tr><td>John</td><td>Doe</td></tr></tbody></table>')\n    expect(result).not_to be_empty\n    expect(result.to_s).to include('Full Name')\n    expect(result.to_s).to include('John')\n    expect(result.to_s).to include('Doe')\n  end\n\n  it 'unordered_list: Unordered list' do\n    result = HtmlToMarkdownRs.convert('<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>')\n    expect(result.to_s).to include('- Item 1')\n    expect(result.to_s).to include('- Item 2')\n    expect(result.to_s).to include('- Item 3')\n  end\nend\n"
  },
  {
    "path": "test_apps/ruby/spec/smoke_spec.rb",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:de5ba4a9d7c17954739ff094153d93f08097ba6b6e12eacd85cffc2a3bbe5d44\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n# frozen_string_literal: true\n\nrequire 'html_to_markdown_rs'\nrequire 'json'\n\nRSpec.describe 'smoke' do\n  it 'smoke_empty_string: Empty string produces empty output' do\n    result = HtmlToMarkdownRs.convert('')\n    expect(result.strip).to eq('')\n  end\n\n  it 'smoke_simple_heading: H1 heading converts to ATX markdown' do\n    result = HtmlToMarkdownRs.convert('<h1>Title</h1>')\n    expect(result.to_s).to include('# Title')\n  end\n\n  it 'smoke_simple_paragraph: Simple paragraph converts correctly' do\n    result = HtmlToMarkdownRs.convert('<p>Hello World</p>')\n    expect(result.strip).to eq('Hello World')\n    expect(result).not_to be_empty\n  end\nend\n"
  },
  {
    "path": "test_apps/rust/Cargo.toml",
    "content": "# This file is auto-generated by alef — DO NOT EDIT.\n# alef:hash:237ba9ca20fe5962ea8e019e5d7f41a1223c3a4691026127e604981e5c9dff16\n# To regenerate: alef generate\n# To verify freshness: alef verify --exit-code\n# Issues & docs: https://github.com/kreuzberg-dev/alef\n\n[workspace]\n\n[package]\nname = \"html_to_markdown_rs-e2e-rust\"\nversion = \"3.4.0-rc.25\"\nedition = \"2021\"\nlicense = \"MIT\"\npublish = false\n\n[dependencies]\nhtml_to_markdown_rs = { package = \"html-to-markdown-rs\", version = \"3.2.0\", default-features = false, features = [\"full\", \"metadata\", \"visitor\", \"serde\", \"inline-images\"] }\nserde_json = \"1\"\n\n[package.metadata.cargo-machete]\nignored = [\"serde_json\"]\n"
  },
  {
    "path": "test_apps/rust/tests/conversion_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e8207b1f5470a6db7dc695eb06b631be1cd58cd85fe61c2d27012368389deb97\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: conversion\n\n#[tokio::test]\nasync fn test_blockquote_multiple_paragraphs() {\n    // Blockquote with multiple paragraphs has each paragraph prefixed\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_blockquote_nested() {\n    // Nested blockquote produces double-prefixed lines\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_blockquote_simple() {\n    // Simple blockquote\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_blockquote_with_list() {\n    // Blockquote containing a list preserves list items inside quote\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_bold_and_italic() {\n    // Nested bold and italic\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_bold_strong() {\n    // Strong tag converts to bold\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_code_block() {\n    // Code block with language preserves content\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_code_block_no_language() {\n    // Code block without a language class preserves content\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_code_inline_in_paragraph() {\n    // Inline code element nested inside a paragraph\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_code_with_backticks_in_content() {\n    // Inline code containing backtick characters is properly escaped\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_mark_highlight() {\n    // mark tag produces highlighted output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_strikethrough_del() {\n    // del tag converts to GFM strikethrough\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_strikethrough_s() {\n    // s tag converts to GFM strikethrough\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_subscript() {\n    // sub tag content is preserved\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_superscript() {\n    // sup tag content is preserved\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_emphasis_underline_u() {\n    // u tag content is preserved in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_form_input_elements() {\n    // Form input elements produce readable output without form mechanics\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_form_select_options() {\n    // Select element with options produces readable output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_form_textarea() {\n    // Textarea element produces readable output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h1() {\n    // H1 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h2() {\n    // H2 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h3() {\n    // H3 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h4() {\n    // H4 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h5() {\n    // H5 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_heading_h6() {\n    // H6 heading\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_image_figure_figcaption() {\n    // Figure with figcaption preserves both image and caption\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_image_linked() {\n    // Image inside an anchor produces a linked image\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_image_no_alt() {\n    // Image without alt text produces image markdown\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_image_simple() {\n    // Image with alt text\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_image_with_title() {\n    // Image with title attribute includes title in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_inline_code() {\n    // Inline code\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_italic_em() {\n    // Em tag converts to italic\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_line_break_br_tag() {\n    // Single br tag produces a line break in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_line_break_hr_tag() {\n    // hr tag produces a horizontal separator between content\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_line_break_multiple_br() {\n    // Multiple consecutive br tags in sequence\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_anchor_fragment() {\n    // Fragment-only anchor link is preserved\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_empty_href() {\n    // Link with empty href produces output with the link text\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_image_inside() {\n    // Image inside a link produces a linked image\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_mailto() {\n    // Mailto link is preserved with mailto: scheme\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_simple() {\n    // Simple link\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_with_bold_text() {\n    // Link containing bold text preserves formatting\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_link_with_title() {\n    // Link with title attribute\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_definition_dl() {\n    // Definition list with dt and dd elements\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_item_multiple_paragraphs() {\n    // List item containing multiple paragraphs\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_mixed_nested() {\n    // Mixed list: ordered list nested inside unordered list\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_nested_ordered() {\n    // Nested ordered list with two levels of depth\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_nested_unordered() {\n    // Nested unordered list with two levels of depth\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_list_task_checkboxes() {\n    // Task list with checked and unchecked checkboxes\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_ordered_list() {\n    // Ordered list\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_paragraph_multiple() {\n    // Multiple paragraphs are separated by a blank line\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_paragraph_nested_divs() {\n    // Text nested inside divs is extracted correctly\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_paragraph_simple() {\n    // Simple paragraph converts to plain text\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_paragraph_with_inline_formatting() {\n    // Paragraph with bold, italic, and a link\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_paragraph_with_line_breaks() {\n    // Paragraph with br tags produces line breaks in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_abbr() {\n    // Abbreviation element text is preserved\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_article() {\n    // Article element wrapping content preserves inner content\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_definition_list() {\n    // Definition list with term and description\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_details_summary() {\n    // Details and summary elements produce readable output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_hr() {\n    // Horizontal rule produces a separator in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_mark_highlight() {\n    // Mark tag produces highlighted output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_section_with_heading() {\n    // Section element with heading preserves structure\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_semantic_sub_superscript() {\n    // Subscript and superscript elements are preserved in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_simple_table() {\n    // Simple table with header\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_table_empty() {\n    // Empty table produces no output or minimal output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_table_no_thead() {\n    // Table without thead uses first row as implied header\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_table_pipe_chars_in_content() {\n    // Table cells containing pipe characters are escaped in output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_table_with_alignment() {\n    // Table with column alignment attributes\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_table_with_colspan() {\n    // Table with colspan attribute in a header cell\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_unordered_list() {\n    // Unordered list\n    // TODO: implement when a callable API is available for this fixture type.\n}\n"
  },
  {
    "path": "test_apps/rust/tests/smoke_test.rs",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:2a18d2b1c4e469f05f9f7e2255effef95a49a49848cd81018f9e7510d42a153b\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\n//! E2e tests for category: smoke\n\n#[tokio::test]\nasync fn test_smoke_empty_string() {\n    // Empty string produces empty output\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_smoke_simple_heading() {\n    // H1 heading converts to ATX markdown\n    // TODO: implement when a callable API is available for this fixture type.\n}\n\n#[tokio::test]\nasync fn test_smoke_simple_paragraph() {\n    // Simple paragraph converts correctly\n    // TODO: implement when a callable API is available for this fixture type.\n}\n"
  },
  {
    "path": "test_apps/wasm/.nvmrc",
    "content": "18\n"
  },
  {
    "path": "test_apps/wasm/README.md",
    "content": "# WebAssembly Test App for html-to-markdown\n\nValidates the published `@kreuzberg/html-to-markdown-wasm` package from npm in real-world conditions across all target environments (browser, Node.js, Deno, Cloudflare Workers).\n\n## Purpose\n\nThis test app:\n\n- Tests the **published npm package** (not local path dependencies)\n- Verifies WASM module loads and functions correctly\n- Validates HTML to Markdown conversion across all platform targets\n- Ensures async operations work as expected\n- Tests comprehensive error handling\n- Validates bundle size constraints\n- Covers all major HTML elements and edge cases\n\n## Prerequisites\n\n- Node.js 18+ (check `.nvmrc`)\n- pnpm (recommended) or npm\n\n## Setup\n\n```bash\n# Install dependencies (installs published WASM package from npm)\npnpm install\n```\n\nThe package.json specifies version `2.24.1` of the WASM package, ensuring it tests the published npm package rather than a local build.\n\n## Run Tests\n\n```bash\n# All tests\npnpm test\n\n# Smoke tests only (fast, basic functionality)\npnpm test:smoke\n\n# Comprehensive tests (fixture-driven, edge cases, bundle size)\npnpm test:comprehensive\n```\n\n## Test Coverage\n\n### Smoke Tests (smoke.spec.ts)\n\n**Module Loading:**\n\n- Package import verification\n- Function exports validation\n- Version information checks\n\n**Basic Conversion:**\n\n- Simple HTML to Markdown conversion\n- Heading support\n- List handling (ordered/unordered)\n- Link conversion\n- Empty input handling\n\n**Async Operations:**\n\n- Async conversion support (if available)\n- Sync conversion verification\n\n**Error Handling:**\n\n- Malformed HTML resilience\n- Very long input handling (10KB+)\n- Special HTML character escaping\n- Null/undefined input safety\n- XSS prevention (script tag escaping)\n\n### Comprehensive Tests (comprehensive.spec.ts)\n\n**Fixture-Based Tests:**\n\n- Load and run fixture files from `../fixtures/`\n- Basic HTML conversions from JSON fixtures\n\n**HTML Element Coverage:**\n\n- All heading levels (h1-h6)\n- Paragraphs and text formatting\n- Unordered and ordered lists\n- Nested lists\n- Links with URLs\n- Bold/strong text\n- Italic/emphasis text\n- Inline code\n- Code blocks\n- Blockquotes\n- Horizontal rules\n- Images (if supported)\n- Tables (if supported)\n\n**Edge Cases & Special Scenarios:**\n\n- Nested HTML structures\n- Mixed content types\n- Special HTML characters (&, <, >, etc.)\n- HTML entity decoding\n- Excessive whitespace normalization\n- Empty elements\n- Deeply nested structures (5+ levels)\n- Very long content (5000+ characters)\n- Unicode and special characters\n- Multi-language content (English, French, German, Chinese)\n\n**Bundle Size Validation:**\n\n- Verifies WASM binary size is within acceptable range (1KB - 2MB)\n- Logs actual bundle size in KB\n- Warns if size constraints are violated\n\n**Module Functionality:**\n\n- Result consistency (same input = same output)\n- Multiple sequential conversions\n- Options parameter support\n\n## Environment Support\n\nThis test app validates the WASM module works across all target platforms:\n\n- **Browser** (ESM modules, modern browsers)\n- **Node.js** (CommonJS/ESM, v18+)\n- **Deno** (ES modules)\n- **Cloudflare Workers** (edge computing)\n\nAll tests use standard Node.js testing patterns compatible with all platforms.\n\n## Test Fixtures\n\nFixtures are stored in `../fixtures/`:\n\n- `basic-html.json` - Core HTML element conversions\n- `complex-html.json` - Complex nested structures\n- `edge-cases.json` - Edge case handling\n- `metadata-extraction.json` - Metadata extraction tests\n- `real-world.json` - Real-world HTML samples\n\n## Notes\n\n- Tests validate the **published npm package**, ensuring release quality\n- No local development builds or path dependencies are used\n- vitest is configured for ESM modules (Node.js native support)\n- All tests are async-safe and await WASM module initialization\n- Tests are comprehensive enough to catch API breaking changes\n- Bundle size validation helps track performance regressions\n\n## Troubleshooting\n\nIf tests fail:\n\n1. **Package not found**: Ensure version 2.24.1 has been published to npm\n2. **WASM initialization error**: Check Node.js version (18+ required)\n3. **File not found errors**: Ensure fixture files exist in `../fixtures/`\n4. **Test timeouts**: WASM initialization may be slow on first run\n\n## CI Integration\n\nThis test app is designed to run in CI/CD pipelines post-release to validate published packages before marking the release as stable.\n"
  },
  {
    "path": "test_apps/wasm/globalSetup.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:0486a07c350d3ec303d0fa504d58f9d080332fc2e85e1323b9d369282c77dd6f\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { spawn } from 'child_process';\nimport { resolve } from 'path';\n\nlet serverProcess;\n\nexport async function setup() {\n  // Mock server binary must be pre-built (e.g. by CI or `cargo build --manifest-path e2e/rust/Cargo.toml --bin mock-server --release`)\n  serverProcess = spawn(\n    resolve(__dirname, '../rust/target/release/mock-server'),\n    [resolve(__dirname, '../../fixtures')],\n    { stdio: ['pipe', 'pipe', 'inherit'] }\n  );\n\n  const url = await new Promise((resolve, reject) => {\n    serverProcess.stdout.on('data', (data) => {\n      const match = data.toString().match(/MOCK_SERVER_URL=(.*)/);\n      if (match) resolve(match[1].trim());\n    });\n    setTimeout(() => reject(new Error('Mock server startup timeout')), 30000);\n  });\n\n  process.env.MOCK_SERVER_URL = url;\n}\n\nexport async function teardown() {\n  if (serverProcess) {\n    serverProcess.stdin.end();\n    serverProcess.kill();\n  }\n}\n"
  },
  {
    "path": "test_apps/wasm/package.json",
    "content": "{\n  \"name\": \"@kreuzberg/html-to-markdown-wasm-e2e-wasm\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"@kreuzberg/html-to-markdown-wasm\": \"3.4.0-rc.25\",\n    \"vite-plugin-top-level-await\": \"^1.4.0\",\n    \"vite-plugin-wasm\": \"^3.4.0\",\n    \"vitest\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "test_apps/wasm/tests/conversion.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:6cb57b68adbdf14dece887ad0c93bb0843c671cb2b78ee5bbcdd34882fe33ef4\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from 'vitest';\n\ndescribe('conversion', () => {\n\n\n});\n"
  },
  {
    "path": "test_apps/wasm/tests/smoke.test.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:eaba1f346171b55022f87ac08e1c12352f40bcf91aa6695fdbb1d7daea69f4b0\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { describe, expect, it } from 'vitest';\n\ndescribe('smoke', () => {\n\n\n});\n"
  },
  {
    "path": "test_apps/wasm/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"strictNullChecks\": false,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"tests/**/*.ts\", \"vitest.config.ts\"]\n}\n"
  },
  {
    "path": "test_apps/wasm/vitest.config.ts",
    "content": "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:e311fbaf1cb2f59860066311e42edb72f59229eb5e97534c6d8c09356b5e70c5\n// To regenerate: alef generate\n// To verify freshness: alef verify --exit-code\n// Issues & docs: https://github.com/kreuzberg-dev/alef\nimport { defineConfig } from 'vitest/config';\nimport wasm from 'vite-plugin-wasm';\nimport topLevelAwait from 'vite-plugin-top-level-await';\n\nexport default defineConfig({\n  plugins: [wasm(), topLevelAwait()],\n  test: {\n    include: ['tests/**/*.test.ts'],\n    globalSetup: './globalSetup.ts',\n  },\n});\n"
  },
  {
    "path": "test_documents/html/issues/gh-121-hacker-news.html",
    "content": "<html lang=\"en\" op=\"news\"><head><meta name=\"referrer\" content=\"origin\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link rel=\"stylesheet\" type=\"text/css\" href=\"news.css?8mtsBUeVoFRRiqm1uxVX\"><link rel=\"icon\" href=\"y18.svg\"><link rel=\"alternate\" type=\"application/rss+xml\" title=\"RSS\" href=\"rss\"><title>Hacker News</title></head><body><center><table id=\"hnmain\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"85%\" bgcolor=\"#f6f6ef\"><tr><td bgcolor=\"#ff6600\"><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"padding:2px\"><tr><td style=\"width:18px;padding-right:4px\"><a href=\"https://news.ycombinator.com\"><img src=\"y18.svg\" width=\"18\" height=\"18\" style=\"border:1px white solid; display:block\"></a></td><td style=\"line-height:12pt; height:10px;\"><span class=\"pagetop\"><b class=\"hnname\"><a href=\"news\">Hacker News</a></b><a href=\"newest\">new</a> | <a href=\"front\">past</a> | <a href=\"newcomments\">comments</a> | <a href=\"ask\">ask</a> | <a href=\"show\">show</a> | <a href=\"jobs\">jobs</a> | <a href=\"submit\" rel=\"nofollow\">submit</a></span></td><td style=\"text-align:right;padding-right:4px;\"><span class=\"pagetop\"><a href=\"login?goto=news\">login</a></span></td></tr></table></td></tr><tr style='height:10px'/><tr id=\"bigbox\"><td><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr class=\"athing submission\" id=\"46001889\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">1.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46001889' href='vote?id=46001889&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://allenai.org/blog/olmo3\">Olmo 3: Charting a path through the model flow to lead open-source AI</a><span class=\"sitebit comhead\"> (<a href=\"from?site=allenai.org\"><span class=\"sitestr\">allenai.org</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46001889\">269 points</span> by <a href=\"user?id=mseri\" class=\"hnuser\">mseri</a> <span class=\"age\" title=\"2025-11-21T06:50:14 1763707814\"><a href=\"item?id=46001889\">9 hours ago</a></span> <span id=\"unv_46001889\"></span> | <a href=\"hide?id=46001889&amp;goto=news\">hide</a> | <a href=\"item?id=46001889\">61&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46004386\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">2.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46004386' href='vote?id=46004386&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://xnacly.me/posts/2025/building-a-minimal-viable-armv7-emulator/\">Building a Minimal Viable Armv7 Emulator from Scratch</a><span class=\"sitebit comhead\"> (<a href=\"from?site=xnacly.me\"><span class=\"sitestr\">xnacly.me</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46004386\">38 points</span> by <a href=\"user?id=xnacly\" class=\"hnuser\">xnacly</a> <span class=\"age\" title=\"2025-11-21T13:30:36 1763731836\"><a href=\"item?id=46004386\">2 hours ago</a></span> <span id=\"unv_46004386\"></span> | <a href=\"hide?id=46004386&amp;goto=news\">hide</a> | <a href=\"item?id=46004386\">6&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46004293\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">3.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46004293' href='vote?id=46004293&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://jslegenddev.substack.com/p/making-a-small-rpg\">Making a Small RPG</a><span class=\"sitebit comhead\"> (<a href=\"from?site=jslegenddev.substack.com\"><span class=\"sitestr\">jslegenddev.substack.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46004293\">39 points</span> by <a href=\"user?id=ibobev\" class=\"hnuser\">ibobev</a> <span class=\"age\" title=\"2025-11-21T13:23:16 1763731396\"><a href=\"item?id=46004293\">2 hours ago</a></span> <span id=\"unv_46004293\"></span> | <a href=\"hide?id=46004293&amp;goto=news\">hide</a> | <a href=\"item?id=46004293\">11&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46002161\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">4.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46002161' href='vote?id=46002161&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://lcamtuf.substack.com/p/its-hard-to-build-an-oscillator\">It&#x27;s hard to build an oscillator</a><span class=\"sitebit comhead\"> (<a href=\"from?site=lcamtuf.substack.com\"><span class=\"sitestr\">lcamtuf.substack.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46002161\">153 points</span> by <a href=\"user?id=chmaynard\" class=\"hnuser\">chmaynard</a> <span class=\"age\" title=\"2025-11-21T07:45:53 1763711153\"><a href=\"item?id=46002161\">8 hours ago</a></span> <span id=\"unv_46002161\"></span> | <a href=\"hide?id=46002161&amp;goto=news\">hide</a> | <a href=\"item?id=46002161\">59&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45937350\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">5.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45937350' href='vote?id=45937350&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://www.cnn.com/2025/11/12/science/bees-visual-stimulus-study-scli-intl\">Scientists now know that bees can process time, a first in insects</a><span class=\"sitebit comhead\"> (<a href=\"from?site=cnn.com\"><span class=\"sitestr\">cnn.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45937350\">107 points</span> by <a href=\"user?id=Brajeshwar\" class=\"hnuser\">Brajeshwar</a> <span class=\"age\" title=\"2025-11-15T13:32:12 1763213532\"><a href=\"item?id=45937350\">8 hours ago</a></span> <span id=\"unv_45937350\"></span> | <a href=\"hide?id=45937350&amp;goto=news\">hide</a> | <a href=\"item?id=45937350\">50&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45993296\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">6.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45993296' href='vote?id=45993296&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://blog.google/technology/ai/nano-banana-pro/\">Nano Banana Pro</a><span class=\"sitebit comhead\"> (<a href=\"from?site=blog.google\"><span class=\"sitestr\">blog.google</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45993296\">1157 points</span> by <a href=\"user?id=meetpateltech\" class=\"hnuser\">meetpateltech</a> <span class=\"age\" title=\"2025-11-20T15:04:23 1763651063\"><a href=\"item?id=45993296\">1 day ago</a></span> <span id=\"unv_45993296\"></span> | <a href=\"hide?id=45993296&amp;goto=news\">hide</a> | <a href=\"item?id=45993296\">640&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46003144\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">7.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46003144' href='vote?id=46003144&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://martin.janiczek.cz/2025/11/21/fawk-llms-can-write-a-language-interpreter.html\">FAWK: LLMs can write a language interpreter</a><span class=\"sitebit comhead\"> (<a href=\"from?site=janiczek.cz\"><span class=\"sitestr\">janiczek.cz</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46003144\">136 points</span> by <a href=\"user?id=todsacerdoti\" class=\"hnuser\">todsacerdoti</a> <span class=\"age\" title=\"2025-11-21T10:28:49 1763720929\"><a href=\"item?id=46003144\">5 hours ago</a></span> <span id=\"unv_46003144\"></span> | <a href=\"hide?id=46003144&amp;goto=news\">hide</a> | <a href=\"item?id=46003144\">114&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45898185\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">8.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45898185' href='vote?id=45898185&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://www.stavros.io/posts/i-converted-a-rotary-phone-into-a-meeting-handset/\">I converted a rotary phone into a meeting handset</a><span class=\"sitebit comhead\"> (<a href=\"from?site=stavros.io\"><span class=\"sitestr\">stavros.io</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45898185\">96 points</span> by <a href=\"user?id=todsacerdoti\" class=\"hnuser\">todsacerdoti</a> <span class=\"age\" title=\"2025-11-12T09:37:41 1762940261\"><a href=\"item?id=45898185\">7 hours ago</a></span> <span id=\"unv_45898185\"></span> | <a href=\"hide?id=45898185&amp;goto=news\">hide</a> | <a href=\"item?id=45898185\">49&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45917182\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">9.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45917182' href='vote?id=45917182&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://portofcontext.com\">Open Source and Local Code Mode MCP in Deno Sandboxes</a><span class=\"sitebit comhead\"> (<a href=\"from?site=portofcontext.com\"><span class=\"sitestr\">portofcontext.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45917182\">56 points</span> by <a href=\"user?id=pmkelly4444\" class=\"hnuser\">pmkelly4444</a> <span class=\"age\" title=\"2025-11-13T16:53:43 1763052823\"><a href=\"item?id=45917182\">4 hours ago</a></span> <span id=\"unv_45917182\"></span> | <a href=\"hide?id=45917182&amp;goto=news\">hide</a> | <a href=\"item?id=45917182\">16&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45994854\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">10.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45994854' href='vote?id=45994854&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://blog.google/products/android/quick-share-airdrop/\">Android and iPhone users can now share files, starting with the Pixel 10</a><span class=\"sitebit comhead\"> (<a href=\"from?site=blog.google\"><span class=\"sitestr\">blog.google</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45994854\">775 points</span> by <a href=\"user?id=abraham\" class=\"hnuser\">abraham</a> <span class=\"age\" title=\"2025-11-20T17:04:34 1763658274\"><a href=\"item?id=45994854\">23 hours ago</a></span> <span id=\"unv_45994854\"></span> | <a href=\"hide?id=45994854&amp;goto=news\">hide</a> | <a href=\"item?id=45994854\">470&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46004364\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">11.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46004364' href='vote?id=46004364&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1627423\">EXIF orientation info in PNGs isn&#x27;t used for image-orientation: from-image</a><span class=\"sitebit comhead\"> (<a href=\"from?site=bugzilla.mozilla.org\"><span class=\"sitestr\">bugzilla.mozilla.org</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46004364\">57 points</span> by <a href=\"user?id=justin-reeves\" class=\"hnuser\">justin-reeves</a> <span class=\"age\" title=\"2025-11-21T13:29:14 1763731754\"><a href=\"item?id=46004364\">2 hours ago</a></span> <span id=\"unv_46004364\"></span> | <a href=\"hide?id=46004364&amp;goto=news\">hide</a> | <a href=\"item?id=46004364\">49&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45937183\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">12.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45937183' href='vote?id=45937183&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://wasmgroundup.com/\">WebAssembly from the Ground Up</a><span class=\"sitebit comhead\"> (<a href=\"from?site=wasmgroundup.com\"><span class=\"sitestr\">wasmgroundup.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45937183\">199 points</span> by <a href=\"user?id=gurjeet\" class=\"hnuser\">gurjeet</a> <span class=\"age\" title=\"2025-11-15T13:06:38 1763211998\"><a href=\"item?id=45937183\">12 hours ago</a></span> <span id=\"unv_45937183\"></span> | <a href=\"hide?id=45937183&amp;goto=news\">hide</a> | <a href=\"item?id=45937183\">44&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45905850\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">13.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45905850' href='vote?id=45905850&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://fex-emu.com/\">FEX-emu – Run x86 applications on ARM64 Linux devices</a><span class=\"sitebit comhead\"> (<a href=\"from?site=fex-emu.com\"><span class=\"sitestr\">fex-emu.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45905850\">244 points</span> by <a href=\"user?id=open-paren\" class=\"hnuser\">open-paren</a> <span class=\"age\" title=\"2025-11-12T20:15:51 1762978551\"><a href=\"item?id=45905850\">17 hours ago</a></span> <span id=\"unv_45905850\"></span> | <a href=\"hide?id=45905850&amp;goto=news\">hide</a> | <a href=\"item?id=45905850\">101&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46003686\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">14.</span></td><td><img src=\"s.gif\" height=\"1\" width=\"14\"></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://www.ycombinator.com/companies/roundtable/jobs/irJTEsg-sales-development-representative\">Roundtable (YC S23) Is Hiring Two Sales Development Representatives (SDRs)</a><span class=\"sitebit comhead\"> (<a href=\"from?site=ycombinator.com\"><span class=\"sitestr\">ycombinator.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"age\" title=\"2025-11-21T12:00:02 1763726402\"><a href=\"item?id=46003686\">4 hours ago</a></span> | <a href=\"hide?id=46003686&amp;goto=news\">hide</a></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45937830\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">15.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45937830' href='vote?id=45937830&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://nautil.us/ancient-roman-glass-reveals-a-hidden-language-1247932/\">Ancient Roman Glass Reveals a Hidden &quot;Language&quot;</a><span class=\"sitebit comhead\"> (<a href=\"from?site=nautil.us\"><span class=\"sitestr\">nautil.us</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45937830\">20 points</span> by <a href=\"user?id=DrierCycle\" class=\"hnuser\">DrierCycle</a> <span class=\"age\" title=\"2025-11-15T14:55:58 1763218558\"><a href=\"item?id=45937830\">4 hours ago</a></span> <span id=\"unv_45937830\"></span> | <a href=\"hide?id=45937830&amp;goto=news\">hide</a> | <a href=\"item?id=45937830\">1&nbsp;comment</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45952654\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">16.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45952654' href='vote?id=45952654&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://bytesauna.com/post/my-favorite-math-problem\">My Favorite Math Problem</a><span class=\"sitebit comhead\"> (<a href=\"from?site=bytesauna.com\"><span class=\"sitestr\">bytesauna.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45952654\">13 points</span> by <a href=\"user?id=mapehe\" class=\"hnuser\">mapehe</a> <span class=\"age\" title=\"2025-11-17T11:22:54 1763378574\"><a href=\"item?id=45952654\">3 hours ago</a></span> <span id=\"unv_45952654\"></span> | <a href=\"hide?id=45952654&amp;goto=news\">hide</a> | <a href=\"item?id=45952654\">5&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46003778\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">17.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46003778' href='vote?id=46003778&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://www.heise.de/en/news/How-a-French-judge-was-digitally-cut-off-by-the-USA-11087561.html\">How a French judge was digitally cut off by the USA</a><span class=\"sitebit comhead\"> (<a href=\"from?site=heise.de\"><span class=\"sitestr\">heise.de</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46003778\">131 points</span> by <a href=\"user?id=i-con\" class=\"hnuser\">i-con</a> <span class=\"age\" title=\"2025-11-21T12:12:41 1763727161\"><a href=\"item?id=46003778\">4 hours ago</a></span> <span id=\"unv_46003778\"></span> | <a href=\"hide?id=46003778&amp;goto=news\">hide</a> | <a href=\"item?id=46003778\">138&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46005130\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">18.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46005130' href='vote?id=46005130&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://stackoverflow.com/questions/79817124/is-c26-getting-destructive-move-semantics\">Is C++26 getting destructive move semantics?</a><span class=\"sitebit comhead\"> (<a href=\"from?site=stackoverflow.com\"><span class=\"sitestr\">stackoverflow.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46005130\">13 points</span> by <a href=\"user?id=signa11\" class=\"hnuser\">signa11</a> <span class=\"age\" title=\"2025-11-21T14:52:36 1763736756\"><a href=\"item?id=46005130\">1 hour ago</a></span> <span id=\"unv_46005130\"></span> | <a href=\"hide?id=46005130&amp;goto=news\">hide</a> | <a href=\"item?id=46005130\">3&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45954228\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">19.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45954228' href='vote?id=45954228&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://littlemountainman.github.io/2025/11/17/tens/\">Show HN: 32V TENS device from built from scratch under $100</a><span class=\"sitebit comhead\"> (<a href=\"from?site=littlemountainman.github.io\"><span class=\"sitestr\">littlemountainman.github.io</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45954228\">52 points</span> by <a href=\"user?id=autonomydriver\" class=\"hnuser\">autonomydriver</a> <span class=\"age\" title=\"2025-11-17T15:06:32 1763391992\"><a href=\"item?id=45954228\">9 hours ago</a></span> <span id=\"unv_45954228\"></span> | <a href=\"hide?id=45954228&amp;goto=news\">hide</a> | <a href=\"item?id=45954228\">11&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46002138\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">20.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46002138' href='vote?id=46002138&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://tech.stonecharioteer.com/posts/2025/qtile-window-manager/\">The Qtile Window Manager: A Python-Powered Tiling Experience</a><span class=\"sitebit comhead\"> (<a href=\"from?site=stonecharioteer.com\"><span class=\"sitestr\">stonecharioteer.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46002138\">42 points</span> by <a href=\"user?id=stonecharioteer\" class=\"hnuser\">stonecharioteer</a> <span class=\"age\" title=\"2025-11-21T07:41:15 1763710875\"><a href=\"item?id=46002138\">8 hours ago</a></span> <span id=\"unv_46002138\"></span> | <a href=\"hide?id=46002138&amp;goto=news\">hide</a> | <a href=\"item?id=46002138\">14&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45997212\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">21.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45997212' href='vote?id=45997212&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://github.com/ravynsoft/ravynos\">New OS aims to provide (some) compatibility with macOS</a><span class=\"sitebit comhead\"> (<a href=\"from?site=github.com/ravynsoft\"><span class=\"sitestr\">github.com/ravynsoft</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45997212\">286 points</span> by <a href=\"user?id=kasajian\" class=\"hnuser\">kasajian</a> <span class=\"age\" title=\"2025-11-20T20:24:42 1763670282\"><a href=\"item?id=45997212\">19 hours ago</a></span> <span id=\"unv_45997212\"></span> | <a href=\"hide?id=45997212&amp;goto=news\">hide</a> | <a href=\"item?id=45997212\">136&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45999038\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">22.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45999038' href='vote?id=45999038&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://rein.pk/over-regulation-is-doubling-the-cost\">Over-regulation is doubling the cost</a><span class=\"sitebit comhead\"> (<a href=\"from?site=rein.pk\"><span class=\"sitestr\">rein.pk</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45999038\">285 points</span> by <a href=\"user?id=bilsbie\" class=\"hnuser\">bilsbie</a> <span class=\"age\" title=\"2025-11-20T22:58:06 1763679486\"><a href=\"item?id=45999038\">17 hours ago</a></span> <span id=\"unv_45999038\"></span> | <a href=\"hide?id=45999038&amp;goto=news\">hide</a> | <a href=\"item?id=45999038\">531&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45913281\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">23.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45913281' href='vote?id=45913281&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://eli.thegreenplace.net/2025/hilbert-space-treating-functions-as-vectors/\">Hilbert space: Treating functions as vectors</a><span class=\"sitebit comhead\"> (<a href=\"from?site=thegreenplace.net\"><span class=\"sitestr\">thegreenplace.net</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45913281\">114 points</span> by <a href=\"user?id=signa11\" class=\"hnuser\">signa11</a> <span class=\"age\" title=\"2025-11-13T10:41:08 1763030468\"><a href=\"item?id=45913281\">14 hours ago</a></span> <span id=\"unv_45913281\"></span> | <a href=\"hide?id=45913281&amp;goto=news\">hide</a> | <a href=\"item?id=45913281\">43&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45995913\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">24.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45995913' href='vote?id=45995913&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://london.publicinsights.uk\" rel=\"nofollow\">Show HN: Search London StreetView panoramas by text</a><span class=\"sitebit comhead\"> (<a href=\"from?site=london.publicinsights.uk\"><span class=\"sitestr\">london.publicinsights.uk</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45995913\">6 points</span> by <a href=\"user?id=dfworks\" class=\"hnuser\">dfworks</a> <span class=\"age\" title=\"2025-11-20T18:27:51 1763663271\"><a href=\"item?id=45995913\">3 hours ago</a></span> <span id=\"unv_45995913\"></span> | <a href=\"hide?id=45995913&amp;goto=news\">hide</a> | <a href=\"item?id=45995913\">7&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"46002989\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">25.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_46002989' href='vote?id=46002989&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://arstechnica.com/gadgets/2025/11/hp-and-dell-disable-hevc-support-built-into-their-laptops-cpus/\">HP and Dell disable HEVC support built into their laptops&#x27; CPUs</a><span class=\"sitebit comhead\"> (<a href=\"from?site=arstechnica.com\"><span class=\"sitestr\">arstechnica.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_46002989\">176 points</span> by <a href=\"user?id=latexr\" class=\"hnuser\">latexr</a> <span class=\"age\" title=\"2025-11-21T10:01:37 1763719297\"><a href=\"item?id=46002989\">6 hours ago</a></span> <span id=\"unv_46002989\"></span> | <a href=\"hide?id=46002989&amp;goto=news\">hide</a> | <a href=\"item?id=46002989\">102&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45963350\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">26.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45963350' href='vote?id=45963350&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://joshua.hu/ai-slop-okta-nextjs-0auth-security-vulnerability\">Okta&#x27;s NextJS-0auth troubles</a><span class=\"sitebit comhead\"> (<a href=\"from?site=joshua.hu\"><span class=\"sitestr\">joshua.hu</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45963350\">347 points</span> by <a href=\"user?id=ramimac\" class=\"hnuser\">ramimac</a> <span class=\"age\" title=\"2025-11-18T10:17:20 1763461040\"><a href=\"item?id=45963350\">1 day ago</a></span> <span id=\"unv_45963350\"></span> | <a href=\"hide?id=45963350&amp;goto=news\">hide</a> | <a href=\"item?id=45963350\">134&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45996585\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">27.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45996585' href='vote?id=45996585&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://duckdb.org/2025/11/19/encryption-in-duckdb\">Data-at-Rest Encryption in DuckDB</a><span class=\"sitebit comhead\"> (<a href=\"from?site=duckdb.org\"><span class=\"sitestr\">duckdb.org</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45996585\">206 points</span> by <a href=\"user?id=chmaynard\" class=\"hnuser\">chmaynard</a> <span class=\"age\" title=\"2025-11-20T19:26:12 1763666772\"><a href=\"item?id=45996585\">20 hours ago</a></span> <span id=\"unv_45996585\"></span> | <a href=\"hide?id=45996585&amp;goto=news\">hide</a> | <a href=\"item?id=45996585\">22&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45976693\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">28.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45976693' href='vote?id=45976693&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://mobomaps.com\">Free interactive tool that shows you how PCIe lanes work on motherboards</a><span class=\"sitebit comhead\"> (<a href=\"from?site=mobomaps.com\"><span class=\"sitestr\">mobomaps.com</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45976693\">243 points</span> by <a href=\"user?id=tagyro\" class=\"hnuser\">tagyro</a> <span class=\"age\" title=\"2025-11-19T07:13:00 1763536380\"><a href=\"item?id=45976693\">1 day ago</a></span> <span id=\"unv_45976693\"></span> | <a href=\"hide?id=45976693&amp;goto=news\">hide</a> | <a href=\"item?id=45976693\">57&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45995834\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">29.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45995834' href='vote?id=45995834&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://www.ntsb.gov/Documents/Prelimiary%20Report%20DCA26MA024.pdf\">NTSB Preliminary Report – UPS Boeing MD-11F Crash [pdf]</a><span class=\"sitebit comhead\"> (<a href=\"from?site=ntsb.gov\"><span class=\"sitestr\">ntsb.gov</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45995834\">195 points</span> by <a href=\"user?id=gregsadetsky\" class=\"hnuser\">gregsadetsky</a> <span class=\"age\" title=\"2025-11-20T18:20:59 1763662859\"><a href=\"item?id=45995834\">21 hours ago</a></span> <span id=\"unv_45995834\"></span> | <a href=\"hide?id=45995834&amp;goto=news\">hide</a> | <a href=\"item?id=45995834\">208&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"athing submission\" id=\"45995816\"><td align=\"right\" valign=\"top\" class=\"title\"><span class=\"rank\">30.</span></td><td valign=\"top\" class=\"votelinks\"><center><a id='up_45995816' href='vote?id=45995816&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class=\"title\"><span class=\"titleline\"><a href=\"https://lionsos.org\">The Lions Operating System</a><span class=\"sitebit comhead\"> (<a href=\"from?site=lionsos.org\"><span class=\"sitestr\">lionsos.org</span></a>)</span></span></td></tr><tr><td colspan=\"2\"></td><td class=\"subtext\"><span class=\"subline\"><span class=\"score\" id=\"score_45995816\">190 points</span> by <a href=\"user?id=plunderer\" class=\"hnuser\">plunderer</a> <span class=\"age\" title=\"2025-11-20T18:19:31 1763662771\"><a href=\"item?id=45995816\">21 hours ago</a></span> <span id=\"unv_45995816\"></span> | <a href=\"hide?id=45995816&amp;goto=news\">hide</a> | <a href=\"item?id=45995816\">57&nbsp;comments</a></span></td></tr><tr class=\"spacer\" style=\"height:5px\"></tr><tr class=\"morespace\" style=\"height:10px\"></tr><tr><td colspan=\"2\"></td><td class='title'><a href='?p=2' class='morelink' rel='next'>More</a></td></tr></table></td></tr><tr><td><img src=\"s.gif\" height=\"10\" width=\"0\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"1\"><tr><td bgcolor=\"#ff6600\"></td></tr></table><br>\n<center><span class=\"yclinks\"><a href=\"newsguidelines.html\">Guidelines</a> | <a href=\"newsfaq.html\">FAQ</a> | <a href=\"lists\">Lists</a> | <a href=\"https://github.com/HackerNews/API\">API</a> | <a href=\"security.html\">Security</a> | <a href=\"https://www.ycombinator.com/legal/\">Legal</a> | <a href=\"https://www.ycombinator.com/apply/\">Apply to YC</a> | <a href=\"mailto:hn@ycombinator.com\">Contact</a></span><br><br>\n<form method=\"get\" action=\"//hn.algolia.com/\">Search: <input type=\"text\" name=\"q\" size=\"17\" autocorrect=\"off\" spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\"></form></center></td></tr></table></center></body><script type=\"text/javascript\" src=\"hn.js?8mtsBUeVoFRRiqm1uxVX\"></script></html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-121-hacker-news.md",
    "content": "\n\n- [https://news.ycombinator.com](https://news.ycombinator.com) **[Hacker News](news)**[new](newest) | [past](front) | [comments](newcomments) | [ask](ask) | [show](show) | [jobs](jobs) | [submit](submit) [login](login?goto=news)\n- 1. [vote?id=46001889&how=up&goto=news](vote?id=46001889&how=up&goto=news) [Olmo 3: Charting a path through the model flow to lead open-source AI](https://allenai.org/blog/olmo3) ([allenai.org](from?site=allenai.org))\n- 269 points by [mseri](user?id=mseri) [9 hours ago](item?id=46001889) | [hide](hide?id=46001889&goto=news) | [61 comments](item?id=46001889)\n- 2. [vote?id=46004386&how=up&goto=news](vote?id=46004386&how=up&goto=news) [Building a Minimal Viable Armv7 Emulator from Scratch](https://xnacly.me/posts/2025/building-a-minimal-viable-armv7-emulator/) ([xnacly.me](from?site=xnacly.me))\n- 38 points by [xnacly](user?id=xnacly) [2 hours ago](item?id=46004386) | [hide](hide?id=46004386&goto=news) | [6 comments](item?id=46004386)\n- 3. [vote?id=46004293&how=up&goto=news](vote?id=46004293&how=up&goto=news) [Making a Small RPG](https://jslegenddev.substack.com/p/making-a-small-rpg) ([jslegenddev.substack.com](from?site=jslegenddev.substack.com))\n- 39 points by [ibobev](user?id=ibobev) [2 hours ago](item?id=46004293) | [hide](hide?id=46004293&goto=news) | [11 comments](item?id=46004293)\n- 4. [vote?id=46002161&how=up&goto=news](vote?id=46002161&how=up&goto=news) [It's hard to build an oscillator](https://lcamtuf.substack.com/p/its-hard-to-build-an-oscillator) ([lcamtuf.substack.com](from?site=lcamtuf.substack.com))\n- 153 points by [chmaynard](user?id=chmaynard) [8 hours ago](item?id=46002161) | [hide](hide?id=46002161&goto=news) | [59 comments](item?id=46002161)\n- 5. [vote?id=45937350&how=up&goto=news](vote?id=45937350&how=up&goto=news) [Scientists now know that bees can process time, a first in insects](https://www.cnn.com/2025/11/12/science/bees-visual-stimulus-study-scli-intl) ([cnn.com](from?site=cnn.com))\n- 107 points by [Brajeshwar](user?id=Brajeshwar) [8 hours ago](item?id=45937350) | [hide](hide?id=45937350&goto=news) | [50 comments](item?id=45937350)\n- 6. [vote?id=45993296&how=up&goto=news](vote?id=45993296&how=up&goto=news) [Nano Banana Pro](https://blog.google/technology/ai/nano-banana-pro/) ([blog.google](from?site=blog.google))\n- 1157 points by [meetpateltech](user?id=meetpateltech) [1 day ago](item?id=45993296) | [hide](hide?id=45993296&goto=news) | [640 comments](item?id=45993296)\n- 7. [vote?id=46003144&how=up&goto=news](vote?id=46003144&how=up&goto=news) [FAWK: LLMs can write a language interpreter](https://martin.janiczek.cz/2025/11/21/fawk-llms-can-write-a-language-interpreter.html) ([janiczek.cz](from?site=janiczek.cz))\n- 136 points by [todsacerdoti](user?id=todsacerdoti) [5 hours ago](item?id=46003144) | [hide](hide?id=46003144&goto=news) | [114 comments](item?id=46003144)\n- 8. [vote?id=45898185&how=up&goto=news](vote?id=45898185&how=up&goto=news) [I converted a rotary phone into a meeting handset](https://www.stavros.io/posts/i-converted-a-rotary-phone-into-a-meeting-handset/) ([stavros.io](from?site=stavros.io))\n- 96 points by [todsacerdoti](user?id=todsacerdoti) [7 hours ago](item?id=45898185) | [hide](hide?id=45898185&goto=news) | [49 comments](item?id=45898185)\n- 9. [vote?id=45917182&how=up&goto=news](vote?id=45917182&how=up&goto=news) [Open Source and Local Code Mode MCP in Deno Sandboxes](https://portofcontext.com) ([portofcontext.com](from?site=portofcontext.com))\n- 56 points by [pmkelly4444](user?id=pmkelly4444) [4 hours ago](item?id=45917182) | [hide](hide?id=45917182&goto=news) | [16 comments](item?id=45917182)\n- 10. [vote?id=45994854&how=up&goto=news](vote?id=45994854&how=up&goto=news) [Android and iPhone users can now share files, starting with the Pixel 10](https://blog.google/products/android/quick-share-airdrop/) ([blog.google](from?site=blog.google))\n- 775 points by [abraham](user?id=abraham) [23 hours ago](item?id=45994854) | [hide](hide?id=45994854&goto=news) | [470 comments](item?id=45994854)\n- 11. [vote?id=46004364&how=up&goto=news](vote?id=46004364&how=up&goto=news) [EXIF orientation info in PNGs isn't used for image-orientation: from-image](https://bugzilla.mozilla.org/show_bug.cgi?id=1627423) ([bugzilla.mozilla.org](from?site=bugzilla.mozilla.org))\n- 57 points by [justin-reeves](user?id=justin-reeves) [2 hours ago](item?id=46004364) | [hide](hide?id=46004364&goto=news) | [49 comments](item?id=46004364)\n- 12. [vote?id=45937183&how=up&goto=news](vote?id=45937183&how=up&goto=news) [WebAssembly from the Ground Up](https://wasmgroundup.com/) ([wasmgroundup.com](from?site=wasmgroundup.com))\n- 199 points by [gurjeet](user?id=gurjeet) [12 hours ago](item?id=45937183) | [hide](hide?id=45937183&goto=news) | [44 comments](item?id=45937183)\n- 13. [vote?id=45905850&how=up&goto=news](vote?id=45905850&how=up&goto=news) [FEX-emu – Run x86 applications on ARM64 Linux devices](https://fex-emu.com/) ([fex-emu.com](from?site=fex-emu.com))\n- 244 points by [open-paren](user?id=open-paren) [17 hours ago](item?id=45905850) | [hide](hide?id=45905850&goto=news) | [101 comments](item?id=45905850)\n- 14. [Roundtable (YC S23) Is Hiring Two Sales Development Representatives (SDRs)](https://www.ycombinator.com/companies/roundtable/jobs/irJTEsg-sales-development-representative) ([ycombinator.com](from?site=ycombinator.com))\n- [4 hours ago](item?id=46003686) | [hide](hide?id=46003686&goto=news)\n- 15. [vote?id=45937830&how=up&goto=news](vote?id=45937830&how=up&goto=news) [Ancient Roman Glass Reveals a Hidden \"Language\"](https://nautil.us/ancient-roman-glass-reveals-a-hidden-language-1247932/) ([nautil.us](from?site=nautil.us))\n- 20 points by [DrierCycle](user?id=DrierCycle) [4 hours ago](item?id=45937830) | [hide](hide?id=45937830&goto=news) | [1 comment](item?id=45937830)\n- 16. [vote?id=45952654&how=up&goto=news](vote?id=45952654&how=up&goto=news) [My Favorite Math Problem](https://bytesauna.com/post/my-favorite-math-problem) ([bytesauna.com](from?site=bytesauna.com))\n- 13 points by [mapehe](user?id=mapehe) [3 hours ago](item?id=45952654) | [hide](hide?id=45952654&goto=news) | [5 comments](item?id=45952654)\n- 17. [vote?id=46003778&how=up&goto=news](vote?id=46003778&how=up&goto=news) [How a French judge was digitally cut off by the USA](https://www.heise.de/en/news/How-a-French-judge-was-digitally-cut-off-by-the-USA-11087561.html) ([heise.de](from?site=heise.de))\n- 131 points by [i-con](user?id=i-con) [4 hours ago](item?id=46003778) | [hide](hide?id=46003778&goto=news) | [138 comments](item?id=46003778)\n- 18. [vote?id=46005130&how=up&goto=news](vote?id=46005130&how=up&goto=news) [Is C++26 getting destructive move semantics?](https://stackoverflow.com/questions/79817124/is-c26-getting-destructive-move-semantics) ([stackoverflow.com](from?site=stackoverflow.com))\n- 13 points by [signa11](user?id=signa11) [1 hour ago](item?id=46005130) | [hide](hide?id=46005130&goto=news) | [3 comments](item?id=46005130)\n- 19. [vote?id=45954228&how=up&goto=news](vote?id=45954228&how=up&goto=news) [Show HN: 32V TENS device from built from scratch under $100](https://littlemountainman.github.io/2025/11/17/tens/) ([littlemountainman.github.io](from?site=littlemountainman.github.io))\n- 52 points by [autonomydriver](user?id=autonomydriver) [9 hours ago](item?id=45954228) | [hide](hide?id=45954228&goto=news) | [11 comments](item?id=45954228)\n- 20. [vote?id=46002138&how=up&goto=news](vote?id=46002138&how=up&goto=news) [The Qtile Window Manager: A Python-Powered Tiling Experience](https://tech.stonecharioteer.com/posts/2025/qtile-window-manager/) ([stonecharioteer.com](from?site=stonecharioteer.com))\n- 42 points by [stonecharioteer](user?id=stonecharioteer) [8 hours ago](item?id=46002138) | [hide](hide?id=46002138&goto=news) | [14 comments](item?id=46002138)\n- 21. [vote?id=45997212&how=up&goto=news](vote?id=45997212&how=up&goto=news) [New OS aims to provide (some) compatibility with macOS](https://github.com/ravynsoft/ravynos) ([github.com/ravynsoft](from?site=github.com/ravynsoft))\n- 286 points by [kasajian](user?id=kasajian) [19 hours ago](item?id=45997212) | [hide](hide?id=45997212&goto=news) | [136 comments](item?id=45997212)\n- 22. [vote?id=45999038&how=up&goto=news](vote?id=45999038&how=up&goto=news) [Over-regulation is doubling the cost](https://rein.pk/over-regulation-is-doubling-the-cost) ([rein.pk](from?site=rein.pk))\n- 285 points by [bilsbie](user?id=bilsbie) [17 hours ago](item?id=45999038) | [hide](hide?id=45999038&goto=news) | [531 comments](item?id=45999038)\n- 23. [vote?id=45913281&how=up&goto=news](vote?id=45913281&how=up&goto=news) [Hilbert space: Treating functions as vectors](https://eli.thegreenplace.net/2025/hilbert-space-treating-functions-as-vectors/) ([thegreenplace.net](from?site=thegreenplace.net))\n- 114 points by [signa11](user?id=signa11) [14 hours ago](item?id=45913281) | [hide](hide?id=45913281&goto=news) | [43 comments](item?id=45913281)\n- 24. [vote?id=45995913&how=up&goto=news](vote?id=45995913&how=up&goto=news) [Show HN: Search London StreetView panoramas by text](https://london.publicinsights.uk) ([london.publicinsights.uk](from?site=london.publicinsights.uk))\n- 6 points by [dfworks](user?id=dfworks) [3 hours ago](item?id=45995913) | [hide](hide?id=45995913&goto=news) | [7 comments](item?id=45995913)\n- 25. [vote?id=46002989&how=up&goto=news](vote?id=46002989&how=up&goto=news) [HP and Dell disable HEVC support built into their laptops' CPUs](https://arstechnica.com/gadgets/2025/11/hp-and-dell-disable-hevc-support-built-into-their-laptops-cpus/) ([arstechnica.com](from?site=arstechnica.com))\n- 176 points by [latexr](user?id=latexr) [6 hours ago](item?id=46002989) | [hide](hide?id=46002989&goto=news) | [102 comments](item?id=46002989)\n- 26. [vote?id=45963350&how=up&goto=news](vote?id=45963350&how=up&goto=news) [Okta's NextJS-0auth troubles](https://joshua.hu/ai-slop-okta-nextjs-0auth-security-vulnerability) ([joshua.hu](from?site=joshua.hu))\n- 347 points by [ramimac](user?id=ramimac) [1 day ago](item?id=45963350) | [hide](hide?id=45963350&goto=news) | [134 comments](item?id=45963350)\n- 27. [vote?id=45996585&how=up&goto=news](vote?id=45996585&how=up&goto=news) [Data-at-Rest Encryption in DuckDB](https://duckdb.org/2025/11/19/encryption-in-duckdb) ([duckdb.org](from?site=duckdb.org))\n- 206 points by [chmaynard](user?id=chmaynard) [20 hours ago](item?id=45996585) | [hide](hide?id=45996585&goto=news) | [22 comments](item?id=45996585)\n- 28. [vote?id=45976693&how=up&goto=news](vote?id=45976693&how=up&goto=news) [Free interactive tool that shows you how PCIe lanes work on motherboards](https://mobomaps.com) ([mobomaps.com](from?site=mobomaps.com))\n- 243 points by [tagyro](user?id=tagyro) [1 day ago](item?id=45976693) | [hide](hide?id=45976693&goto=news) | [57 comments](item?id=45976693)\n- 29. [vote?id=45995834&how=up&goto=news](vote?id=45995834&how=up&goto=news) [NTSB Preliminary Report – UPS Boeing MD-11F Crash [pdf]](https://www.ntsb.gov/Documents/Prelimiary%20Report%20DCA26MA024.pdf) ([ntsb.gov](from?site=ntsb.gov))\n- 195 points by [gregsadetsky](user?id=gregsadetsky) [21 hours ago](item?id=45995834) | [hide](hide?id=45995834&goto=news) | [208 comments](item?id=45995834)\n- 30. [vote?id=45995816&how=up&goto=news](vote?id=45995816&how=up&goto=news) [The Lions Operating System](https://lionsos.org) ([lionsos.org](from?site=lionsos.org))\n- 190 points by [plunderer](user?id=plunderer) [21 hours ago](item?id=45995816) | [hide](hide?id=45995816&goto=news) | [57 comments](item?id=45995816)\n- [More](?p=2)\n- [Guidelines](newsguidelines.html) | [FAQ](newsfaq.html) | [Lists](lists) | [API](https://github.com/HackerNews/API) | [Security](security.html) | [Legal](https://www.ycombinator.com/legal/) | [Apply to YC](https://www.ycombinator.com/apply/) | [Contact](mailto:hn@ycombinator.com)\n"
  },
  {
    "path": "test_documents/html/issues/gh-121-minimal-failing.html",
    "content": "<html lang=\"en\">\n  <body>\n    <app-root _nghost-oiu-c0=\"\" ng-version=\"8.2.14\"\n      ><div _ngcontent-oiu-c0=\"\" class=\"root-layout\">\n        <div _ngcontent-oiu-c0=\"\" class=\"nav-bar\">\n          <app-nav-bar _ngcontent-oiu-c0=\"\" _nghost-oiu-c1=\"\"\n            ><div\n              _ngcontent-oiu-c1=\"\"\n              class=\"header-container d-flex justify-content-between align-items-center px-20-px\"\n            >\n              <span _ngcontent-oiu-c1=\"\" role=\"button\"\n                ><i\n                  _ngcontent-oiu-c1=\"\"\n                  class=\"anticon trigger anticon-menu-fold\"\n                  nz-icon=\"\"\n                ></i\n              ></span>\n              <div _ngcontent-oiu-c1=\"\" class=\"d-flex align-self-center\">\n                <div _ngcontent-oiu-c1=\"\" class=\"mr-13-px\">\n                  <app-internal-app _ngcontent-oiu-c1=\"\" _nghost-oiu-c4=\"\"\n                    ><div _ngcontent-oiu-c4=\"\" class=\"more-app\">\n                      <div _ngcontent-oiu-c4=\"\" class=\"icon\">\n                        <span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span>\n                      </div>\n                    </div>\n                    <!----></app-internal-app\n                  >\n                </div>\n                <div _ngcontent-oiu-c1=\"\">\n                  <nz-avatar\n                    _ngcontent-oiu-c1=\"\"\n                    nz-popover=\"\"\n                    nzpopoverplacement=\"bottomRight\"\n                    nzpopovertrigger=\"click\"\n                    nzsrc=\"assets/img/icon/user.svg\"\n                    class=\"ant-avatar ant-avatar-circle ant-avatar-image\"\n                    style=\"width: 30px; height: 30px; line-height: 30px\"\n                    ><!----><!----><img\n                      src=\"assets/img/icon/user.svg\"\n                      class=\"ng-star-inserted\"\n                    /><!----></nz-avatar\n                  >\n                </div>\n              </div>\n            </div>\n            <!----></app-nav-bar\n          >\n        </div>\n        <div _ngcontent-oiu-c0=\"\" class=\"side-bar\">\n          <app-side-bar _ngcontent-oiu-c0=\"\" _nghost-oiu-c2=\"\"\n            ><!---->\n            <div\n              _ngcontent-oiu-c2=\"\"\n              class=\"sidebar-container ng-star-inserted\"\n            >\n              <div\n                _ngcontent-oiu-c2=\"\"\n                class=\"brand-logo d-flex align-items-center\"\n              >\n                <img\n                  _ngcontent-oiu-c2=\"\"\n                  alt=\"STS Appraisal logo\"\n                  tabindex=\"0\"\n                  src=\"assets/img/icon/sts.svg\"\n                />\n              </div>\n              <div _ngcontent-oiu-c2=\"\" class=\"menu-container\">\n                <ul _ngcontent-oiu-c2=\"\">\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item current-focus\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/team-appraisal\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        Team Appraisal </span\n                      ><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"caret ng-star-inserted\"\n                        ><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/caret.svg\"\n                        ></svg-icon></span\n                      ><!----></a\n                    ><!----><!----><!---->\n                    <div\n                      _ngcontent-oiu-c2=\"\"\n                      appcollapsemenu=\"\"\n                      class=\"children-container ng-star-inserted\"\n                      style=\"height: 81px\"\n                    >\n                      <ul _ngcontent-oiu-c2=\"\">\n                        <!----><!----><!---->\n                        <li _ngcontent-oiu-c2=\"\" class=\"ng-star-inserted\">\n                          <a\n                            _ngcontent-oiu-c2=\"\"\n                            class=\"menu-item\"\n                            routerlinkactive=\"current-active\"\n                            href=\"/team-appraisal/team\"\n                            ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\"\n                              ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                                _ngcontent-oiu-c2=\"\"\n                                src=\"assets/img/icon/filled.svg\"\n                                class=\"ng-star-inserted\"\n                              ></svg-icon\n                              ><!----></span\n                            ><span _ngcontent-oiu-c2=\"\" class=\"label-menu\"\n                              >Team</span\n                            ></a\n                          >\n                        </li>\n                        <!----><!----><!---->\n                        <li _ngcontent-oiu-c2=\"\" class=\"ng-star-inserted\">\n                          <a\n                            _ngcontent-oiu-c2=\"\"\n                            class=\"menu-item current-active\"\n                            routerlinkactive=\"current-active\"\n                            href=\"/team-appraisal/pending\"\n                            ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\"\n                              ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                                _ngcontent-oiu-c2=\"\"\n                                src=\"assets/img/icon/filled.svg\"\n                                class=\"ng-star-inserted\"\n                              ></svg-icon\n                              ><!----></span\n                            ><span _ngcontent-oiu-c2=\"\" class=\"label-menu\"\n                              >Pending</span\n                            ></a\n                          >\n                        </li>\n                        <!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!---->\n                      </ul>\n                    </div>\n                    <!---->\n                  </li>\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/my-appraisal\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        My Appraisals </span\n                      ><!----><!----></a\n                    ><!----><!----><!----><!---->\n                  </li>\n</li></ul></body></html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-121-spa-app.html",
    "content": "<html lang=\"en\">\n  <body>\n    <app-root _nghost-oiu-c0=\"\" ng-version=\"8.2.14\"\n      ><div _ngcontent-oiu-c0=\"\" class=\"root-layout\">\n        <div _ngcontent-oiu-c0=\"\" class=\"nav-bar\">\n          <app-nav-bar _ngcontent-oiu-c0=\"\" _nghost-oiu-c1=\"\"\n            ><div\n              _ngcontent-oiu-c1=\"\"\n              class=\"header-container d-flex justify-content-between align-items-center px-20-px\"\n            >\n              <span _ngcontent-oiu-c1=\"\" role=\"button\"\n                ><i\n                  _ngcontent-oiu-c1=\"\"\n                  class=\"anticon trigger anticon-menu-fold\"\n                  nz-icon=\"\"\n                ></i\n              ></span>\n              <div _ngcontent-oiu-c1=\"\" class=\"d-flex align-self-center\">\n                <div _ngcontent-oiu-c1=\"\" class=\"mr-13-px\">\n                  <app-internal-app _ngcontent-oiu-c1=\"\" _nghost-oiu-c4=\"\"\n                    ><div _ngcontent-oiu-c4=\"\" class=\"more-app\">\n                      <div _ngcontent-oiu-c4=\"\" class=\"icon\">\n                        <span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span\n                        ><span _ngcontent-oiu-c4=\"\"></span>\n                      </div>\n                    </div>\n                    <!----></app-internal-app\n                  >\n                </div>\n                <div _ngcontent-oiu-c1=\"\">\n                  <nz-avatar\n                    _ngcontent-oiu-c1=\"\"\n                    nz-popover=\"\"\n                    nzpopoverplacement=\"bottomRight\"\n                    nzpopovertrigger=\"click\"\n                    nzsrc=\"assets/img/icon/user.svg\"\n                    class=\"ant-avatar ant-avatar-circle ant-avatar-image\"\n                    style=\"width: 30px; height: 30px; line-height: 30px\"\n                    ><!----><!----><img\n                      src=\"assets/img/icon/user.svg\"\n                      class=\"ng-star-inserted\"\n                    /><!----></nz-avatar\n                  >\n                </div>\n              </div>\n            </div>\n            <!----></app-nav-bar\n          >\n        </div>\n        <div _ngcontent-oiu-c0=\"\" class=\"side-bar\">\n          <app-side-bar _ngcontent-oiu-c0=\"\" _nghost-oiu-c2=\"\"\n            ><!---->\n            <div\n              _ngcontent-oiu-c2=\"\"\n              class=\"sidebar-container ng-star-inserted\"\n            >\n              <div\n                _ngcontent-oiu-c2=\"\"\n                class=\"brand-logo d-flex align-items-center\"\n              >\n                <img\n                  _ngcontent-oiu-c2=\"\"\n                  alt=\"STS Appraisal logo\"\n                  tabindex=\"0\"\n                  src=\"assets/img/icon/sts.svg\"\n                />\n              </div>\n              <div _ngcontent-oiu-c2=\"\" class=\"menu-container\">\n                <ul _ngcontent-oiu-c2=\"\">\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item current-focus\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/team-appraisal\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        Team Appraisal </span\n                      ><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"caret ng-star-inserted\"\n                        ><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/caret.svg\"\n                        ></svg-icon></span\n                      ><!----></a\n                    ><!----><!----><!---->\n                    <div\n                      _ngcontent-oiu-c2=\"\"\n                      appcollapsemenu=\"\"\n                      class=\"children-container ng-star-inserted\"\n                      style=\"height: 81px\"\n                    >\n                      <ul _ngcontent-oiu-c2=\"\">\n                        <!----><!----><!---->\n                        <li _ngcontent-oiu-c2=\"\" class=\"ng-star-inserted\">\n                          <a\n                            _ngcontent-oiu-c2=\"\"\n                            class=\"menu-item\"\n                            routerlinkactive=\"current-active\"\n                            href=\"/team-appraisal/team\"\n                            ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\"\n                              ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                                _ngcontent-oiu-c2=\"\"\n                                src=\"assets/img/icon/filled.svg\"\n                                class=\"ng-star-inserted\"\n                              ></svg-icon\n                              ><!----></span\n                            ><span _ngcontent-oiu-c2=\"\" class=\"label-menu\"\n                              >Team</span\n                            ></a\n                          >\n                        </li>\n                        <!----><!----><!---->\n                        <li _ngcontent-oiu-c2=\"\" class=\"ng-star-inserted\">\n                          <a\n                            _ngcontent-oiu-c2=\"\"\n                            class=\"menu-item current-active\"\n                            routerlinkactive=\"current-active\"\n                            href=\"/team-appraisal/pending\"\n                            ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\"\n                              ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                                _ngcontent-oiu-c2=\"\"\n                                src=\"assets/img/icon/filled.svg\"\n                                class=\"ng-star-inserted\"\n                              ></svg-icon\n                              ><!----></span\n                            ><span _ngcontent-oiu-c2=\"\" class=\"label-menu\"\n                              >Pending</span\n                            ></a\n                          >\n                        </li>\n                        <!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!---->\n                      </ul>\n                    </div>\n                    <!---->\n                  </li>\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/my-appraisal\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        My Appraisals </span\n                      ><!----><!----></a\n                    ><!----><!----><!----><!---->\n                  </li>\n                  <!----><!----><!----><!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/my-feedbacks\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        My Feedbacks </span\n                      ><!----><!----></a\n                    ><!----><!----><!----><!---->\n                  </li>\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/my-position\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        My Position </span\n                      ><!----><!----></a\n                    ><!----><!----><!----><!---->\n                  </li>\n                  <!----><!----><!---->\n                  <li\n                    _ngcontent-oiu-c2=\"\"\n                    class=\"first-level-menu ng-star-inserted\"\n                  >\n                    <a\n                      _ngcontent-oiu-c2=\"\"\n                      class=\"menu-item\"\n                      routerlinkactive=\"current-focus\"\n                      href=\"/my-competencies\"\n                      ><span _ngcontent-oiu-c2=\"\" class=\"icon-menu\" title=\"\"\n                        ><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><svg-icon\n                          _ngcontent-oiu-c2=\"\"\n                          src=\"assets/img/icon/filled.svg\"\n                          class=\"ng-star-inserted\"\n                        ></svg-icon\n                        ><!----></span\n                      ><!----><!----><span\n                        _ngcontent-oiu-c2=\"\"\n                        class=\"label-menu ng-star-inserted\"\n                      >\n                        My Competencies </span\n                      ><!----><!----></a\n                    ><!----><!----><!----><!---->\n                  </li>\n                  <!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!----><!---->\n                </ul>\n              </div>\n            </div>\n            <!----></app-side-bar\n          >\n        </div>\n        <div _ngcontent-oiu-c0=\"\" class=\"breadcrumb-bar\">\n          <app-breadcrumb _ngcontent-oiu-c0=\"\" _nghost-oiu-c3=\"\"\n            ><div\n              _ngcontent-oiu-c3=\"\"\n              class=\"breadcrumb-container container-block pb-0\"\n            >\n              <a _ngcontent-oiu-c3=\"\" class=\"breadcrumb-path previous\" href=\"/\"\n                >Home</a\n              ><span _ngcontent-oiu-c3=\"\" class=\"breadcrumb-path px-13-px\"\n                ><i\n                  _ngcontent-oiu-c3=\"\"\n                  nz-icon=\"\"\n                  nztheme=\"fill\"\n                  nztype=\"caret-right\"\n                  class=\"anticon anticon-caret-right\"\n                ></i></span\n              ><!----><!----><!----><!----><!----><!----><a\n                _ngcontent-oiu-c3=\"\"\n                class=\"text-capitalize breadcrumb-path ng-star-inserted\"\n                href=\"/team-appraisal\"\n                >Team Appraisal</a\n              ><span\n                _ngcontent-oiu-c3=\"\"\n                class=\"breadcrumb-path px-13-px ng-star-inserted\"\n                ><i\n                  _ngcontent-oiu-c3=\"\"\n                  nz-icon=\"\"\n                  nztheme=\"fill\"\n                  nztype=\"caret-right\"\n                  class=\"anticon anticon-caret-right\"\n                ></i></span\n              ><!----><!----><!----><span\n                _ngcontent-oiu-c3=\"\"\n                class=\"text-capitalize breadcrumb-path current-path ng-star-inserted\"\n                >Pending</span\n              ><!----><!----><!----><!----><!---->\n            </div>\n            <!----></app-breadcrumb\n          >\n        </div>\n        <div _ngcontent-oiu-c0=\"\" class=\"main-content\">\n          <div _ngcontent-oiu-c0=\"\" class=\"container-block\">\n            <div\n              _ngcontent-oiu-c0=\"\"\n              class=\"inner-main-content inner-layout-container\"\n            >\n              <router-outlet _ngcontent-oiu-c0=\"\"></router-outlet\n              ><app-pending-appraisal _nghost-oiu-c6=\"\" class=\"ng-star-inserted\"\n                ><div _ngcontent-oiu-c6=\"\">\n                  <div\n                    _ngcontent-oiu-c6=\"\"\n                    class=\"header-layout-container p-20px d-flex align-self-center\"\n                  >\n                    <h3 _ngcontent-oiu-c6=\"\" class=\"header-text m-0\">\n                      Pending Appraisals\n                    </h3>\n                  </div>\n                  <!---->\n                  <div _ngcontent-oiu-c6=\"\" class=\"p-20px ng-star-inserted\">\n                    <!---->\n                    <div _ngcontent-oiu-c6=\"\" class=\"clearfix ng-star-inserted\">\n                      <p\n                        _ngcontent-oiu-c6=\"\"\n                        class=\"mb-2 mt-0 py-2 px-2 text-white people bg-info d-inline-block float-right notice-text\"\n                      >\n                        <i\n                          _ngcontent-oiu-c6=\"\"\n                          nz-icon=\"\"\n                          nztheme=\"outline\"\n                          nztype=\"info-circle\"\n                          class=\"anticon anticon-info-circle\"\n                        ></i\n                        > \n                        <em _ngcontent-oiu-c6=\"\"\n                          >The system collects colleagues who work together in a\n                          project, if you have ever not worked directly with any\n                          Reviewee then you are able to select 'Cannot\n                          review'.</em\n                        >\n                      </p>\n                    </div>\n                    <!----><!----><!---->\n                  </div>\n                  <!---->\n                </div></app-pending-appraisal\n              >\n            </div>\n          </div>\n        </div>\n      </div>\n      <!----></app-root\n    >\n\n    <div class=\"cdk-overlay-container\">\n      <div>\n        <div id=\"cdk-overlay-0\" class=\"cdk-overlay-pane\" style=\"z-index: 1010\">\n          <nz-notification-container\n            ><div\n              class=\"ant-notification ant-notification-topRight\"\n              style=\"top: 24px; right: 0px\"\n            >\n              <!---->\n            </div></nz-notification-container\n          >\n        </div>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-121-spa-app.md",
    "content": "![](assets/img/icon/user.svg)\n\n![STS Appraisal logo](assets/img/icon/sts.svg)\n\n- [Team Appraisal](/team-appraisal)\n\n  * [Team](/team-appraisal/team)\n  * [Pending](/team-appraisal/pending)\n\n- [My Appraisals](/my-appraisal)\n- [My Feedbacks](/my-feedbacks)\n- [My Position](/my-position)\n- [My Competencies](/my-competencies)\n\n[Home](/) > [Team Appraisal](/team-appraisal) > Pending\n\n### Pending Appraisals\n\n*The system collects colleagues who work together in a\n project, if you have ever not worked directly with any\n Reviewee then you are able to select 'Cannot\n review'.*\n"
  },
  {
    "path": "test_documents/html/issues/gh-127-issue.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<html>\n        <head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n            <title>\n                MW643 - كريب نص كلوش | Aya\n            </title>\n        </head>\n        <body>\n            <div class=\"we-app-block\"></div>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n    <noscript></noscript>\n    <template id=\"drawer-default-template\">\n  <div part=\"base\">\n    <div part=\"overlay\"></div>\n\n    <div part=\"content\">\n\n\n      <div part=\"body\">\n        <slot></slot>\n      </div>\n\n\n    </div>\n  </div>\n</template><template id=\"modal-default-template\">\n  <div part=\"base\">\n    <div part=\"overlay\"></div>\n\n    <div part=\"content\">\n\n\n      <div part=\"body\">\n        <slot></slot>\n      </div>\n    </div>\n  </div>\n</template><template id=\"popover-default-template\">\n  <div part=\"base\">\n    <div part=\"overlay\"></div>\n\n    <div part=\"content\">\n\n\n      <div part=\"body\">\n        <slot></slot>\n      </div>\n    </div>\n  </div>\n</template><template id=\"header-search-default-template\">\n  <div part=\"base\">\n    <div part=\"overlay\"></div>\n\n    <div part=\"content\">\n      <slot></slot>\n    </div>\n  </div>\n</template><template id=\"video-media-default-template\">\n  <slot></slot>\n\n\n</template><loading-bar class=\"loading-bar\" aria-hidden=\"true\"></loading-bar>\n    <a href=\"https://www.aya.app/products/mw643-%D9%83%D8%B1%D9%8A%D8%A8-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4#main\" allow-hash-change=\"\" class=\"skip-to-content sr-only\">التخطي إلى المحتوى</a>\n\n    <span id=\"header-scroll-tracker\" style=\"position: absolute; width: 1px; height: 1px; top: var(--header-scroll-tracker-offset, 10px); left: 0;\"></span>\n<aside id=\"shopify-section-sections--20550695944412__announcement-bar\" class=\"shopify-section shopify-section-group-header-group shopify-section--announcement-bar\">\n\n  <height-observer variable=\"announcement-bar\">\n    <div class=\"announcement-bar color-scheme color-scheme--scheme-3\"><announcement-bar-carousel allow-swipe=\"\" autoplay=\"5\" id=\"carousel-sections--20550695944412__announcement-bar\" class=\"announcement-bar__carousel\"><p class=\"prose heading is-selected\">اطلبي من تطبيق آيـا واستمتعي بـ 50% كاش باك على طلبك الاول. <a href=\"https://mawsim.go.link?adj_t=1ka8521o\" target=\"_blank\" title=\"تحميل تطبيق آيا للعبايات\">تحميل الان</a></p></announcement-bar-carousel></div>\n  </height-observer>\n\n  </aside>\n\n<section id=\"shopify-section-sections--20550697091292__cart-drawer\" class=\"shopify-section shopify-section-group-overlay-group shopify-section--cart-drawer\">\n\n</section>\n<main id=\"main\" class=\"anchor\">\n      <section id=\"shopify-section-template--20550701482204__main\" class=\"shopify-section shopify-section--main-product\">\n\n\n<div id=\"imgModal\" class=\"modal tabby-modal\">\n  <div class=\"modal-text\">\n    <span class=\"close\">×</span>\n    <h3>50% كاش باك</h3>\n    <span class=\"subtitle\">بحد أقصى 100</span>\n    <span class=\"subtitle2\">تطبق الشروط والاحكام:</span>\n\n    <ul>\n      <li>تنتهي صلاحية الكاش باك خلال فترة 60 يوم.</li>\n      <li>يتم الحصول الحصول علي الكاش باك بعد 14 يوم من تاريخ التوصيل</li>\n      <li>يكون الكاش باك عبارة عن رصيد يتم اضافتة علي محفظتك بتطبيق Aya ويتم استخدامه بدون اي حد ادني للانفاق</li>\n    </ul>\n  </div>\n</div>\n<div class=\"section-spacing section-spacing--tight color-scheme color-scheme--scheme-1 color-scheme--bg-609ecfcfee2f667ac6c12366fc6ece56\">\n  <div class=\"container container--lg\">\n    <product-rerender id=\"product-info-8846749008092-template--20550701482204__main\" observe-form=\"product-form-main-8846749008092-template--20550701482204__main\" allow-partial-rerender=\"\">\n      <div class=\"product\">\n\n<product-gallery class=\"product-gallery\" form=\"product-form-main-8846749008092-template--20550701482204__main\" filtered-indexes=\"[]\" allow-zoom=\"3\"><open-lightbox-button class=\"contents\">\n      <button class=\"product-gallery__zoom-button circle-button circle-button--sm md:hidden\">\n        <span class=\"sr-only\">تكبير</span></button>\n    </open-lightbox-button><div class=\"product-gallery__image-list\">\n\n    <div class=\"contents\"><scroll-carousel adaptive-height=\"\" id=\"product-gallery-carousel-8846749008092-template--20550701482204__main\" class=\"product-gallery__carousel scroll-area full-bleed md:unbleed is-scrollable\" role=\"region\"><div class=\"product-gallery__media snap-center is-initial\" data-media-type=\"image\" data-media-id=\"34368799015132\" role=\"group\" aria-label=\"العنصر 1 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800063708\" role=\"group\" aria-label=\"العنصر 2 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368801767644\" role=\"group\" aria-label=\"العنصر 3 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800129244\" role=\"group\" aria-label=\"العنصر 4 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800194780\" role=\"group\" aria-label=\"العنصر 5 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800227548\" role=\"group\" aria-label=\"العنصر 6 من 6\">\n<div class=\"media-wrapper\"></div>\n            </div></scroll-carousel></div>\n</div><safe-sticky class=\"product-gallery__thumbnail-list hidden md:block\" style=\"top: 56px;\">\n        <product-gallery-navigation align-selected=\"\" aria-controls=\"product-gallery-carousel-8846749008092-template--20550701482204__main\" class=\"product-gallery__thumbnail-scroller bleed md:unbleed\"><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"1\" data-media-id=\"34368799015132\" aria-current=\"true\" aria-label=\"الانتقال إلى العنصر 1\">\n              </button><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"2\" data-media-id=\"34368800063708\" aria-current=\"false\" aria-label=\"الانتقال إلى العنصر 2\">\n              </button><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"3\" data-media-id=\"34368801767644\" aria-current=\"false\" aria-label=\"الانتقال إلى العنصر 3\">\n              </button><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"4\" data-media-id=\"34368800129244\" aria-current=\"false\" aria-label=\"الانتقال إلى العنصر 4\">\n              </button><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"5\" data-media-id=\"34368800194780\" aria-current=\"false\" aria-label=\"الانتقال إلى العنصر 5\">\n              </button><button type=\"button\" class=\"product-gallery__thumbnail\" data-media-type=\"image\" data-media-position=\"6\" data-media-id=\"34368800227548\" aria-current=\"false\" aria-label=\"الانتقال إلى العنصر 6\">\n              </button></product-gallery-navigation>\n      </safe-sticky><carousel-navigation class=\"page-dots align-self-center  md:hidden\" aria-controls=\"product-gallery-carousel-8846749008092-template--20550701482204__main\"><button type=\"button\" class=\"tap-area\" aria-current=\"true\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 1</span>\n            </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 2</span>\n            </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 3</span>\n            </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 4</span>\n            </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 5</span>\n            </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n              <span class=\"sr-only\">الانتقال إلى العنصر 6</span>\n            </button></carousel-navigation></product-gallery>\n\n<div id=\"reviews-container\">\n\n  <div id=\"reviews-scroll-wrapper\" style=\"max-height: 600px; overflow-y: auto; \"></div>\n  <div id=\"load-more-wrapper\" style=\"text-align: center; margin-top: 10px;\"></div>\n</div>\n\n<div id=\"review-wrapper\" data-product-id=\"8846749008092\"></div>\n\n\n<safe-sticky class=\"product-info \" style=\"top: -315px;\">\n  <div class=\"product-info__block-list\">\n<div class=\"product-info__block-item\" data-block-id=\"liquid_knNQTL\" data-block-type=\"liquid\"><div class=\"liquid\">\n<div class=\"badges-wrap mobile-only\">\n<div class=\"product-cashback-tag modal-img\">\n  <span class=\"icon-tag\">\n\n  </span>\n  <span class=\"cashback-text\">\n    50% كاش\nباك على طلبك الأول\n  </span>\n\n</div>\n\n\n</div>\n\n\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"vendor\" data-block-type=\"vendor\"><a href=\"https://www.aya.app/collections/%D8%AA%D9%88%D9%84\" class=\"vendor h6 link-faded\">تول</a></div>\n<div class=\"product-info__block-item\" data-block-id=\"title\" data-block-type=\"title\"><h1 class=\"product-title h3\">MW643 - كريب نص كلوش</h1></div>\n<div class=\"product-info__block-item\" data-block-id=\"price\" data-block-type=\"price\"><div class=\"v-stack\">\n<price-list class=\"price-list price-list--product\"><sale-price class=\"h4 text-on-sale\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(62, 86, 38); font-size: 18px; font-style: normal; font-weight: 400; text-decoration: rgb(62, 86, 38);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 18px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price><compare-at-price class=\"h5 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">350.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><s class=\"currency-converter-amount cbb-price-currency-USD Price--compareAt\" style='display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;'><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">93.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></s></span></span></compare-at-price></price-list><p class=\"text-sm text-subdued\">شامل الضريبة.\n<a href=\"https://www.aya.app/policies/shipping-policy\" class=\"link\">يتم حساب رسوم الضرائب</a> عند إتمام الطلب\n</p>\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_rEXMpf\" data-block-type=\"liquid\"><div class=\"liquid\">\n<div class=\"badges-wrap desktop-only\">\n<div class=\"product-cashback-tag modal-img\">\n  <span class=\"icon-tag\">\n\n  </span>\n  <span class=\"cashback-text\">\n    50% كاش\nباك على طلبك الأول\n  </span>\n\n</div>\n\n\n</div>\n\n\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_VgBEgT\" data-block-type=\"liquid\"><div class=\"liquid\"><div class=\"tabby-wrap\">\n  <div class=\"pay-wraps\">\n    <div class=\"pay-icons\"></div>\n     <div class=\"pay-icons\">\n</div>\n  </div>\n  <div class=\"tabby-title\">ادفع على 4 دفعات بدون فوائد بقيمة 139.75</div>\n</div></div></div>\n<div class=\"product-info__block-item\" data-block-id=\"separator_FHTUJH\" data-block-type=\"separator\"><hr></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_6T4Cgj\" data-block-type=\"liquid\"></div>\n<div class=\"product-info__block-item\" data-block-id=\"variant_picker\" data-block-type=\"variant-picker\"><variant-picker class=\"variant-picker v-stack gap-4\" section-id=\"template--20550701482204__main\" form-id=\"product-form-main-8846749008092-template--20550701482204__main\" context=\"main_product\" handle=\"mw643-كريب-نص-كلوش\" update-url=\"\">\n<fieldset class=\"variant-picker__option v-stack gap-2\">\n        <div class=\"variant-picker__option-info h-stack justify-between gap-2\">\n          <div class=\"h-stack gap-1\">\n            <legend>\n\n               المقاس\n\n\n          </legend>\n</div>\n</div>\n<div class=\"variant-picker__option-values h-stack gap-2.5 wrap\">\n            <input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-1-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266949852\" value=\"3294266949852\" form=\"product-form-main-8846749008092-template--20550701482204__main\" checked data-option-position=\"1\"><label class=\"block-swatch  \" for=\"option-value-1-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266949852\"><span>50</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-2-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3208631189724\" value=\"3208631189724\" form=\"product-form-main-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-2-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3208631189724\"><span>52</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-3-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-2708979974364\" value=\"2708979974364\" form=\"product-form-main-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-3-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-2708979974364\"><span>54</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-4-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3208631714012\" value=\"3208631714012\" form=\"product-form-main-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-4-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3208631714012\"><span>56</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-5-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266065116\" value=\"3294266065116\" form=\"product-form-main-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-5-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266065116\"><span>58</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-main-8846749008092-template--20550701482204__main-option1\" id=\"option-value-6-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266327260\" value=\"3294266327260\" form=\"product-form-main-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-6-template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-option1-3294266327260\"><span>60</span>\n    </label>\n          </div>\n</fieldset>\n<noscript><div class=\"form-control\">\n<select id=\"select--template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-id\" class=\"select\" name=\"id\" form=\"product-form-main-8846749008092-template--20550701482204__main\"><option selected value=\"45986598420700\">50 / Black - <span class=\"money\">138.26 SR</span>\n</option>\n<option disabled value=\"45986598453468\">52 / Black - <span class=\"money\">138.26 SR</span>\n</option>\n<option disabled value=\"45986598486236\">54 / Black - <span class=\"money\">138.26 SR</span>\n</option>\n<option disabled value=\"45986598519004\">56 / Black - <span class=\"money\">138.26 SR</span>\n</option>\n<option disabled value=\"45986598551772\">58 / Black - <span class=\"money\">138.26 SR</span>\n</option>\n<option disabled value=\"45986598584540\">60 / Black - <span class=\"money\">138.26 SR</span>\n</option></select><svg aria-hidden=\"true\" focusable=\"false\" fill=\"none\" width=\"10\" class=\"icon icon-dropdown-chevron\" viewbox=\"0 0 10 6\">\n      <path d=\"m1 1 4 4 4-4\" stroke=\"currentColor\" stroke-linecap=\"square\"></path>\n    </svg><label for=\"select--template--20550701482204__main-product-form-main-8846749008092-template--20550701482204__main-id\" class=\"floating-label text-xs\">متغير</label>\n</div></noscript></variant-picker></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_4UDKed\" data-block-type=\"liquid\"><div class=\"liquid\">\n<div class=\"product-size-info\" style=\"margin-bottom: 1rem;\">\n  <div style=\"display: flex; justify-content: space-between; align-items: center; background-color: #35353514; padding: 0.75rem;\">\n    <a href=\"javascript:void(0);\" style=\"color: #000; font-size: 16px; text-decoration: none; display: flex; gap: 5px; align-items: center; width: 100%; justify-content: space-between;\" onclick=\"toggleSizeChart();\">\n      <span style=\"display: flex; align-items: center; gap: 5px;\">\n\n\n          جدول القياسات\n\n      </span>\n      <span id=\"arrow-icon\" style=\"transition: transform 0.3s;\" class=\"arrow\">▼</span>\n    </a>\n  </div>\n\n\n  <div id=\"size-chart-collapse\" style=\"display: none; padding: 1rem; border-top: 1px solid #ccc;\">\n\n\n    <div class=\"buttons-wrap-size\">\n      <button onclick=\"showSizeImage('cm')\" style=\"padding: 0.5rem 1rem; background: #f1f1f1; border: none; cursor: pointer;\" id=\"tab-cm\" class=\"size-tab active\">\n          CM\n      </button>\n      <button onclick=\"showSizeImage('inches')\" style=\"padding: 0.5rem 1rem; background: #f1f1f1; border: none; cursor: pointer;\" id=\"tab-inches\" class=\"size-tab\">\n          INCHES\n      </button>\n    </div>\n\n\n\t<div id=\"image-cm\" class=\"size-image\" style=\"display: block;\">\n\n\n\n\t</div>\n\n\t<div id=\"image-inches\" class=\"size-image\" style=\"display: none;\">\n\n\n\n\t</div>\n\n  </div>\n</div>\n\n\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"buy_buttons\" data-block-type=\"buy-buttons\"><product-form><form method=\"post\" action=\"/cart/add\" id=\"product-form-main-8846749008092-template--20550701482204__main\" accept-charset=\"UTF-8\" class=\"shopify-product-form\" enctype=\"multipart/form-data\">\n<input type=\"hidden\" name=\"form_type\" value=\"product\"><input type=\"hidden\" name=\"utf8\" value=\"✓\"><input type=\"hidden\" name=\"id\" value=\"45986598420700\">\n\n\n\n      <div class=\"v-stack gap-4\">\n<buy-buttons class=\"buy-buttons \" form=\"product-form-main-8846749008092-template--20550701482204__main\">\n<button type=\"submit\" class=\"button w-full\">إضافة إلى السلة</button></buy-buttons>\n      </div>\n<input type=\"hidden\" name=\"product-id\" value=\"8846749008092\"><input type=\"hidden\" name=\"section-id\" value=\"template--20550701482204__main\">\n</form></product-form></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_dUV6Qj\" data-block-type=\"liquid\"><div class=\"liquid\"><div class=\"payemnt-options\">\n\n\n</div></div></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_q7cC3w\" data-block-type=\"liquid\"><div class=\"liquid\">\n<div class=\"accordian-wrap\">\n\n\n  <div class=\"accordion-item static-shipping\">\n    <span>\n\n        الشحن\n\n    </span>\n    <div class=\"accordion-content visible\">\n\n\n<div class=\"custom-edd-box\" id=\"custom-delivery-box\" style=\"visibility: visible;\">\n  <div class=\"dynamic-line\" id=\"custom-delivery-message\">\n\n\n\n\n     <span id=\"custom-delivery-text\" class=\"arabic-edd\">اطلب الآن للحصول على توصيل سريع لتصلك في 1-3 أيام إلى <span class=\"city-name\">miami</span></span>\n\n\n\n  </div>\n</div>\n\n\n\n    </div>\n  </div>\n\n\n  <div class=\"accordion-container\">\n\n    <div class=\"accordion-item\" data-accordion-initialized=\"true\">\n      <span class=\"accordion-header\">\n\n          تفاصيل المنتج\n\n      </span>\n      <span class=\"accordion-toggle\"></span>\n      <div class=\"accordion-content\">\n        <div>القماش: كريب خفيف واقف</div>\n        <div>القصة: نص كلوش</div>\n        <div>مدة التنفيذ: 4-8 ايام عمل</div>\n        <div>المرفقات: طرحة</div>\n      </div>\n    </div>\n\n    <div class=\"accordion-item\" data-accordion-initialized=\"true\">\n      <span class=\"accordion-header\">\n\n          سياسة الاستبدال والاسترجاع\n\n      </span>\n      <span class=\"accordion-toggle\"></span>\n      <div class=\"accordion-content\">\n\n\n\n\n\n\n<div id=\"return-policy-box\" class=\"return-policy\">\n\n\n\n  <p id=\"return-policy-message\">الإرجاع والاستبدال متاح حاليًا داخل السعودية فقط – وقريبًا في باقي دول الخليج!</p>\n\n  <div class=\"return_policy_link\" style=\"display:none\">\n\n        <a href=\"https://www.aya.app/policies/refund-policy\">سياسة الاستبدال والاسترجاع</a>\n\n  </div>\n\n\n</div>\n\n\n\n\n\n\n\n\n      </div>\n    </div>\n\n    <div class=\"accordion-item\" data-accordion-initialized=\"true\">\n      <span class=\"accordion-header\">\n\n          تعليمات الغسيل والعناية\n\n      </span>\n      <span class=\"accordion-toggle\"></span>\n      <div class=\"accordion-content\">\n        غسيل عادي\n      </div>\n    </div>\n\n  </div>\n</div>\n\n\n\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"liquid_778BPT\" data-block-type=\"liquid\"><div class=\"liquid\">\n  <h3 class=\"carusel-title\" style=\"text-align: right; margin: 20px 0;\">كملي ستايلك</h3>\n  <div id=\"cart-success-message\" style=\"display: none; background-color: #dff0d8; color: #3c763d; padding: 10px 15px; margin-bottom: 10px; width: 100%; border-radius: 4px; text-align: right; direction: rtl;\">\n    تمت الإضافة إلى سلة التسوق!\n  </div>\n<div class=\"simple-carousel-wrapper\" dir=\"true\">\n  <div class=\"simple-carousel\" id=\"limited-carousel\" data-rtl=\"true\" style=\"transition: none; transform: translateX(0px);\">\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46528092242140\">\n            <a href=\"https://www.aya.app/products/mwd04-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD04 -  فستان عباية اسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46606450852060\">\n            <a href=\"https://www.aya.app/products/mwd08-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD08 -  طقم من قطعتين باللون الابيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46606459437276\">\n            <a href=\"https://www.aya.app/products/mwd09-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD09 -  طقم من قطعتين باللون الاسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46603511070940\">\n            <a href=\"https://www.aya.app/products/mwd06-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD06 -  فستان عباية مطاطي باللون الاسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46527946326236\">\n            <a href=\"https://www.aya.app/products/mwd01-%D9%81%D8%B3%D8%AA%D8%A7%D9%86\">\n\n              <h3>MWD01 -  فستان عباية كحلى</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46606449705180\">\n            <a href=\"https://www.aya.app/products/mwd07-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD07 -  فستان عباية مطاطي باللون الابيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46528087261404\">\n            <a href=\"https://www.aya.app/products/mwd03-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%86%D9%8A\">\n\n              <h3>MWD03 -  فستان عباية بني</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46528149192924\">\n            <a href=\"https://www.aya.app/products/mwd05-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B1%D9%85%D8%A7%D8%AF%D9%8A\">\n\n              <h3>MWD05 -  فستان عباية رمادي</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46528072909020\">\n            <a href=\"https://www.aya.app/products/mwd02-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%86%D8%A8%D9%8A%D8%AA%D9%8A\">\n\n              <h3>MWD02 -  فستان عباية عنابي</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46813095690460\">\n            <a href=\"https://www.aya.app/products/mwd11-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD11 -  فستان عبايه اسود منقط بأبيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n          <div class=\"carousel-slide carousel-product\" data-variant-id=\"46813091561692\">\n            <a href=\"https://www.aya.app/products/mwd10-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%A8%D9%8A%D8%B6-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD10 -  فستان عبايه ابيض منقط بأسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n\n\n  <div class=\"carousel-slide carousel-product\" data-variant-id=\"46528092242140\">\n            <a href=\"https://www.aya.app/products/mwd04-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD04 -  فستان عباية اسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46606450852060\">\n            <a href=\"https://www.aya.app/products/mwd08-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD08 -  طقم من قطعتين باللون الابيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46606459437276\">\n            <a href=\"https://www.aya.app/products/mwd09-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD09 -  طقم من قطعتين باللون الاسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46603511070940\">\n            <a href=\"https://www.aya.app/products/mwd06-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD06 -  فستان عباية مطاطي باللون الاسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46527946326236\">\n            <a href=\"https://www.aya.app/products/mwd01-%D9%81%D8%B3%D8%AA%D8%A7%D9%86\">\n\n              <h3>MWD01 -  فستان عباية كحلى</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46606449705180\">\n            <a href=\"https://www.aya.app/products/mwd07-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD07 -  فستان عباية مطاطي باللون الابيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46528087261404\">\n            <a href=\"https://www.aya.app/products/mwd03-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%86%D9%8A\">\n\n              <h3>MWD03 -  فستان عباية بني</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46528149192924\">\n            <a href=\"https://www.aya.app/products/mwd05-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B1%D9%85%D8%A7%D8%AF%D9%8A\">\n\n              <h3>MWD05 -  فستان عباية رمادي</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46528072909020\">\n            <a href=\"https://www.aya.app/products/mwd02-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%86%D8%A8%D9%8A%D8%AA%D9%8A\">\n\n              <h3>MWD02 -  فستان عباية عنابي</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46813095690460\">\n            <a href=\"https://www.aya.app/products/mwd11-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%A8%D9%8A%D8%B6\">\n\n              <h3>MWD11 -  فستان عبايه اسود منقط بأبيض</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n<div class=\"carousel-slide carousel-product\" data-variant-id=\"46813091561692\">\n            <a href=\"https://www.aya.app/products/mwd10-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%A8%D9%8A%D8%B6-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%B3%D9%88%D8%AF\">\n\n              <h3>MWD10 -  فستان عبايه ابيض منقط بأسود</h3>\n\n              <p><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 14px; font-style: normal; font-weight: 400; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: Nunito, sans-serif; font-size: 14px; font-weight: 400; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></p>\n            </a>\n            <button class=\"carousel-add-to-cart\"></button>\n          </div>\n</div>\n  <button class=\"carousel-nav prev\" style=\"display: block;\">‹</button>\n  <button class=\"carousel-nav next\" style=\"display: block;\">›</button>\n</div>\n\n\n\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"payment_terms\" data-block-type=\"payment-terms\"><payment-terms class=\"payment-terms\"><form method=\"post\" action=\"/cart/add\" id=\"product-form-main-8846749008092-template--20550701482204__main-payment-installment\" accept-charset=\"UTF-8\" class=\"shopify-product-form\" enctype=\"multipart/form-data\">\n<input type=\"hidden\" name=\"form_type\" value=\"product\"><input type=\"hidden\" name=\"utf8\" value=\"✓\"><input type=\"hidden\" name=\"id\" value=\"45986598420700\"><input type=\"hidden\" name=\"product-id\" value=\"8846749008092\"><input type=\"hidden\" name=\"section-id\" value=\"template--20550701482204__main\">\n</form></payment-terms></div>\n<div class=\"product-info__block-item\" data-block-id=\"AbGdoM0hrMitjYisrO__selleasy_lb_upsell_widget_ba_GW6r9y-1\" data-block-type=\"@app\"><div id=\"shopify-block-AbGdoM0hrMitjYisrO__selleasy_lb_upsell_widget_ba_GW6r9y\" class=\"shopify-block shopify-app-block lb-widget-ba\"></div></div>\n</div></safe-sticky><div id=\"product-extra-information\" class=\"product-content-below-gallery empty:hidden scroll-margin-offset\"></div>\n</div>\n    </product-rerender>\n  </div>\n</div>\n<template id=\"quick-buy-content\">\n  <p class=\"h5\" slot=\"header\">حدِّد الخيارات</p>\n\n  <div class=\"quick-buy-modal__content\">\n    <product-rerender id=\"quick-buy-modal-content\" observe-form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\">\n      <dialog-close-button class=\"contents\">\n        <button type=\"button\" class=\"quick-buy-modal__close-button sm-max:hidden\">\n          <span class=\"sr-only\">إغلاق</span>\n\n  </button>\n      </dialog-close-button>\n\n      <div class=\"quick-buy-modal__gallery-wrapper\">\n\n<product-gallery class=\"product-gallery\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" filtered-indexes=\"[]\"><div class=\"product-gallery__image-list\"><div class=\"product-gallery__carousel-with-arrows\">\n<carousel-prev-button aria-controls=\"product-gallery-carousel-8846749008092-template--20550701482204__main\" class=\"contents\">\n                    <button type=\"button\" class=\"tap-area sm:hidden\">\n                        <span class=\"sr-only\">السابق</span></button>\n                </carousel-prev-button><scroll-carousel adaptive-height=\"\" id=\"product-gallery-carousel-8846749008092-template--20550701482204__main\" class=\"product-gallery__carousel scroll-area \" role=\"region\"><div class=\"product-gallery__media snap-center is-initial\" data-media-type=\"image\" data-media-id=\"34368799015132\" role=\"group\" aria-label=\"العنصر 1 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800063708\" role=\"group\" aria-label=\"العنصر 2 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368801767644\" role=\"group\" aria-label=\"العنصر 3 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800129244\" role=\"group\" aria-label=\"العنصر 4 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800194780\" role=\"group\" aria-label=\"العنصر 5 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div>\n<div class=\"product-gallery__media snap-center \" data-media-type=\"image\" data-media-id=\"34368800227548\" role=\"group\" aria-label=\"العنصر 6 من 6\">\n<div class=\"media-wrapper\"></div>\n                        </div></scroll-carousel><carousel-next-button aria-controls=\"product-gallery-carousel-8846749008092-template--20550701482204__main\" class=\"contents\">\n                    <button type=\"button\" class=\"tap-area sm:hidden\">\n                        <span class=\"sr-only\">التالي</span></button>\n                </carousel-next-button>\n</div></div><carousel-navigation class=\"page-dots align-self-center md-max:hidden \" aria-controls=\"product-gallery-carousel-8846749008092-template--20550701482204__main\"><button type=\"button\" class=\"tap-area\" aria-current=\"true\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 1</span>\n                        </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 2</span>\n                        </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 3</span>\n                        </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 4</span>\n                        </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 5</span>\n                        </button><button type=\"button\" class=\"tap-area\" aria-current=\"false\">\n                            <span class=\"sr-only\">الانتقال إلى العنصر 6</span>\n                        </button></carousel-navigation></product-gallery>\n\n<div class=\"quick-buy-modal__mobile-info v-stack gap-1 justify-center text-center sm:hidden\">\n          <a href=\"https://www.aya.app/products/mw643-%D9%83%D8%B1%D9%8A%D8%A8-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-title h6\">MW643 - كريب نص كلوش</a><price-list class=\"price-list \"><sale-price class=\"h6 text-on-sale\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money\">159.00 SAR</span>\n</sale-price><compare-at-price class=\"h6 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money\">350.00 SAR</span></compare-at-price></price-list>\n</div>\n      </div>\n\n      <div class=\"quick-buy-modal__info-wrapper\">\n<safe-sticky class=\"product-info \">\n  <div class=\"product-info__block-list\">\n<div class=\"product-info__block-item\" data-block-id=\"vendor\" data-block-type=\"vendor\"><a href=\"https://www.aya.app/collections/%D8%AA%D9%88%D9%84\" class=\"vendor h6 link-faded\">تول</a></div>\n<div class=\"product-info__block-item\" data-block-id=\"title\" data-block-type=\"title\"><h2 class=\"product-title h3\">\n                <a href=\"https://www.aya.app/products/mw643-%D9%83%D8%B1%D9%8A%D8%A8-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">MW643 - كريب نص كلوش</a>\n              </h2></div>\n<div class=\"product-info__block-item\" data-block-id=\"price\" data-block-type=\"price\"><div class=\"v-stack\">\n<price-list class=\"price-list price-list--product\"><sale-price class=\"h4 text-on-sale\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money\">159.00 SAR</span>\n</sale-price><compare-at-price class=\"h5 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money\">350.00 SAR</span></compare-at-price></price-list><p class=\"text-sm text-subdued\">شامل الضريبة.\n<a href=\"https://www.aya.app/policies/shipping-policy\" class=\"link\">يتم حساب رسوم الضرائب</a> عند إتمام الطلب\n</p>\n</div></div>\n<div class=\"product-info__block-item\" data-block-id=\"separator_FHTUJH\" data-block-type=\"separator\"><hr></div>\n<div class=\"product-info__block-item\" data-block-id=\"variant_picker\" data-block-type=\"variant-picker\"><variant-picker class=\"variant-picker v-stack gap-4\" section-id=\"template--20550701482204__main\" form-id=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" context=\"quick_buy\" handle=\"mw643-كريب-نص-كلوش\">\n<fieldset class=\"variant-picker__option v-stack gap-2\">\n        <div class=\"variant-picker__option-info h-stack justify-between gap-2\">\n          <div class=\"h-stack gap-1\">\n            <legend>\n\n               المقاس\n\n\n          </legend>\n</div>\n</div>\n<div class=\"variant-picker__option-values h-stack gap-2.5 wrap\">\n            <input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-1-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266949852\" value=\"3294266949852\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" checked data-option-position=\"1\"><label class=\"block-swatch  \" for=\"option-value-1-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266949852\"><span>50</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-2-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3208631189724\" value=\"3208631189724\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-2-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3208631189724\"><span>52</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-3-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-2708979974364\" value=\"2708979974364\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-3-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-2708979974364\"><span>54</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-4-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3208631714012\" value=\"3208631714012\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-4-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3208631714012\"><span>56</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-5-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266065116\" value=\"3294266065116\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-5-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266065116\"><span>58</span>\n    </label><input class=\"sr-only\" type=\"radio\" name=\"product-form-quick-buy-8846749008092-template--20550701482204__main-option1\" id=\"option-value-6-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266327260\" value=\"3294266327260\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" data-option-position=\"1\"><label class=\"block-swatch is-disabled \" for=\"option-value-6-template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-option1-3294266327260\"><span>60</span>\n    </label>\n          </div>\n</fieldset>\n<noscript>&lt;div class=\"form-control\" &gt;&lt;select id=\"select--template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-id\" class=\"select\" name=\"id\" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\"\n\n\n&gt;&lt;option selected=\"selected\"  value=\"45986598420700\"&gt;50 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;option  disabled=\"disabled\" value=\"45986598453468\"&gt;52 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;option  disabled=\"disabled\" value=\"45986598486236\"&gt;54 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;option  disabled=\"disabled\" value=\"45986598519004\"&gt;56 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;option  disabled=\"disabled\" value=\"45986598551772\"&gt;58 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;option  disabled=\"disabled\" value=\"45986598584540\"&gt;60 / Black - &lt;span class=money&gt;138.26 SR&lt;/span&gt;&lt;/option&gt;&lt;/select&gt;&lt;svg aria-hidden=\"true\" focusable=\"false\" fill=\"none\" width=\"10\" class=\"icon icon-dropdown-chevron\" viewBox=\"0 0 10 6\"&gt;\n      &lt;path d=\"m1 1 4 4 4-4\" stroke=\"currentColor\" stroke-linecap=\"square\"/&gt;\n    &lt;/svg&gt;&lt;label for=\"select--template--20550701482204__main-product-form-quick-buy-8846749008092-template--20550701482204__main-id\" class=\"floating-label text-xs\"&gt;متغير&lt;/label&gt;&lt;/div&gt;</noscript></variant-picker></div>\n<div class=\"product-info__block-item\" data-block-id=\"buy_buttons\" data-block-type=\"buy-buttons\"><product-form><form method=\"post\" action=\"/cart/add\" id=\"product-form-quick-buy-8846749008092-template--20550701482204__main\" accept-charset=\"UTF-8\" class=\"shopify-product-form\" enctype=\"multipart/form-data\">\n<input type=\"hidden\" name=\"form_type\" value=\"product\"><input type=\"hidden\" name=\"utf8\" value=\"✓\"><input type=\"hidden\" disabled name=\"id\" value=\"45986598420700\">\n\n\n\n      <div class=\"v-stack gap-4\">\n<buy-buttons class=\"buy-buttons \" form=\"product-form-quick-buy-8846749008092-template--20550701482204__main\">\n<button type=\"submit\" class=\"button w-full\">إضافة إلى السلة</button></buy-buttons>\n      </div>\n<input type=\"hidden\" name=\"product-id\" value=\"8846749008092\"><input type=\"hidden\" name=\"section-id\" value=\"template--20550701482204__main\">\n</form></product-form></div>\n<div class=\"product-info__block-item\" data-block-id=\"payment_terms\" data-block-type=\"payment-terms\"><payment-terms class=\"payment-terms\"><form method=\"post\" action=\"/cart/add\" id=\"product-form-quick-buy-8846749008092-template--20550701482204__main-payment-installment\" accept-charset=\"UTF-8\" class=\"shopify-product-form\" enctype=\"multipart/form-data\">\n<input type=\"hidden\" name=\"form_type\" value=\"product\"><input type=\"hidden\" name=\"utf8\" value=\"✓\"><input type=\"hidden\" name=\"id\" value=\"45986598420700\"><input type=\"hidden\" name=\"product-id\" value=\"8846749008092\"><input type=\"hidden\" name=\"section-id\" value=\"template--20550701482204__main\">\n</form></payment-terms></div>\n<div class=\"product-info__block-item\" data-block-id=\"AbGdoM0hrMitjYisrO__selleasy_lb_upsell_widget_ba_GW6r9y-2\" data-block-type=\"@app\"><div id=\"shopify-block-AbGdoM0hrMitjYisrO__selleasy_lb_upsell_widget_ba_GW6r9y-1\" class=\"shopify-block shopify-app-block lb-widget-ba\"></div></div>\n</div></safe-sticky><a href=\"https://www.aya.app/products/mw643-%D9%83%D8%B1%D9%8A%D8%A8-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"quick-buy-modal__view-more link sm-max:hidden\">عرض التفاصيل</a>\n      </div>\n    </product-rerender>\n  </div>\n</template>\n\n\n\n\n\n\n\n\n\n\n\n\n</section><div id=\"shopify-section-template--20550701482204__accordians_pdp_tqn3mn\" class=\"shopify-section\">\n</div>\n<div id=\"shopify-section-template--20550701482204__tabs_Y9Qz9E\" class=\"shopify-section shopify-section--tabs\"></div>\n<section id=\"shopify-section-template--20550701482204__algolia_recommend_CCQmjm\" class=\"shopify-section shopify-section--algolia-recommend\">\n<div class=\"section-spacing color-scheme color-scheme--scheme-1 tabs-containers color-scheme--bg-609ecfcfee2f667ac6c12366fc6ece56\">\n  <div class=\"container section-stack\">\n\n    <div class=\"tab-buttons\">\n\n\n      <button class=\"tab-btn active\" data-tab=\"tab2\">قد يعجبك أيضًا\n      </button>\n          <button class=\"tab-btn\" data-tab=\"tab1\">منتجات مشابهة\n      </button>\n    </div>\n\n    <div id=\"tab1\" class=\"tab-content\"><div id=\"algolia-related-items\" class=\"algolia-recommend-section\">\n<div class=\"section-header justify-self-center text-center\">\n              <div class=\"prose\">\n\n              </div>\n            </div>\n<product-list class=\"floating-controls-container floating-controls-container--inside floating-controls-container--on-hover\">\n              <carousel-prev-button class=\"floating-controls-container__control\" aria-controls=\"algolia-related-carousel-template--20550701482204__algolia_recommend_CCQmjm\">\n                <button type=\"button\" class=\"prev-next-button prev-next-button--prev circle-button hover:animate-icon-inline\" disabled>\n                  <span class=\"sr-only\">السابق</span></button>\n              </carousel-prev-button>\n\n              <scroll-carousel id=\"algolia-related-carousel-template--20550701482204__algolia_recommend_CCQmjm\" group-cells=\"\" allow-drag=\"\" class=\"algolia-recommend-carousel product-list product-list--carousel scroll-area bleed md:unbleed\">\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw841-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-selling_fast\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW841 - كريب واقف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1147-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1147 - كريب واقف بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw959-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%88-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW959 - كريب واقف و جاكار نص كلوش بليزر</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw966-%D9%83%D8%B1%D9%8A%D8%A8-%D9%85%D8%B9-%D9%85%D8%AE%D9%85%D9%84-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW966 - كريب مع مخمل ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1161-%D8%A7%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%AE%D9%81%D9%8A%D9%81-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1161 - انترنت تطريز خفيف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1024-%D9%85%D8%AE%D9%85%D9%84-%D9%85%D8%B9-%D9%83%D8%AA%D8%A7%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D9%85%D8%B9-%D9%8A%D8%A7%D9%82%D8%A9\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1024 - مخمل مع كتان نص كلوش مع ياقة</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">149.01 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">39.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1160-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%AE%D9%81%D9%8A%D9%81\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1160- حرير ياباني بليزر خفيف</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">350.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">93.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw983-%D8%B4%D8%A7%D9%85%D9%88%D8%A7%D9%87-%D8%B4%D8%AA%D9%88%D9%8A-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW983 - شامواه شتوي نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">189.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">50.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw849-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW849 - كريب واقف ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw645-%D9%83%D8%B1%D9%8A%D8%A8-%D8%AE%D9%81%D9%8A%D9%81-%D9%88%D8%A7%D9%82%D9%81-%D9%85%D8%B9-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-limited_edition\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW645 - كريب خفيف واقف مع جاكار نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1138-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%B4%D9%85%D9%88%D9%87-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1138  - حرير شموه بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1152-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1152 -حرير ياباني بليزر</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">218.99 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">58.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1231-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D9%83%D8%B1%D9%8A%D8%A8-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1231  -نص كلوش كريب ياباني</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">239.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">63.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1134-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%B4%D9%85%D9%88%D9%87-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1134 - حرير شموه بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1136-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%B4%D9%85%D9%88%D9%87-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1136  - حرير شموه بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw960-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-limited_edition\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW960 - كريب واقف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1217-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-selling_fast\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1217 -كريب واقف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1215-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1215 -كريب واقف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">169.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">45.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1320-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A3%D8%B3%D9%88%D8%AF-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%AC%D8%A7%D9%86%D8%A8%D9%8A-%D9%81%D8%A7%D8%AE%D8%B1\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1320  - بليزر أسود بتطريز جانبي فاخر</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">229.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">61.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1139-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%B4%D9%85%D9%88%D9%87-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1139  - حرير شموه بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1025-%D9%85%D8%AE%D9%85%D9%84-%D9%85%D8%B9-%D9%83%D8%AA%D8%A7%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D9%85%D8%B9-%D9%8A%D8%A7%D9%82%D8%A9\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1025 - مخمل مع كتان نص كلوش مع ياقة</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">149.01 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">39.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1251-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%85%D8%B9-%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1251 - كريب واقف مع دانتيل نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">239.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">63.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mwt1248-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%85%D8%B9-%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MWT1248  -  كريب واقف مع دانتيل نص كلوش  سوداء</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">199.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">53.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1315-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1315 - كريب بليزر</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">209.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">55.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1022-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D9%85%D8%B9-%D9%82%D9%8A%D8%B7%D8%A7%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1022 - حرير ياباني مع قيطان  نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">189.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">50.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1216-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D9%85%D8%B9-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1216 -كريب واقف مع جاكار نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">169.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">45.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1274-%D8%A7%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1274 - عباية سوداء فاحمة مع تطريز خطوط في الأمام و الكم</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">139.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">37.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/test-mw645-%D9%83%D8%B1%D9%8A%D8%A8-%D8%AE%D9%81%D9%8A%D9%81-%D9%88%D8%A7%D9%82%D9%81-%D9%85%D8%B9-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-copy\">\n              <div class=\"product-bages\">\n                <div class=\" badge-limited_edition\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>TEST-NGI</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">115.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">30.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n          </scroll-carousel>\n\n              <carousel-next-button class=\"floating-controls-container__control\" aria-controls=\"algolia-related-carousel-template--20550701482204__algolia_recommend_CCQmjm\">\n                <button type=\"button\" class=\"prev-next-button prev-next-button--next circle-button hover:animate-icon-inline\">\n                  <span class=\"sr-only\">التالي</span></button>\n              </carousel-next-button>\n            </product-list>\n</div></div>\n\n    <div id=\"tab2\" class=\"tab-content active\"><div id=\"algolia-bought-together\" class=\"algolia-recommend-section\">\n<div class=\"section-header justify-self-center text-center\">\n              <div class=\"prose\">\n\n              </div>\n            </div>\n<product-list class=\"floating-controls-container floating-controls-container--inside floating-controls-container--on-hover\">\n              <carousel-prev-button class=\"floating-controls-container__control\" aria-controls=\"algolia-bought-carousel-template--20550701482204__algolia_recommend_CCQmjm\">\n                <button type=\"button\" class=\"prev-next-button prev-next-button--prev circle-button hover:animate-icon-inline\" disabled>\n                  <span class=\"sr-only\">السابق</span></button>\n              </carousel-prev-button>\n\n              <scroll-carousel id=\"algolia-bought-carousel-template--20550701482204__algolia_recommend_CCQmjm\" group-cells=\"\" allow-drag=\"\" class=\"algolia-recommend-carousel product-list product-list--carousel scroll-area bleed md:unbleed is-scrollable\">\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw961-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-limited_edition\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW961 - حرير مغسول نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1025-%D9%85%D8%AE%D9%85%D9%84-%D9%85%D8%B9-%D9%83%D8%AA%D8%A7%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4-%D9%85%D8%B9-%D9%8A%D8%A7%D9%82%D8%A9\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1025 - مخمل مع كتان نص كلوش مع ياقة</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">149.01 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">39.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1022-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D9%85%D8%B9-%D9%82%D9%8A%D8%B7%D8%A7%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1022 - حرير ياباني مع قيطان  نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">189.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">50.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw841-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-selling_fast\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW841 - كريب واقف نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw978-%D9%83%D8%B1%D9%8A%D8%A8-%D9%85%D8%B9-%D9%85%D8%AE%D9%85%D9%84-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW978 - كريب مع مخمل ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">169.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">45.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1684-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%84%D8%AF%D9%88%D8%A7%D9%85-%D8%A8%D9%82%D9%85%D8%A7%D8%B4-%D9%83%D9%88%D8%A8%D8%B1%D8%A7\">\n              <div class=\"product-bages\">\n                <div class=\" badge-new\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1684 - عباية سوداء لدوام  بقماش كوبرا</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mwt511-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A3%D8%B3%D9%88%D8%AF-%D9%85%D9%82%D9%84%D9%85\">\n              <div class=\"product-bages\">\n                <div class=\" badge-new\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MWT511 - عباية بليزر أسود مقلم</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.70 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.51</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1171-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%85%D8%B9-%D9%83%D8%B3%D8%B1%D8%A7%D8%AA-%D9%85%D9%86-%D8%A7%D9%84%D8%AE%D9%84%D9%81\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1171 - حرير مع كسرات من الخلف</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">218.99 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">58.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw644-%D8%A7%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D9%85%D8%B9-%D8%AA%D9%81%D8%AA%D9%87-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW644 - انترنت مع تفته نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1147-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%A7%D9%82%D9%81-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D9%85%D8%A8%D8%B7%D9%86\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1147 - كريب واقف بليزر مبطن</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw1154-%D9%83%D8%AA%D8%A7%D9%86-%D8%AA%D8%A7%D9%8A%D9%84%D8%A7%D9%86%D8%AF%D9%8A-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW1154  -كتان تايلاندي بليزر</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">309.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">82.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw662-%D8%A7%D9%88%D9%83%D8%B1%D8%A7%D9%86%D9%8A-%D9%86%D8%A7%D8%B9%D9%85-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-running_out_soon\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW662  - اوكراني ناعم ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw966-%D9%83%D8%B1%D9%8A%D8%A8-%D9%85%D8%B9-%D9%85%D8%AE%D9%85%D9%84-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW966 - كريب مع مخمل ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">159.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">42.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mwt512-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A3%D8%B3%D9%88%D8%AF\">\n              <div class=\"product-bages\">\n                <div class=\" badge-new\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MWT512 - عباية بليزر أسود</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.70 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.51</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw666-%D9%84%D9%86%D9%86-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" badge-selling_fast\"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW666 - لنن نص كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">189.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">50.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n\n            <div class=\"algolia-recommend-item\">\n              <a href=\"https://www.aya.app/products/mw976-%D8%AD%D8%B1%D9%8A%D8%B1-%D9%8A%D8%A7%D8%A8%D8%A7%D9%86%D9%8A-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\">\n              <div class=\"product-bages\">\n                <div class=\" \"></div>\n\n                </div>\n                <div class=\"algolia-recommend-item-content\">\n                  <h3>MW976 - حرير ياباني ربع كلوش</h3>\n                  <div class=\"price skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">350.00 SAR</div>\n<span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(28, 28, 28); font-size: 16px; font-style: normal; font-weight: 600; text-decoration: rgb(28, 28, 28);\"><span style=\"text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: diarabic; font-size: 16px; font-weight: 600; color: inherit; float: none;\" class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">93.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n                </div>\n              </a>\n            </div>\n          </scroll-carousel>\n\n              <carousel-next-button class=\"floating-controls-container__control\" aria-controls=\"algolia-bought-carousel-template--20550701482204__algolia_recommend_CCQmjm\">\n                <button type=\"button\" class=\"prev-next-button prev-next-button--next circle-button hover:animate-icon-inline\">\n                  <span class=\"sr-only\">التالي</span></button>\n              </carousel-next-button>\n            </product-list>\n</div></div>\n\n  </div>\n</div>\n\n\n\n\n\n\n\n\n</section><section id=\"shopify-section-template--20550701482204__pdp_collection_products_FicEDW\" class=\"shopify-section shopify-section--pdp-collection-products\">\n<button class=\"backToTop show\" id=\"backToTop\"></button><div class=\"section-spacing color-scheme color-scheme--scheme-1 tabs-containers color-scheme--bg-609ecfcfee2f667ac6c12366fc6ece56 bordered-section\">\n    <div class=\"container section-stack\">\n<div class=\"section-header justify-self-center text-center\">\n          <div class=\"prose\">\n            <h2 class=\"h2\">المزيد من العبايات</h2>\n          </div>\n        </div>\n<product-list class=\"product-list  justify-center\"><product-card class=\"product-card\" handle=\"mwd04-فستان-عباية-اسود\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd04-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8951962009820\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd04-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD04 -  فستان عباية اسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1537-عباية-صيفية-ملونة\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1537-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-%D9%85%D9%84%D9%88%D9%86%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9010524389596\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1537-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-%D9%85%D9%84%D9%88%D9%86%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1537 - عباية صيفية ملونة</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd09-طقم-من-قطعتين-باللون-الاسود\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd09-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8972121276636\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd09-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD09 -  طقم من قطعتين باللون الاسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1604-عباية-سوداء-بتطريز-ورود-على-الجوانب-black-abaya-with-floral-side-embroidery\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1604-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D9%88%D8%AF-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%86%D8%A8-black-abaya-with-floral-side-embroidery\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9021362241756\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1604-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D9%88%D8%AF-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%86%D8%A8-black-abaya-with-floral-side-embroidery\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1604 -  عباية سوداء بتطريز ورود على الجوانب | Black Abaya with Floral Side Embroidery</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd08-طقم-من-قطعتين-باللون-الابيض\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd08-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8972119146716\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd08-%D8%B7%D9%82%D9%85-%D9%85%D9%86-%D9%82%D8%B7%D8%B9%D8%AA%D9%8A%D9%86-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD08 -  طقم من قطعتين باللون الابيض</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1384-عبايه-لينن-بيضاء-بتطريز-بيبي-بنك-ربع-كلوش\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1384-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D9%84%D9%8A%D9%86%D9%86-%D8%A8%D9%8A%D8%B6%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%86%D9%83-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8957410574556\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1384-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D9%84%D9%8A%D9%86%D9%86-%D8%A8%D9%8A%D8%B6%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%86%D9%83-%D8%B1%D8%A8%D8%B9-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1384 -  عبايه لينن بيضاء بتطريز بيبي بنك ربع كلوش</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1341-عبايه-بتطريز-اسود\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1341-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8949434089692\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1341-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1341 -  عباية بليزر بتطريز اسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">300.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">79.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1592-عباية-سوداء-كريب-بظهر-مخطط-black-crepe-abaya-with-striped-back\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1592-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A8%D8%B8%D9%87%D8%B1-%D9%85%D8%AE%D8%B7%D8%B7-black-crepe-abaya-with-striped-back\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9019617575132\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1592-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A8%D8%B8%D9%87%D8%B1-%D9%85%D8%AE%D8%B7%D8%B7-black-crepe-abaya-with-striped-back\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1592 -  عباية سوداء كريب بظهر مخطط | Black Crepe Abaya with Striped Back</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1561-عباية-شيفون-مورد-chiffon-flowered-abaya\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1561-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D9%85%D9%88%D8%B1%D8%AF-chiffon-flowered-abaya\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9023382716636\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          </div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1561-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D9%85%D9%88%D8%B1%D8%AF-chiffon-flowered-abaya\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1561 - عباية شيفون مورد | Chiffon flowered abaya</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">310.01 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">82.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1545-عباية-بولكا-دوت-نص-كلوش\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1545-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%88%D9%84%D9%83%D8%A7-%D8%AF%D9%88%D8%AA-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9010546213084\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1545-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%88%D9%84%D9%83%D8%A7-%D8%AF%D9%88%D8%AA-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1545 - عباية بولكا دوت نص كلوش</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1552-magestic-garden-عباية-مطبوعة-وردية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1552-magestic-garden-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9018536034524\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1552-magestic-garden-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1552 -  Magestic Garden | عباية مطبوعة</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd03-فستان-عباية-بني\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd03-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%86%D9%8A\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8951960666332\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd03-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%86%D9%8A\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD03 -  فستان عباية بني</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1605-عباية-سوداء-بخطوط-بنفسجية-وتطريز-بنفسجي-black-abaya-with-purple-stripes-and-embroidery\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1605-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AE%D8%B7%D9%88%D8%B7-%D8%A8%D9%86%D9%81%D8%B3%D8%AC%D9%8A%D8%A9-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D9%86%D9%81%D8%B3%D8%AC%D9%8A-black-abaya-with-purple-stripes-and-embroidery\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9021388783836\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1605-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AE%D8%B7%D9%88%D8%B7-%D8%A8%D9%86%D9%81%D8%B3%D8%AC%D9%8A%D8%A9-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D9%86%D9%81%D8%B3%D8%AC%D9%8A-black-abaya-with-purple-stripes-and-embroidery\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1605 - عباية سوداء بخطوط بنفسجية وتطريز بنفسجي | Black Abaya with Purple Stripes and Embroidery</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd06-فستان-عباية-مطاطي-باللون-الاسود\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd06-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8971622514908\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd06-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%B3%D9%88%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD06 -  فستان عباية مطاطي باللون الاسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd05-فستان-عباية-رمادي\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd05-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B1%D9%85%D8%A7%D8%AF%D9%8A\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8951976132828\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd05-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B1%D9%85%D8%A7%D8%AF%D9%8A\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD05 -  فستان عباية رمادي</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1556-back-charm-عباية-سوداء-مطرزة-من-الجهتين-بتفاصيل-فاخرة\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1556-back-charm-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D8%AC%D9%87%D8%AA%D9%8A%D9%86-%D8%A8%D8%AA%D9%81%D8%A7%D8%B5%D9%8A%D9%84-%D9%81%D8%A7%D8%AE%D8%B1%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9014376235228\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1556-back-charm-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D8%AC%D9%87%D8%AA%D9%8A%D9%86-%D8%A8%D8%AA%D9%81%D8%A7%D8%B5%D9%8A%D9%84-%D9%81%D8%A7%D8%AE%D8%B1%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1556 - Back Charm | عباية سوداء مطرزة من الجهتين بتفاصيل فاخرة</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd01-فستان\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd01-%D9%81%D8%B3%D8%AA%D8%A7%D9%86\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8951912988892\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd01-%D9%81%D8%B3%D8%AA%D8%A7%D9%86\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD01 -  فستان عباية كحلى</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd07-فستان-عباية-مطاطي-باللون-الابيض\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd07-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8972118687964\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd07-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A7%D8%B7%D9%8A-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A7%D8%A8%D9%8A%D8%B6\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD07 -  فستان عباية مطاطي باللون الابيض</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">70.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">18.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1162-مخمل-مكسر-نص-كلوش\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1162-%D9%85%D8%AE%D9%85%D9%84-%D9%85%D9%83%D8%B3%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8876713803996\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          </div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1162-%D9%85%D8%AE%D9%85%D9%84-%D9%85%D9%83%D8%B3%D8%B1-%D9%86%D8%B5-%D9%83%D9%84%D9%88%D8%B4\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1162 - مخمل مكسر  نص كلوش</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1577-عباية-كريب-بقصة-أنيقة-elegant-cut-crepe-abaya\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1577-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A8%D9%82%D8%B5%D8%A9-%D8%A3%D9%86%D9%8A%D9%82%D8%A9-elegant-cut-crepe-abaya\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9018391724252\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1577-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A8%D9%82%D8%B5%D8%A9-%D8%A3%D9%86%D9%8A%D9%82%D8%A9-elegant-cut-crepe-abaya\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1577 - عباية كريب بقصة أنيقة | Elegant Cut Crepe Abaya</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1606-عباية-سوداء-بخطوط-وردية-وتطريز-وردي-black-abaya-with-pink-stripes-and-embroidery\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1606-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AE%D8%B7%D9%88%D8%B7-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D8%AF%D9%8A-black-abaya-with-pink-stripes-and-embroidery\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9021370892508\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1606-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AE%D8%B7%D9%88%D8%B7-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D8%AF%D9%8A-black-abaya-with-pink-stripes-and-embroidery\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1606 - عباية سوداء بخطوط وردية وتطريز وردي | Black Abaya with Pink Stripes and Embroidery</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1611-عباية-لنن-اسود-مع-تطريز-بالأكمام\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1611-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D8%B9-%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D8%A7%D9%84%D8%A3%D9%83%D9%85%D8%A7%D9%85\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9030935576796\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1611-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D8%B9-%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D8%A7%D9%84%D8%A3%D9%83%D9%85%D8%A7%D9%85\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1611 - عباية لنن اسود مع تطريز بالأكمام</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw7971-لنن-نص-بشت\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw7971-%D9%84%D9%86%D9%86-%D9%86%D8%B5-%D8%A8%D8%B4%D8%AA\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8848412934364\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw7971-%D9%84%D9%86%D9%86-%D9%86%D8%B5-%D8%A8%D8%B4%D8%AA\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW7971 -  لنن نص بشت</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw7931-لنن-نص-بشت\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw7931-%D9%84%D9%86%D9%86-%D9%86%D8%B5-%D8%A8%D8%B4%D8%AA\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8848414277852\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw7931-%D9%84%D9%86%D9%86-%D9%86%D8%B5-%D8%A8%D8%B4%D8%AA\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW7931 -  لنن نص بشت</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">199.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">53.06</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price><compare-at-price class=\"h6 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">280.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><s class=\"currency-converter-amount cbb-price-currency-USD Price--compareAt\" style='display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;'><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">74.66</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></s></span></span></compare-at-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1551-navy-garden-عباية-مطبوعة-صيفية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1551-navy-garden-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9013690106076\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1551-navy-garden-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1551 - Navy Garden | عباية مطبوعة صيفية</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd02-فستان-عباية-نبيتي\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd02-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%86%D8%A8%D9%8A%D8%AA%D9%8A\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8951949820124\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd02-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%86%D8%A8%D9%8A%D8%AA%D9%8A\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD02 -  فستان عباية عنابي</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">50.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">13.33</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1586-عباية-صيفية-شيفون-بارد-sunflower-gold\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1586-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D8%A8%D8%A7%D8%B1%D8%AF-sunflower-gold\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9018488750300\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1586-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D8%A8%D8%A7%D8%B1%D8%AF-sunflower-gold\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1586 - عباية صيفية شيفون بارد | Sunflower Gold</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1636-magestic-pink-عباية-مطبوعة-وردية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1636-magestic-pink-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9027999072476\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1636-magestic-pink-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D9%88%D8%B1%D8%AF%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1636 -   Magestic Pink | عباية مطبوعة وردية</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1534-عباية-سوداء-بنقاط-بيضاء-ناعمة-بطابع-أنثوي-راقٍ\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1534-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D9%86%D9%82%D8%A7%D8%B7-%D8%A8%D9%8A%D8%B6%D8%A7%D8%A1-%D9%86%D8%A7%D8%B9%D9%85%D8%A9-%D8%A8%D8%B7%D8%A7%D8%A8%D8%B9-%D8%A3%D9%86%D8%AB%D9%88%D9%8A-%D8%B1%D8%A7%D9%82%D9%8D\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9005043253468\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1534-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D9%86%D9%82%D8%A7%D8%B7-%D8%A8%D9%8A%D8%B6%D8%A7%D8%A1-%D9%86%D8%A7%D8%B9%D9%85%D8%A9-%D8%A8%D8%B7%D8%A7%D8%A8%D8%B9-%D8%A3%D9%86%D8%AB%D9%88%D9%8A-%D8%B1%D8%A7%D9%82%D9%8D\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1534 - عباية سوداء بنقاط بيضاء ناعمة بطابع أنثوي راقٍ</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1640-عباية-اسود-مورد\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1640-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%88%D8%B1%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9034868883676\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1640-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%88%D8%B1%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1640 - عباية اسود مورد</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1591-عباية-سوداء-بتطريز-برتقالي-black-abaya-with-orange-embroidery\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1591-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D8%B1%D8%AA%D9%82%D8%A7%D9%84%D9%8A-black-abaya-with-orange-embroidery\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9019750875356\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1591-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D8%A8%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D8%A8%D8%B1%D8%AA%D9%82%D8%A7%D9%84%D9%8A-black-abaya-with-orange-embroidery\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1591 -عباية سوداء بتطريز برتقالي | Black Abaya with Orange Embroidery</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1520-عباية-كريب-اسود-وتطريز-وردي\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1520-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A7%D8%B3%D9%88%D8%AF-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D8%AF%D9%8A\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9000579498204\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1520-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%83%D8%B1%D9%8A%D8%A8-%D8%A7%D8%B3%D9%88%D8%AF-%D9%88%D8%AA%D8%B7%D8%B1%D9%8A%D8%B2-%D9%88%D8%B1%D8%AF%D9%8A\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1520 - عباية كريب اسود وتطريز وردي</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">180.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">47.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price><compare-at-price class=\"h6 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">300.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><s class=\"currency-converter-amount cbb-price-currency-USD Price--compareAt\" style='display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;'><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">79.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></s></span></span></compare-at-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1522-عبايه-اسود-مع-تدخيل-بيبي-بينك-من-ورا-بالطول\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1522-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D8%B9-%D8%AA%D8%AF%D8%AE%D9%8A%D9%84-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%8A%D9%86%D9%83-%D9%85%D9%86-%D9%88%D8%B1%D8%A7-%D8%A8%D8%A7%D9%84%D8%B7%D9%88%D9%84\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9007765258460\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-running_out_soon\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1522-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D8%B9-%D8%AA%D8%AF%D8%AE%D9%8A%D9%84-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%8A%D9%86%D9%83-%D9%85%D9%86-%D9%88%D8%B1%D8%A7-%D8%A8%D8%A7%D9%84%D8%B7%D9%88%D9%84\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1522 - Whisper of Pink | عباية سوداء بلمسة وردية</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1639-عباية-مقلم-اخضر-بوردات\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1639-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D9%82%D9%84%D9%85-%D8%A7%D8%AE%D8%B6%D8%B1-%D8%A8%D9%88%D8%B1%D8%AF%D8%A7%D8%AA\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9034865574108\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1639-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D9%82%D9%84%D9%85-%D8%A7%D8%AE%D8%B6%D8%B1-%D8%A8%D9%88%D8%B1%D8%AF%D8%A7%D8%AA\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1639 - عباية مقلم اخضر بوردات</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1601-عباية-بليزر-باللون-الأسود-الفاحم-charcoal-black-blazer-style-abaya\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1601-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A3%D8%B3%D9%88%D8%AF-%D8%A7%D9%84%D9%81%D8%A7%D8%AD%D9%85-charcoal-black-blazer-style-abaya\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9020426649820\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1601-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A8%D8%A7%D9%84%D9%84%D9%88%D9%86-%D8%A7%D9%84%D8%A3%D8%B3%D9%88%D8%AF-%D8%A7%D9%84%D9%81%D8%A7%D8%AD%D9%85-charcoal-black-blazer-style-abaya\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1601 -  عباية بليزر باللون الأسود الفاحم | Charcoal Black Blazer-Style Abaya</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1614-عباية-مطبوعة-حرير-جاكار-pink-jacquard\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1614-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-pink-jacquard\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9032359968988\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1614-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%AD%D8%B1%D9%8A%D8%B1-%D8%AC%D8%A7%D9%83%D8%A7%D8%B1-pink-jacquard\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1614 - عباية مطبوعة حرير جاكار - Pink Jacquard</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1539-عباية-زهور-صيفية-copy\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1539-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B2%D9%87%D9%88%D8%B1-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-copy\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9010543755484\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1539-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B2%D9%87%D9%88%D8%B1-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9-copy\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1540 - عباية صيفية شيفون بارد</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1539-عباية-زهور-صيفية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1539-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B2%D9%87%D9%88%D8%B1-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9010541854940\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1539-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B2%D9%87%D9%88%D8%B1-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1539 - عباية زهور صيفية</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd11-فستان-عبايه-اسود-منقط-بأبيض\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd11-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%A8%D9%8A%D8%B6\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9028009754844\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-running_out_soon\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd11-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%A8%D9%8A%D8%B6\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD11 -  فستان عبايه اسود منقط بأبيض</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1418-عباية-صيفيه-مطبوعة-بورد-copy\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1418-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D9%87-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%A8%D9%88%D8%B1%D8%AF-copy\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8966707314908\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-best_seller\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1418-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B5%D9%8A%D9%81%D9%8A%D9%87-%D9%85%D8%B7%D8%A8%D9%88%D8%B9%D8%A9-%D8%A8%D9%88%D8%B1%D8%AF-copy\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1419 -عباية صيفية مطبوعة باللون الازرق</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">350.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">93.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1578-black-linen-elegance-عباية-لينن-هندي-مطرزة-يدويًا\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1578-black-linen-elegance-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%8A%D9%86%D9%86-%D9%87%D9%86%D8%AF%D9%8A-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D9%8B%D8%A7\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9018397065436\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1578-black-linen-elegance-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%8A%D9%86%D9%86-%D9%87%D9%86%D8%AF%D9%8A-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D9%8B%D8%A7\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1578 - Black Linen Elegance | عباية لينن هندي مطرزة يدويًا</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1219-كريب-ودانتيل-من-الخلف\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1219-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D8%AE%D9%84%D9%81\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8913034477788\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1219-%D9%83%D8%B1%D9%8A%D8%A8-%D9%88%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D8%AE%D9%84%D9%81\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1219 - كريب ودانتيل من الخلف</a><price-list class=\"price-list \"><sale-price class=\"h6 text-on-sale\">\n          <span class=\"sr-only\">السعر بعد الخصم</span>السعر من <span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">104.35 SAR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgb(62, 86, 38); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgb(62, 86, 38);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">27.82</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span></sale-price><compare-at-price class=\"h6 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">209.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><s class=\"currency-converter-amount cbb-price-currency-USD Price--compareAt\" style='display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;'><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">55.73</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></s></span></span></compare-at-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1446-عباية-تربل-كلوش-مزينة-بالدانتيل-الذهبي-copy\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1446-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B1%D8%A8%D9%84-%D9%83%D9%84%D9%88%D8%B4-%D9%85%D8%B2%D9%8A%D9%86%D8%A9-%D8%A8%D8%A7%D9%84%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D8%A7%D9%84%D8%B0%D9%87%D8%A8%D9%8A-copy\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8971594760412\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1446-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B1%D8%A8%D9%84-%D9%83%D9%84%D9%88%D8%B4-%D9%85%D8%B2%D9%8A%D9%86%D8%A9-%D8%A8%D8%A7%D9%84%D8%AF%D8%A7%D9%86%D8%AA%D9%8A%D9%84-%D8%A7%D9%84%D8%B0%D9%87%D8%A8%D9%8A-copy\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1445 -عباية من الشيفون المطرز بالاسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1500-عباية-لنن-بيج-صيفية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1500-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A8%D9%8A%D8%AC-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9007794979036\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1500-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A8%D9%8A%D8%AC-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1500 - عباية لنن موف  | Soft Lavender</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1542-عباية-لنن-بيبي-بنك-صيفية\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1542-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%86%D9%83-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9010556141788\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-limited_edition\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1542-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D9%84%D9%86%D9%86-%D8%A8%D9%8A%D8%A8%D9%8A-%D8%A8%D9%86%D9%83-%D8%B5%D9%8A%D9%81%D9%8A%D8%A9\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1542 - عباية لنن بيبي بنك صيفية</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mwd10-فستان-عبايه-ابيض-منقط-بأسود\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mwd10-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%A8%D9%8A%D8%B6-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%B3%D9%88%D8%AF\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9028008902876\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-running_out_soon\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mwd10-%D9%81%D8%B3%D8%AA%D8%A7%D9%86-%D8%B9%D8%A8%D8%A7%D9%8A%D9%87-%D8%A7%D8%A8%D9%8A%D8%B6-%D9%85%D9%86%D9%82%D8%B7-%D8%A8%D8%A3%D8%B3%D9%88%D8%AF\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MWD10 -  فستان عبايه ابيض منقط بأسود</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">90.02 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">24.00</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1622-عباية-شيفون-طباعة-اسود-ملون-color-splash\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1622-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D8%B7%D8%A8%D8%A7%D8%B9%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%84%D9%88%D9%86-color-splash\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9023458115804\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          </div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1622-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B4%D9%8A%D9%81%D9%88%D9%86-%D8%B7%D8%A8%D8%A7%D8%B9%D8%A9-%D8%A7%D8%B3%D9%88%D8%AF-%D9%85%D9%84%D9%88%D9%86-color-splash\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1622 -عباية شيفون طباعة اسود ملون -Color Splash</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">270.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">71.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1347-عباية-بياقة-بليزر-بطبقات-بيليسية-مطرزة-بلون-عاجي-copy-1\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1347-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%8A%D8%A7%D9%82%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A8%D8%B7%D8%A8%D9%82%D8%A7%D8%AA-%D8%A8%D9%8A%D9%84%D9%8A%D8%B3%D9%8A%D8%A9-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D8%A8%D9%84%D9%88%D9%86-%D8%B9%D8%A7%D8%AC%D9%8A-copy-1\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---8953916653788\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          </div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1347-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%8A%D8%A7%D9%82%D8%A9-%D8%A8%D9%84%D9%8A%D8%B2%D8%B1-%D8%A8%D8%B7%D8%A8%D9%82%D8%A7%D8%AA-%D8%A8%D9%8A%D9%84%D9%8A%D8%B3%D9%8A%D8%A9-%D9%85%D8%B7%D8%B1%D8%B2%D8%A9-%D8%A8%D9%84%D9%88%D9%86-%D8%B9%D8%A7%D8%AC%D9%8A-copy-1\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1361 - عباية مطبوعة بلون الرمادي والأزرق</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">249.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">66.39</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price><compare-at-price class=\"h6 text-subdued line-through\">\n    <span class=\"sr-only\">السعر قبل الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">390.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><s class=\"currency-converter-amount cbb-price-currency-USD Price--compareAt\" style='display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;'><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">103.99</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></s></span></span></compare-at-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1686-عباية-بقماش-ديجتال-بالكامل\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1686-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%82%D9%85%D8%A7%D8%B4-%D8%AF%D9%8A%D8%AC%D8%AA%D8%A7%D9%84-%D8%A8%D8%A7%D9%84%D9%83%D8%A7%D9%85%D9%84\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9053603627228\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-new\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1686-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%A8%D9%82%D9%85%D8%A7%D8%B4-%D8%AF%D9%8A%D8%AC%D8%AA%D8%A7%D9%84-%D8%A8%D8%A7%D9%84%D9%83%D8%A7%D9%85%D9%84\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1686 - عباية بقماش ديجتال بالكامل</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n<product-card class=\"product-card\" handle=\"mw1582-عباية-سوداء-كريب-black-crepe-abaya\"><div class=\"product-card__figure\">\n<a href=\"https://www.aya.app/products/mw1582-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%83%D8%B1%D9%8A%D8%A8-black-crepe-abaya\" class=\"product-card__media\" draggable=\"false\" data-instant=\"\"></a><button type=\"button\" aria-controls=\"product-quick-buy-template--20550701482204__pdp_collection_products_FicEDW---9018447364316\" class=\"product-card__quick-add-button\">\n            <span class=\"sr-only\">حدِّد الخيارات</span></button>\n\n          <div class=\"badge-selling_fast\">\n        <span class=\"cashback-text\">\n\n        </span>\n      </div>\n</div><div class=\"product-card__info empty:hidden\"><div class=\"v-stack justify-items-center gap-2\"><div class=\"v-stack justify-items-center gap-1\">\n<a href=\"https://www.aya.app/products/mw1582-%D8%B9%D8%A8%D8%A7%D9%8A%D8%A9-%D8%B3%D9%88%D8%AF%D8%A7%D8%A1-%D9%83%D8%B1%D9%8A%D8%A8-black-crepe-abaya\" class=\"product-title h6 line-clamp\" style=\"--line-clamp-count: 1\" data-instant=\"\">MW1582 - عباية سوداء كريب | Black Crepe Abaya</a><price-list class=\"price-list \"><sale-price class=\"h6 text-subdued\">\n  <span class=\"sr-only\">السعر بعد الخصم</span><span class=\"money skiptranslate notranslate\" data-cbb-price-processed=\"true\" style=\"display: none;\">290.00 SR</span><span class=\"currency-converter-wrapper-amount-box cbb-desktop-view skiptranslate notranslate\" style=\"display: inline-block; white-space: nowrap; margin: 0px; padding: 0px; text-decoration: inherit;\"><span class=\"currency-converter-amount-box\" style=\"display: inline-block; white-space: nowrap; padding: 0px; line-height: initial; color: rgba(28, 28, 28, 0.65); font-size: 12px; font-style: normal; font-weight: 400; text-decoration: rgba(28, 28, 28, 0.65);\"><span style='text-decoration: inherit; display: inline-block; margin-right: 3px; font-family: \"Instrument Sans\", sans-serif; font-size: 12px; font-weight: 400; color: inherit; float: none;' class=\"currency-converter-amount cbb-price-currency-USD\"><span class=\"cbb-price-symbol\" style=\"padding: 0px 1px; color: inherit; float: none; margin-right: 0px;\">$</span><span class=\"cbb-price-digits\" style=\"padding: 0px 1px; color: inherit; float: none;\">77.32</span><span class=\"cbb-price-code\" style=\"margin-left: 3px; padding: 0px 1px; color: inherit; float: none;\">USD</span></span></span></span>\n</sale-price></price-list>\n</div></div></div>\n</product-card>\n</product-list><div class=\"load-more-container\" style=\"text-align: center; margin-top: 2rem;\">\n          <button id=\"load-more-btn-template--20550701482204__pdp_collection_products_FicEDW\" class=\"load-more-btn btn btn--primary sm-max:hidden\" data-collection-handle=\"all-products\" data-current-page=\"1\" data-total-pages=\"5\" data-section-id=\"template--20550701482204__pdp_collection_products_FicEDW\" style=\"padding: 12px 24px; border-radius: 6px; border: none; background: var(--color-button-background, #000); color: var(--color-button-text, #fff); cursor: pointer; font-size: 16px; font-weight: 500; transition: all 0.3s ease;\">عرض المزيد من المنتجات\n          </button>\n          <div id=\"load-more-loader-template--20550701482204__pdp_collection_products_FicEDW\" class=\"load-more-loader\" style=\"display: none; margin-top: 1rem;\">\n            <div class=\"spinner\" style=\"width: 24px; height: 24px; border: 2px solid #f3f3f3; border-top: 2px solid #333; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;\"></div>\n          </div>\n        </div>\n</div>\n</div>\n\n\n\n</section>\n\n<section id=\"shopify-section-sections--20550695846108__text-with-icons\" class=\"shopify-section shopify-section-group-footer-group shopify-section--text-with-icons\"><div class=\"section-spacing section-spacing--tight color-scheme color-scheme--scheme-1 color-scheme--bg-609ecfcfee2f667ac6c12366fc6ece56 bordered-section\">\n    <div class=\"container\">\n      <div class=\"v-stack gap-8\"><div class=\"text-with-icons text-with-icons--stacked\" role=\"region\" style=\"--border-color: var(--text-color) / 0.15;\">\n<div class=\"text-with-icons__item is-selected \" role=\"group\" aria-label=\"العنصر 1 من 5\">\n              <div class=\"v-stack gap-6 justify-items-center sm:justify-items-center\"><div class=\"v-stack gap-2 text-center sm:text-center\">\n<p class=\"h6\"><strong>تسوقي وانتي مطمّنة</strong></p>\n<div class=\"prose\"><p>موثوقين في معروف وتحت مظلة وزارة التجارة السعودية<br><br></p></div>\n</div></div>\n            </div>\n<div class=\"text-with-icons__item  \" role=\"group\" aria-label=\"العنصر 2 من 5\">\n              <div class=\"v-stack gap-6 justify-items-center sm:justify-items-center\"><div class=\"v-stack gap-2 text-center sm:text-center\">\n<p class=\"h6\"><strong>من قلبنا لقلب الخليج</strong></p>\n<div class=\"prose\"><p>طلبك بيوصلك بكل دول الخليج خلال وقت قياسي</p></div>\n</div></div>\n            </div>\n<div class=\"text-with-icons__item  \" role=\"group\" aria-label=\"العنصر 3 من 5\">\n              <div class=\"v-stack gap-6 justify-items-center sm:justify-items-center\"><div class=\"v-stack gap-2 text-center sm:text-center\">\n<p class=\"h6\"><strong>مكان التريندات</strong></p>\n<div class=\"prose\"><p>تلقين عندنا اخر تريندات العبايات اول بأول</p></div>\n</div></div>\n            </div>\n<div class=\"text-with-icons__item  \" role=\"group\" aria-label=\"العنصر 4 من 5\">\n              <div class=\"v-stack gap-6 justify-items-center sm:justify-items-center\"><div class=\"v-stack gap-2 text-center sm:text-center\">\n<p class=\"h6\"><strong> ترجيع خلال ٧ أيام</strong></p>\n<div class=\"prose\">\n<p>7 ايام، لو ما عجبتك رجعيها بدون سبب </p>\n<p><a href=\"https://www.aya.app/pages/refund-policy\" title=\"سياسة الاستبدال والاسترجاع\">حسب الشروط والأحكام</a></p>\n</div>\n</div></div>\n            </div>\n<div class=\"text-with-icons__item  \" role=\"group\" aria-label=\"العنصر 5 من 5\">\n              <div class=\"v-stack gap-6 justify-items-center sm:justify-items-center\"><div class=\"v-stack gap-2 text-center sm:text-center\">\n<p class=\"h6\"><strong>سهلناها عليك</strong></p>\n<div class=\"prose\"><p>نجمع لك كل براندات العبايات في مكان واحد</p></div>\n</div></div>\n            </div>\n</div></div>\n    </div>\n  </div></section>\n</main><div tabindex=\"-1\" aria-hidden=\"true\" id=\"web-pixels-manager-sandbox-container\" data-shopify-privacy=\"exclude\" style=\"height: 0px !important; width: 0px !important; position: fixed !important; visibility: hidden !important; overflow: hidden !important; z-index: -100 !important; margin: 0px !important; padding: 0px !important; border: 0px !important;\">\n<div class=\"crawlee-iframe-replacement\">\n\n</div>\n<div class=\"crawlee-iframe-replacement\">\n\n</div>\n<div class=\"crawlee-iframe-replacement\">\n\n</div>\n<div class=\"crawlee-iframe-replacement\">\n\n</div>\n</div>\n<div class=\"crawlee-iframe-replacement\"></div>\n\n\n\n\n\n<div id=\"shopify-block-AbE03ejQvdkZvLzQya__4293466245358296089\" class=\"shopify-block shopify-app-block\">\n\n\n<div id=\"sizefox-enable\" data-btn-horizontal=\"right\" data-btn-vertical=\"\" data-btn-horizontal-mobile=\"right\"></div>\n\n\n</div>\n<div id=\"shopify-block-AQVRUbEQvdXlid0dWQ__11735689235857492391\" class=\"shopify-block shopify-app-block\">\n\n\n\n<div class=\"algolia-config-money-format\" style=\"display: none;\"><span class=\"money\">{{amount}} SR</span></div>\n\n\n\n\n\n\n</div>\n<div id=\"shopify-block-AdDRwVHp0N1J4OFFja__13070091535480640111\" class=\"shopify-block shopify-app-block\">\n<input type=\"hidden\" class=\"aph_shop_plan\" value=\"7118\">\n\n<input type=\"hidden\" class=\"aph_product_collection\" value=\"443175796956,441510887644,450715975900,449376780508,450713419996,444548317404,449631977692,442528989404,453564367068,443379941596,450213576924,\">\n\n</div>\n<div id=\"shopify-block-AbXlxMFBiZSt3TUI4V__17198465164502963513\" class=\"shopify-block shopify-app-block\">\n\n\n\n\n\n</div>\n<div id=\"shopify-block-AM05QbkZpaG5MWU93V__18228966181090706294\" class=\"shopify-block shopify-app-block\">\n\n\n\n\n\n\n\n\n\n\n\n\n</div>\n<ul class=\"currency-converter-chooser skiptranslate notranslate\" style=\"list-style-type: none; text-align: left; border: 1px solid rgb(216, 216, 216); background-color: rgb(255, 255, 255); border-radius: 2px; position: absolute; z-index: 100000000; margin: 0px; padding: 2px; max-height: 400px; min-width: 90px; min-height: 50px; overflow-y: auto; opacity: 0; display: none; transition: opacity 0.3s;\">\n<li class=\"currency-converter-chooser-item cbb-currency-code-refresh\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216);\"><span style=\"vertical-align: middle; margin-left: 2px;\">Autodetect currency</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-BHD\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216);\"><span style=\"vertical-align: middle; margin-left: 2px;\">Bahraini Dinar (BHD)</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-KWD\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216);\"><span style=\"vertical-align: middle; margin-left: 2px;\">Kuwaiti Dinar (KWD)</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-QAR\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216);\"><span style=\"vertical-align: middle; margin-left: 2px;\">Qatari Riyal (QAR)</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-SAR\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216);\"><span style=\"vertical-align: middle; margin-left: 2px;\">Saudi Riyal (SAR)</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-AED\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px;\"><span style=\"vertical-align: middle; margin-left: 2px;\">United Arab Emirates Dirham (AED)</span></li>\n</ul>\n<ul class=\"currency-converter-chooser skiptranslate notranslate\" style=\"list-style-type: none; text-align: left; border: 1px solid rgb(216, 216, 216); background-color: rgb(255, 255, 255); border-radius: 2px; position: absolute; z-index: 100000000; margin: 0px; padding: 2px; max-height: 400px; min-width: 70px; min-height: 50px; overflow-y: auto; opacity: 0; display: none; transition: opacity 0.3s;\">\n<li class=\"currency-converter-chooser-item cbb-currency-code-refresh\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216); font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">Auto</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-AED\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216); font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">AED</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-BHD\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216); font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">BHD</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-KWD\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216); font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">KWD</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-QAR\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; border-bottom: 1px solid rgb(216, 216, 216); font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">QAR</span></li>\n<li class=\"currency-converter-chooser-item cbb-currency-code-SAR\" style=\"list-style-type: none; display: list-item; cursor: pointer; color: rgb(51, 51, 51); background-color: rgb(255, 255, 255); margin: 0px; padding: 1px; white-space: nowrap; overflow-x: hidden; max-width: 220px; font-weight: normal;\"><span style=\"vertical-align: middle; margin-left: 2px;\">SAR</span></li>\n</ul>\n<div></div>\n<webengagedata><div id=\"webklipper-publisher-widget-container\">\n<div class=\"crawlee-iframe-replacement\"></div>\n<div class=\"crawlee-iframe-replacement\"></div>\n<div id=\"webengage-notification-inbox-container\"><div><section id=\"we-notification-inbox-section\" class=\"we-notification-inbox-section\"><div id=\"we-notification-inbox-body\" class=\"we-notification-inbox-body\">\n</div>\n</section></div></div>\n</div></webengagedata><div id=\"sizefox-container\" class=\"sizefox-container-flex sizefox-container-left\"><div><div class=\"ss-size-chart-float-button smartsize-trigger smartsize-trigger--floating MuiBox-root css-3lgz5e\"> <span class=\"form__label smartsize-button-text smartsize-button__label\" style=\"margin-left: 7px;\">جدول المقاسات</span> </div></div></div>\n        </body>\n    </html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-134-pre-code.html",
    "content": "<pre>\n<code class=\"language-js code-highlight\"><span class=\"code-line\">            ├── <span class=\"token maybe-class-name\">Metadata</span><span class=\"token operator\">:</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Title</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Key</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Tempo</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Meter</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Style</span>\n</span><span class=\"code-line\">            │   ├── <span class=\"token maybe-class-name\">Feel</span>\n</span><span class=\"code-line\">            <span class=\"token operator\">|</span>   ├── $customProperty\n</span><span class=\"code-line\">            ├── <span class=\"token maybe-class-name\">Section1</span><span class=\"token operator\">:</span>\n</span><span class=\"code-line\">            │   ├── measures\n</span><span class=\"code-line\">            │   │   ├── chords\n</span><span class=\"code-line\">            │   │   │   ├── rhythms<span class=\"token punctuation\">,</span> articulations and other symbols\n</span><span class=\"code-line\">            │   ├── comments\n</span><span class=\"code-line\">            ├── <span class=\"token maybe-class-name\">Section2</span><span class=\"token operator\">:</span>\n</span><span class=\"code-line\">            <span class=\"token operator spread\">...</span>\n</span></code>\n</pre>\n"
  },
  {
    "path": "test_documents/html/issues/gh-134-pre-code.md",
    "content": "```js\n├── Metadata:\n│   ├── Title\n│   ├── Key\n│   ├── Tempo\n│   ├── Meter\n│   ├── Style\n│   ├── Feel\n|   ├── $customProperty\n├── Section1:\n│   ├── measures\n│   │   ├── chords\n│   │   │   ├── rhythms, articulations and other symbols\n│   ├── comments\n├── Section2:\n...\n\n\n```\n"
  },
  {
    "path": "test_documents/html/issues/gh-140-table-cell-pipe-with-escape-misc.md",
    "content": "\n\n| Character | Name |\n| --- | --- |\n| \\- | Dash |\n| / | Forward\\-Slash |\n| , | Comma |\n| . | Period |\n| \\~ | Tilde |\n| \\# | Octothorpe (\"Hash\" or \"Number\" sign) |\n| @ | At symbol |\n| \\& | Ampersand |\n| * | Asterisk |\n| _ | Underscore |\n| \\+ | Plus |\n| \\| | Pipe |\n"
  },
  {
    "path": "test_documents/html/issues/gh-140-table-cell-pipe.html",
    "content": "<table><thead><tr><th>Character</th><th>Name</th></tr></thead><tbody><tr><td>-</td><td>Dash</td></tr><tr><td>/</td><td>Forward-Slash</td></tr><tr><td>,</td><td>Comma</td></tr><tr><td>.</td><td>Period</td></tr><tr><td>~</td><td>Tilde</td></tr><tr><td>#</td><td>Octothorpe (\"Hash\" or \"Number\" sign)</td></tr><tr><td>@</td><td>At symbol</td></tr><tr><td>&amp;</td><td>Ampersand</td></tr><tr><td>*</td><td>Asterisk</td></tr><tr><td>_</td><td>Underscore</td></tr><tr><td>+</td><td>Plus</td></tr><tr><td>|</td><td>Pipe</td></tr></tbody></table>\n"
  },
  {
    "path": "test_documents/html/issues/gh-140-table-cell-pipe.md",
    "content": "\n\n| Character | Name |\n| --- | --- |\n| - | Dash |\n| / | Forward-Slash |\n| , | Comma |\n| . | Period |\n| ~ | Tilde |\n| # | Octothorpe (\"Hash\" or \"Number\" sign) |\n| @ | At symbol |\n| & | Ampersand |\n| * | Asterisk |\n| _ | Underscore |\n| + | Plus |\n| \\| | Pipe |\n"
  },
  {
    "path": "test_documents/html/issues/gh-143-links-wordwrap.html",
    "content": "<div class=\"ml-6\"><ul class=\"\"><li class=\"\"><a href=\"#background\">Background</a></li><li class=\"\"><a href=\"#chordtext-reference\">ChordText™ Reference</a><ul class=\"\"><li class=\"\"><a href=\"#overview-of-chordtext\">Overview of ChordText™</a></li><li class=\"\"><a href=\"#metadata\">Metadata</a></li><li class=\"\"><a href=\"#section-headers\">Section headers</a></li><li class=\"\"><a href=\"#descriptive-comments\">Descriptive comments</a></li><li class=\"\"><a href=\"#measures\">Measures</a><ul class=\"\"><li class=\"\"><a href=\"#split-bar-with-an-underline\">Split bar with an underline</a></li><li class=\"\"><a href=\"#split-bar-with-parentheses\">Split bar with parentheses</a></li><li class=\"\"><a href=\"#measure-group-separator\">Measure group separator</a></li></ul></li><li class=\"\"><a href=\"#chords\">Chords</a><ul class=\"\"><li class=\"\"><a href=\"#diatonic-chords\">Diatonic chords</a></li><li class=\"\"><a href=\"#non-diatonic-chords\">Non-diatonic chords</a></li><li class=\"\"><a href=\"#chord-qualities\">Chord qualities</a></li><li class=\"\"><a href=\"#no-chord---nc-tacet-or-rest\">No Chord - N.C., tacet or rest</a></li><li class=\"\"><a href=\"#beyond-triads---tetrads-extended-and-altered-chords\">Beyond triads - tetrads, extended and altered chords</a><ul class=\"\"><li class=\"\"><a href=\"#general-exponent-form-\">General \"exponent\" form (**)</a></li><li class=\"\"><a href=\"#altered-chord-examples-and-shorthand-forms\">Altered chord examples and shorthand forms</a></li></ul></li><li class=\"\"><a href=\"#slash-chords-inversions\">Slash chords (inversions)</a></li></ul></li><li class=\"\"><a href=\"#rhythms--articulations\">Rhythms &amp; Articulations</a><ul class=\"\"><li class=\"\"><a href=\"#diamonds\">Diamonds</a></li><li class=\"\"><a href=\"#pushes\">Pushes</a></li><li class=\"\"><a href=\"#tick-marks\">Tick marks</a></li><li class=\"\"><a href=\"#ties\">Ties</a></li><li class=\"\"><a href=\"#mutes--cut-offs--marcato\">Mutes / cut-offs / marcato</a></li><li class=\"\"><a href=\"#fermatas-bird-eyes\">Fermatas (\"bird eyes\")</a></li><li class=\"\"><a href=\"#walk-up-︎-and-walk-down-︎\">Walk-up (↗︎) and Walk-down (↘︎)</a></li><li class=\"\"><a href=\"#meter-changes-inline\">Meter changes (inline)</a></li><li class=\"\"><a href=\"#combining-into-complex-rhythms\">Combining into complex rhythms</a></li></ul></li><li class=\"\"><a href=\"#modulations\">Modulations</a></li><li class=\"\"><a href=\"#freeform-text\">Freeform text</a><ul class=\"\"><li class=\"\"><a href=\"#single-line-comment\">Single-line comment</a></li><li class=\"\"><a href=\"#multi-line-comment\">Multi-line comment</a></li><li class=\"\"><a href=\"#inline-comment\">Inline comment</a></li></ul></li><li class=\"\"><a href=\"#song-form\">Song form</a><ul class=\"\"><li class=\"\"><a href=\"#repeats\">Repeats</a><ul class=\"\"><li class=\"\"><a href=\"#repeat-multiple-times\">Repeat multiple times</a></li><li class=\"\"><a href=\"#multi-measure-repeats\">Multi-measure repeats</a></li></ul></li></ul></li></ul></li><li class=\"\"><a href=\"#putting-it-all-together---an-example-chart\">Putting it all together - an example chart</a><ul class=\"\"><li class=\"\"><a href=\"#example-chordtext\">Example ChordText™</a></li><li class=\"\"><a href=\"#example-pdf-by-jotchord\">Example PDF by JotChord™</a></li></ul></li></ul></div>\n"
  },
  {
    "path": "test_documents/html/issues/gh-143-links-wordwrap.md",
    "content": "- [Background](#background)\n- [ChordText™ Reference](#chordtext-reference)\n  * [Overview of ChordText™](#overview-of-chordtext)\n  * [Metadata](#metadata)\n  * [Section headers](#section-headers)\n  * [Descriptive comments](#descriptive-comments)\n  * [Measures](#measures)\n    + [Split bar with an underline](#split-bar-with-an-underline)\n    + [Split bar with parentheses](#split-bar-with-parentheses)\n    + [Measure group separator](#measure-group-separator)\n  * [Chords](#chords)\n    + [Diatonic chords](#diatonic-chords)\n    + [Non-diatonic chords](#non-diatonic-chords)\n    + [Chord qualities](#chord-qualities)\n    + [No Chord - N.C., tacet or rest](#no-chord---nc-tacet-or-rest)\n    + [Beyond triads - tetrads, extended and altered chords](#beyond-triads---tetrads-extended-and-altered-chords)\n      - [General \"exponent\" form (**)](#general-exponent-form-)\n      - [Altered chord examples and shorthand forms](#altered-chord-examples-and-shorthand-forms)\n    + [Slash chords (inversions)](#slash-chords-inversions)\n  * [Rhythms & Articulations](#rhythms--articulations)\n    + [Diamonds](#diamonds)\n    + [Pushes](#pushes)\n    + [Tick marks](#tick-marks)\n    + [Ties](#ties)\n    + [Mutes / cut-offs / marcato](#mutes--cut-offs--marcato)\n    + [Fermatas (\"bird eyes\")](#fermatas-bird-eyes)\n    + [Walk-up (↗︎) and Walk-down (↘︎)](#walk-up-︎-and-walk-down-︎)\n    + [Meter changes (inline)](#meter-changes-inline)\n    + [Combining into complex rhythms](#combining-into-complex-rhythms)\n  * [Modulations](#modulations)\n  * [Freeform text](#freeform-text)\n    + [Single-line comment](#single-line-comment)\n    + [Multi-line comment](#multi-line-comment)\n    + [Inline comment](#inline-comment)\n  * [Song form](#song-form)\n    + [Repeats](#repeats)\n      - [Repeat multiple times](#repeat-multiple-times)\n      - [Multi-measure repeats](#multi-measure-repeats)\n- [Putting it all together - an example chart](#putting-it-all-together---an-example-chart)\n  * [Example ChordText™](#example-chordtext)\n  * [Example PDF by JotChord™](#example-pdf-by-jotchord)\n"
  },
  {
    "path": "test_documents/html/issues/gh-190/firsteigen.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en-US\">\n<head>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\r\n\r\n\n\n\n\n\t<title>How to Improve Data Quality: 12 Effective Strategies</title>\n\n\n\n\n\n\n\n\n\t<meta property=\"article:published_time\" content=\"2022-05-03T12:00:00+00:00\" />\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\r\n\r\n\n\n\n\n\n <noscript><img alt=\"\" src=\"https://secure.gift2pair.com/211550.png\" style=\"display:none;\" /></noscript>\n\n\n\n\n\n\n\n\n\n\n\n\n\n<noscript><img src=\"//[p1.zemanta.com/v2/p/ns/45625/PAGE\\_VIEW/](http://p1.zemanta.com/v2/p/ns/45625/PAGE_VIEW/)\" referrerpolicy=\"no-referrer-when-downgrade\" height=\"1\" width=\"1\" border=\"0\" alt=\"\" /></noscript>\n</head>\n<body class=\"wp-singular post-template-default single single-post postid-1641 single-format-standard wp-theme-bb-theme fl-theme-builder-singular fl-theme-builder-singular-blog-redesign-copy fl-theme-builder-header fl-theme-builder-header-new-header fl-theme-builder-footer fl-theme-builder-footer-new-footer fl-framework-bootstrap fl-preset-default fl-full-width fl-has-sidebar fl-scroll-to-top fl-search-active fl-submenu-toggle has-blocks\" itemscope=\"itemscope\" itemtype=\"https://schema.org/WebPage\">\n\n\n<noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=GTM-M2JN3S4\"\nheight=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>\n\n\n<a aria-label=\"Skip to content\" class=\"fl-screen-reader-text\" href=\"#fl-main-content\">Skip to content</a><div class=\"fl-page\">\n\t<header class=\"fl-builder-content fl-builder-content-7071 fl-builder-global-templates-locked\" data-post-id=\"7071\" data-type=\"header\" data-sticky=\"1\" data-sticky-on=\"all\" data-sticky-breakpoint=\"medium\" data-shrink=\"0\" data-overlay=\"0\" data-overlay-bg=\"transparent\" data-shrink-image-height=\"50px\" role=\"banner\" itemscope=\"itemscope\" itemtype=\"http://schema.org/WPHeader\"><div class=\"fl-row fl-row-full-width fl-row-bg-color fl-node-17tx5amdzci4\" data-node=\"17tx5amdzci4\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-pmsla9vj7h1d fl-col-group-custom-width\" data-node=\"pmsla9vj7h1d\">\n\t\t\t<div class=\"fl-col fl-node-lmfu27a9cvxj fl-col-small fl-col-small-full-width\" data-node=\"lmfu27a9cvxj\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-photo fl-node-xe1tr9ug80a5\" data-node=\"xe1tr9ug80a5\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-photo fl-photo-align-center\" itemscope itemtype=\"https://schema.org/ImageObject\">\n\t<div class=\"fl-photo-content fl-photo-img-webp\">\n\t\t\t\t<a href=\"/\" target=\"_self\" itemprop=\"url\">\n\t\t\t\t<img loading=\"lazy\" decoding=\"async\" class=\"fl-photo-img wp-image-7075 size-full\" src=\"https://firsteigen.com/wp-content/uploads/2024/10/FirstEigen-Logo.webp\" alt=\"FirstEigen Logo\" itemprop=\"image\" height=\"42\" width=\"185\" title=\"FirstEigen Logo\"  data-no-lazy=\"1\" />\n\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-ilwq1x7orfng\" data-node=\"ilwq1x7orfng\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-menu fl-node-l1ujev7q0i3o\" data-node=\"l1ujev7q0i3o\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-menu fl-menu-responsive-toggle-medium-mobile\">\n\t<button class=\"fl-menu-mobile-toggle hamburger\" aria-label=\"Menu\"><span class=\"fl-menu-icon svg-container\"><svg version=\"1.1\" class=\"hamburger-menu\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 512 512\">\n<rect class=\"fl-hamburger-menu-top\" width=\"512\" height=\"102\"/>\n<rect class=\"fl-hamburger-menu-middle\" y=\"205\" width=\"512\" height=\"102\"/>\n<rect class=\"fl-hamburger-menu-bottom\" y=\"410\" width=\"512\" height=\"102\"/>\n</svg>\n</span></button>\t<div class=\"fl-clear\"></div>\n\t<nav aria-label=\"Menu\" itemscope=\"itemscope\" itemtype=\"https://schema.org/SiteNavigationElement\"><ul id=\"menu-main-menu\" class=\"menu fl-menu-horizontal fl-toggle-arrows\"><li id=\"menu-item-43\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a>Products</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t<li id=\"menu-item-10173\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/databuck/\">DataBuck Data Quality and Trust</a></li>\t<li id=\"menu-item-10165\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/data-matching-software/\">DataBuck Data Matching</a></li>\t<li id=\"menu-item-10196\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/data-remediation/\">Data Remediation</a></li></ul></li><li id=\"menu-item-44\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a>Resources</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t<li id=\"menu-item-10306\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/resources/\">Resources</a></li>\t<li id=\"menu-item-27\" class=\"menu-item menu-item-type-post_type menu-item-object-page current_page_parent\"><a href=\"https://firsteigen.com/blog/\">Blog</a></li>\t<li id=\"menu-item-1009\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/white-papers/\">White Papers</a></li>\t<li id=\"menu-item-1008\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/case-studies/\">Case Studies</a></li>\t<li id=\"menu-item-1954\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/reports/\">Reports</a></li>\t<li id=\"menu-item-2415\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/webinars/\">Webinars</a></li>\t<li id=\"menu-item-1708\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/news/\">News</a></li></ul></li><li id=\"menu-item-1207\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a>Solutions</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t<li id=\"menu-item-8901\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a>DQ for Cloud</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t\t<li id=\"menu-item-1906\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a>AWS</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t\t\t<li id=\"menu-item-1209\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/ai-driven-data-validation-in-aws/\">AWS S3</a></li>\t\t\t<li id=\"menu-item-1904\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/data-pipeline/\">AWS Glue</a></li>\t\t\t<li id=\"menu-item-1997\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/aws-deequ-lp/\">Advanced Deequ</a></li></ul></li>\t\t<li id=\"menu-item-2518\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/azure/\">Azure</a></li>\t\t<li id=\"menu-item-1330\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/snowflake-data-quality-lp/\">Snowflake</a></li>\t\t<li id=\"menu-item-3015\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/databricks-services/\">Databricks</a></li>\t\t<li id=\"menu-item-3032\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/ai-driven-gcp-data-validation-solution/\">Google Cloud</a></li></ul></li>\t<li id=\"menu-item-1202\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/data-catalog-lp/\">DQ for Catalog</a></li></ul></li><li id=\"menu-item-10401\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-has-children fl-has-submenu\"><div class=\"fl-has-submenu-container\"><a href=\"https://firsteigen.com/about/\">About</a><span class=\"fl-menu-toggle\"></span></div><ul class=\"sub-menu\">\t<li id=\"menu-item-24\" class=\"menu-item menu-item-type-post_type menu-item-object-page\"><a href=\"https://firsteigen.com/contact-us/\">Contact Us</a></li></ul></li><li id=\"menu-item-3515\" class=\"nav-bar_button menu-item menu-item-type-custom menu-item-object-custom\"><a href=\"https://calendly.com/seth-rao/autonomous-data-quality--intro\">SCHEDULE A DEMO</a></li></ul></nav></div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-scol96igxtka fl-col-small fl-col-small-full-width fl-visible-desktop\" data-node=\"scol96igxtka\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div id=\"header-button\" class=\"fl-module fl-module-uabb-button fl-node-257riadf6bgq\" data-node=\"257riadf6bgq\">\n\t<div class=\"fl-module-content fl-node-content\">\n\r\n<div class=\"uabb-module-content uabb-button-wrap uabb-creative-button-wrap uabb-button-width-auto uabb-creative-button-width-auto uabb-button-right uabb-creative-button-right uabb-button-tablet-center uabb-creative-button-tablet-center uabb-button-reponsive-center uabb-creative-button-reponsive-center\">\r\n\t\t\t<a href=\"https://calendly.com/seth-rao/autonomous-data-quality--intro?\" target=\"_blank\" rel=\"noopener\" class=\"uabb-button  uabb-creative-button uabb-creative-gradient-btn   \"  role=\"button\" aria-label=\"Book a Demo\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-button-text uabb-creative-button-text\">Book a Demo</span>\r\n\r\n\r\n\t\t</a>\r\n\t</div>\r\n\r\n\r\n\r\n\r\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n</header><div class=\"uabb-js-breakpoint\" style=\"display: none;\"></div>\t<div id=\"fl-main-content\" class=\"fl-page-content\" itemprop=\"mainContentOfPage\" role=\"main\">\n\n\t\t<div class=\"fl-builder-content fl-builder-content-5246 fl-builder-global-templates-locked\" data-post-id=\"5246\"><div id=\"hero-section-bg\" class=\"fl-row fl-row-full-width fl-row-bg-color fl-node-2ax8jvf46m1w\" data-node=\"2ax8jvf46m1w\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-blfecj2ostq7\" data-node=\"blfecj2ostq7\">\n\t\t\t<div class=\"fl-col fl-node-g4klpvh3z8d1\" data-node=\"g4klpvh3z8d1\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"fl-row fl-row-full-width fl-row-bg-none fl-node-hpi8g0fdt31x\" data-node=\"hpi8g0fdt31x\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-1twpdjm8qb4y\" data-node=\"1twpdjm8qb4y\">\n\t\t\t<div class=\"fl-col fl-node-5fz7m306irc2\" data-node=\"5fz7m306irc2\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-html fl-node-hdv2nzjclixy\" data-node=\"hdv2nzjclixy\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-html\">\n\n    <div id=\"progress-container\">\n        <div id=\"progress-bar\"></div>\n    </div>\n\n\n\n\n    </div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div id=\"flex-box\" class=\"fl-row fl-row-fixed-width fl-row-bg-photo fl-node-f9jqnlxgwt17\" data-node=\"f9jqnlxgwt17\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-mloqzjw3bvd8\" data-node=\"mloqzjw3bvd8\">\n\t\t\t<div class=\"fl-col fl-node-73mt8xu62sgp fl-col-small fl-col-small-full-width\" data-node=\"73mt8xu62sgp\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-html fl-node-xnsgv1ilufpo\" data-node=\"xnsgv1ilufpo\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-html\">\n\t<div class=\"blog-tag white-space blog-expert-button\"><a href=\"https://firsteigen.com/blog/category/post/data-quality/\" rel=\"tag\" class=\"data-quality\">Data Quality</a>\n</div></div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-photo fl-node-iodgc98z310b\" data-node=\"iodgc98z310b\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-photo fl-photo-crop-circle fl-photo-align-center\" itemscope itemtype=\"https://schema.org/ImageObject\">\n\t<div class=\"fl-photo-content fl-photo-img-webp\">\n\t\t\t\t<img decoding=\"async\" class=\"fl-photo-img wp-image-2811\" src=\"https://firsteigen.com/wp-content/uploads/bb-plugin/cache/Seth-circle.webp\" alt=\"Digital image representing Informatica data quality.\" itemprop=\"image\"  />\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-rich-text fl-node-3jb4wzx5vo6l\" data-node=\"3jb4wzx5vo6l\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p>Seth Rao</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-rich-text fl-node-79rhkcglj2tx\" data-node=\"79rhkcglj2tx\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p>CEO at FirstEigen</p>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-2jbcol5pgyt6\" data-node=\"2jbcol5pgyt6\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-info-box fl-node-zrbisykut4pj fw-400\" data-node=\"zrbisykut4pj\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"uabb-module-content uabb-infobox infobox-left \">\r\n\t<div class=\"uabb-infobox-left-right-wrap\">\r\n\t<div class=\"uabb-infobox-content\">\r\n\t\t\t<div class='uabb-infobox-title-wrap'><h1 class=\"uabb-infobox-title\">How to Improve Data Quality: 12 Essential Strategies for Effective Data Quality Improvement</h1></div>\t\t</div>\t</div>\r\n</div>\r\n\t</div>\n</div>\n<div class=\"fl-module fl-module-fl-post-info fl-node-47d1nytv5xm0\" data-node=\"47d1nytv5xm0\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<span class=\"fl-post-info-modified_date\">LAST UPDATED: Dec 16, 2024</span>\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"fl-row fl-row-fixed-width fl-row-bg-none fl-node-bpq2tscg7l9m blog-content-container\" data-node=\"bpq2tscg7l9m\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-1gdv53mbj7y8 fl-col-group-custom-width\" data-node=\"1gdv53mbj7y8\">\n\t\t\t<div id=\"flex-box\" class=\"fl-col fl-node-2g5cfumtql4v\" data-node=\"2g5cfumtql4v\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-pp-breadcrumbs fl-node-st5upc4efi3r\" data-node=\"st5upc4efi3r\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"pp-breadcrumbs pp-breadcrumbs-yoast\">\n\t<p id=\"breadcrumbs\"><span><span><a href=\"https://firsteigen.com/\">Home</a></span> › <span><a href=\"https://firsteigen.com/blog/\">Blog</a></span> › <span class=\"breadcrumb_last\" aria-current=\"page\">How to Improve Data Quality: 12 Essential Strategies for Effective Data Quality Improvement</span></span></p></div>\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div id=\"toc-dropdown\" class=\"fl-col fl-node-8ykjwed75aon fl-col-small fl-col-small-full-width\" data-node=\"8ykjwed75aon\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div id=\"toc-dropdown-container\" class=\"fl-module fl-module-uabb-table-of-contents fl-node-pw9ixakjeno3\" data-node=\"pw9ixakjeno3\">\n\t<div class=\"fl-module-content fl-node-content\">\n\r\n<div class=\"uabb-parent-wrapper-toc uabb-toc-hidden\">\r\n\t<div class=\"uabb-toc-container\">\r\n\t\t<div class =\"uabb-heading-block\">\r\n\t\t<span class=\"uabb-toc-heading\">Table of Contents</span>\r\n\t\t<div id=\"uabb-toc-toggle\" class=\"uabb-toggle-toc\" >\r\n\t\t<span class=\"uabb-icon\">\r\n\t\t\t<i class=\"ua-icon ua-icon-chevron-down2\"></i>\r\n\t\t</span>\r\n\t</div>\r\n\t</div>\r\n\t\t<div id=\"uabb-toc-togglecontents\">\r\n\t\t<div class=\"uabb-toc-content-heading\">\r\n\t\t\t\t\t<ul id=\"uabb-toc-wrapper\" class=\"toc-lists toc_none_bullet\" ></ul>\r\n\t\t\t\t\t</div>\r\n\t</div>\r\n\t<div class=\"uabb-toc-empty-note\">\r\n\t\t<span>Add a header to begin generating the table of contents</span>\r\n\t</div>\r\n\t\t</div>\r\n\t</div>\r\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-57uf4ylvrn9c fl-col-small fl-col-small-full-width social-share-icons\" data-node=\"57uf4ylvrn9c\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t\t</div>\n</div>\n\t</div>\n\n<div class=\"fl-col-group fl-node-0etnkpfidy2j fl-col-group-custom-width\" data-node=\"0etnkpfidy2j\">\n\t\t\t<div class=\"fl-col fl-node-jad2kixlcpzw fl-col-small fl-col-small-full-width blog-left-side-card\" data-node=\"jad2kixlcpzw\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div id=\"toc-wrapper\" class=\"fl-module fl-module-pp-toc fl-node-2nrga6j7p45o\" data-node=\"2nrga6j7p45o\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"pp-toc-container\">\n\t\t<div class=\"pp-toc-header\">\n\t\t<div class=\"pp-toc-header-title\">Table of Content</div>\n\t\t\t</div>\n\t<div class=\"pp-toc-separator\"></div>\n\n\t<div class=\"pp-toc-body\">\n\t\t\t\t\t<ul class=\"pp-toc-list-wrapper pp-toc-list-icon\"></ul>\n\t\t\t\t</div>\n</div>\n\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-0buq6ikasmg1 fl-col-has-cols blog-content\" data-node=\"0buq6ikasmg1\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div id=\"blog-content-section\" class=\"fl-module fl-module-fl-post-content fl-node-vwlx35kh4dya blog-content-container\" data-node=\"vwlx35kh4dya\">\n\t<div class=\"fl-module-content fl-node-content\">\n\n<p>Organizations need to improve <a href=\"https://firsteigen.com/blog/how-to-ensure-data-quality-in-your-data-lakes-pipelines-and-warehouses/\">data quality to ensure</a> they’re always using accurate and useful data. According to Experian&#8217;s <a href=\"https://edqcdncmsprod.azureedge.net/491de5/globalassets/white-papers/global-data-management-research-2021.pdf\"><em>2021 Global Data Management Research</em></a> report, businesses say that poor <a href=\"https://firsteigen.com/blog/6-key-data-quality-metrics-you-should-be-tracking/\">quality data</a>:</p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Wastes resources and increases costs (40%)</li>\n\n\n\n<li>Damages the reliability of analytics (36%)</li>\n\n\n\n<li>Negatively impacts reputation and customer trust (32%)</li>\n\n\n\n<li>Negatively affects the customer experience (32%)</li>\n\n\n\n<li>Slows down <a href=\"/blog/data-quality-management-key-to-digital-transformation-success\">digital transformation</a> and hinders key business initiatives (31%)</li>\n</ul>\n\n\n\n<p>Improving data quality resolves these issues and results in several significant benefits, including more </p>\n\n\n\n<p>reliable analytics, efficient operations, and reduced costs. That is why Gartner projects that in 2022, <a href=\"https://www.gartner.com/smarterwithgartner/how-to-improve-your-data-quality\">70% of organizations</a> will use <a href=\"https://firsteigen.com/2022/03/6-key-data-quality-metrics-you-should-be-tracking/\">key metrics</a> to track data quality – and, in doing so, improve it by 60%.&nbsp;</p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img decoding=\"async\" src=\"https://lh5.googleusercontent.com/6eTE5QVYUe4ikTyabwG4yzv8ITdzC4Jy7NZ-l68UBHprnN1p39pRk8vDn5N39yUoVWmHajIUUxXLjF42Ycczxf80MlGF4VYC1iMhf9kCXeBy8WzRNjB1XzZrDPp2BpmpBMfiMl3FRkKR39_dprg\" alt=\"Benefits of improving data quality.\"/></figure></div>\n\n\n<p class=\"has-text-align-center\"><strong><a href=\"https://financesonline.com/big-data-statistics/\">Data Source</a></strong></p>\n\n\n\n<p>There are several things you can do to <a href=\"https://firsteigen.com/blog/9-factors-to-improve-data-reliability/\">improve the data</a> quality in your organization. It isn&#8217;t difficult to do – if you&#8217;re committed to doing it.</p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Quick Takeaways</strong></h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>Ensuring high data quality provides more reliable analysis and more efficient operations</em></li>\n\n\n\n<li><em>To improve data quality, start by assessing your data and defining what is acceptable quality</em></li>\n\n\n\n<li><em>Organizations also need to eliminate data silos, correct errors upfront, and promote a data-driven culture</em></li>\n\n\n\n<li><em>The best way to improve data quality is to use an automated data quality management solution, such as DataBuck</em></li>\n</ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Understanding Data Quality Improvement</strong></h2>\n\n\n\n<p>The process to improve data quality involves several steps aimed at enhancing the accuracy, consistency, and reliability of data within an organization. This process typically includes identifying data issues, implementing data governance practices, and utilizing tools for data validation and cleansing.</p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Why Focus on Improving Data Quality?</strong></h3>\n\n\n\n<p>Improving data quality is essential for making informed decisions, optimizing operational efficiency, and increasing customer satisfaction. By following a structured process, organizations can ensure their data is accurate, complete, and ready to support business goals.</p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-improve-data-quality-in-your-organization\"><strong>How to Improve Data Quality in Your Organization?</strong></h2>\n\n\n\n<p>How to improve data quality in your organization is a critical concern for businesses aiming to enhance operational efficiency and decision-making. High-quality data leads to more accurate insights and better outcomes.&nbsp;</p>\n\n\n\n<p>Here are 12 effective steps your organization can take to improve data quality, ensuring that your business processes are both effective and efficient. By following these steps, you can enhance the reliability and usefulness of your data, ultimately driving better business performance.</p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img decoding=\"async\" src=\"https://lh3.googleusercontent.com/LeYsjO1YR-e8qocL8-kc-LSsxzBYJoUwy4Enb93BPzwItY-4Bc6hRiYorCY0xo6IqmwtLcO6PUuDEoTsGMeyTZkhtG15TT3-A3wqE0BoPJm6ujunC3lXD28EYxQoyBLmkKlgItfy6Z66JkJMEsM\" alt=\"Priority of improving data quality over next 6-12 months.\"/></figure></div>\n\n\n<p class=\"has-text-align-center\"><a href=\"https://edqcdncmsprod.azureedge.net/491de5/globalassets/white-papers/global-data-management-research-2021.pdf\">Data Source</a></p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-1-assess-your-data\"><strong>1. Assess Your Data</strong></h3>\n\n\n\n<p>Before you can improve the quality of your data, you have to understand what data you possess. That means conducting a formal data assessment to determine:</p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>What data you collect</li>\n\n\n\n<li>Where it is stored</li>\n\n\n\n<li>Who accesses it</li>\n\n\n\n<li>Current format (structured vs. unstructured, etc.)</li>\n</ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-2-define-acceptable-data-quality\"><strong>2. Define Acceptable Data Quality&nbsp;</strong></h3>\n\n\n\n<p>You also need to define what your organization considers <a href=\"https://marketinginsidergroup.com/strategy/how-to-clean-dirty-data-and-increase-your-marketing-roi/\">acceptable data quality</a>. If data cannot be 100% accurate and relevant, how close to perfect do you need to get? You may need to establish different data quality (DQ) standards for different data types and for different uses of that data.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-3-correct-data-errors-up-front\"><strong>3. Correct Data Errors Up Front</strong></h3>\n\n\n\n<p>Identifying and fixing data problems is part of any <a href=\"https://firsteigen.com/2022/02/data-quality-management-the-complete-guide/\">data quality management</a> (DQM) initiative. You make DQM easier, however, when you ingest clean data. That means designing systems to ensure more accurate data entry and flagging inaccurate or incomplete records before they enter the system.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-4-eliminate-data-silos\"><strong>4. Eliminate Data Silos</strong></h3>\n\n\n\n<p>A large enterprise often silos data within different departments or physical locations. When this happens, it is not easy to obtain a comprehensive view of your business or efficiently locate and use all the data you possess. Data silos, operating independently with their own rule sets, are also prone to <a href=\"/blog/10-common-data-quality-issues-and-how-to-solve-them/\">data quality issues</a>. You need to centralize all your data to make it more usable and to ensure that all data is subject to the same DQM processes and requirements.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-5-make-data-accessible-to-all-users\"><strong>5. Make Data Accessible to All Users</strong></h3>\n\n\n\n<p>Data silos also have the unfortunate effect of isolating valuable data from many of the employees who need it. The data you collect needs to be high quality and accessible to a broad range of potential users. This argues in favor of cloud-based file sharing that employees can access from any location, especially those working remotely.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-6-use-the-correct-data\"><strong>6. Use the Correct Data</strong></h3>\n\n\n\n<p>Your organization collects plenty of data – but are you collecting the correct information? Even more important, are you choosing the right input to use for your various analyses? You need to tap into a diverse collection of resources while filtering out those not relevant to your current needs. This also means capturing the right data in the first place – your data collection efforts need to reflect your projected data needs.</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-7-impose-a-defined-set-of-values-for-common-data\"><strong>7. Impose a Defined Set of Values for Common Data</strong></h3>\n\n\n\n<p>Many data errors come from users entering freeform data. Consider the scenario where you allow users to manually enter a state name. Some users enter “MN,” some enter “Minn,” some enter “Minnesota,” and others misspell it as “Minesota” – all of which lead to significant errors in your data. Instead, offer users a defined list of values or options for common fields so that, in this example, they can only select approved state abbreviations from a drop-down list. This will give a cleaner and more consistent data set than other methods.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-8-secure-your-data\"><strong>8. Secure Your Data</strong></h3>\n\n\n\n<p>You must secure valuable data from unauthorized access. You need to comply with relevant privacy regulations and other requirements to ensure customer data doesn&#8217;t fall into the wrong hands. This is especially true for protecting against data breaches and cyberattacks and ensuring the wrong users cannot edit data and compromise its integrity. This requires employing various data security methods while still enabling access to authorized users in your organization.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-9-promote-a-data-driven-culture\"><strong>9. Promote a Data-Driven Culture</strong></h3>\n\n\n\n<p>An effective data quality improvement process requires the participation of all your employees, from the C-suite to the administrative pool. Initiate regular training on data quality and key DQM processes to ensure that everyone does their part.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-10-appoint-a-data-steward\"><strong>10. Appoint a Data Steward</strong></h3>\n\n\n\n<p>If your organization truly takes data quality seriously, you should consider appointing a data steward to oversee your DQM efforts. This individual should be responsible for analyzing your data quality, conducting regular DQ reviews, and implanting new approaches to DQM. The data steward should also train your staff on DQM procedures and improve DQ over the long run.&nbsp;</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-11-conduct-regular-dq-reviews\"><strong>11. Conduct Regular DQ Reviews</strong></h3>\n\n\n\n<p>Finally, to ensure that your data quality improvement efforts retain their effectiveness, you should conduct regular reviews of your organization&#8217;s data quality. These reviews will tell you if you are making progress and, if not, where you need to make further improvements. These reviews should be the purview of your company&#8217;s data steward.</p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-12-employ-a-robust-data-quality-management-solution\"><strong>12. Employ a Robust Data Quality Management Solution</strong></h3>\n\n\n\n<p>Finally, one of the most effective ways to improve your organization&#8217;s data quality is to use an automated <a href=\"https://firsteigen.com/2022/02/how-to-automate-data-monitoring-3-key-areas-to-streamline/\">data monitoring solution</a>, such as FirstEigen&#8217;s DataBuck. An automated <a href=\"/blog/the-1-2-3-guide-to-data-quality-monitoring/\">DQM platform</a> automatically analyzes your data, identifies existing issues, and then &#8220;cleans&#8221; or deletes <a href=\"https://firsteigen.com/blog/cost-of-bad-data/\">bad data</a>. This is a much more effective and faster approach to DQM than trying to do it all manually.&nbsp;</p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Introduction to Databuck, autonomous cloud data quality monitoring &amp; validation by FirstEigen\" width=\"500\" height=\"281\" src=\"https://www.youtube.com/embed/2hMtdcF3ZAw?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n</div></figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-let-databuck-enhance-your-data-quality-improvement-process\"><strong>Let DataBuck Enhance Your Data Quality Improvement Process&nbsp;</strong></h2>\n\n\n\n<p>When you want to improve the quality of your firm&#8217;s data, turn to the experts at FirstEigen. Our <a href=\"https://firsteigen.com/databuck/\">DataBuck</a> software is an autonomous data quality management solution that automates more than 70% of the <a href=\"/blog/the-1-2-3-guide-to-data-quality-monitoring/\">data monitoring process</a>. When you use DataBuck, you know that your company&#8217;s data is complete, accurate, and utterly reliable.&nbsp;</p>\n\n\n\n<p><strong><em><a href=\"/contact-us/\">Contact FirstEigen</a> today to learn about using DataBuck to improve your organization&#8217;s data quality.</em></strong></p>\n\n\n\n<p>Check out these articles on Data Trustability, Observability &amp; Data Quality Management-</p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"/blog/6-key-data-quality-metrics-you-should-be-tracking/\">6 Key Data Quality Metrics You Should Be Tracking</a></li>\n\n\n\n<li><a href=\"/blog/how-to-scale-your-data-quality-operations-with-ai-and-ml/\">How to Scale Your Data Quality Operations with AI and ML?</a></li>\n\n\n\n<li><a href=\"/blog/12-things-you-can-do-to-improve-data-quality/\">12 Things You Can Do to Improve Data Quality</a></li>\n\n\n\n<li><a href=\"/blog/how-to-ensure-data-integrity-during-cloud-migrations/\">How to Ensure Data Integrity During Cloud Migrations?</a></li>\n\n\n\n<li><a href=\"/blog/data-reliability/\">How to Ensure Data Reliability?</a></li>\n</ul>\n\t</div>\n</div>\n\n<div class=\"fl-col-group fl-node-pf372qd0k1vz fl-col-group-nested\" data-node=\"pf372qd0k1vz\">\n\t\t\t<div id=\"faq-section-container\" class=\"fl-col fl-node-0m2citjspde5\" data-node=\"0m2citjspde5\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-heading fl-node-za3yhxjqwgpn\" data-node=\"za3yhxjqwgpn\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<h2 class=\"fl-heading\">\n\t\t<span class=\"fl-heading-text\">FAQs</span>\n\t</h2>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-pp-faq fl-node-f0habexzy7rd\" data-node=\"f0habexzy7rd\">\n\t<div class=\"fl-module-content fl-node-content\">\n\n<div class=\"pp-faq pp-faq-collapse\">\n\t\t\t<div id=\"pp-faq-f0habexzy7rd-1\" class=\"pp-faq-item\">\n\t\t\t<div class=\"pp-faq-button\">\n\n\t\t\t\t<span class=\"pp-faq-button-label\">\n\t\t\t\t\tWhat are the rules that help ensure the quality of data?\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-open ua-icon ua-icon-chevron-with-circle-down pp-faq-icon-right\"></span>\n\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-close ua-icon ua-icon-chevron-with-circle-up pp-faq-icon-right\"></span>\n\n\t\t\t</div>\n\t\t\t<div class=\"pp-faq-content fl-clearfix\">\n\t\t\t\t<div class=\"pp-faq-content-text\">\n\t\t\t\t\t<p></p>\n<p>To ensure data quality, follow these key rules:</p>\n<p></p>\n<p></p>\n<ol class=\"wp-block-list\"></p>\n<li><strong>Accuracy: </strong>Verify that data is correct and represents reality.</li>\n<p></p>\n<p></p>\n<li><strong>Consistency: </strong>Ensure uniformity across different datasets and systems.</li>\n<p></p>\n<p></p>\n<li><strong>Completeness:</strong> Check that all necessary data fields are filled.</li>\n<p></p>\n<p></p>\n<li><strong>Timeliness:</strong> Keep data updated and relevant.</li>\n<p></p>\n<p></p>\n<li><strong>Validity: </strong>Ensure data conforms to the required format and standards.</li>\n<p></ol>\n<p></p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t\t<div id=\"pp-faq-f0habexzy7rd-2\" class=\"pp-faq-item\">\n\t\t\t<div class=\"pp-faq-button\">\n\n\t\t\t\t<span class=\"pp-faq-button-label\">\n\t\t\t\t\tWhat is the first and most significant part of data quality?\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-open ua-icon ua-icon-chevron-with-circle-down pp-faq-icon-right\"></span>\n\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-close ua-icon ua-icon-chevron-with-circle-up pp-faq-icon-right\"></span>\n\n\t\t\t</div>\n\t\t\t<div class=\"pp-faq-content fl-clearfix\">\n\t\t\t\t<div class=\"pp-faq-content-text\">\n\t\t\t\t\t<p></p>\n<p>The most significant part of data quality is <strong>accuracy</strong>. Accurate data serves as the foundation for reliable analysis, decision-making, and operations. Without accuracy, other aspects like consistency and completeness lose their value.</p>\n<p></p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t\t<div id=\"pp-faq-f0habexzy7rd-3\" class=\"pp-faq-item\">\n\t\t\t<div class=\"pp-faq-button\">\n\n\t\t\t\t<span class=\"pp-faq-button-label\">\n\t\t\t\t\tHow to improve data quality in healthcare?\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-open ua-icon ua-icon-chevron-with-circle-down pp-faq-icon-right\"></span>\n\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-close ua-icon ua-icon-chevron-with-circle-up pp-faq-icon-right\"></span>\n\n\t\t\t</div>\n\t\t\t<div class=\"pp-faq-content fl-clearfix\">\n\t\t\t\t<div class=\"pp-faq-content-text\">\n\t\t\t\t\t<p></p>\n<p>To improve data quality in healthcare:</p>\n<p></p>\n<p></p>\n<ol class=\"wp-block-list\"></p>\n<li><strong>Implement Standardized Processes:</strong> Use uniform procedures for data entry and management.</li>\n<p></p>\n<p></p>\n<li><strong>Regular Audits:</strong> Conduct frequent checks to identify and correct errors.</li>\n<p></p>\n<p></p>\n<li><strong>Training:</strong> Educate staff on the importance of data accuracy and proper entry methods.</li>\n<p></p>\n<p></p>\n<li><strong>Use Automated Tools:</strong> Deploy software to detect and correct data issues in real time.</li>\n<p></ol>\n<p></p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t\t<div id=\"pp-faq-f0habexzy7rd-4\" class=\"pp-faq-item\">\n\t\t\t<div class=\"pp-faq-button\">\n\n\t\t\t\t<span class=\"pp-faq-button-label\">\n\t\t\t\t\tWhat Are the Essential Components of a Data Quality Improvement Strategy?\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-open ua-icon ua-icon-chevron-with-circle-down pp-faq-icon-right\"></span>\n\t\t\t\t\t<span class=\"pp-faq-button-icon pp-faq-close ua-icon ua-icon-chevron-with-circle-up pp-faq-icon-right\"></span>\n\n\t\t\t</div>\n\t\t\t<div class=\"pp-faq-content fl-clearfix\">\n\t\t\t\t<div class=\"pp-faq-content-text\">\n\t\t\t\t\t<p></p>\n<p>The essential components of a data quality improvement strategy include:</p>\n<p></p>\n<p></p>\n<ol class=\"wp-block-list\"></p>\n<li><strong>Data Assessment:</strong> Evaluating the current state of your data to identify errors, inconsistencies, and areas that need improvement.</li>\n<p></p>\n<p></p>\n<li><strong>Clear Objectives: </strong>Setting specific, measurable goals for data quality that align with business needs and objectives.</li>\n<p></p>\n<p></p>\n<li><strong>Data Governance: </strong>Establishing policies, procedures, and responsibilities to ensure data quality is maintained across the organization.</li>\n<p></p>\n<p></p>\n<li><strong>Data Quality Tools:</strong> Utilizing software solutions to automate data cleansing, validation, and monitoring processes.</li>\n<p></p>\n<p></p>\n<li><strong>Training and Awareness: </strong>Educating staff on the importance of data quality and how they can contribute to maintaining it.</li>\n<p></p>\n<p></p>\n<li><strong>Continuous Monitoring: </strong>Regularly reviewing and updating the strategy to adapt to new challenges and maintain high data quality standards.</li>\n<p></ol>\n<p></p>\n<p></p>\n<p>These components work together to ensure your data remains accurate, consistent, and reliable, supporting informed decision-making and business success.</p>\n<p></p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n<div class=\"fl-module fl-module-separator fl-node-6uq138id9k7j\" data-node=\"6uq138id9k7j\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-separator\"></div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-zeksupmg3ov7 fl-col-small fl-col-small-full-width fl-col-has-cols fl-visible-desktop blog-right-side-card\" data-node=\"zeksupmg3ov7\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-html fl-node-xp3v6wu4lekn social-share\" data-node=\"xp3v6wu4lekn\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-html\">\n\t<ul class=\"end-post-layout\">\n           <li class=\"social-link\">\n              <a href=\"http://www.facebook.com/sharer.php?u=https://firsteigen.com/blog/12-things-you-can-do-to-improve-data-quality/\" target=\"blank\" class=\"social-link-anchor facebook\">\n                 <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M16 8.04815C15.9998 6.51935 15.5616 5.02261 14.7372 3.73513C13.9128 2.44765 12.7368 1.42335 11.3483 0.783484C9.95985 0.143622 8.41713 -0.0850018 6.90277 0.124678C5.38842 0.334359 3.96585 0.973562 2.8035 1.96662C1.64114 2.95967 0.787659 4.26498 0.344106 5.72802C-0.0994477 7.19107 -0.114502 8.75057 0.300725 10.2219C0.715953 11.6932 1.54407 13.0148 2.68704 14.0301C3.83002 15.0454 5.23998 15.7119 6.75 15.9508V10.3608H4.71867V8.04815H6.75V6.28548C6.75 4.28082 7.94467 3.17282 9.77133 3.17282C10.3714 3.18142 10.9701 3.23356 11.5627 3.32882V5.29815H10.5533C10.2495 5.25782 9.94208 5.3398 9.69867 5.52607C9.45526 5.71235 9.2958 5.98766 9.25533 6.29148C9.2439 6.37657 9.24211 6.46267 9.25 6.54815V8.04815H11.4667L11.112 10.3608H9.24533V15.9508C11.128 15.6541 12.8429 14.695 14.0813 13.2462C15.3196 11.7974 16 9.95408 16 8.04815Z\" fill=\"white\"/>\n</svg>\n\n                 <span class=\"screen-reader-text\">Facebook</span>\n              </a>\n           </li>\n\n        <li class=\"social-link\">\n              <a href=\"https://twitter.com/share?url=https://firsteigen.com/blog/12-things-you-can-do-to-improve-data-quality/&text=How to Improve Data Quality: 12 Essential Strategies for Effective Data Quality Improvement\" target=\"blank\" class=\"social-link-anchor twitter\">\n                 <svg width=\"18\" height=\"16\" viewBox=\"0 0 18 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<g clip-path=\"url(#clip0_16_1151)\">\n<path d=\"M16.7527 0C14.7766 2.13889 12.7999 4.27722 10.8239 6.41611C10.7473 6.49895 10.673 6.5835 10.5851 6.68062C12.8976 9.77413 15.201 12.8562 17.5313 15.9743C17.4194 15.9834 17.3417 15.9949 17.2645 15.9954C15.7466 15.9966 14.2287 15.9931 12.7108 16.0006C12.5291 16.0017 12.4206 15.9452 12.3132 15.8001C10.8758 13.8668 9.43221 11.9382 7.98971 10.0089C7.92287 9.91981 7.85489 9.8324 7.75263 9.69815C7.66979 9.80269 7.61095 9.88781 7.54068 9.96379C5.7377 11.9142 3.93244 13.8622 2.13289 15.8155C2.01349 15.9452 1.89866 16.0063 1.71928 16.0006C1.30224 15.9869 0.8852 15.9966 0.46759 15.9971C0.46759 15.9783 0.46759 15.9594 0.46759 15.94C0.531574 15.8886 0.603556 15.8446 0.658971 15.7852C2.75445 13.5235 4.84821 11.26 6.94197 8.99604C6.99224 8.94176 7.03623 8.88178 7.09051 8.81608C7.02309 8.72239 6.96425 8.63784 6.90255 8.55558C5.56803 6.76974 4.23236 4.98504 2.89955 3.19863C2.10604 2.13318 1.31652 1.06602 0.52529 0C2.21401 0 3.9033 0 5.59259 0C7.02709 1.91952 8.46102 3.83847 9.9058 5.7717C9.96464 5.718 10.0012 5.68944 10.0315 5.6563C11.7259 3.82533 13.4204 1.99436 15.1131 0.161674C15.1553 0.115971 15.1811 0.0542721 15.2148 0C15.7272 0 16.2396 0 16.7521 0L16.7527 0ZM2.71389 1.10201C2.80929 1.23398 2.86642 1.31567 2.92583 1.39565C3.45941 2.11033 3.99357 2.82444 4.52772 3.53854C7.29388 7.23933 10.0618 10.9384 12.8222 14.6432C12.9622 14.8306 13.1044 14.9083 13.3398 14.9008C13.9088 14.8831 14.4784 14.8957 15.0479 14.8946C15.1279 14.8946 15.2079 14.8826 15.3307 14.8729C15.2422 14.7529 15.1822 14.67 15.1205 14.5878C13.8368 12.8705 12.5537 11.1526 11.27 9.43536C9.25339 6.73774 7.23733 4.04013 5.2184 1.34481C5.14813 1.25112 5.04702 1.11515 4.9579 1.11286C4.23179 1.09401 3.50455 1.10201 2.71446 1.10201H2.71389Z\" fill=\"white\"/>\n</g>\n<defs>\n<clipPath id=\"clip0_16_1151\">\n<rect width=\"17.0637\" height=\"16\" fill=\"white\" transform=\"translate(0.46814)\"/>\n</clipPath>\n</defs>\n</svg>\n\n                 <span class=\"screen-reader-text\">Twitter</span>\n              </a>\n           </li>\n\n          <li class=\"social-link\">\n              <a href=\"http://www.linkedin.com/sharing/share-offsite/?url=https://firsteigen.com/blog/12-things-you-can-do-to-improve-data-quality/\" target=\"blank\" class=\"social-link-anchor linkedin\">\n                 <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M15.3 0H0.7C0.3 0 0 0.3 0 0.7V15.4C0 15.7 0.3 16 0.7 16H15.4C15.8 16 16.1 15.7 16.1 15.3V0.7C16 0.3 15.7 0 15.3 0ZM4.7 13.6H2.4V6H4.8V13.6H4.7ZM3.6 5C2.8 5 2.2 4.3 2.2 3.6C2.2 2.8 2.8 2.2 3.6 2.2C4.4 2.2 5 2.8 5 3.6C4.9 4.3 4.3 5 3.6 5ZM13.6 13.6H11.2V9.9C11.2 9 11.2 7.9 10 7.9C8.8 7.9 8.6 8.9 8.6 9.9V13.7H6.2V6H8.5V7C8.8 6.4 9.6 5.8 10.7 5.8C13.1 5.8 13.5 7.4 13.5 9.4V13.6H13.6Z\" fill=\"white\"/>\n</svg>\n\n                 <span class=\"screen-reader-text\">LinkedIn</span>\n              </a>\n           </li>\n\n         <li class=\"social-link\">\n              <a href=\"mailto:?subject=Please%20visit%20this%20link%20https://firsteigen.com/blog/12-things-you-can-do-to-improve-data-quality/&body=Hello!%20I%20thought%20you%20would%20find%20this%20blog%20interesting:%20How to Improve Data Quality: 12 Essential Strategies for Effective Data Quality Improvement.%20Here%20is%20the%20website%20link:%20https://firsteigen.com/blog/12-things-you-can-do-to-improve-data-quality/.%20Thank%20you.\" target=\"blank\" class=\"social-link-anchor email\">\n                 <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<g clip-path=\"url(#clip0_16_1155)\">\n<path d=\"M14 1H2C1.46957 1 0.960859 1.21071 0.585786 1.58579C0.210714 1.96086 0 2.46957 0 3L0 3.4L8 7.9L16 3.5V3C16 2.46957 15.7893 1.96086 15.4142 1.58579C15.0391 1.21071 14.5304 1 14 1Z\" fill=\"white\"/>\n<path d=\"M7.5 9.89995L0 5.69995V13C0 13.5304 0.210714 14.0391 0.585786 14.4142C0.960859 14.7892 1.46957 15 2 15H14C14.5304 15 15.0391 14.7892 15.4142 14.4142C15.7893 14.0391 16 13.5304 16 13V5.69995L8.5 9.89995C8.3424 9.96919 8.17214 10.0049 8 10.0049C7.82786 10.0049 7.6576 9.96919 7.5 9.89995Z\" fill=\"white\"/>\n</g>\n<defs>\n<clipPath id=\"clip0_16_1155\">\n<rect width=\"16\" height=\"16\" fill=\"white\"/>\n</clipPath>\n</defs>\n</svg>\n\n                 <span class=\"screen-reader-text\">Mail</span>\n              </a>\n           </li>\n        </ul>\n\n        </div>\n\t</div>\n</div>\n\n<div class=\"fl-col-group fl-node-9u6gozy47be2 fl-col-group-nested\" data-node=\"9u6gozy47be2\">\n\t\t\t<div class=\"fl-col fl-node-j6v1xupmq5t7\" data-node=\"j6v1xupmq5t7\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-t2h1ruqvpyeb\" data-node=\"t2h1ruqvpyeb\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p style=\"text-align: left;\">Discover How Fortune 500 Companies Use DataBuck to Cut Data Validation Costs by 50%</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-button fl-node-sbl42rhixkyf\" data-node=\"sbl42rhixkyf\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-button-wrap fl-button-width-auto fl-button-center\">\n\t\t\t<a href=\"https://calendly.com/seth-rao/autonomous-data-quality--intro\" target=\"_blank\" class=\"fl-button\" role=\"button\" rel=\"noopener\" >\n\t\t\t\t\t\t\t<span class=\"fl-button-text\">Schedule DataBuck Demo Today!</span>\n\t\t\t\t\t</a>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n<div class=\"fl-module fl-module-pp-modal-box fl-node-kd47mbirzsv3\" data-node=\"kd47mbirzsv3\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t\t<div id=\"modal-kd47mbirzsv3\" class=\"pp-modal-wrap has-overlay-animation\" role=\"dialog\" aria-labelledby=\"modal-title-kd47mbirzsv3\">\n\t<div class=\"pp-modal-container\">\n\t\t\t\t<div class=\"pp-modal layout-standard\">\n\t\t\t\t\t\t<div class=\"pp-modal-body\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"pp-modal-header\">\n\t\t\t\t\t\t\t\t\t\t\t<p id=\"modal-title-kd47mbirzsv3\" class=\"pp-modal-title\"></p>\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"pp-modal-close box-top-right\" role=\"button\" tabindex=\"0\" aria-label=\"Close\">\n\t\t\t\t\t\t\t<div class=\"bar-wrap\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<span class=\"bar-1\"></span>\n\t\t\t\t\t\t\t\t<span class=\"bar-2\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"pp-modal-content\">\n\t\t\t\t\t<div class=\"pp-modal-content-inner\">\n\t\t\t\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4267 size-medium\" src=\"https://firsteigen.com/wp-content/uploads/2024/06/gmail-container-icon-283x300.webp\" alt=\"gmail icon container\" width=\"100\" height=\"106\" srcset=\"https://firsteigen.com/wp-content/uploads/2024/06/gmail-container-icon-283x300.webp 283w, https://firsteigen.com/wp-content/uploads/2024/06/gmail-container-icon.webp 401w\" sizes=\"auto, (max-width: 100px) 100vw, 100px\" /></p>\n<p style=\" font-size: 24px; line-height: 30px; \">Enter your email address to access to downloads</p>\n<p>\n                <div class='gf_browser_gecko gform_wrapper gravity-theme' id='gform_wrapper_12' >\n                        <div class='gform_heading'>\n                            <span class='gform_description'></span>\n                        </div><form method='post' enctype='multipart/form-data'  id='gform_12'  action='/blog/12-things-you-can-do-to-improve-data-quality/' >\n                        <div class='gform_body gform-body'><div id='gform_fields_12' class='gform_fields top_label form_sublabel_below description_below'><div id=\"field_12_3\"  class=\"gfield gfield_contains_required field_sublabel_below field_description_below gfield_visibility_visible\"  data-js-reload=\"field_12_3\"><label class='gfield_label' for='input_12_3' >Email<span class=\"gfield_required\"><span class=\"gfield_required gfield_required_text\">(Required)</span></span></label><div class='ginput_container ginput_container_email'>\n                            <input name='input_3' id='input_12_3' type='text' value='' class='large'    aria-required=\"true\" aria-invalid=\"false\"  />\n                        </div></div><div id=\"field_12_4\"  class=\"gfield gform_validation_container field_sublabel_below field_description_below gfield_visibility_visible\"  data-js-reload=\"field_12_4\"><label class='gfield_label' for='input_12_4' >Phone</label><div class='ginput_container'><input name='input_4' id='input_12_4' type='text' value='' /></div><div class='gfield_description' id='gfield_description_12_4'>This field is for validation purposes and should be left unchanged.</div></div></div></div>\n        <div class='gform_footer top_label'> <input type='submit' id='gform_submit_button_12' class='gform_button button gform-button--width-full' value='Submit'  onclick='if(window[\"gf_submitting_12\"]){return false;}  window[\"gf_submitting_12\"]=true;  ' onkeypress='if( event.keyCode == 13 ){ if(window[\"gf_submitting_12\"]){return false;} window[\"gf_submitting_12\"]=true;  jQuery(\"#gform_12\").trigger(\"submit\",[true]); }' />\n            <input type='hidden' class='gform_hidden' name='is_submit_12' value='1' />\n            <input type='hidden' class='gform_hidden' name='gform_submit' value='12' />\n\n            <input type='hidden' class='gform_hidden' name='gform_unique_id' value='' />\n            <input type='hidden' class='gform_hidden' name='state_12' value='WyJbXSIsIjNhNGE1NjdmZGI3ZjE3ZjYzNDA0NDM2MjM2NGYwZWM3Il0=' />\n            <input type='hidden' class='gform_hidden' name='gform_target_page_number_12' id='gform_target_page_number_12' value='0' />\n            <input type='hidden' class='gform_hidden' name='gform_source_page_number_12' id='gform_source_page_number_12' value='1' />\n            <input type='hidden' name='gform_field_values' value='' />\n\n        </div>\n                        </form>\n                        </div></p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<div class=\"pp-modal-overlay\"></div>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"fl-row fl-row-full-width fl-row-bg-none fl-node-yregpd9wzfi1 recent-posts\" data-node=\"yregpd9wzfi1\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-lfcke1i69waj\" data-node=\"lfcke1i69waj\">\n\t\t\t<div class=\"fl-col fl-node-basp4u0qtfm9\" data-node=\"basp4u0qtfm9\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-zwgp9a1q5v4k recent-posts\" data-node=\"zwgp9a1q5v4k\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p>Recent Posts</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-blog-posts fl-node-6qt830g4fhw9 related-post-container\" data-node=\"6qt830g4fhw9\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"uabb-module-content uabb-blog-posts uabb-blog-posts-carousel uabb-post-grid-3 \">\r\n\t\t<div class=\"uabb-blog-posts-col-3 uabb-post-wrapper   \">\r\n\t\t\t\t<div class=\"uabb-blog-posts-shadow clearfix\">\r\n\r\n\t\t\t<div class=\"uabb-blog-post-inner-wrap uabb-thumbnail-position-top  uabb-empty-img\">\r\n\r\n\t\t\t<div class=\"uabb-post-thumbnail uabb-crop-thumbnail \">\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"https://firsteigen.com/blog/managing-tariff-implications-through-data-integrity-in-global-supply-chains/\" target=\"_blank\" rel=\"noopener\" title=\"Managing Tariff Implications Through Data Integrity in Global Supply Chains\">\r\n\t\t\t\t<img decoding=\"async\" src=\"https://firsteigen.com/wp-content/uploads/bb-plugin/cache/postpic-custom_crop.png\" alt=\"\" />\r\n\t\t\t\t</a>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t<div class=\"uabb-blog-post-content\">\r\n\t\t\t\t\t\t<div class=\"uabb-post-heading uabb-blog-post-section\">\r\n\t\t\t\t<a href=https://firsteigen.com/blog/managing-tariff-implications-through-data-integrity-in-global-supply-chains/ title=\"Managing Tariff Implications Through Data Integrity in Global Supply Chains\" tabindex=\"0\" class=\"\">Managing Tariff Implications Through Data Integrity in Global Supply Chains</a>\t\t\t</div>\r\n\t\t\t\t\t\t\t<p class=\"uabb-post-meta uabb-blog-post-section\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-meta-date\">\r\n\t\t\t\t<i aria-hidden=\"true\" class=\"\"></i>\r\n\t\t\t25 Apr 2025\t\t\t</span>\r\n\t\t\t\t\t\t\t</p>\r\n\t\t\t\t\t\t\t\t\t<div class=\"uabb-blog-posts-description uabb-blog-post-section uabb-text-editor\">Introduction In today's global marketplace, supply chains span continents. From consumer electronics to industrial machinery, companies rely ...</div>\r\n\t\t\t\t\t<div class=\"uabb-blog-post-section\">\r\n<div class=\"uabb-module-content uabb-button-wrap uabb-creative-button-wrap uabb-button-width-auto uabb-creative-button-width-auto uabb-button-left uabb-creative-button-left uabb-button-has-icon uabb-creative-button-has-icon\">\r\n\t\t\t<a href=\"https://firsteigen.com/blog/managing-tariff-implications-through-data-integrity-in-global-supply-chains/\" target=\"_blank\" rel=\"noopener\" class=\"uabb-button ast-button uabb-creative-button uabb-creative-default-btn   \"  role=\"button\" aria-label=\"Learn more\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-button-text uabb-creative-button-text\">Learn more</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<i class=\"uabb-button-icon uabb-creative-button-icon uabb-button-icon-after uabb-creative-button-icon-after fas fa-arrow-circle-right\"></i>\r\n\r\n\r\n\t\t</a>\r\n\t</div>\r\n\r\n\r\n\r\n\r\n</div>\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t</div>\r\n\t</div>\r\n\t\t\t<div class=\"uabb-blog-posts-col-3 uabb-post-wrapper   \">\r\n\t\t\t\t<div class=\"uabb-blog-posts-shadow clearfix\">\r\n\r\n\t\t\t<div class=\"uabb-blog-post-inner-wrap uabb-thumbnail-position-top  uabb-empty-img\">\r\n\r\n\t\t\t<div class=\"uabb-post-thumbnail uabb-crop-thumbnail \">\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"https://firsteigen.com/blog/data-quality-issues-affecting-the-pharmaceutical-industry-finding-a-solution/\" target=\"_blank\" rel=\"noopener\" title=\"Data Quality Issues Affecting the Pharmaceutical Industry: Finding a Solution\">\r\n\t\t\t\t<img decoding=\"async\" src=\"https://firsteigen.com/wp-content/uploads/bb-plugin/cache/qtq80-aPGCrW-scaled-custom_crop.jpeg\" alt=\"Pharmaceutical Industry\" />\r\n\t\t\t\t</a>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t<div class=\"uabb-blog-post-content\">\r\n\t\t\t\t\t\t<div class=\"uabb-post-heading uabb-blog-post-section\">\r\n\t\t\t\t<a href=https://firsteigen.com/blog/data-quality-issues-affecting-the-pharmaceutical-industry-finding-a-solution/ title=\"Data Quality Issues Affecting the Pharmaceutical Industry: Finding a Solution\" tabindex=\"0\" class=\"\">Data Quality Issues Affecting the Pharmaceutical Industry: Finding a Solution</a>\t\t\t</div>\r\n\t\t\t\t\t\t\t<p class=\"uabb-post-meta uabb-blog-post-section\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-meta-date\">\r\n\t\t\t\t<i aria-hidden=\"true\" class=\"\"></i>\r\n\t\t\t16 Jan 2025\t\t\t</span>\r\n\t\t\t\t\t\t\t</p>\r\n\t\t\t\t\t\t\t\t\t<div class=\"uabb-blog-posts-description uabb-blog-post-section uabb-text-editor\">Pharmaceutical enterprises worldwide navigate a complex ecosystem where vast amounts of sensitive datasets are central to their ...</div>\r\n\t\t\t\t\t<div class=\"uabb-blog-post-section\">\r\n<div class=\"uabb-module-content uabb-button-wrap uabb-creative-button-wrap uabb-button-width-auto uabb-creative-button-width-auto uabb-button-left uabb-creative-button-left uabb-button-has-icon uabb-creative-button-has-icon\">\r\n\t\t\t<a href=\"https://firsteigen.com/blog/data-quality-issues-affecting-the-pharmaceutical-industry-finding-a-solution/\" target=\"_blank\" rel=\"noopener\" class=\"uabb-button ast-button uabb-creative-button uabb-creative-default-btn   \"  role=\"button\" aria-label=\"Learn more\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-button-text uabb-creative-button-text\">Learn more</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<i class=\"uabb-button-icon uabb-creative-button-icon uabb-button-icon-after uabb-creative-button-icon-after fas fa-arrow-circle-right\"></i>\r\n\r\n\r\n\t\t</a>\r\n\t</div>\r\n\r\n\r\n\r\n\r\n</div>\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t</div>\r\n\t</div>\r\n\t\t\t<div class=\"uabb-blog-posts-col-3 uabb-post-wrapper   \">\r\n\t\t\t\t<div class=\"uabb-blog-posts-shadow clearfix\">\r\n\r\n\t\t\t<div class=\"uabb-blog-post-inner-wrap uabb-thumbnail-position-top  uabb-empty-img\">\r\n\r\n\t\t\t<div class=\"uabb-post-thumbnail uabb-crop-thumbnail \">\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"https://firsteigen.com/blog/agentic-data-trust-next-frontier-for-data-management/\" target=\"_blank\" rel=\"noopener\" title=\"Agentic Data Trust: Next Frontier for Data Management\">\r\n\t\t\t\t<img decoding=\"async\" src=\"https://firsteigen.com/wp-content/uploads/bb-plugin/cache/data-management-scaled-custom_crop.webp\" alt=\"Data Management\" />\r\n\t\t\t\t</a>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t<div class=\"uabb-blog-post-content\">\r\n\t\t\t\t\t\t<div class=\"uabb-post-heading uabb-blog-post-section\">\r\n\t\t\t\t<a href=https://firsteigen.com/blog/agentic-data-trust-next-frontier-for-data-management/ title=\"Agentic Data Trust: Next Frontier for Data Management\" tabindex=\"0\" class=\"\">Agentic Data Trust: Next Frontier for Data Management</a>\t\t\t</div>\r\n\t\t\t\t\t\t\t<p class=\"uabb-post-meta uabb-blog-post-section\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-meta-date\">\r\n\t\t\t\t<i aria-hidden=\"true\" class=\"\"></i>\r\n\t\t\t05 Jan 2025\t\t\t</span>\r\n\t\t\t\t\t\t\t</p>\r\n\t\t\t\t\t\t\t\t\t<div class=\"uabb-blog-posts-description uabb-blog-post-section uabb-text-editor\">As data grows exponentially, ensuring accuracy, security, and compliance is increasingly challenging. Traditional rule-based data quality checks—whether ...</div>\r\n\t\t\t\t\t<div class=\"uabb-blog-post-section\">\r\n<div class=\"uabb-module-content uabb-button-wrap uabb-creative-button-wrap uabb-button-width-auto uabb-creative-button-width-auto uabb-button-left uabb-creative-button-left uabb-button-has-icon uabb-creative-button-has-icon\">\r\n\t\t\t<a href=\"https://firsteigen.com/blog/agentic-data-trust-next-frontier-for-data-management/\" target=\"_blank\" rel=\"noopener\" class=\"uabb-button ast-button uabb-creative-button uabb-creative-default-btn   \"  role=\"button\" aria-label=\"Learn more\">\r\n\t\t\t\t\t\t\t<span class=\"uabb-button-text uabb-creative-button-text\">Learn more</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<i class=\"uabb-button-icon uabb-creative-button-icon uabb-button-icon-after uabb-creative-button-icon-after fas fa-arrow-circle-right\"></i>\r\n\r\n\r\n\t\t</a>\r\n\t</div>\r\n\r\n\r\n\r\n\r\n</div>\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t</div>\r\n\t</div>\r\n\t\t\t</div>\r\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"fl-row fl-row-full-width fl-row-bg-gradient fl-node-6gibkn9ywutd\" data-node=\"6gibkn9ywutd\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-eqrykpdima65\" data-node=\"eqrykpdima65\">\n\t\t\t<div class=\"fl-col fl-node-sgkhp4f0vc9o\" data-node=\"sgkhp4f0vc9o\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-html fl-node-ryf9joe5dnqx\" data-node=\"ryf9joe5dnqx\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-html\">\n\n\n\n\n\n<div class=\"fl-builder-content fl-builder-content-5263 fl-builder-template fl-builder-row-template fl-builder-global-templates-locked\" data-post-id=\"5263\"><div class=\"fl-row fl-row-fixed-width fl-row-bg-gradient fl-node-7kheqzitonx6\" data-node=\"7kheqzitonx6\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-full-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-ihtlg1r8msv3\" data-node=\"ihtlg1r8msv3\">\n\t\t\t<div class=\"fl-col fl-node-4czyunvrhitd fl-col-has-cols\" data-node=\"4czyunvrhitd\">\n\t<div class=\"fl-col-content fl-node-content\">\n\n<div class=\"fl-col-group fl-node-ytj9gd8kvqcl fl-col-group-nested\" data-node=\"ytj9gd8kvqcl\">\n\t\t\t<div id=\"blog-cta-content\" class=\"fl-col fl-node-94ge278p1ybl\" data-node=\"94ge278p1ybl\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-3gp09no8mixq\" data-node=\"3gp09no8mixq\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p style=\"text-align: center;\">Get Started!</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-rich-text fl-node-kyoi5sb2z8xt footer-cta-text\" data-node=\"kyoi5sb2z8xt\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p style=\"text-align: center;\">Meet with our expert team and learn how FirstEigen can help you achieve high data quality with less effort.</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-button fl-node-x659n4upsg1f\" data-node=\"x659n4upsg1f\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-button-wrap fl-button-width-auto fl-button-center fl-button-has-icon\">\n\t\t\t<a href=\"https://calendly.com/seth-rao/autonomous-data-quality--intro\" target=\"_blank\" class=\"fl-button\" role=\"button\" rel=\"noopener\" >\n\t\t\t\t\t\t\t<span class=\"fl-button-text\">Talk to a DataBuck Expert</span>\n\t\t\t\t\t\t<i class=\"fl-button-icon fl-button-icon-after fas fa-calendar-alt\" aria-hidden=\"true\"></i>\n\t\t\t</a>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\n<div class=\"fl-col-group fl-node-s8zdtyho0axq fl-col-group-nested\" data-node=\"s8zdtyho0axq\">\n\t\t\t<div id=\"cta-bg\" class=\"fl-col fl-node-ns8y0of5lk43\" data-node=\"ns8y0of5lk43\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-photo fl-node-r427us5eoxt9\" data-node=\"r427us5eoxt9\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-photo fl-photo-align-center\" itemscope itemtype=\"https://schema.org/ImageObject\">\n\t<div class=\"fl-photo-content fl-photo-img-png\">\n\t\t\t\t<img decoding=\"async\" class=\"fl-photo-img\" src=\"https://firsteigen.com/wp-content/uploads/2024/08/Footer-bg.png\" alt=\"\" itemprop=\"image\"  />\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n</div><div class=\"uabb-js-breakpoint\" style=\"display: none;\"></div></div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n</div><div class=\"uabb-js-breakpoint\" style=\"display: none;\"></div>\n\t</div>\n\t<footer class=\"fl-builder-content fl-builder-content-7090 fl-builder-global-templates-locked\" data-post-id=\"7090\" data-type=\"footer\" itemscope=\"itemscope\" itemtype=\"http://schema.org/WPFooter\"><div id=\"footer_wrapper\" class=\"fl-row fl-row-full-width fl-row-bg-color fl-node-1glf2iosz0vt\" data-node=\"1glf2iosz0vt\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-vqjrxkco8sld\" data-node=\"vqjrxkco8sld\">\n\t\t\t<div class=\"fl-col fl-node-mcfra0wqi8oz fl-col-small fl-col-small-full-width\" data-node=\"mcfra0wqi8oz\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-photo fl-node-5osl3gjktydw\" data-node=\"5osl3gjktydw\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-photo fl-photo-align-\" itemscope itemtype=\"https://schema.org/ImageObject\">\n\t<div class=\"fl-photo-content fl-photo-img-webp\">\n\t\t\t\t<a href=\"/\" target=\"_self\" itemprop=\"url\">\n\t\t\t\t<img loading=\"lazy\" decoding=\"async\" class=\"fl-photo-img wp-image-7075 size-full\" src=\"https://firsteigen.com/wp-content/uploads/2024/10/FirstEigen-Logo.webp\" alt=\"FirstEigen Logo\" itemprop=\"image\" height=\"42\" width=\"185\" title=\"FirstEigen Logo\"  />\n\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-rich-text fl-node-uvc6gpidxbl1\" data-node=\"uvc6gpidxbl1\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p>1212 S Naper Ste 119-220<br />\nNaperville, IL 60540</p>\n</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-icon fl-node-81xjvscu3arg\" data-node=\"81xjvscu3arg\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-icon-wrap\">\n\t<span class=\"fl-icon\">\n\t\t\t\t\t\t\t\t<a href=\"tel:(385) 393-4436\" target=\"_self\" tabindex=\"-1\" aria-hidden=\"true\" aria-labelledby=\"fl-icon-text-81xjvscu3arg\">\n\t\t\t\t\t\t\t<i class=\"fas fa-phone-volume\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t</a>\n\t\t\t</span>\n\t\t\t<div id=\"fl-icon-text-81xjvscu3arg\" class=\"fl-icon-text\">\n\t\t\t\t\t\t<a href=\"tel:(385) 393-4436\" target=\"_self\" class=\"fl-icon-text-link fl-icon-text-wrap\">\n\t\t\t\t\t\t<p>(385) 393-4436</p>\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-icon fl-node-0k9w6exbiurt\" data-node=\"0k9w6exbiurt\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-icon-wrap\">\n\t<span class=\"fl-icon\">\n\t\t\t\t\t\t\t\t<a href=\"mailto:contact@firsteigen.com\" target=\"_self\" tabindex=\"-1\" aria-hidden=\"true\" aria-labelledby=\"fl-icon-text-0k9w6exbiurt\">\n\t\t\t\t\t\t\t<i class=\"ua-icon ua-icon-icon-3-mail-envelope-closed\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t</a>\n\t\t\t</span>\n\t\t\t<div id=\"fl-icon-text-0k9w6exbiurt\" class=\"fl-icon-text\">\n\t\t\t\t\t\t<a href=\"mailto:contact@firsteigen.com\" target=\"_self\" class=\"fl-icon-text-link fl-icon-text-wrap\">\n\t\t\t\t\t\t<p>contact@firsteigen.com</p>\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t</div>\n\t</div>\n</div>\n<div class=\"fl-module fl-module-pp-social-icons fl-node-fqrtck12sadm\" data-node=\"fqrtck12sadm\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"pp-social-icons pp-social-icons-left pp-social-icons-horizontal pp-responsive-center\">\n\t<span class=\"pp-social-icon\" itemscope itemtype=\"http://schema.org/Organization\">\n\n\t\t<a itemprop=\"sameAs\" href=\"https://www.linkedin.com/company/firsteigen/\" target=\"_blank\" title=\"LinkedIn\" aria-label=\"LinkedIn\" role=\"button\" rel=\"noopener\" >\n\t\t\t\t\t\t\t<i class=\"fab fa-linkedin\"></i>\n\t\t\t\t\t</a>\n\t</span>\n\t\t<span class=\"pp-social-icon\" itemscope itemtype=\"http://schema.org/Organization\">\n\n\t\t<a itemprop=\"sameAs\" href=\"https://www.youtube.com/@FirstEigenData\" target=\"_blank\" title=\"YouTube\" aria-label=\"YouTube\" role=\"button\" rel=\"noopener\" >\n\t\t\t\t\t\t\t<i class=\"fab fa-youtube\"></i>\n\t\t\t\t\t</a>\n\t</span>\n\t</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-fgh5jzvpdbek fl-col-small fl-col-small-full-width\" data-node=\"fgh5jzvpdbek\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-dsv9a1fzqgnt\" data-node=\"dsv9a1fzqgnt\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p><span class=\"footer_heading-text mb-16\">Products</span></p>\n<p><a href=\"/data-observability\">DataBuck Observability</a></p>\n<p><a href=\"/data-trustability\">DataBuck Trustability</a></p>\n<p><a href=\"/databuck\">DataBuck Data Quality</a></p>\n<p><a href=\"/data-matching-software\">DataBuck Data Matching</a></p>\n<p><a href=\"/data-pipeline\">DataBuck Data Pipeline</a></p>\n<p><a href=\"/eigenrules\">DataBuck Eigen Rules</a></p>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-x0yfi3qb4cv1 fl-col-small fl-col-small-full-width\" data-node=\"x0yfi3qb4cv1\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-do75lie62b0m\" data-node=\"do75lie62b0m\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p><span class=\"footer_heading-text mb-16\">Resources</span></p>\n<p><a href=\"/blog\">Blog</a></p>\n<p><a href=\"/white-papers\">White Papers</a></p>\n<p><a href=\"/case-studies\">Case studies</a></p>\n<p><a href=\"/reports\">Reports</a></p>\n<p><a href=\"/webinars\">Webinars</a></p>\n<p><a href=\"/news\">News</a></p>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-hgkci5rwftn8 fl-col-small fl-col-small-full-width\" data-node=\"hgkci5rwftn8\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-tju6sf73oyzd\" data-node=\"tju6sf73oyzd\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p><span class=\"footer_heading-text mb-16\">Solutions</span></p>\n<p><a href=\"/aws/\">DQ for Cloud</a></p>\n<p><a href=\"/azure/\">Azure</a></p>\n<p><a href=\"/snowflake-data-quality-lp/\">Snowflake</a></p>\n<p><a href=\"/databricks-services/\">Databricks</a></p>\n<p><a href=\"/gcp/\">Google Cloud</a></p>\n<p><a href=\"/data-catalog-lp\">DQ for Catlog</a></p>\n</div>\n\t</div>\n</div>\n\t</div>\n</div>\n\t\t\t<div class=\"fl-col fl-node-8n134pt67jme fl-col-small fl-col-small-full-width\" data-node=\"8n134pt67jme\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t<div class=\"fl-module fl-module-rich-text fl-node-akdeqx8tyzsu\" data-node=\"akdeqx8tyzsu\">\n\t<div class=\"fl-module-content fl-node-content\">\n\t\t<div class=\"fl-rich-text\">\n\t<p><span style=\"color: var(--accent-darker); font-size: 30px; font-weight: 500;\">Subscribe!</span></p>\n<p>Sign up for our newsletter!</p>\n</div>\n\t</div>\n</div>\n<div id=\"subscribe-button\" class=\"fl-module fl-module-pp-subscribe-form fl-node-zi4o1yvretca\" data-node=\"zi4o1yvretca\">\n\t<div class=\"fl-module-content fl-node-content\">\n\n\t<div class=\"pp-subscribe-form pp-subscribe-form-stacked pp-subscribe-form-name-show pp-form pp-clearfix\" >\n\n\n\t\t<div class=\"pp-subscribe-form-inner pp-clearfix\">\n\n\n\n\t\t\t\t<div class=\"pp-form-field pp-name-field\">\n\t\t\t\t\t\t\t\t\t<input id=\"pp-subscribe-form-name-zi4o1yvretca\" type=\"text\" name=\"pp-subscribe-form-name\" placeholder=\"Name\" value=\"\" />\n\t\t\t\t\t<div class=\"pp-form-error-message\">Please enter your name.</div>\n\t\t\t\t</div>\n\n\n\t\t\t<div class=\"pp-form-field pp-email-field\">\n\t\t\t\t\t\t\t<input id=\"pp-subscribe-form-email-zi4o1yvretca\" type=\"email\" name=\"pp-subscribe-form-email\" placeholder=\"Email Address\" value=\"\" />\n\t\t\t\t<div class=\"pp-form-error-message\">Please enter a valid email address.</div>\n\t\t\t</div>\n\n\n\n\n\t\t\t<div class=\"pp-form-button pp-button-wrap\" data-wait-text=\"Please Wait...\">\n\n\t\t\t\t<div class=\"fl-button-wrap fl-button-width-full fl-button-has-icon\">\n\t\t\t<a href=\"#\" target=\"_self\" class=\"fl-button pp-button\" role=\"button\">\n\t\t\t\t\t\t\t<span class=\"fl-button-text\">Subscribe!</span>\n\t\t\t\t\t\t<i class=\"fl-button-icon fl-button-icon-after fa fas fa-paper-plane\"></i>\n\t\t\t</a>\n</div>\n\n\t\t\t</div>\n\n\n\t\t\t<div class=\"pp-form-error-message\">Something went wrong. Please check your entries and try again.</div>\n\t\t</div>\n\t\t\t</div>\n\n\t</div>\n</div>\n\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"fl-row fl-row-full-width fl-row-bg-gradient fl-node-0oulxcez2fp7\" data-node=\"0oulxcez2fp7\">\n\t<div class=\"fl-row-content-wrap\">\n\t\t<div class=\"uabb-row-separator uabb-top-row-separator\" >\r\n</div>\r\n\t\t\t\t\t\t<div class=\"fl-row-content fl-row-fixed-width fl-node-content\">\n\n<div class=\"fl-col-group fl-node-2p3bz46gywn1\" data-node=\"2p3bz46gywn1\">\n\t\t\t<div class=\"fl-col fl-node-1piyjdoqrh2k\" data-node=\"1piyjdoqrh2k\">\n\t<div class=\"fl-col-content fl-node-content\">\n\t\t</div>\n</div>\n\t</div>\n\t\t</div>\n\t</div>\n</div>\n</footer><div class=\"uabb-js-breakpoint\" style=\"display: none;\"></div>\t</div>\n\r\n\r\n\n\n\n\n<a href=\"#\" id=\"fl-to-top\"><span class=\"sr-only\">Scroll To Top</span><i class=\"fas fa-chevron-up\" aria-hidden=\"true\"></i></a>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t\t</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-190/kimbrain.html",
    "content": "<!DOCTYPE html>\n<html lang=\"ko\">\n\n                                                                                <head>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<meta property=\"article:published_time\" content=\"2023-05-09T02:26:08+09:00\"/>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <title>(ROS2 기초)6. 코드 스타일, ROS2에서 파이썬으로 기초 코딩하기</title>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n                </head>\n\n\n                                                <body id=\"tt-body-page\" class=\"headerslogundisplayon headerbannerdisplayoff listmorenumber listmorebuttonmobile use-banner-wrp  use-menu-topnavmenu-wrp\">\n\n\n\n\n\n\n    <div id=\"wrap\" class=\"wrap-right\">\n\n\n      <header class=\"header\">\n\t\t\t\t<div class=\"line-bottom display-none\"></div>\n\n        <div class=\"inner-header  topnavmenu\">\n\n          <div class=\"box-header\">\n            <h1 class=\"title-logo\">\n              <a href=\"https://kimbrain.tistory.com/\" title=\"자동차 설계하기..\" class=\"link_logo\">\n\n\n                  자동차 설계하기..\n\n              </a>\n            </h1>\n\n\n\t\t\t\t\t\t<div class=\"util use-top\">\n\t\t\t\t\t\t\t<div class=\"search\">\n\t\t\t\t\t\t\t<input class=\"searchInput\" type=\"text\" name=\"search\" value=\"\" placeholder=\"Search...\" onkeypress=\"if (event.keyCode == 13) { requestSearch('.util.use-top .searchInput') }\"/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\n          </div>\n\n\n          <div class=\"area-align\">\n\n\n\n\n            <div class=\"area-gnb\">\n              <nav class=\"topnavmenu\">\n                <ul>\n  <li class=\"t_menu_link_1 first\"><a href=\"https://kimbrain.tistory.com/\" target=\"\">Home</a></li>\n  <li class=\"t_menu_link_2\"><a href=\"https://github.com/brainKimDu\" target=\"\">GitHub</a></li>\n  <li class=\"t_menu_link_3 last\"><a href=\"https://kimbrain.tistory.com/guestbook\" target=\"\">guestBook</a></li>\n</ul>\n              </nav>\n            </div>\n\n            <button type=\"button\" class=\"button-menu\">\n              <svg xmlns=\"//www.w3.org/2000/svg\" width=\"20\" height=\"14\" viewBox=\"0 0 20 14\">\n                <path fill=\"#333\" fill-rule=\"evenodd\" d=\"M0 0h20v2H0V0zm0 6h20v2H0V6zm0 6h20v2H0v-2z\" />\n              </svg>\n            </button>\n\n\n\n              <div class=\"area-promotion height400 bannermobile-on\" style=\"background-image:url('https://tistory1.daumcdn.net/tistory/5496518/skinSetting/a2ceaca7ab05451989d2e6f207ee3cdb');\">\n                <div class=\"inner-promotion\">\n                  <div class=\"box-promotion\">\n\n                      <strong style=\"color: #FFFFFF\">..</strong>\n\n\n\n\n                      <a href=\"https://kimbrain.tistory.com/43?category=578236\" class=\"link-promotion\">Cover Letter</a>\n\n\n\n                  </div>\n                </div>\n              </div>\n\n\n\n          </div>\n\n\n        </div>\n\n\n      </header>\n\n\n\n\n\n      <div id=\"container\">\n\n        <main class=\"main\">\n\n\n          <div class=\"area-main\">\n\n\n            <div class=\"area-common\" >\n\n            </div>\n\n\n\n            <div class=\"area-view\">\n\n\n\n\n\n\n\n    <div class=\"article-header\" thumbnail=\"https://img1.daumcdn.net/thumb/R1440x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcks6lf%2Fbtsesb05unl%2FAAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DCenpB3dGU42yGauZ7qn9UjHfGL0%253D\" style=\"background-image:url('')\">\n      <div class=\"inner-header\">\n        <div class=\"box-meta\">\n          <p class=\"category\">공부#Robotics#자율주행/(ROS2) 기초</p>\n          <h2 class=\"title-article\">(ROS2 기초)6. 코드 스타일, ROS2에서 파이썬으로 기초 코딩하기</h2>\n          <div class=\"box-info\">\n            <span class=\"writer\">BrainKimDu</span>\n            <span class=\"date\">2023. 5. 9. 02:26</span>\n          </div>\n        </div>\n      </div>\n    </div>\n\n\n    <div class=\"article-view\">\n\n\n\n\n            <div class=\"contents_style\"><p data-ke-size=\"size16\"><b>참고도서 1. ROS 2로 시작하는 로봇 프로그래밍. 표윤석 .&nbsp; 루비페이퍼 . 2021&nbsp;</b><b><br></b><b>참고도서 2.&nbsp;</b><span style=\"background-color: #ffffff;\"><span style=\"color: #333333;\"><b>ROS2 혼자공부하는 로봇SW 직접 만들고 코딩하자</b></span></span><b>&nbsp;. 민형기 .&nbsp; 잇플 . 2022</b><b><br></b><b>참고자료 1. PinkWink(민형기)님의 ROS2 강의자료</b><b><br></b><b>참고자료 2. ROS2 humble documeation</b><br>ROS2를 복습하는 김에 제대로 다시 한 번 정리하고 넘어가고 싶어서 이 시리즈를 작성합니다.&nbsp;<br>이 글은 Turtlebot3 패키지를 제대로 이해하고, 다양한 센서를 부착하면서 활용하는 것을 목표로 합니다.<br>또한 글이 끝날 때 ROS와 관련된 문제를 하나 만들어서 직접 풀어보도록 합니다.<br>이번 시간에는 ROS에서 코드 스타일과 Python으오 코딩하는 기초를 다룹니다.</p><hr data-ke-type=\"horizontalRule\" data-ke-style=\"style5\"><h1 style=\"background-color: #ffffff; color: #1f2328; text-align: start;\">ROS 프로그래밍 규칙</h1><p data-ke-size=\"size16\">이 부분은 표윤석님의 책에서 대부분의 내용을 발췌하여 작성됩니다.&nbsp;<br><a href=\"https://cafe.naver.com/openrt/24436\" target=\"_blank\"><span>https://cafe.naver.com/openrt/24436</span></a></p><figure data-ke-type=\"opengraph\" data-og-title=\"023 ROS 프로그래밍 규칙 (코드 스타일)\" data-ke-align=\"alignCenter\" data-og-description=\"Created Date: 2020.10.23 Modified Date: 2020.10.26 revision 11 * 로봇 운영체제 ROS 강좌 목차: https://cafe...\" data-og-host=\"cafe.naver.com\" data-og-source-url=\"https://cafe.naver.com/openrt/24436\" data-og-image=\"https://blog.kakaocdn.net/dna/bO7fMN/hySyq5CPRz/AAAAAAAAAAAAAAAAAAAAAAdXrSRyfWJiISAR-ByrBjWhh7rye0HKdE_jAkI8twyg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=TvzZOOhSx0jYsxKiC79u27KrIMo%3D\" data-og-url=\"https://cafe.naver.com/openrt/24436\"><a href=\"https://cafe.naver.com/openrt/24436\" target=\"_blank\" data-source-url=\"https://cafe.naver.com/openrt/24436\"><div class=\"og-image\" style=\"background-image: url('https://blog.kakaocdn.net/dna/bO7fMN/hySyq5CPRz/AAAAAAAAAAAAAAAAAAAAAAdXrSRyfWJiISAR-ByrBjWhh7rye0HKdE_jAkI8twyg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=TvzZOOhSx0jYsxKiC79u27KrIMo%3D')\"> </div><div class=\"og-text\"><p class=\"og-title\">023 ROS 프로그래밍 규칙 (코드 스타일)</p><p class=\"og-desc\">Created Date: 2020.10.23 Modified Date: 2020.10.26 revision 11 * 로봇 운영체제 ROS 강좌 목차: https://cafe...</p><p class=\"og-host\">cafe.naver.com</p></div></a></figure><p data-ke-size=\"size16\">&nbsp;<br><b>기본 이름 규칙</b><br>snake_case, CamelCased, ALL_CAPITALS와 같이 3종류의 네이밍을 기본으로 사용합니다.<br>파일 이름 및 변수명, 함수명에는 모두 소문자로 하며 <span style=\"color: #333333;\">snake_case을 사용합니다. </span>가독성을 해치는 축약어는 사용하지 않으며 확장자명은 모두 소문자로 표기합니다. 타입이나 클래스는 CamelCased 이름 규칙을 사용하고 상수는 ALL_CAPITALS 이름 규칙을 사용합니다. 단 인터페이스 파일명은 CamelCased 규칙을 따릅니다. 다만 앞서 살펴보았던 특정 목적에 따라 만들어지는 파일(패키지를 만들면 자동으로 만들어주는 파일)은 예외적으로 대소문자 규칙을 따르지 않고, 고유의 이름을 사용합니다.<br>이게 무슨 말인지, 위의 말을 <span style=\"color: #ee2323;\">ChatGPT</span>에 넣고 한 번 코드를 짜달라고 부탁을 해보겠습니다.<br><span style=\"background-color: #ffc1c8;\"><span style=\"color: #343541;\">ROS2의 코드 스타일에 대한 말입니다. 파일 이름 및 변수명, 함수명에는 모두 소문자로 하며 snake_case을 사용한다. 가독성을 해치는 축약어는 사용하지 않으며 확장자명은 모두 소문자로 표기한다. 타입이나 클래스는 CamelCased 이름 규칙을 사용하고 상수는 ALL_CAPITALS 이름 규칙을 사용한다. 단 인터페이스 파일명은 CamelCased 규칙을 따른다. 다만 앞서 살펴보았던 특정 목적에 따라 만들어지는 파일(패키지를 만들면 자동으로 만들어주는 파일)은 예외적으로 대소문자 규칙을 따르지 않고, 고유의 이름을 사용합니다. 위에서 설명하는 snake_case, CamelCased, ALL_CAPITALS란 무엇인지 설명해주시고, 위에서 설명한 예시가 모두 포함되는 예시코드를 C++과 python으로 작성해주세요</span></span><br><span style=\"background-color: #dddddd;\">snake_case: 단어들을 모두 소문자로 쓰고, 각 단어 사이에는 언더스코어(_)를 사용하여 단어를 구분하는 명명 규칙입니다. 예를 들어, \"ros2_node\"와 같은 변수나 함수 이름을 snake_case로 작성합니다.</span><br><span style=\"background-color: #dddddd;\">CamelCased: 단어의 첫 글자는 모두 대문자로 쓰고, 나머지 글자는 소문자로 쓰며, 단어들 사이에 공백 대신 대문자를 사용하여 단어를 구분하는 명명 규칙입니다. 예를 들어, \"Ros2Node\"와 같은 클래스 이름을 CamelCased로 작성합니다.</span><br><span style=\"background-color: #dddddd;\">ALL_CAPITALS: 모든 글자를 대문자로 쓰는 명명 규칙입니다. 예를 들어, \"MAX_ITERATIONS\"와 같은 상수 이름을 ALL_CAPITALS로 작성합니다.</span><br><span style=\"background-color: #dddddd;\">아래는 C++와 Python에서 ROS2 코드 스타일을 따르는 예시 코드입니다.</span><br><span style=\"background-color: #dddddd;\">C++ 예시:</span></p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>#include \"rclcpp/rclcpp.hpp\"\n\n// Snake_case를 사용한 변수 이름\nint my_variable = 0;\n\nclass MyNode : public rclcpp::Node\n{\npublic:\n&nbsp;&nbsp;MyNode() : Node(\"my_node\")\n&nbsp;&nbsp;{\n&nbsp;&nbsp;&nbsp;&nbsp;// CamelCased를 사용한 함수 이름\n&nbsp;&nbsp;&nbsp;&nbsp;timer_ = create_wall_timer(std::chrono::seconds(1), &amp;MyNode::myCallback, this);\n&nbsp;&nbsp;}\n\nprivate:\n&nbsp;&nbsp;// CamelCased를 사용한 멤버 변수 이름\n&nbsp;&nbsp;rclcpp::TimerBase::SharedPtr timer_;\n\n&nbsp;&nbsp;// CamelCased를 사용한 함수 이름\n&nbsp;&nbsp;void myCallback()\n&nbsp;&nbsp;{\n&nbsp;&nbsp;&nbsp;&nbsp;// ALL_CAPITALS를 사용한 상수 이름\n&nbsp;&nbsp;&nbsp;&nbsp;const int MAX_ITERATIONS = 10;\n&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 0; i &lt; MAX_ITERATIONS; i++) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RCLCPP_INFO(get_logger(), \"Iteration %d\", i);\n&nbsp;&nbsp;&nbsp;&nbsp;}\n&nbsp;&nbsp;}\n};\n\nint main(int argc, char **argv)\n{\n&nbsp;&nbsp;rclcpp::init(argc, argv);\n&nbsp;&nbsp;auto node = std::make_shared&lt;MyNode&gt;();\n&nbsp;&nbsp;rclcpp::spin(node);\n&nbsp;&nbsp;rclcpp::shutdown();\n&nbsp;&nbsp;return 0;\n}</code></pre><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\n\n# snake_case를 사용한 변수 이름\nmy_variable = 0\n\nclass MyNode(rclpy.node.Node):\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('my_node')\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# CamelCased를 사용한 함수 이름\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(1.0, self.my_callback)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# CamelCased를 사용한 함수 이름\n&nbsp;&nbsp;&nbsp;&nbsp;def my_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# ALL_CAPITALS를 사용한 상수 이름\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MAX_ITERATIONS = 10\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for i in range(MAX_ITERATIONS):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('Iteration %d' % i)\n\ndef main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n&nbsp;&nbsp;&nbsp;&nbsp;node = MyNode()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(node)\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()\n\nif __name__ == '__main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><p data-ke-size=\"size16\">&nbsp;<br>&nbsp;<br><b>C++ 의 ROS 스타일</b><br>C++의 코드 스타일은 Google C++ Style Guide를 사용하고 있으며, ROS의 특성에 따라 일부를 수정해서 사용한다.<br>1. 기본규칙 : C++ 14 Standard를 준수한다.<br>2. 라인 길이 : 최대 100문자<br>3. 이름 규칙<br>- CamelCased, snake_case, ALL_CAPITALS만을 사용한다.<br>CamelCased : 타입, 클래스, 구조체, 열거형<br>snake_case : 파일, 패키지, 인터페이스, 네임스페이스, 변수, 함수, 메소드<br>ALL_CAPITALS&nbsp; : 상수, 메크로<br>- 소스 파일은 cpp 확장자를 사용한다.<br>- 헤더파일은 hpp 확장자를 사용한다.<br>- 전역변수를 반드시 사용해야할 경우 접두어 g_ 를 붙인다.<br>- 클래스 맴버 변수는 마지막줄에 밑줄 _을 붙인다.<br>4. 공백문자 대 탭<br>- 기본 들여쓰기는 공백 문자 2개를 사용한다 (탭을 사용하지 않는다.)<br>- Class 의 접근지정자는 들여쓰기 하지 않는다.&nbsp;<br>5. 괄호<br>- if, else, do, while, for 구문에 괄호를 사용한다.<br>- 괄호를 사용하는 올바른 방법은 다음을 참고한다.<br>먼저 C++에서의 코딩 방법이다.</p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>int main(int argc, char **argv)\n{\n&nbsp;&nbsp;if (condition){\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return 0;\n&nbsp;&nbsp;&nbsp;&nbsp;} else {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return 1;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n}\n\nif (this &amp;&amp; that || both){\n\n}\n\n// 긴 조건문인 경우\nif (\n&nbsp;&nbsp;this &amp;&amp; that || both &amp;&amp; this &amp;&amp; that || both %% this &amp;&amp; that || both\n)\n{\n\n}\n\n// 짧은 함수 호출문\ncall_func(foo, bar);\n\n// 긴 함수 호출문\ncall_func(\n&nbsp;&nbsp;foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar,\n&nbsp;&nbsp;foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar);\n\n\n// 매우 긴 함수 인수를 사용해야하는 경우, 가독성을 위해 개행\ncall_func(\n&nbsp;&nbsp;bang,\n&nbsp;&nbsp;fooooooooooooooooooooooooooooooooo,\n&nbsp;&nbsp;bar, bat);\n\nReturnType LongClassName::ReallyReallyReallyReallyLongFunctonName(\n&nbsp;&nbsp;Type par_name1,&nbsp;&nbsp;//2 스페이스 들여쓰기\n&nbsp;&nbsp;Type par_name1,\n&nbsp;&nbsp;Type par_name1,)\n{\n&nbsp;&nbsp;DoSomething();\n}\n\nMyClass::MyClass(int var)\n: some_var_(var), // 첫 구문은 들여쓰기를 사용하지 않고, 이후부터는 스페이스 들여쓰기\n&nbsp;&nbsp;some_other_var_(var+1)\n{\n&nbsp;&nbsp;Dosomething();\n}</code></pre><p data-ke-size=\"size16\">6. 주석<br>- 문서 주석은 /** */를 사용한다.<br>- 구현 주석은 //을 사용한다.<br>7. 린터(Linters)<br>- C++ 코드 스타일의 자동 오류 검출을 위하여 ament_cppint, ament_uncrustify를 사용하고 정적 코드 분석이 필요한 경우 ament_cppcheck를 사용한다.<br>8. 기타<br>- Boost 라이브러리의 사용은 가능한 피하고 어쩔 수 없을 경우에만 사용한다.<br>- 포인터 구문은 char * c 처럼 사용하며 char* c나 char *c를 허용하지 않는다.<br>- 중첩 템플릿은 set&lt;list&lt;string&gt;&gt; 처럼 사용한다.<br>&nbsp;<br><b>Python Style</b><br>Python 코드 스타일은 Python Enhancement Proposals의 PEP 8을 준수한다.<br>1. 기본 규칙<br>- 파이썬 3(파이썬 3.5이상)을 사용한다,<br>2. 라인 길이<br>- 최대 100문자<br>3. 이름 규칙(Naming)<br>- CamelCased, snake_case, ALL_CAPITALS만을 사용한다.<br>CamelCased : 타입, 클래스<br>snake_case : 파일, 패키지, 인터페이스, 모듈, 변수, 함수, 메소드<br>ALL_CAPITALS : 상수<br>4. 공백 문자 대 탭<br>- 기본 들여쓰기는 공백 문자 4개를 사용한다 (탭 사용금지)<br>- Hanging indent(문자 중간에 들여쓰기를 사용하는 형식)의 사용 방법은 다음 예제를 보자.<br>- 괄호 및 공백 사용은 다음 예제를 참고하자.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>foo = function(var_one, var_two, var_three, var_four)\n\ndef long_long_long_long_function_name(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var_one;\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var_two,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var_three,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var_four):\n&nbsp;&nbsp;&nbsp;&nbsp;\n&nbsp;&nbsp;&nbsp;&nbsp;print(var_one)</code></pre><p data-ke-size=\"size16\">다음의 방식만 허용되며 이 외는 허용하지 않는다.<br>5. 괄호<br>- 괄호는 계산식 및 배열 인덱스로 사용하며, 자료형에 따라 적절한 괄호를 사용한다.<br>6. 주석<br>- 문서 주석은 \"\"\"을 사용하며 Docstring Conventions을 기술한 PEP 257을 준수한다.<br>- 구현 주석은 #을 사용한다.<br>7. 린터(Linters)<br>- 파이썬 코드 스타일의 자동 오류 검출을 위해 ament_flake8을 사용하자.<br>8. 기타<br>- 모든 문자는 큰 따옴표(\")가 아닌 작은 따옴표(')를 사용하여 표현한다.<br>&nbsp;</p><h1 style=\"background-color: #ffffff; color: #1f2328; text-align: start;\">ROS 프로그래밍 기초 (파이썬)</h1><p data-ke-size=\"size16\">이전시간에 우리는 파이썬 패키지와 C++ 패키지를 만들었습니다. 그 패키지에서 이어서 진행을 합니다. 우선 첫시간에 설정했던 VScode가 있어야 합니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"734\" data-origin-height=\"102\"><span data-url=\"https://blog.kakaocdn.net/dna/cks6lf/btsesb05unl/AAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=CenpB3dGU42yGauZ7qn9UjHfGL0%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/cks6lf/btsesb05unl/AAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=CenpB3dGU42yGauZ7qn9UjHfGL0%3D\"><img src=\"https://blog.kakaocdn.net/dna/cks6lf/btsesb05unl/AAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=CenpB3dGU42yGauZ7qn9UjHfGL0%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcks6lf%2Fbtsesb05unl%2FAAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DCenpB3dGU42yGauZ7qn9UjHfGL0%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"734\" height=\"102\" data-origin-width=\"734\" data-origin-height=\"102\"/></span></figure>\n<p data-ke-size=\"size16\">이렇게 하면 VScode가 열리게 됩니다. 코딩을 하면서 주의할 점이라면 ROS와 VScode가 조금 어디선가 어긋나는 부분이 있기 때문에 VScode 는 오류라고 뱉는 경우가 있지만 정상적으로 돌아가는 경우가 있습니다. ROS에서부터는 짜증나더라도 일단 colcon build에서 문제가 생기지 않는다면 무시해도 좋습니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1242\" data-origin-height=\"377\"><span data-url=\"https://blog.kakaocdn.net/dna/c7crb2/btsescMsPVc/AAAAAAAAAAAAAAAAAAAAAD4FeeKhgmtEgjyQL4mScQ01nLZOkj4j2P4e9nyoOgjU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=syHi4FnKl2w%2F%2Fz%2BNsXBtDaWAG4A%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/c7crb2/btsescMsPVc/AAAAAAAAAAAAAAAAAAAAAD4FeeKhgmtEgjyQL4mScQ01nLZOkj4j2P4e9nyoOgjU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=syHi4FnKl2w%2F%2Fz%2BNsXBtDaWAG4A%3D\"><img src=\"https://blog.kakaocdn.net/dna/c7crb2/btsescMsPVc/AAAAAAAAAAAAAAAAAAAAAD4FeeKhgmtEgjyQL4mScQ01nLZOkj4j2P4e9nyoOgjU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=syHi4FnKl2w%2F%2Fz%2BNsXBtDaWAG4A%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fc7crb2%2FbtsescMsPVc%2FAAAAAAAAAAAAAAAAAAAAAD4FeeKhgmtEgjyQL4mScQ01nLZOkj4j2P4e9nyoOgjU%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DsyHi4FnKl2w%252F%252Fz%252BNsXBtDaWAG4A%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1242\" height=\"377\" data-origin-width=\"1242\" data-origin-height=\"377\"/></span></figure>\n<p data-ke-size=\"size16\">다음의 모습에서 ros_study_py 폴더 안에 파이썬 파일을 하나 만듭시다. my_node.py 라는 파일을 하나 만듭니다. 그리고 다음과 같이 작성합니다.</p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>def main():\n&nbsp;&nbsp;&nbsp;&nbsp;print(\"Hello World\")\n\nif __name__ == 'main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1242\" data-origin-height=\"377\"><span data-url=\"https://blog.kakaocdn.net/dna/MSv23/btsequ8a6ZL/AAAAAAAAAAAAAAAAAAAAAG8ldLjZ8cD1M1cPvqmllC2uN7ApvMit3acbcsci8fAn/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Y7FaxkIv%2BMpMmYN6JU6zMhRZtpU%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/MSv23/btsequ8a6ZL/AAAAAAAAAAAAAAAAAAAAAG8ldLjZ8cD1M1cPvqmllC2uN7ApvMit3acbcsci8fAn/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Y7FaxkIv%2BMpMmYN6JU6zMhRZtpU%3D\"><img src=\"https://blog.kakaocdn.net/dna/MSv23/btsequ8a6ZL/AAAAAAAAAAAAAAAAAAAAAG8ldLjZ8cD1M1cPvqmllC2uN7ApvMit3acbcsci8fAn/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Y7FaxkIv%2BMpMmYN6JU6zMhRZtpU%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FMSv23%2Fbtsequ8a6ZL%2FAAAAAAAAAAAAAAAAAAAAAG8ldLjZ8cD1M1cPvqmllC2uN7ApvMit3acbcsci8fAn%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DY7FaxkIv%252BMpMmYN6JU6zMhRZtpU%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1242\" height=\"377\" data-origin-width=\"1242\" data-origin-height=\"377\"/></span></figure>\n<p data-ke-size=\"size16\">그리고 항상 기억해야 할 것이 저장입니다. 저장을 안하고 빌드를 하면, 이전의 상태로 다시 빌드하는 것과 같습니다. 자 이제 빌드를 진행해봅시다.<br>ROS환경을 불러오고 colcon build한 후에&nbsp;</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"732\" data-origin-height=\"390\"><span data-url=\"https://blog.kakaocdn.net/dna/bXlk05/btsepNmVP1E/AAAAAAAAAAAAAAAAAAAAAEC-FgvI3MdLURD7Nl3pmJjpKgc_j4vLwqrH9slDbpYS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=etHcr3%2FO5JPITlCbTUpR3sVQCdc%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bXlk05/btsepNmVP1E/AAAAAAAAAAAAAAAAAAAAAEC-FgvI3MdLURD7Nl3pmJjpKgc_j4vLwqrH9slDbpYS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=etHcr3%2FO5JPITlCbTUpR3sVQCdc%3D\"><img src=\"https://blog.kakaocdn.net/dna/bXlk05/btsepNmVP1E/AAAAAAAAAAAAAAAAAAAAAEC-FgvI3MdLURD7Nl3pmJjpKgc_j4vLwqrH9slDbpYS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=etHcr3%2FO5JPITlCbTUpR3sVQCdc%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbXlk05%2FbtsepNmVP1E%2FAAAAAAAAAAAAAAAAAAAAAEC-FgvI3MdLURD7Nl3pmJjpKgc_j4vLwqrH9slDbpYS%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DetHcr3%252FO5JPITlCbTUpR3sVQCdc%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"732\" height=\"390\" data-origin-width=\"732\" data-origin-height=\"390\"/></span></figure>\n<p data-ke-size=\"size16\">다시 메타 패키지 환경을 불러와야합니다. 이 파일은 ./install/setup.bash에 있습니다. 주의하실 점이라면 Ros환경과 메타 패키지 환경이 둘 다 불러와 있어야 됩니다.</p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>source ./install/setup.bash</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"734\" data-origin-height=\"119\"><span data-url=\"https://blog.kakaocdn.net/dna/UUAyv/btsequG5NAT/AAAAAAAAAAAAAAAAAAAAAIrC17lg-zjaPQvEvE0gF_VFFNVQhMIWwjQCNY6bEPjr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=yYSNvg8Cx2TNoOt82plsagIGuOQ%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/UUAyv/btsequG5NAT/AAAAAAAAAAAAAAAAAAAAAIrC17lg-zjaPQvEvE0gF_VFFNVQhMIWwjQCNY6bEPjr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=yYSNvg8Cx2TNoOt82plsagIGuOQ%3D\"><img src=\"https://blog.kakaocdn.net/dna/UUAyv/btsequG5NAT/AAAAAAAAAAAAAAAAAAAAAIrC17lg-zjaPQvEvE0gF_VFFNVQhMIWwjQCNY6bEPjr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=yYSNvg8Cx2TNoOt82plsagIGuOQ%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FUUAyv%2FbtsequG5NAT%2FAAAAAAAAAAAAAAAAAAAAAIrC17lg-zjaPQvEvE0gF_VFFNVQhMIWwjQCNY6bEPjr%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DyYSNvg8Cx2TNoOt82plsagIGuOQ%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"734\" height=\"119\" data-origin-width=\"734\" data-origin-height=\"119\"/></span></figure>\n<p data-ke-size=\"size16\">그러나 방금 작성한 파일이 터미널에서 보이지 않습니다. 이유가 뭘까요?</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1115\" data-origin-height=\"591\"><span data-url=\"https://blog.kakaocdn.net/dna/bNRuFy/btseqbVnkNy/AAAAAAAAAAAAAAAAAAAAAOYvlZcZrZiF_5ck29ll2p6wXD6j6dMDgqNyIZvqeTUx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=1f%2Fa8G69XYWj0%2FJ4%2B3gMlv1X3M8%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bNRuFy/btseqbVnkNy/AAAAAAAAAAAAAAAAAAAAAOYvlZcZrZiF_5ck29ll2p6wXD6j6dMDgqNyIZvqeTUx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=1f%2Fa8G69XYWj0%2FJ4%2B3gMlv1X3M8%3D\"><img src=\"https://blog.kakaocdn.net/dna/bNRuFy/btseqbVnkNy/AAAAAAAAAAAAAAAAAAAAAOYvlZcZrZiF_5ck29ll2p6wXD6j6dMDgqNyIZvqeTUx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=1f%2Fa8G69XYWj0%2FJ4%2B3gMlv1X3M8%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbNRuFy%2FbtseqbVnkNy%2FAAAAAAAAAAAAAAAAAAAAAOYvlZcZrZiF_5ck29ll2p6wXD6j6dMDgqNyIZvqeTUx%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D1f%252Fa8G69XYWj0%252FJ4%252B3gMlv1X3M8%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1115\" height=\"591\" data-origin-width=\"1115\" data-origin-height=\"591\"/></span></figure>\n<p data-ke-size=\"size16\">setup.py에서 entry_points를 지정해주어야 터미널상에서 접근이 가능합니다. 다음과 같이 작성합니다.&nbsp;</p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>'my_node = ros_study_py.my_node:main'</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1115\" data-origin-height=\"591\"><span data-url=\"https://blog.kakaocdn.net/dna/dR5SSD/btseqs3BTqp/AAAAAAAAAAAAAAAAAAAAABYVrpBM5GHwL6l3DTSDNNCOo9XfT9S74zRVNPgoRKls/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jX7sIqAzJyqvtpWdhqeUZOeFE4Q%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/dR5SSD/btseqs3BTqp/AAAAAAAAAAAAAAAAAAAAABYVrpBM5GHwL6l3DTSDNNCOo9XfT9S74zRVNPgoRKls/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jX7sIqAzJyqvtpWdhqeUZOeFE4Q%3D\"><img src=\"https://blog.kakaocdn.net/dna/dR5SSD/btseqs3BTqp/AAAAAAAAAAAAAAAAAAAAABYVrpBM5GHwL6l3DTSDNNCOo9XfT9S74zRVNPgoRKls/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jX7sIqAzJyqvtpWdhqeUZOeFE4Q%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FdR5SSD%2Fbtseqs3BTqp%2FAAAAAAAAAAAAAAAAAAAAABYVrpBM5GHwL6l3DTSDNNCOo9XfT9S74zRVNPgoRKls%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DjX7sIqAzJyqvtpWdhqeUZOeFE4Q%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1115\" height=\"591\" data-origin-width=\"1115\" data-origin-height=\"591\"/></span></figure>\n<p data-ke-size=\"size16\">그리고 다시 빌드를 하고 실행해봅니다.</p><pre data-ke-type=\"codeblock\" class=\"cpp\" data-ke-language=\"cpp\"><code>ros2 run ros_study_py my_node</code></pre><p data-ke-size=\"size16\">다음의 명령어를 실행합니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"733\" data-origin-height=\"403\"><span data-url=\"https://blog.kakaocdn.net/dna/Z7Rod/btsesfCxJoT/AAAAAAAAAAAAAAAAAAAAAATuVF9rVVuabsOngIJtySmpQ4tB34ElNLp68Pcy3vI_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=IqUmMbxYCHOzzKNbJKy%2ByJFBkFc%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/Z7Rod/btsesfCxJoT/AAAAAAAAAAAAAAAAAAAAAATuVF9rVVuabsOngIJtySmpQ4tB34ElNLp68Pcy3vI_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=IqUmMbxYCHOzzKNbJKy%2ByJFBkFc%3D\"><img src=\"https://blog.kakaocdn.net/dna/Z7Rod/btsesfCxJoT/AAAAAAAAAAAAAAAAAAAAAATuVF9rVVuabsOngIJtySmpQ4tB34ElNLp68Pcy3vI_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=IqUmMbxYCHOzzKNbJKy%2ByJFBkFc%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FZ7Rod%2FbtsesfCxJoT%2FAAAAAAAAAAAAAAAAAAAAAATuVF9rVVuabsOngIJtySmpQ4tB34ElNLp68Pcy3vI_%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DIqUmMbxYCHOzzKNbJKy%252ByJFBkFc%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"733\" height=\"403\" data-origin-width=\"733\" data-origin-height=\"403\"/></span></figure>\n<p data-ke-size=\"size16\">정말 간단한 원리입니다. 그냥 Ros로 my_node.py 파일을 실행한 것입니다. 이 파일이 어디 있는지 찾지를 못해서 실행이 안되었던 것이 였습니다. setup.py에서 entry_points를 잡아주니 파일을 찾게 되었고 (실행가능하게 ) Ros상에서 실행이 가능하게 되었습니다.&nbsp;<br>근데, 내가 ROS를 다룰 줄 안다 이야기할 수 있다면 최소한 topic 을 직접 발행하고, 구독하는 정도의 코드는 짤 줄 알아야합니다. 그러나 지금당장 코드를 짜기에는 난이도가 있습니다. 그러니 먼저 예시코드를 Documentation에서 가져오도록 하겠습니다.<a href=\"https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html\" target=\"_blank\"><span>https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html</span></a></p><figure data-ke-type=\"opengraph\" data-og-title=\"Writing a simple publisher and subscriber (Python) — ROS 2 Documentation: Foxy  documentation\" data-ke-align=\"alignCenter\" data-og-description=\"Navigate into ros2_ws/src/py_pubsub/py_pubsub. Recall that this directory is a Python package with the same name as the ROS 2 package it’s nested in. Now there will be a new file named publisher_member_function.py adjacent to __init__.py. Open the file u\" data-og-host=\"docs.ros.org\" data-og-source-url=\"https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html\" data-og-url=\"https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html\"><a href=\"https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html\" target=\"_blank\" data-source-url=\"https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html\"><div class=\"og-image\"></div><div class=\"og-text\"><p class=\"og-title\">Writing a simple publisher and subscriber (Python) — ROS 2 Documentation: Foxy&nbsp;&nbsp;documentation</p><p class=\"og-desc\">Navigate into ros2_ws/src/py_pubsub/py_pubsub. Recall that this directory is a Python package with the same name as the ROS 2 package it’s nested in. Now there will be a new file named publisher_member_function.py adjacent to __init__.py. Open the file u</p><p class=\"og-host\">docs.ros.org</p></div></a></figure><p data-ke-size=\"size16\">파일을 하나 작성합니다. ros_study_py 폴더 안에 다음을 작성합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String\n\n\nclass MinimalPublisher(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_publisher')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_ = self.create_publisher(String, 'topic', 10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer_period = 0.5&nbsp;&nbsp;# seconds\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(timer_period, self.timer_callback)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0\n\n&nbsp;&nbsp;&nbsp;&nbsp;def timer_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = String()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.data = 'Hello World: %d' % self.i\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish(msg)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('Publishing: \"%s\"' % msg.data)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i += 1\n\n\ndef main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher = MinimalPublisher()\n\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(minimal_publisher)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# Destroy the node explicitly\n&nbsp;&nbsp;&nbsp;&nbsp;# (optional - otherwise it will be done automatically\n&nbsp;&nbsp;&nbsp;&nbsp;# when the garbage collector destroys the node object)\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher.destroy_node()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()\n\n\nif __name__ == '__main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><p data-ke-size=\"size16\">처음부터 구조가 보이지는 않을 것입니다. 일단 ROS의 동작 시스템을 한 번 설명을 해보자면<br>Topic 을 발행하는 친구는 보통 타이머를 묶어서 몇 주기로 topic 을 publish할지 결정하게 됩니다. 타이머를 사용하는게 아니라면 센서에서 신호가 들어오는 순간 publishing을 하기도 합니다. 지금의 상황에서는 몇초마다 숫자를 늘려가면서 publishing하는 예제임으로 타이머를 달아서 작동을 하게 합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String</code></pre><p data-ke-size=\"size16\">rclpy는 ros의 python모듈입니다. 보통 rclpy도 import 하고 node도 사용하므로 node도 import 합니다. 여기서 몇초마다 숫자를 늘려가면서 string 형태로 publishing하기 때문에 ros의 기본 메시지 타입인 String에 넣어서 topic으로 발행하려고 합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>class MinimalPublisher(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_publisher')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_ = self.create_publisher(String, 'topic', 10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer_period = 0.5&nbsp;&nbsp;# seconds\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(timer_period, self.timer_callback)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0</code></pre><p data-ke-size=\"size16\">보통 노드는 Class형태로 선언하며, Node의 구현은 Node 클래스를 상속함으로써 구현됩니다.&nbsp;<br>super().__inti__ 뒤에 들어가는 'minimal_publisher'는 Node의 이름을 나타냅니다.&nbsp;<br>여기서 중요한게 self.publisher_ 는 변수이름이라 그냥 이름을 publisher라고 하였을 뿐 별 이유는 없지만 그 뒤에가 publisher하는 변수로 만들어 줍니다. self.create_pulisher(String(메시지타입), 'topic'(토픽의 이름), 10)<br>timer_period는 0.5초 마다로 만들어서 self.timer = self.create_timer(timer_period, self.timer_callback) 0.5초마다 self.timer_callback이라는 함수를 실행하게 합니다.<br>self.i는 메시지에 넣어 발행할 숫자겠죠?</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code> 노드 클래스 내부에 있는\n \tdef timer_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = String()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.data = 'Hello World: %d' % self.i\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish(msg)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('Publishing: \"%s\"' % msg.data)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i += 1</code></pre><p data-ke-size=\"size16\">def timer_callback은 메시지 형식을 msg라는 변수에 저장하고, data에다가 string을 넣고, topic을 발행합니다. 그리고 이를 log에 남깁니다.<br>여기서 잠깐.. 왜 call_back이라는 함수를 쓸까요? call_back은 어떤 이벤트가 발생했을때 실행하는 함수입니다. 여기서는 timer가 0.5초마다 call_back함수를 호출합니다.&nbsp;<br>콜백 함수 구현에는 member function, lambda, local function 방법이 있으며, 이 예제는 member function 방법을 사용했다. 이에 대한 개념은 파이썬을 배울 때 배우는데, 나중에 추가하도록 하겠다.&nbsp;<br>&nbsp;</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>def main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher = MinimalPublisher()\n\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(minimal_publisher)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# Destroy the node explicitly\n&nbsp;&nbsp;&nbsp;&nbsp;# (optional - otherwise it will be done automatically\n&nbsp;&nbsp;&nbsp;&nbsp;# when the garbage collector destroys the node object)\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher.destroy_node()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()\n\n\nif __name__ == '__main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><p data-ke-size=\"size16\">마지막으로 실행을 담당하는 부분으로 대부분 형식이 거의 정해져 있다고 볼 수 있습니다. rclpy.init은 초기화를 하고 위에서 작성한 노드를 클래스를 node변수로 생성한 다음에 rclpy.spin으로 생성한 노드를 spin시켜 지정된 콜백 함수가 실행될 수 있도록 합니다. 그리고 종료되는 상황에서는 node를 소멸시키기 위해 shutdown을 사용합니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1235\" data-origin-height=\"896\"><span data-url=\"https://blog.kakaocdn.net/dna/bgcIPf/btsergvbg1V/AAAAAAAAAAAAAAAAAAAAAGPK2kFLt0x33KYVG8quAlEN0_s2KWovwyzGowf3V6tw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=6%2FWZgUXcoGNiJSA8M0XuiTMqmXg%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bgcIPf/btsergvbg1V/AAAAAAAAAAAAAAAAAAAAAGPK2kFLt0x33KYVG8quAlEN0_s2KWovwyzGowf3V6tw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=6%2FWZgUXcoGNiJSA8M0XuiTMqmXg%3D\"><img src=\"https://blog.kakaocdn.net/dna/bgcIPf/btsergvbg1V/AAAAAAAAAAAAAAAAAAAAAGPK2kFLt0x33KYVG8quAlEN0_s2KWovwyzGowf3V6tw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=6%2FWZgUXcoGNiJSA8M0XuiTMqmXg%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbgcIPf%2Fbtsergvbg1V%2FAAAAAAAAAAAAAAAAAAAAAGPK2kFLt0x33KYVG8quAlEN0_s2KWovwyzGowf3V6tw%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D6%252FWZgUXcoGNiJSA8M0XuiTMqmXg%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1235\" height=\"896\" data-origin-width=\"1235\" data-origin-height=\"896\"/></span></figure>\n<p data-ke-size=\"size16\">저는 이름을 my_publisher.py로 만들었습니다. 그러면 우선 이를 setup.py에 등록을 하고 다음의 문제를 생각해봅시다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"843\" data-origin-height=\"474\"><span data-url=\"https://blog.kakaocdn.net/dna/bYluXX/btseqmbEgQC/AAAAAAAAAAAAAAAAAAAAAFgdFmBSD_T1pwiWE-gtBxAgxuY0HPU3kPwDQqKLBqrx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=KnkedaeU7APIzMhCdlscCEhc6BU%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bYluXX/btseqmbEgQC/AAAAAAAAAAAAAAAAAAAAAFgdFmBSD_T1pwiWE-gtBxAgxuY0HPU3kPwDQqKLBqrx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=KnkedaeU7APIzMhCdlscCEhc6BU%3D\"><img src=\"https://blog.kakaocdn.net/dna/bYluXX/btseqmbEgQC/AAAAAAAAAAAAAAAAAAAAAFgdFmBSD_T1pwiWE-gtBxAgxuY0HPU3kPwDQqKLBqrx/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=KnkedaeU7APIzMhCdlscCEhc6BU%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbYluXX%2FbtseqmbEgQC%2FAAAAAAAAAAAAAAAAAAAAAFgdFmBSD_T1pwiWE-gtBxAgxuY0HPU3kPwDQqKLBqrx%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DKnkedaeU7APIzMhCdlscCEhc6BU%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"843\" height=\"474\" data-origin-width=\"843\" data-origin-height=\"474\"/></span></figure>\n<pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>'my_publisher = ros_study_py.my_publisher:main'</code></pre><p data-ke-size=\"size16\">빌드해보고 잘되는지 실행을 해봅시다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"640\" data-origin-height=\"82\"><span data-url=\"https://blog.kakaocdn.net/dna/sQ703/btserghD7dM/AAAAAAAAAAAAAAAAAAAAAPvfUrnURVfB8x5AiYpBDu42trQASuTXSasNpMieXyl3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=5yUol9zX4FE3SiDc8pvF3jnxFoU%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/sQ703/btserghD7dM/AAAAAAAAAAAAAAAAAAAAAPvfUrnURVfB8x5AiYpBDu42trQASuTXSasNpMieXyl3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=5yUol9zX4FE3SiDc8pvF3jnxFoU%3D\"><img src=\"https://blog.kakaocdn.net/dna/sQ703/btserghD7dM/AAAAAAAAAAAAAAAAAAAAAPvfUrnURVfB8x5AiYpBDu42trQASuTXSasNpMieXyl3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=5yUol9zX4FE3SiDc8pvF3jnxFoU%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FsQ703%2FbtserghD7dM%2FAAAAAAAAAAAAAAAAAAAAAPvfUrnURVfB8x5AiYpBDu42trQASuTXSasNpMieXyl3%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D5yUol9zX4FE3SiDc8pvF3jnxFoU%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"640\" height=\"82\" data-origin-width=\"640\" data-origin-height=\"82\"/></span></figure>\n<figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"740\" data-origin-height=\"248\"><span data-url=\"https://blog.kakaocdn.net/dna/278wj/btsepO7cmbX/AAAAAAAAAAAAAAAAAAAAAP0jL4K5VbDlMMMCF4PVGcy6VkUAlJR5daKsiyl4rK6q/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=aJK%2Ba1RrzqHxxzJY2Ry2wt4kkc0%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/278wj/btsepO7cmbX/AAAAAAAAAAAAAAAAAAAAAP0jL4K5VbDlMMMCF4PVGcy6VkUAlJR5daKsiyl4rK6q/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=aJK%2Ba1RrzqHxxzJY2Ry2wt4kkc0%3D\"><img src=\"https://blog.kakaocdn.net/dna/278wj/btsepO7cmbX/AAAAAAAAAAAAAAAAAAAAAP0jL4K5VbDlMMMCF4PVGcy6VkUAlJR5daKsiyl4rK6q/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=aJK%2Ba1RrzqHxxzJY2Ry2wt4kkc0%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2F278wj%2FbtsepO7cmbX%2FAAAAAAAAAAAAAAAAAAAAAP0jL4K5VbDlMMMCF4PVGcy6VkUAlJR5daKsiyl4rK6q%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DaJK%252Ba1RrzqHxxzJY2Ry2wt4kkc0%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"740\" height=\"248\" data-origin-width=\"740\" data-origin-height=\"248\"/></span></figure>\n<p data-ke-size=\"size16\">정상적으로 빌드가 완료되었습니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"747\" data-origin-height=\"106\"><span data-url=\"https://blog.kakaocdn.net/dna/mA7q6/btserECskYk/AAAAAAAAAAAAAAAAAAAAACBAYxsn5-1nAhTxps041pRdczc4jxndNDpXsP70U0e_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=He4FUEEITYy7lGa%2Bv1fURyRbQ00%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/mA7q6/btserECskYk/AAAAAAAAAAAAAAAAAAAAACBAYxsn5-1nAhTxps041pRdczc4jxndNDpXsP70U0e_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=He4FUEEITYy7lGa%2Bv1fURyRbQ00%3D\"><img src=\"https://blog.kakaocdn.net/dna/mA7q6/btserECskYk/AAAAAAAAAAAAAAAAAAAAACBAYxsn5-1nAhTxps041pRdczc4jxndNDpXsP70U0e_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=He4FUEEITYy7lGa%2Bv1fURyRbQ00%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FmA7q6%2FbtserECskYk%2FAAAAAAAAAAAAAAAAAAAAACBAYxsn5-1nAhTxps041pRdczc4jxndNDpXsP70U0e_%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DHe4FUEEITYy7lGa%252Bv1fURyRbQ00%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"747\" height=\"106\" data-origin-width=\"747\" data-origin-height=\"106\"/></span></figure>\n<p data-ke-size=\"size16\">정상적으로 출력을 합니다. 그러나 우리는 위의 코드에서 이 부분을 package.xml에 명시해주는 것이 좋습니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"843\" data-origin-height=\"474\"><span data-url=\"https://blog.kakaocdn.net/dna/beQulc/btset7jAfC5/AAAAAAAAAAAAAAAAAAAAAD-Tbo16QAmxRSYN7Wz-31lZGHOEvXQvQeBGJSwkcWfS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Swepfk8Hc%2BO52glXyDKm7bOzDkQ%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/beQulc/btset7jAfC5/AAAAAAAAAAAAAAAAAAAAAD-Tbo16QAmxRSYN7Wz-31lZGHOEvXQvQeBGJSwkcWfS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Swepfk8Hc%2BO52glXyDKm7bOzDkQ%3D\"><img src=\"https://blog.kakaocdn.net/dna/beQulc/btset7jAfC5/AAAAAAAAAAAAAAAAAAAAAD-Tbo16QAmxRSYN7Wz-31lZGHOEvXQvQeBGJSwkcWfS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=Swepfk8Hc%2BO52glXyDKm7bOzDkQ%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbeQulc%2Fbtset7jAfC5%2FAAAAAAAAAAAAAAAAAAAAAD-Tbo16QAmxRSYN7Wz-31lZGHOEvXQvQeBGJSwkcWfS%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DSwepfk8Hc%252BO52glXyDKm7bOzDkQ%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"843\" height=\"474\" data-origin-width=\"843\" data-origin-height=\"474\"/></span></figure>\n<pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>&lt;depend&gt;reclpy&lt;/depend&gt;\n&lt;depend&gt;std_msgs&lt;/depend&gt;</code></pre><p data-ke-size=\"size16\">다시 빌드를 진행합니다. 그러면 정상적으로 작동합니다.<br>&nbsp;<br>이제 이를 받아주는 친구를 작성하도록 합시다. 도큐먼트에서 배포하는 subscribe의 코드입니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String\n\n\nclass MinimalSubscriber(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_subscriber')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription = self.create_subscription(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'topic',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.listener_callback,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription&nbsp;&nbsp;# prevent unused variable warning\n\n&nbsp;&nbsp;&nbsp;&nbsp;def listener_callback(self, msg):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('I heard: \"%s\"' % msg.data)\n\n\ndef main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_subscriber = MinimalSubscriber()\n\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(minimal_subscriber)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# Destroy the node explicitly\n&nbsp;&nbsp;&nbsp;&nbsp;# (optional - otherwise it will be done automatically\n&nbsp;&nbsp;&nbsp;&nbsp;# when the garbage collector destroys the node object)\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_subscriber.destroy_node()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()\n\n\nif __name__ == '__main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><p data-ke-size=\"size16\">subscribe도 비슷합니다. 하나씩 코드를 살펴보도록 하겠습니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String</code></pre><p data-ke-size=\"size16\">아무래도 publisher와 동일한 메시지 타입을 가져야 구독을 할 수 있겠죠.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>class MinimalSubscriber(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_subscriber')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription = self.create_subscription(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'topic',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.listener_callback,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription&nbsp;&nbsp;# prevent unused variable warning\n\n&nbsp;&nbsp;&nbsp;&nbsp;def listener_callback(self, msg):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('I heard: \"%s\"' % msg.data)</code></pre><p data-ke-size=\"size16\">간단합니다. 구독을 하는 노드를 만들고, 구독을 위한 변수를 선언합니다. self.create_subscription과 함께 메시지타입, 토픽이름, 그리고 토픽을 받게되면 실행할 call_back함수를 선언합니다.<br>그리고 call_back함수에서는 토픽을 받으면 이를 log로 남기게 됩니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>def main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_subscriber = MinimalSubscriber()\n\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(minimal_subscriber)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# Destroy the node explicitly\n&nbsp;&nbsp;&nbsp;&nbsp;# (optional - otherwise it will be done automatically\n&nbsp;&nbsp;&nbsp;&nbsp;# when the garbage collector destroys the node object)\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_subscriber.destroy_node()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()</code></pre><p data-ke-size=\"size16\">위에서 설명한 것과 동일합니다. rclpy를 초기화하고, node를 실행합니다. 그래서 위의 코드를 my_subscriber.py로 만들고 setup.py를 수정하면 다음과 같습니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"843\" data-origin-height=\"474\"><span data-url=\"https://blog.kakaocdn.net/dna/bN4BgZ/btsesc6L3B1/AAAAAAAAAAAAAAAAAAAAAD0A91cWrmTzfFsVCQ5f6gGJunR4mWCZVh07ZGqupX7B/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=nqZjSSWGRvXIsMP%2Bmn6auqWYNlU%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bN4BgZ/btsesc6L3B1/AAAAAAAAAAAAAAAAAAAAAD0A91cWrmTzfFsVCQ5f6gGJunR4mWCZVh07ZGqupX7B/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=nqZjSSWGRvXIsMP%2Bmn6auqWYNlU%3D\"><img src=\"https://blog.kakaocdn.net/dna/bN4BgZ/btsesc6L3B1/AAAAAAAAAAAAAAAAAAAAAD0A91cWrmTzfFsVCQ5f6gGJunR4mWCZVh07ZGqupX7B/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=nqZjSSWGRvXIsMP%2Bmn6auqWYNlU%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbN4BgZ%2Fbtsesc6L3B1%2FAAAAAAAAAAAAAAAAAAAAAD0A91cWrmTzfFsVCQ5f6gGJunR4mWCZVh07ZGqupX7B%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DnqZjSSWGRvXIsMP%252Bmn6auqWYNlU%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"843\" height=\"474\" data-origin-width=\"843\" data-origin-height=\"474\"/></span></figure>\n<p data-ke-size=\"size16\">이제 빌드하고 두 파일을 ros run으로 실행시킵니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>ros2 run ros_study_py my_publisher</code></pre><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>ros2 run ros_study_py my_subscriber</code></pre><p data-ke-size=\"size16\">&nbsp;<br>그러나 아무리 실행해도 둘이 통신이 되질 않을 것입니다. 그 이유는 간단합니다.&nbsp;<br>humble 환경도 불러야하고, ros_domain도 맞추어야하고, ros_study 메타 패키지 환경도 불러와야합니다. 그러니&nbsp;<br>gedit ~/.bashrc로 다음을 입력합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>alias ros_study=\"humble; source ~/ros_study/install/local_setup.bash; echo \\\"ros_study workspace is activated.\\\"\"</code></pre><p data-ke-size=\"size16\">그러면 다음에는 환경을 불러올때 ros_study만 입력하면됩니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"751\" data-origin-height=\"463\"><span data-url=\"https://blog.kakaocdn.net/dna/kPTNi/btseqn2GZFR/AAAAAAAAAAAAAAAAAAAAAE6oouxvqRA2zL-OJTllAMcHcOXy3gvWlemu9jTh21ke/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=ttm0ejHQfG4mTeSS04%2F%2Bz5IIUdY%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/kPTNi/btseqn2GZFR/AAAAAAAAAAAAAAAAAAAAAE6oouxvqRA2zL-OJTllAMcHcOXy3gvWlemu9jTh21ke/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=ttm0ejHQfG4mTeSS04%2F%2Bz5IIUdY%3D\"><img src=\"https://blog.kakaocdn.net/dna/kPTNi/btseqn2GZFR/AAAAAAAAAAAAAAAAAAAAAE6oouxvqRA2zL-OJTllAMcHcOXy3gvWlemu9jTh21ke/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=ttm0ejHQfG4mTeSS04%2F%2Bz5IIUdY%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FkPTNi%2Fbtseqn2GZFR%2FAAAAAAAAAAAAAAAAAAAAAE6oouxvqRA2zL-OJTllAMcHcOXy3gvWlemu9jTh21ke%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3Dttm0ejHQfG4mTeSS04%252F%252Bz5IIUdY%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"751\" height=\"463\" data-origin-width=\"751\" data-origin-height=\"463\"/></span></figure>\n<p data-ke-size=\"size16\">그러면 정상적으로 통신이 가능해집니다.<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br><b>문제 : publisher를 하는 중에 통신 상태가 원할하지 못해 예기치 못한 문제가 발생할 것이라 예상된다. 이 경우를 대비해 publish하는 데이터는 버퍼에 10개까지 저장하고자 한다. 이 경우 QoS 설정을 통해서 이를 보여라.</b><br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;QoS는 이론 설명을 제외했으나, <span style=\"background-color: #ffffff;\"><span style=\"color: #202122;\">Quality of Service의 약자로 데이터를 주고받을 때 약속에 대한 내용입니다. 실제 로봇을 다룰 때 정말 중요한 내용 중 하나입니다.</span></span><br>&nbsp;<br>두 파일 모두 이렇게 수정하시면 됩니다. 수정된 부분만 적으면 다음과 같습니다.&nbsp;</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String\nfrom rclpy.qos import QoSProfile\n\n\nclass my_publisher(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_publisher')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;qos_profile = QoSProfile(depth=10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_ = self.create_publisher(String, 'topic', qos_profile)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer_period = 0.5&nbsp;&nbsp;# seconds\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(timer_period, self.timer_callback)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0</code></pre><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom std_msgs.msg import String\nfrom rclpy.qos import QoSProfile\n\nclass my_subscriber(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('minimal_subscriber')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;qos_profile = QoSProfile(depth=10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription = self.create_subscription(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'topic',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.listener_callback,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;qos_profile)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.subscription&nbsp;&nbsp;# prevent unused variable warning\n\n&nbsp;&nbsp;&nbsp;&nbsp;def listener_callback(self, msg):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('I heard: \"%s\"' % msg.data)</code></pre><p data-ke-size=\"size16\">이제 실행하면 QoS가 적용됩니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"742\" data-origin-height=\"479\"><span data-url=\"https://blog.kakaocdn.net/dna/4WPuq/btseqmW2f6g/AAAAAAAAAAAAAAAAAAAAAJBC6cWkOVRWtiectd4XEAIzN3EzEk_5QWsD6PUXnN9Z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=kPCNTv81JIM3%2FnLXft8z%2FPm3Cp0%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/4WPuq/btseqmW2f6g/AAAAAAAAAAAAAAAAAAAAAJBC6cWkOVRWtiectd4XEAIzN3EzEk_5QWsD6PUXnN9Z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=kPCNTv81JIM3%2FnLXft8z%2FPm3Cp0%3D\"><img src=\"https://blog.kakaocdn.net/dna/4WPuq/btseqmW2f6g/AAAAAAAAAAAAAAAAAAAAAJBC6cWkOVRWtiectd4XEAIzN3EzEk_5QWsD6PUXnN9Z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=kPCNTv81JIM3%2FnLXft8z%2FPm3Cp0%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2F4WPuq%2FbtseqmW2f6g%2FAAAAAAAAAAAAAAAAAAAAAJBC6cWkOVRWtiectd4XEAIzN3EzEk_5QWsD6PUXnN9Z%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DkPCNTv81JIM3%252FnLXft8z%252FPm3Cp0%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"742\" height=\"479\" data-origin-width=\"742\" data-origin-height=\"479\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br><b>생각해볼 문제</b><br><b>turtlesim을 한 번 움직여 봅시다. 거북이가 간단하게 오른쪽으로 1초간 이동했다가 왼쪽으로 1초간 이동하는 topic을 발행해봅시다.&nbsp;</b><br>&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>우선 타이머가 필요할 것입니다. 1초마다 거북이의 이동을 바꿔야하니까요.<br>&nbsp;<br>이전시간에 Turtlesim을 움직이는 topic은 cmd_vel이라고 알고 있었습니다. 그러나 이렇게 문제를 풀면 ros 엔지니어라고 하기 좀 민망합니다.<br>우선 터틀심을 실행합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>humble\nros2 run turtlesim turtlesim_node</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1256\" data-origin-height=\"485\"><span data-url=\"https://blog.kakaocdn.net/dna/cb3mGK/btseqKJ4IUl/AAAAAAAAAAAAAAAAAAAAAEAAqNuC6mvgJu_t_pMEdkjTUHsi9QJydTjWgSENbYpY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=wlT6yfmWLXm8dhU81w2XleCmCTI%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/cb3mGK/btseqKJ4IUl/AAAAAAAAAAAAAAAAAAAAAEAAqNuC6mvgJu_t_pMEdkjTUHsi9QJydTjWgSENbYpY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=wlT6yfmWLXm8dhU81w2XleCmCTI%3D\"><img src=\"https://blog.kakaocdn.net/dna/cb3mGK/btseqKJ4IUl/AAAAAAAAAAAAAAAAAAAAAEAAqNuC6mvgJu_t_pMEdkjTUHsi9QJydTjWgSENbYpY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=wlT6yfmWLXm8dhU81w2XleCmCTI%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcb3mGK%2FbtseqKJ4IUl%2FAAAAAAAAAAAAAAAAAAAAAEAAqNuC6mvgJu_t_pMEdkjTUHsi9QJydTjWgSENbYpY%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DwlT6yfmWLXm8dhU81w2XleCmCTI%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1256\" height=\"485\" data-origin-width=\"1256\" data-origin-height=\"485\"/></span></figure>\n<p data-ke-size=\"size16\">이 상태에서 어떻게 해야 topic을 볼 수 있을까요. humble로 환경을 불러오고 다음을 입력합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>humble\nros2 topic list</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"729\" data-origin-height=\"498\"><span data-url=\"https://blog.kakaocdn.net/dna/nSuwS/btseqYgVXjU/AAAAAAAAAAAAAAAAAAAAAAdzlYldRijqvei0sZCRwQqrUwOLM2CEH1Ozj8LAZrca/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jFez7KsNKcYQmXcPgb%2BC9aQhciU%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/nSuwS/btseqYgVXjU/AAAAAAAAAAAAAAAAAAAAAAdzlYldRijqvei0sZCRwQqrUwOLM2CEH1Ozj8LAZrca/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jFez7KsNKcYQmXcPgb%2BC9aQhciU%3D\"><img src=\"https://blog.kakaocdn.net/dna/nSuwS/btseqYgVXjU/AAAAAAAAAAAAAAAAAAAAAAdzlYldRijqvei0sZCRwQqrUwOLM2CEH1Ozj8LAZrca/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=jFez7KsNKcYQmXcPgb%2BC9aQhciU%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FnSuwS%2FbtseqYgVXjU%2FAAAAAAAAAAAAAAAAAAAAAAdzlYldRijqvei0sZCRwQqrUwOLM2CEH1Ozj8LAZrca%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DjFez7KsNKcYQmXcPgb%252BC9aQhciU%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"729\" height=\"498\" data-origin-width=\"729\" data-origin-height=\"498\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;그러면 왠지 의심스러운게 cmd_vel입니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"739\" data-origin-height=\"48\"><span data-url=\"https://blog.kakaocdn.net/dna/bs3rgQ/btseqwEZppY/AAAAAAAAAAAAAAAAAAAAAH0YNuKVzGAmWS8VGcuSdpHTYYreRIIK33SE9bVUctPt/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=z5NXM4toQSosJ8EQ5ZHOSI6FI%2FY%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bs3rgQ/btseqwEZppY/AAAAAAAAAAAAAAAAAAAAAH0YNuKVzGAmWS8VGcuSdpHTYYreRIIK33SE9bVUctPt/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=z5NXM4toQSosJ8EQ5ZHOSI6FI%2FY%3D\"><img src=\"https://blog.kakaocdn.net/dna/bs3rgQ/btseqwEZppY/AAAAAAAAAAAAAAAAAAAAAH0YNuKVzGAmWS8VGcuSdpHTYYreRIIK33SE9bVUctPt/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=z5NXM4toQSosJ8EQ5ZHOSI6FI%2FY%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fbs3rgQ%2FbtseqwEZppY%2FAAAAAAAAAAAAAAAAAAAAAH0YNuKVzGAmWS8VGcuSdpHTYYreRIIK33SE9bVUctPt%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3Dz5NXM4toQSosJ8EQ5ZHOSI6FI%252FY%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"739\" height=\"48\" data-origin-width=\"739\" data-origin-height=\"48\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;이렇게 확인하면 당연히 안보입니다. 거북이가 움직이고 있지 않으니까요.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>ros2 topic info /turtle1/cmd_vel</code></pre><p data-ke-size=\"size16\">&nbsp;</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"740\" data-origin-height=\"223\"><span data-url=\"https://blog.kakaocdn.net/dna/nCEYc/btsep7FO9zD/AAAAAAAAAAAAAAAAAAAAAE_mEs5oxI1kYnGe7rbeL85_8U8eFFJlLlryCJwRm82P/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=4F4br%2Fch2UAD36jVMvLYF062QhE%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/nCEYc/btsep7FO9zD/AAAAAAAAAAAAAAAAAAAAAE_mEs5oxI1kYnGe7rbeL85_8U8eFFJlLlryCJwRm82P/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=4F4br%2Fch2UAD36jVMvLYF062QhE%3D\"><img src=\"https://blog.kakaocdn.net/dna/nCEYc/btsep7FO9zD/AAAAAAAAAAAAAAAAAAAAAE_mEs5oxI1kYnGe7rbeL85_8U8eFFJlLlryCJwRm82P/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=4F4br%2Fch2UAD36jVMvLYF062QhE%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FnCEYc%2Fbtsep7FO9zD%2FAAAAAAAAAAAAAAAAAAAAAE_mEs5oxI1kYnGe7rbeL85_8U8eFFJlLlryCJwRm82P%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D4F4br%252Fch2UAD36jVMvLYF062QhE%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"740\" height=\"223\" data-origin-width=\"740\" data-origin-height=\"223\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;Twist 토픽을 발행하고 있군요. 근데 이 토픽은 어떻게 생긴걸까요?</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>ros2 interface show geometry_msgs/msg/Twist</code></pre><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"740\" data-origin-height=\"223\"><span data-url=\"https://blog.kakaocdn.net/dna/bvtBhM/btsequtyAy8/AAAAAAAAAAAAAAAAAAAAAEC2IxR89SmmVu118B5CPELrsvoHx262n6yCcvct_oLP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=YsAT2a%2Fj2jA9wOSRiVDJzJ31Sg0%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bvtBhM/btsequtyAy8/AAAAAAAAAAAAAAAAAAAAAEC2IxR89SmmVu118B5CPELrsvoHx262n6yCcvct_oLP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=YsAT2a%2Fj2jA9wOSRiVDJzJ31Sg0%3D\"><img src=\"https://blog.kakaocdn.net/dna/bvtBhM/btsequtyAy8/AAAAAAAAAAAAAAAAAAAAAEC2IxR89SmmVu118B5CPELrsvoHx262n6yCcvct_oLP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=YsAT2a%2Fj2jA9wOSRiVDJzJ31Sg0%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbvtBhM%2FbtsequtyAy8%2FAAAAAAAAAAAAAAAAAAAAAEC2IxR89SmmVu118B5CPELrsvoHx262n6yCcvct_oLP%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DYsAT2a%252Fj2jA9wOSRiVDJzJ31Sg0%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"740\" height=\"223\" data-origin-width=\"740\" data-origin-height=\"223\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;뭔가 이동을 담당하는 토픽으로 보입니다. 그러면 이 토픽을 발행하면 될 것 같습니다. 새로운 파일로 my_turtlesim.py라는 파일을 만듭니다.<br>publisher파일에서 조금 때올건 때옵시다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom rclpy.qos import QoSProfile\n\n\nclass my_publisher(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;qos_profile = QoSProfile(depth=10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_ = self.create_publisher(, '', qos_profile)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer_period =&nbsp;&nbsp;\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(timer_period, self.timer_callback)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0\n\n&nbsp;&nbsp;&nbsp;&nbsp;def timer_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg =\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info()\n\n\ndef main(args=None):\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.init(args=args)\n\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher = my_publisher()\n\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.spin(minimal_publisher)\n\n&nbsp;&nbsp;&nbsp;&nbsp;# Destroy the node explicitly\n&nbsp;&nbsp;&nbsp;&nbsp;# (optional - otherwise it will be done automatically\n&nbsp;&nbsp;&nbsp;&nbsp;# when the garbage collector destroys the node object)\n&nbsp;&nbsp;&nbsp;&nbsp;minimal_publisher.destroy_node()\n&nbsp;&nbsp;&nbsp;&nbsp;rclpy.shutdown()\n\n\nif __name__ == '__main__':\n&nbsp;&nbsp;&nbsp;&nbsp;main()</code></pre><p data-ke-size=\"size16\">이 상황에서 시작을 합시다. 메시지 타입이 아까 뭐였죠?</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>geometry_msgs/msg/Twist</code></pre><p data-ke-size=\"size16\">&nbsp;이 메시지 타입이니 다음처럼 적어줍니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>import rclpy\nfrom rclpy.node import Node\n\nfrom rclpy.qos import QoSProfile\nfrom geometry_msgs.msg import Twist</code></pre><p data-ke-size=\"size16\">그 다음에 노드 부분을 수정합니다. 토픽의 이름은&nbsp;</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>/turtle1/cmd_vel</code></pre><p data-ke-size=\"size16\">그럼 다음처럼 수정이 되겠죠?</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>class my_publisher(Node):\n\n&nbsp;&nbsp;&nbsp;&nbsp;def __init__(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super().__init__('move_turtlesim')\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;qos_profile = QoSProfile(depth=10)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_ = self.create_publisher(Twist, '/turtle1/cmd_vel', qos_profile)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer_period =&nbsp;&nbsp;1\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.timer = self.create_timer(timer_period, self.timer_callback)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0</code></pre><p data-ke-size=\"size16\">&nbsp;<br>그리고 메시지의 포멧에 맞게 코딩을 할때는 이렇게 해야합니다.</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>def timer_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = Twist()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.linear.x = 2.0\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish(msg)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('move -&gt;')</code></pre><p data-ke-size=\"size16\">이렇게 코딩을 하면 됩니다. 만약에 ROS 확장이 잘 다운로드 되어있다면 다음과 같이 표시됩니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"541\" data-origin-height=\"372\"><span data-url=\"https://blog.kakaocdn.net/dna/qrYW4/btsetIxtgRi/AAAAAAAAAAAAAAAAAAAAAGUSRNwcF6NziKtddzdzrRCksxDgA9hXn4p2b9eBDNsi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=07gY%2BbxLwYhHbYqeFjPKqZIfwTg%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/qrYW4/btsetIxtgRi/AAAAAAAAAAAAAAAAAAAAAGUSRNwcF6NziKtddzdzrRCksxDgA9hXn4p2b9eBDNsi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=07gY%2BbxLwYhHbYqeFjPKqZIfwTg%3D\"><img src=\"https://blog.kakaocdn.net/dna/qrYW4/btsetIxtgRi/AAAAAAAAAAAAAAAAAAAAAGUSRNwcF6NziKtddzdzrRCksxDgA9hXn4p2b9eBDNsi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=07gY%2BbxLwYhHbYqeFjPKqZIfwTg%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FqrYW4%2FbtsetIxtgRi%2FAAAAAAAAAAAAAAAAAAAAAGUSRNwcF6NziKtddzdzrRCksxDgA9hXn4p2b9eBDNsi%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D07gY%252BbxLwYhHbYqeFjPKqZIfwTg%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"541\" height=\"372\" data-origin-width=\"541\" data-origin-height=\"372\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;이제 검증을 해봅시다. 이상태로 저장하고, setup.py에 등록을 합시다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"740\" data-origin-height=\"223\"><span data-url=\"https://blog.kakaocdn.net/dna/d4bwex/btsevmgCylT/AAAAAAAAAAAAAAAAAAAAAA6Mgz9CbEyBj3BdtFwjE3SRJI2M2l_I_hQYR3p9kYmv/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=VdutMScy7I0OSOrw5lQEaW8AR54%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/d4bwex/btsevmgCylT/AAAAAAAAAAAAAAAAAAAAAA6Mgz9CbEyBj3BdtFwjE3SRJI2M2l_I_hQYR3p9kYmv/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=VdutMScy7I0OSOrw5lQEaW8AR54%3D\"><img src=\"https://blog.kakaocdn.net/dna/d4bwex/btsevmgCylT/AAAAAAAAAAAAAAAAAAAAAA6Mgz9CbEyBj3BdtFwjE3SRJI2M2l_I_hQYR3p9kYmv/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=VdutMScy7I0OSOrw5lQEaW8AR54%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fd4bwex%2FbtsevmgCylT%2FAAAAAAAAAAAAAAAAAAAAAA6Mgz9CbEyBj3BdtFwjE3SRJI2M2l_I_hQYR3p9kYmv%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DVdutMScy7I0OSOrw5lQEaW8AR54%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"740\" height=\"223\" data-origin-width=\"740\" data-origin-height=\"223\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;이제 빌드하고 실행해봅시다.&nbsp;</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1231\" data-origin-height=\"474\"><span data-url=\"https://blog.kakaocdn.net/dna/bQh4yS/btsepJd82K6/AAAAAAAAAAAAAAAAAAAAADePmmIeHnBfwYZWSiPrwLBSYwXlyw6h6HYLobYem8Xy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=7tu1u%2B2cn5tDSq3whfsUgfv1sZI%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/bQh4yS/btsepJd82K6/AAAAAAAAAAAAAAAAAAAAADePmmIeHnBfwYZWSiPrwLBSYwXlyw6h6HYLobYem8Xy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=7tu1u%2B2cn5tDSq3whfsUgfv1sZI%3D\"><img src=\"https://blog.kakaocdn.net/dna/bQh4yS/btsepJd82K6/AAAAAAAAAAAAAAAAAAAAADePmmIeHnBfwYZWSiPrwLBSYwXlyw6h6HYLobYem8Xy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=7tu1u%2B2cn5tDSq3whfsUgfv1sZI%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbQh4yS%2FbtsepJd82K6%2FAAAAAAAAAAAAAAAAAAAAADePmmIeHnBfwYZWSiPrwLBSYwXlyw6h6HYLobYem8Xy%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D7tu1u%252B2cn5tDSq3whfsUgfv1sZI%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1231\" height=\"474\" data-origin-width=\"1231\" data-origin-height=\"474\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;잘움직입니다. 이제 부터는 코딩싸움입니다. 간단하게 if문으로 끝낼 수 있는 코딩이죠..</p><pre data-ke-type=\"codeblock\" class=\"python\" data-ke-language=\"python\"><code>def timer_callback(self):\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = Twist()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if self.i == 0:\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 1\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.linear.x = 2.0\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish(msg)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('move-&gt;')\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elif self.i == 1:\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.i = 0\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.linear.x = -2.0\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.publisher_.publish(msg)\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.get_logger().info('move&lt;-')</code></pre><p data-ke-size=\"size16\">&nbsp;다음처럼 수정하고 실행시키면 됩니다.</p><figure class=\"imageblock alignCenter\" data-ke-mobileStyle=\"widthOrigin\" data-origin-width=\"1231\" data-origin-height=\"474\"><span data-url=\"https://blog.kakaocdn.net/dna/Q0lVV/btseqcmoRXX/AAAAAAAAAAAAAAAAAAAAAPsccxZfy7QJpl2hzPP5TLW1c0CCTwTbvIy6Mf1n8aw7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=zOkWnuey90DmyhNuqH0oze2z%2BL4%3D\" data-phocus=\"https://blog.kakaocdn.net/dna/Q0lVV/btseqcmoRXX/AAAAAAAAAAAAAAAAAAAAAPsccxZfy7QJpl2hzPP5TLW1c0CCTwTbvIy6Mf1n8aw7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=zOkWnuey90DmyhNuqH0oze2z%2BL4%3D\"><img src=\"https://blog.kakaocdn.net/dna/Q0lVV/btseqcmoRXX/AAAAAAAAAAAAAAAAAAAAAPsccxZfy7QJpl2hzPP5TLW1c0CCTwTbvIy6Mf1n8aw7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=zOkWnuey90DmyhNuqH0oze2z%2BL4%3D\" srcset=\"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FQ0lVV%2FbtseqcmoRXX%2FAAAAAAAAAAAAAAAAAAAAAPsccxZfy7QJpl2hzPP5TLW1c0CCTwTbvIy6Mf1n8aw7%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DzOkWnuey90DmyhNuqH0oze2z%252BL4%253D\" onerror=\"this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';\" loading=\"lazy\" width=\"1231\" height=\"474\" data-origin-width=\"1231\" data-origin-height=\"474\"/></span></figure>\n<p data-ke-size=\"size16\">&nbsp;다음처럼 왔다갔다 움직이게 됩니다.<br>&nbsp;<br>&nbsp;<br>위 문제로 깨달았으면 하는 점은 ROS 코딩에서 뼈대가 있다는 점이라는 것 같습니다.&nbsp;<br>&nbsp;<br>&nbsp;<br>&nbsp;<br>다음시간에는 지금까지의 과정을 C++로 한 번 만들어보도록 하겠습니다. 그리고 Turtlebot을 활용하는 문제도 제작해보도록 하죠.</p></div>\n\n\n\n\n                    <div class=\"container_postbtn #post_button_group\">\n  <div class=\"postbtn_like\">\n<div class=\"wrap_btn\" id=\"reaction-538\" data-tistory-react-app=\"Reaction\"></div><div class=\"wrap_btn wrap_btn_share\"><button type=\"button\" class=\"btn_post sns_btn btn_share\" aria-expanded=\"false\" data-thumbnail-url=\"https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcks6lf%2Fbtsesb05unl%2FAAAAAAAAAAAAAAAAAAAAALeZPofjiYqf0qQ4N73FHLsMRkqCWehRQGjvRQz1wdgi%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DCenpB3dGU42yGauZ7qn9UjHfGL0%253D\" data-title=\"(ROS2 기초)6. 코드 스타일, ROS2에서 파이썬으로 기초 코딩하기\" data-description=\"참고도서 1. ROS 2로 시작하는 로봇 프로그래밍. 표윤석 . 루비페이퍼 . 2021 참고도서 2. ROS2 혼자공부하는 로봇SW 직접 만들고 코딩하자 . 민형기 . 잇플 . 2022 참고자료 1. PinkWink(민형기)님의 ROS2 강의자료 참고자료 2. ROS2 humble documeation ROS2를 복습하는 김에 제대로 다시 한 번 정리하고 넘어가고 싶어서 이 시리즈를 작성합니다. 이 글은 Turtlebot3 패키지를 제대로 이해하고, 다양한 센서를 부착하면서 활용하는 것을 목표로 합니다. 또한 글이 끝날 때 ROS와 관련된 문제를 하나 만들어서 직접 풀어보도록 합니다. 이번 시간에는 ROS에서 코드 스타일과 Python으오 코딩하는 기초를 다룹니다.ROS 프로그래밍 규칙이 부분은 표윤석님..\" data-profile-image=\"https://tistory1.daumcdn.net/tistory/5496518/attach/48439c311aa3409b9d683aa2b30dc543\" data-profile-name=\"BrainKimDu\" data-pc-url=\"https://kimbrain.tistory.com/538\" data-relative-pc-url=\"/538\" data-blog-title=\"자동차 설계하기..\"><span class=\"ico_postbtn ico_share\">공유하기</span></button>\n  <div class=\"layer_post\" id=\"tistorySnsLayer\"></div>\n</div><div class=\"wrap_btn wrap_btn_etc\" data-entry-id=\"538\" data-entry-visibility=\"public\" data-category-visibility=\"public\"><button type=\"button\" class=\"btn_post btn_etc2\" aria-expanded=\"false\"><span class=\"ico_postbtn ico_etc\">게시글 관리</span></button>\n  <div class=\"layer_post\" id=\"tistoryEtcLayer\"></div>\n</div></div>\n<button type=\"button\" class=\"btn_menu_toolbar btn_subscription #subscribe\" data-blog-id=\"5496518\" data-url=\"https://kimbrain.tistory.com/538\" data-device=\"web_pc\" data-tiara-action-name=\"구독 버튼_클릭\"><em class=\"txt_state\"></em><strong class=\"txt_tool_id\">자동차 설계하기..</strong><span class=\"img_common_tistory ico_check_type1\"></span></button><div class=\"postbtn_ccl\" data-ccl-type=\"1\" data-ccl-derive=\"2\">\n    <a href=\"https://creativecommons.org/licenses/by-nc-nd/4.0/deed.ko\" target=\"_blank\" class=\"link_ccl\" rel=\"license\">\n        <span class=\"bundle_ccl\">\n            <span class=\"ico_postbtn ico_ccl1\">저작자표시</span> <span class=\"ico_postbtn ico_ccl2\">비영리</span> <span class=\"ico_postbtn ico_ccl3\">변경금지</span>\n        </span>\n        <span class=\"screen_out\">(새창열림)</span>\n    </a>\n</div>\n  <div data-tistory-react-app=\"SupportButton\"></div>\n</div>\n\n\n<div class=\"another_category another_category_color_gray\">\n  <h4>'<a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89\">공부#Robotics#자율주행</a> &gt; <a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%28ROS2%29%20%EA%B8%B0%EC%B4%88\">(ROS2) 기초</a>' 카테고리의 다른 글</h4>\n  <table>\n    <tr>\n      <th><a href=\"/540\">(ROS2 기초)8. 토픽, 서비스, 액션 인터페이스 작성하기</a>&nbsp;&nbsp;<span>(0)</span></th>\n      <td>2023.05.12</td>\n    </tr>\n    <tr>\n      <th><a href=\"/539\">(ROS2 기초)7. ROS2에서 C++으로 기초 코딩하기</a>&nbsp;&nbsp;<span>(1)</span></th>\n      <td>2023.05.11</td>\n    </tr>\n    <tr>\n      <th><a href=\"/537\">(ROS2 기초)5. ROS에서 코딩하기 전 알아야할 내용</a>&nbsp;&nbsp;<span>(0)</span></th>\n      <td>2023.05.08</td>\n    </tr>\n    <tr>\n      <th><a href=\"/536\">(ROS2 기초)4. ROS에서 사용하는 도구</a>&nbsp;&nbsp;<span>(1)</span></th>\n      <td>2023.05.08</td>\n    </tr>\n    <tr>\n      <th><a href=\"/535\">(ROS2 기초)3. ROS의 Turtlesim으로 ROS의 인터페이스와 파라미터 이해하기</a>&nbsp;&nbsp;<span>(0)</span></th>\n      <td>2023.05.06</td>\n    </tr>\n  </table>\n</div>\n\n\n\n    </div>\n\n\n    <div class=\"article-footer\">\n\n\n\n      <div class=\"article-page\">\n        <h3 class=\"title-footer\">'공부#Robotics#자율주행/(ROS2) 기초'의 다른글</h3>\n        <ul>\n          <li>\n\n              <a href=\"/537\"><span>이전글</span><strong>(ROS2 기초)5. ROS에서 코딩하기 전 알아야할 내용</strong></a>\n\n          </li>\n          <li><span>현재글</span><strong>(ROS2 기초)6. 코드 스타일, ROS2에서 파이썬으로 기초 코딩하기</strong></li>\n          <li>\n\n              <a href=\"/539\"><span>다음글</span><strong>(ROS2 기초)7. ROS2에서 C++으로 기초 코딩하기</strong></a>\n\n          </li>\n        </ul>\n      </div>\n\n\n\n        <div class=\"article-related\">\n          <h3 class=\"title-footer\">관련글</h3>\n          <ul class=\"list-related\">\n\n              <li class=\"item-related\">\n                <a href=\"/540?category=639383\" class=\"link-related\">\n                  <span class=\"thumnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2F5ID1C%2FbtseSVyne6z%2FAAAAAAAAAAAAAAAAAAAAAEuk2sH2_LU2hiXK6QRdrZnX5D2mZIa2jo14PoaB8JEB%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DDtuO8AQMJdGL7V5yFm28aJzvufM%253D')\"></span>\n                  <div class=\"box_content\">\n                    <strong>(ROS2 기초)8. 토픽, 서비스, 액션 인터페이스 작성하기</strong>\n                    <span class=\"date\">2023.05.12</span>\n                  </div>\n                </a>\n              </li>\n\n              <li class=\"item-related\">\n                <a href=\"/539?category=639383\" class=\"link-related\">\n                  <span class=\"thumnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fbmr0Vs%2FbtseTDh1fOy%2FAAAAAAAAAAAAAAAAAAAAAHPb2LD1xHQD5Nb2-JqdFQLUMITrr6wkSWGdFFfFAewr%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3Diu7evTAyFPFXahtzkvByQnKeeoc%253D')\"></span>\n                  <div class=\"box_content\">\n                    <strong>(ROS2 기초)7. ROS2에서 C++으로 기초 코딩하기</strong>\n                    <span class=\"date\">2023.05.11</span>\n                  </div>\n                </a>\n              </li>\n\n              <li class=\"item-related\">\n                <a href=\"/537?category=639383\" class=\"link-related\">\n                  <span class=\"thumnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FS7rCf%2Fbtsesd5Gofg%2FAAAAAAAAAAAAAAAAAAAAAJZDGJ258wuNepdEi9ydt9qArotXLFitYYNfBiAYNslg%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DOb79LIMMAlgd1XkPfAKovKYkME0%253D')\"></span>\n                  <div class=\"box_content\">\n                    <strong>(ROS2 기초)5. ROS에서 코딩하기 전 알아야할 내용</strong>\n                    <span class=\"date\">2023.05.08</span>\n                  </div>\n                </a>\n              </li>\n\n              <li class=\"item-related\">\n                <a href=\"/536?category=639383\" class=\"link-related\">\n                  <span class=\"thumnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2F6qwx9%2FbtsepO0fmXZ%2FAAAAAAAAAAAAAAAAAAAAAOg38xyu-8SmJW0JGD_ARs79SJhBYYjsEqDBwfgNpwRp%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3D9brTT08MWzMP9XeQlczsw5PqCvo%253D')\"></span>\n                  <div class=\"box_content\">\n                    <strong>(ROS2 기초)4. ROS에서 사용하는 도구</strong>\n                    <span class=\"date\">2023.05.08</span>\n                  </div>\n                </a>\n              </li>\n\n          </ul>\n        </div>\n\n\n\n\n      <div class=\"article-reply\">\n\n        <div class=\"box-total\">\n          <a href=\"#rp\" onclick=\"\">댓글 <span><span id=\"commentCount538_0\">2</span></span></a>\n        </div>\n\n        <div data-tistory-react-app=\"Namecard\"></div><div id=\"entry538Comment\">\n\n          <div class=\"area-reply\">\n\n              <ul class=\"list-reply\">\n\n\n            <li id=\"none_display_division_for_comment_list_538\" style=\"display: none;\"></li>\n\n              </ul>\n\n          </div>\n\n\n          <form method=\"post\" action=\"/comment/add/538\" onsubmit=\"return false\" style=\"margin: 0\">\n\n\n<form method=\"post\">\n  <div class=\"area-write\">\n\n\n        <div class=\"box-account\">\n          <input type=\"text\" name=\"name\" value=\"\"\n            title=\"이름\" placeholder=\"이름\" />\n          <input type=\"password\" name=\"password\" value=\"\"\n            title=\"비밀번호\" maxlength=\"12\" placeholder=\"비밀번호\" />\n        </div>\n\n\n    <div class=\"box-textarea\">\n      <label for=\"comment\" class=\"screen_out\">댓글</label>\n      <textarea id=\"comment\" name=\"comment\" placeholder=\"내용을 입력해주세요.\"></textarea>\n    </div>\n\n    <div class=\"box-write\">\n      <label class=\"xe-label\">\n        <input type=\"checkbox\" name=\"secret\" id=\"secret\">\n        <span class=\"xe-input-helper\"></span>\n        <span class=\"xe-label-text\">비밀글</span>\n      </label>\n      <button type=\"button\" class=\"btn_register\" onclick=\"addComment(this, 538); return false;\">등록</button>\n    </div>\n\n  </div>\n</form>\n\n\n</form>\n        </div>\n\n\n\n      </div>\n\n\n    </div>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n            </div>\n\n\n\n\n\n\n          </div>\n\n\n\n          <aside class=\"area-aside\">\n\n\n\n\t\t\t\t\t\t\t\t<div class=\"box-profile \" style=\"background-image:url('');\">\n\t\t\t\t\t\t\t\t\t<div class=\"inner-box\">\n\t\t\t\t\t\t\t\t\t\t<img src=\"https://tistory1.daumcdn.net/tistory/5496518/attach/48439c311aa3409b9d683aa2b30dc543\" class=\"img-profile\" alt=\"프로필사진\">\n\t\t\t\t\t\t\t\t\t\t<p class=\"text-profile\">로봇, 자율주행, C++, 딥러닝 하다가.. 자동차의 시스템과 기능안전 설계 엔지니어로 일을 하고 있습니다.</p>\n\n\t\t\t\t\t\t\t\t\t\t<div class=\"box-sns\">\n\n\n\n\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\n\n\n\n\n                  <div class=\"box-category box-category-2depth\">\n                    <nav>\n                      <ul class=\"tt_category\"><li class=\"\"><a href=\"/category\" class=\"link_tit\"> 분류 전체보기 <span class=\"c_cnt\">(212)</span> </a>\n  <ul class=\"category_list\"><li class=\"\"><a href=\"/category/%E3%85%87%20%EB%82%98%EB%8A%94%20%EB%88%84%EA%B5%AC%EC%9D%B8%EA%B0%80%3F\" class=\"link_item\"> ㅇ 나는 누구인가? <span class=\"c_cnt\">(1)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EC%9D%BC%EC%83%81\" class=\"link_item\"> ㅇ 일상 <span class=\"c_cnt\">(36)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%E3%85%87%20%EC%9D%BC%EC%83%81/%EC%9E%A1%EB%8F%99%EC%82%AC%EB%8B%88\" class=\"link_sub_item\"> 잡동사니 <span class=\"c_cnt\">(18)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EC%9D%BC%EC%83%81/%EB%8C%80%EC%99%B8%ED%99%9C%EB%8F%99%20%ED%9B%84%EA%B8%B0\" class=\"link_sub_item\"> 대외활동 후기 <span class=\"c_cnt\">(16)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EC%9D%BC%EC%83%81/%EC%BB%A4%EB%A6%AC%EC%96%B4%20%EA%B4%80%EB%A6%AC\" class=\"link_sub_item\"> 커리어 관리 <span class=\"c_cnt\">(1)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8\" class=\"link_item\"> ㅇ 프로젝트 <span class=\"c_cnt\">(0)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8\" class=\"link_item\"> ㅇ공부#자동차 <span class=\"c_cnt\">(8)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/%EC%A0%84%EA%B8%B0%EC%B0%A8\" class=\"link_sub_item\"> 전기차 <span class=\"c_cnt\">(3)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/Aspice\" class=\"link_sub_item\"> Aspice <span class=\"c_cnt\">(3)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/SysML\" class=\"link_sub_item\"> SysML <span class=\"c_cnt\">(0)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/%EA%B8%B0%EB%8A%A5%EC%95%88%EC%A0%84%20%28ISO%2026262%29\" class=\"link_sub_item\"> 기능안전 (ISO 26262) <span class=\"c_cnt\">(1)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/%EC%8B%9C%EC%8A%A4%ED%85%9C%20%EC%84%A4%EA%B3%84\" class=\"link_sub_item\"> 시스템 설계 <span class=\"c_cnt\">(1)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%90%EB%8F%99%EC%B0%A8/AUTOSAR\" class=\"link_sub_item\"> AUTOSAR <span class=\"c_cnt\">(0)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C\" class=\"link_item\"> ㅇ공부#임베디드 <span class=\"c_cnt\">(9)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C/%EC%8B%9C%EC%8A%A4%ED%85%9C%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D\" class=\"link_sub_item\"> 시스템 프로그래밍 <span class=\"c_cnt\">(5)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%EA%B3%B5%EB%B6%80%23%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C/Embedded%20%EC%8B%A4%EC%8A%B5\" class=\"link_sub_item\"> Embedded 실습 <span class=\"c_cnt\">(4)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EA%B3%B5%EB%B6%80%23%EC%96%B8%EC%96%B4\" class=\"link_item\"> ㅇ 공부#언어 <span class=\"c_cnt\">(50)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%E3%85%87%20%EA%B3%B5%EB%B6%80%23%EC%96%B8%EC%96%B4/%28C%2B%2B%29%EA%B8%B0%EC%B4%88\" class=\"link_sub_item\"> (C++)기초 <span class=\"c_cnt\">(11)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EA%B3%B5%EB%B6%80%23%EC%96%B8%EC%96%B4/%28C%2B%2B%29%20%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%20%EB%B0%8F%20STL\" class=\"link_sub_item\"> (C++) 자료구조 및 STL <span class=\"c_cnt\">(10)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EA%B3%B5%EB%B6%80%23%EC%96%B8%EC%96%B4/%28C%2B%2B%29%20%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98\" class=\"link_sub_item\"> (C++) 알고리즘 <span class=\"c_cnt\">(8)</span> </a></li>\n<li class=\"\"><a href=\"/category/%E3%85%87%20%EA%B3%B5%EB%B6%80%23%EC%96%B8%EC%96%B4/%EA%B8%B0%ED%83%80\" class=\"link_sub_item\"> 기타 <span class=\"c_cnt\">(1)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89\" class=\"link_item\"> 공부#Robotics#자율주행 <span class=\"c_cnt\">(57)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%28ROS2%29%20%EA%B8%B0%EC%B4%88\" class=\"link_sub_item\"> (ROS2) 기초 <span class=\"c_cnt\">(9)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%28ROS2%29Path%20Planning\" class=\"link_sub_item\"> (ROS2)Path Planning <span class=\"c_cnt\">(7)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%28ROS2%29%EC%A3%BC%ED%96%89%EB%A1%9C%EB%B4%87\" class=\"link_sub_item\"> (ROS2)주행로봇 <span class=\"c_cnt\">(6)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/autoware\" class=\"link_sub_item\"> autoware <span class=\"c_cnt\">(4)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/carla\" class=\"link_sub_item\"> carla <span class=\"c_cnt\">(7)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%EC%84%A4%EC%B9%98%EA%B0%99%EC%9D%80%EA%B1%B0\" class=\"link_sub_item\"> 설치같은거 <span class=\"c_cnt\">(5)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/GIT%26GITHUB\" class=\"link_sub_item\"> GIT&amp;GITHUB <span class=\"c_cnt\">(3)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EA%B3%B5%EB%B6%80%23Robotics%23%EC%9E%90%EC%9C%A8%EC%A3%BC%ED%96%89/%28%EB%A6%AC%EB%88%85%EC%8A%A4%29%20%EC%84%A4%EC%B9%98%26%ED%8C%81\" class=\"link_sub_item\"> (리눅스) 설치&amp;팁 <span class=\"c_cnt\">(16)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8\" class=\"link_item\"> 취업전 프로젝트 <span class=\"c_cnt\">(70)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/TEAM_%EC%84%9C%EC%9A%B8%EC%8B%9C%20%EA%B5%90%ED%86%B5%20%EC%9D%B8%ED%94%84%EB%9D%BC%20%EB%B6%84%EC%84%9D\" class=\"link_sub_item\"> TEAM_서울시 교통 인프라 분석 <span class=\"c_cnt\">(12)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/TEAM_%EC%8A%A4%EB%A7%88%ED%8A%B8%20%ED%8C%A9%ED%86%A0%EB%A6%AC\" class=\"link_sub_item\"> TEAM_스마트 팩토리 <span class=\"c_cnt\">(12)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/TEAM_%EC%8B%A4%EB%82%B4%EC%99%B8%EB%B0%B0%EC%86%A1%EB%A1%9C%EB%B4%87\" class=\"link_sub_item\"> TEAM_실내외배송로봇 <span class=\"c_cnt\">(13)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/TEAM_%EC%9A%B4%EB%8F%99%EB%B3%B4%EC%A1%B0%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8\" class=\"link_sub_item\"> TEAM_운동보조프로그램 <span class=\"c_cnt\">(13)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/TEAM_Carla-Autoware-bridge\" class=\"link_sub_item\"> TEAM_Carla-Autoware-bridge <span class=\"c_cnt\">(4)</span> </a></li>\n<li class=\"\"><a href=\"/category/%EC%B7%A8%EC%97%85%EC%A0%84%20%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8/%28Toy%29_project\" class=\"link_sub_item\"> (Toy)_project <span class=\"c_cnt\">(16)</span> </a></li>\n</ul>\n</li>\n<li class=\"\"><a href=\"/category/---------%EB%B9%84%EA%B3%B5%EA%B0%9C----------\" class=\"link_item\"> ---------비공개---------- <span class=\"c_cnt\">(0)</span> </a>\n  <ul class=\"sub_category_list\"><li class=\"\"><a href=\"/category/---------%EB%B9%84%EA%B3%B5%EA%B0%9C----------/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%20%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4\" class=\"link_sub_item\"> 알고리즘 문제풀이 <span class=\"c_cnt\">(0)</span> </a></li>\n<li class=\"\"><a href=\"/category/---------%EB%B9%84%EA%B3%B5%EA%B0%9C----------/%EB%B9%84%EA%B3%B5%EA%B0%9C\" class=\"link_sub_item\"> 비공개 <span class=\"c_cnt\">(0)</span> </a></li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n\n                    </nav>\n                  </div>\n\n\n                <div class=\"box-tag\">\n                  <h3 class=\"title-sidebar\">Tag</h3>\n                  <div class=\"box_tag\">\n\n                  </div>\n                </div>\n\n\n                <div class=\"box-recent\">\n                  <h3 class=\"title-sidebar blind\">최근글과 인기글</h3>\n                  <ul class=\"tab-recent\">\n                    <li class=\"tab-button recent_button on\"><a class=\"tab-button\" href=\"#\" onclick=\"return false;\">최근글</a></li>\n                    <li class=\"tab-button sidebar_button\"><a class=\"tab-button\" href=\"#\" onclick=\"return false;\">인기글</a></li>\n                  </ul>\n                  <ul class=\"list-recent\">\n\n                      <li>\n                        <a href=\"/652\" class=\"link-recent\">\n\n                          <div class=\"box-recent\">\n                            <strong>FOSS for All Conference 2025 참석 후기</strong>\n                            <span>2025.11.11 20:09</span>\n                          </div>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/651\" class=\"link-recent\">\n\n                          <div class=\"box-recent\">\n                            <strong>대한민국 미래모빌리티 엑스포 2025를 다녀왔습니다.</strong>\n                            <span>2025.10.26 19:51</span>\n                          </div>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/650\" class=\"link-recent\">\n\n                            <p class=\"thumbnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbFZY6w%2FbtsQ2vvFJgg%2FAAAAAAAAAAAAAAAAAAAAAMnuDsqD_kRRwqdYtecUh_dzB2jAYvVD3a-X93QlnfxR%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DffuOTjwe%252B9ejYa0zi0qGtb8b1QM%253D')\"></p>\n\n                          <div class=\"box-recent\">\n                            <strong>자동차 전장 개발자 신입 취업 준비법</strong>\n                            <span>2025.10.08 05:08</span>\n                          </div>\n                        </a>\n                      </li>\n\n                  </ul>\n\n                  <ul class=\"list-recent list-tab\" style=\"display: none\">\n\n                      <li>\n                        <a href=\"/537\" class=\"link-recent\">\n\n                            <p class=\"thumbnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FS7rCf%2Fbtsesd5Gofg%2FAAAAAAAAAAAAAAAAAAAAAJZDGJ258wuNepdEi9ydt9qArotXLFitYYNfBiAYNslg%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DOb79LIMMAlgd1XkPfAKovKYkME0%253D')\"></p>\n\n                          <div class=\"box-recent\">\n                            <strong>(ROS2 기초)5. ROS에서 코딩하기 전 알아야할 내용</strong>\n                            <span>2023.05.08 23:19</span>\n                          </div>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/621\" class=\"link-recent\">\n\n                            <p class=\"thumbnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fb3UTdZ%2FbtsytOrNkUu%2FAAAAAAAAAAAAAAAAAAAAANOUg5iLfuwLZ8lEQPeXdNVvAgBL5QsOCk3e59f5gkRH%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DKdnbvIn5PKQwIQPLYPccnyu7H7s%253D')\"></p>\n\n                          <div class=\"box-recent\">\n                            <strong>PinkLab에서 6개월의 인턴을 진행하면서</strong>\n                            <span>2023.10.13 23:02</span>\n                          </div>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/515\" class=\"link-recent\">\n\n                            <p class=\"thumbnail\" style=\"background-image:url('https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcss9i1%2FbtsdjnnNv7Z%2FAAAAAAAAAAAAAAAAAAAAAAYY8cuS2HAstSxdqCYZtEwQFOJFjm3XKec1j7qIDRiu%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1769871599%26allow_ip%3D%26allow_referer%3D%26signature%3DDDQC1obOk6gl3GXA%252FR5%252BJ0mCCjo%253D')\"></p>\n\n                          <div class=\"box-recent\">\n                            <strong>(CARLA) 1. PythonAPI를 이용해서 CARLA 사용하기</strong>\n                            <span>2023.04.30 04:17</span>\n                          </div>\n                        </a>\n                      </li>\n\n                  </ul>\n                </div>\n\n\n                <div class=\"box-reply\">\n                  <h3 class=\"title-sidebar\">최근댓글</h3>\n                  <ul class=\"list-sidebar\">\n\n                      <li>\n                        <a href=\"/646#comment15263572\" class=\"link-sidebar\">\n                          <strong>들렸다 갑니다~</strong>\n                          <p>문자life</p>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/346#comment15237654\" class=\"link-sidebar\">\n                          <strong>안녕하세요 혹시 글 중간에 &quot;복선도&quot; 그림은 어떤 프로그램으로 그린 건가요? 손그림이 섞여⋯</strong>\n                          <p>cuffyluv</p>\n                        </a>\n                      </li>\n\n                      <li>\n                        <a href=\"/646#comment15207597\" class=\"link-sidebar\">\n                          <strong>왔다 갑니다.</strong>\n                          <p>인포info</p>\n                        </a>\n                      </li>\n\n                  </ul>\n                </div>\n\n\n\n                  <div class=\"box-notice\">\n                    <h3 class=\"title-sidebar\">공지사항</h3>\n                    <ul class=\"list-sidebar\">\n\n                    </ul>\n                  </div>\n\n\n\n                <div class=\"box-plugins\">\n                  <h3 class=\"title-sidebar blind\">페이스북 트위터 플러그인</h3>\n                  <ul class=\"tab-sns\">\n                    <li class=\"tab-button item-facebook on\"><a class=\"tab-button\" href=\"#\" onclick=\"return false;\">Facebook</a></li>\n                    <li class=\"tab-button item-twitter\"><a class=\"tab-button\" href=\"#\" onclick=\"return false;\">Twitter</a></li>\n                  </ul>\n\n                  <div class=\"plugin-facebook\">\n                    <div id=\"fb-root\"></div>\n\n                    <div class=\"fb-page\" data-href=\"\" data-tabs=\"timeline\" data-small-header=\"true\" data-adapt-container-width=\"true\" data-hide-cover=\"true\" data-show-facepile=\"false\"><blockquote cite=\"\" class=\"fb-xfbml-parse-ignore\"><a href=\"\"></a></blockquote>\n                    </div>\n                  </div>\n\n                  <div class=\"plugin-twitter\" style=\"display: none;\">\n                    <a class=\"twitter-timeline\" href=\"\"></a>\n                  </div>\n                </div>\n\n\n                <div class=\"box-archive\">\n                  <h3 class=\"title-sidebar\">Archives</h3>\n                  <ul class=\"list-sidebar\">\n\n                      <li><a href=\"/archive/202511\" class=\"link-sidebar\">2025/11</a></li>\n\n                      <li><a href=\"/archive/202510\" class=\"link-sidebar\">2025/10</a></li>\n\n                      <li><a href=\"/archive/202509\" class=\"link-sidebar\">2025/09</a></li>\n\n                  </ul>\n                </div>\n\n\n                <div class=\"box-calendar\">\n                  <h3 class=\"title-sidebar\"><span class=\"blind\">Calendar</span></h3>\n                  <div class=\"inner-calendar\"><table class=\"tt-calendar\" cellpadding=\"0\" cellspacing=\"1\" style=\"width: 100%; table-layout: fixed\">\n  <caption class=\"cal_month\"><a href=\"/archive/202512\" title=\"1개월 앞의 달력을 보여줍니다.\">«</a> &nbsp; <a href=\"/archive/202601\" title=\"현재 달의 달력을 보여줍니다.\">2026/01</a> &nbsp; <a href=\"/archive/202602\" title=\"1개월 뒤의 달력을 보여줍니다.\">»</a></caption>\n  <thead>\n    <tr>\n      <th class=\"cal_week2\">일</th>\n      <th class=\"cal_week1\">월</th>\n      <th class=\"cal_week1\">화</th>\n      <th class=\"cal_week1\">수</th>\n      <th class=\"cal_week1\">목</th>\n      <th class=\"cal_week1\">금</th>\n      <th class=\"cal_week1\">토</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr class=\"cal_week cal_current_week\">\n      <td class=\"cal_day1 cal_day2\"> </td>\n      <td class=\"cal_day1 cal_day2\"> </td>\n      <td class=\"cal_day1 cal_day2\"> </td>\n      <td class=\"cal_day1 cal_day2\"> </td>\n      <td class=\"cal_day cal_day3\">1</td>\n      <td class=\"cal_day cal_day3\">2</td>\n      <td class=\"cal_day cal_day3\">3</td>\n    </tr>\n    <tr class=\"cal_week\">\n      <td class=\"cal_day cal_day3 cal_day_sunday\">4</td>\n      <td class=\"cal_day cal_day3\">5</td>\n      <td class=\"cal_day cal_day3\">6</td>\n      <td class=\"cal_day cal_day3\">7</td>\n      <td class=\"cal_day cal_day3\">8</td>\n      <td class=\"cal_day cal_day3\">9</td>\n      <td class=\"cal_day cal_day3\">10</td>\n    </tr>\n    <tr class=\"cal_week\">\n      <td class=\"cal_day cal_day3 cal_day_sunday\">11</td>\n      <td class=\"cal_day cal_day3\">12</td>\n      <td class=\"cal_day cal_day3\">13</td>\n      <td class=\"cal_day cal_day3\">14</td>\n      <td class=\"cal_day cal_day3\">15</td>\n      <td class=\"cal_day cal_day3\">16</td>\n      <td class=\"cal_day cal_day3\">17</td>\n    </tr>\n    <tr class=\"cal_week\">\n      <td class=\"cal_day cal_day3 cal_day_sunday\">18</td>\n      <td class=\"cal_day cal_day3\">19</td>\n      <td class=\"cal_day cal_day3\">20</td>\n      <td class=\"cal_day cal_day4\">21</td>\n      <td class=\"cal_day cal_day3\">22</td>\n      <td class=\"cal_day cal_day3\">23</td>\n      <td class=\"cal_day cal_day3\">24</td>\n    </tr>\n    <tr class=\"cal_week\">\n      <td class=\"cal_day cal_day3 cal_day_sunday\">25</td>\n      <td class=\"cal_day cal_day3\">26</td>\n      <td class=\"cal_day cal_day3\">27</td>\n      <td class=\"cal_day cal_day3\">28</td>\n      <td class=\"cal_day cal_day3\">29</td>\n      <td class=\"cal_day cal_day3\">30</td>\n      <td class=\"cal_day cal_day3\">31</td>\n    </tr>\n  </tbody>\n</table></div>\n                </div>\n\n\n                <div class=\"box-visit\">\n                  <h3 class=\"title-sidebar\"><span class=\"blind\">방문자수</span>Total</h3>\n                  <p class=\"text-total\">142,653</p>\n                  <ul>\n                    <li class=\"item-visit\">Today : 20</li>\n                    <li class=\"item-visit\">Yesterday : 226</li>\n                  </ul>\n                </div>\n\n\n\n\t\t\t\t\t\t<div class=\"util use-sidebar\">\n\t\t\t\t\t\t\t<div class=\"search\">\n                <label for=\"searchInput\" class=\"screen_out\">블로그 내 검색</label>\n\t\t\t\t\t\t\t\t<input id=\"searchInput\" class=\"searchInput\" type=\"text\" name=\"search\" value=\"\" placeholder=\"검색내용을 입력하세요.\" onkeypress=\"if (event.keyCode == 13) { requestSearch('.util.use-sidebar .searchInput') }\">\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n          </aside>\n\n\n        </main>\n\n      </div>\n\n\n\n\n\n      <footer id=\"footer\">\n\n        <div class=\"inner-footer\">\n          <div class=\"box-policy\">\n\n\n\n          </div>\n          <div>\n            <p class=\"text-info\">Copyright © Kakao Corp. All rights reserved.</p>\n            <address></address>\n          </div>\n\n          <div class=\"box-site\">\n            <button type=\"button\" data-toggle=\"xe-dropdown\" aria-expanded=\"false\">관련사이트</button>\n            <ul>\n\n            </ul>\n          </div>\n        </div>\n\n      </footer>\n\n\n    </div>\n\n\n<div class=\"#menubar menu_toolbar \">\n  <h2 class=\"screen_out\">티스토리툴바</h2>\n<div class=\"btn_tool\"><button class=\"btn_menu_toolbar btn_subscription  #subscribe\" data-blog-id=\"5496518\" data-url=\"https://kimbrain.tistory.com\" data-device=\"web_pc\"><strong class=\"txt_tool_id\">자동차 설계하기..</strong><em class=\"txt_state\">구독하기</em><span class=\"img_common_tistory ico_check_type1\"></span></button></div></div>\n<div class=\"#menubar menu_toolbar \"></div>\n<div class=\"layer_tooltip\">\n  <div class=\"inner_layer_tooltip\">\n    <p class=\"desc_g\"></p>\n  </div>\n</div>\n<div id=\"editEntry\" style=\"position:absolute;width:1px;height:1px;left:-100px;top:-100px\"></div>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n                <div style=\"margin:0; padding:0; border:none; background:none; float:none; clear:none; z-index:0\"></div>\n\n\n\n\n\n\n\n\n\n\n                </body>\n</html>\n"
  },
  {
    "path": "test_documents/html/issues/gh-190/rbloggers.html",
    "content": "<!DOCTYPE html>\n<html class=\"no-js\" lang=\"en-US\"\n\tprefix=\"og: https://ogp.me/ns#\"  prefix=\"og: http://ogp.me/ns#\">\n<head>\n\n\n<title>Three-Way Analysis of Variance: Simple Second-Order Interaction Effects and Simple Main Effects | R-bloggers</title>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t\t<meta property=\"article:published_time\" content=\"2017-05-15T08:40:25+00:00\" />\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<meta property=\"article:published_time\" content=\"2017-05-15T02:40:25-06:00\" />\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\r\n\r\n\r\n\r\n\n\n\n\n\n\n\n\n\n\n\n\n</head>\n<body class=\"post-template-default single single-post postid-150446 single-format-standard\">\n<div class=\"mh-container\">\n<div class=\"wrapper-corporate\">\n<header class=\"header-wrap\">\n\t<a href=\"https://www.r-bloggers.com/\" title=\"R-bloggers\" rel=\"home\">\n<div class=\"logo-wrap\" role=\"banner\">\n<img src=\"https://www.r-bloggers.com/wp-content/uploads/2020/07/R_02.webp\" height=\"90\" width=\"290\" alt=\"R-bloggers\" />\n<div class=\"logo logo-overlay\">\n<h1 class=\"logo-name\">R-bloggers</h1>\n<h2 class=\"logo-desc\">R news and tutorials contributed by hundreds of R bloggers</h2>\n</div>\n</div>\n</a>\n\t<nav class=\"main-nav clearfix\">\n\t\t<div class=\"menu-top-nav-container\"><ul id=\"menu-top-nav\" class=\"menu\"><li id=\"menu-item-48314\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-48314\"><a href=\"https://www.r-bloggers.com\">Home</a></li>\n<li id=\"menu-item-92333\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-92333\"><a href=\"https://www.r-bloggers.com/about/\">About</a></li>\n<li id=\"menu-item-50111\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-50111\"><a href=\"https://feeds.feedburner.com/RBloggers\">RSS</a></li>\n<li id=\"menu-item-48313\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-48313\"><a href=\"https://www.r-bloggers.com/add-your-blog/\">add your blog!</a></li>\n<li id=\"menu-item-111419\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-111419\"><a href=\"https://www.r-bloggers.com/2015/12/how-to-learn-r-2/\">Learn R</a></li>\n<li id=\"menu-item-75513\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-75513\"><a href=\"https://www.r-users.com/\">R jobs</a>\n<ul class=\"sub-menu\">\n\t<li id=\"menu-item-76945\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-76945\"><a href=\"https://www.r-users.com/submit-job/\">Submit a new job (it&#8217;s free)</a></li>\n\t<li id=\"menu-item-76946\" class=\"menu-item menu-item-type-custom menu-item-object-custom menu-item-76946\"><a href=\"https://www.r-users.com/\">Browse latest jobs (also free)</a></li>\n</ul>\n</li>\n<li id=\"menu-item-48311\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-48311\"><a href=\"https://www.r-bloggers.com/contact-us/\">Contact us</a></li>\n</ul></div>\t</nav>\n</header>\n<div class=\"mh-wrapper clearfix\">\n\t<div class=\"mh-content left\"><article class=\"post-150446 post type-post status-publish format-standard hentry category-r-bloggers\">\n\t<header class=\"post-header\">\n\t\t<h1 class=\"entry-title\">Three-Way Analysis of Variance: Simple Second-Order Interaction Effects and Simple Main Effects</h1>\n\t\t<p class=\"meta post-meta\">Posted on <span class=\"updated\">May 15, 2017</span>  by <span class=\"vcard author\"><a class=\"fn\" href=\"https://www.r-bloggers.com/author/bogdan-anastasiei/\">Bogdan Anastasiei</a></span>  in <a href=\"https://www.r-bloggers.com/category/r-bloggers/\" rel=\"category tag\">R bloggers</a> | 0 Comments</p>\n\t</header>\n\t<div class=\"entry clearfix\">\n\t\t\t\t<p class=\"syndicated-attribution\">\r\n\r\n<div style=\"border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;\">\r\n[This article was first published on  <strong><a href=\"http://r-posts.com/three-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects/\"> R-posts.com</a></strong>, and kindly contributed to <a href=\"https://www.r-bloggers.com/\" rel=\"nofollow\">R-bloggers</a>].  (You can report issue about the content on this page <a href=\"https://www.r-bloggers.com/contact-us/\">here</a>)\r\n<hr>Want to share your content on R-bloggers?<a href=\"https://www.r-bloggers.com/add-your-blog/\" rel=\"nofollow\"> click here</a> if you have a blog, or <a href=\"http://r-posts.com/\" rel=\"nofollow\"> here</a> if you don't.\r\n</div></p>\n<aside class=\"mashsb-container mashsb-main mashsb-stretched\"><div class=\"mashsb-box\"><div class=\"mashsb-buttons\"><a class=\"mashicon-facebook mash-large mash-center mashsb-noshadow\" href=\"https://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.r-bloggers.com%2F2017%2F05%2Fthree-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects%2F\" target=\"_blank\" rel=\"nofollow\"><span class=\"icon\"></span><span class=\"text\">Share</span></a><a class=\"mashicon-twitter mash-large mash-center mashsb-noshadow\" href=\"https://twitter.com/intent/tweet?text=Three-Way%20Analysis%20of%20Variance%3A%20Simple%20Second-Order%20Interaction%20Effects%20and%20Simple%20Main%20Effects&url=https://www.r-bloggers.com/2017/05/three-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects/&via=Rbloggers\" target=\"_blank\" rel=\"nofollow\"><span class=\"icon\"></span><span class=\"text\">Tweet</span></a><div class=\"onoffswitch2 mash-large mashsb-noshadow\" style=\"display:none;\"></div></div>\n            </div>\n                <div style=\"clear:both;\"></div></aside>\n            In this article we will show how to run a three-way analysis of variance when both the third-order interaction effect and the second-order interaction effects are statistically significant. This type of analysis can become pretty tedious, especially when our factors have many levels, so we will try to explain it here as clearly as possible. (If you want to watch me doing these analyses live, <a href=\"http://www.statistics-with-r.com/\" rel=\"nofollow\" target=\"_blank\">get my free course on statistical analysis with R here</a>.)<br />\r\n <br />\r\n First of all, let’s present the fictitious data we are going to work with. Let’s suppose that a pharmaceutical company is planning to launch a new vitamin that allegedly improves the employees’ resistance to effort. The vitamin is tested on a sample of 720 employees, divided into three groups: employees who take a placebo (the control group), employees who take the vitamin in low dose and employees who take the vitamin in high dose. Half of the employees are male, and half are female. Moreover, we have both blue collar employees and white collar employees in our sample.\r\n\r\nThe resistance to effort is measured on a scale whatsoever, from 1 to 30 (30 being the highest resistance). Our goal is to determine whether the effort resistance is influenced by three factors: dose of vitamin (placebo, low dose, and high dose), gender (male, female) and type of employee (blue collar, white collar). You can find the experiment data in CSV format <a href=\"http://statistics-with-r.com/datasets/vitamin3.csv\" rel=\"nofollow\" target=\"_blank\">here</a>.\r\n\r\n<strong>Third-order interaction effect</strong>\r\n\r\nFirst of all, let’s check whether the third-order interaction effect is significant. We are going to run the analysis using the <em>aov</em> function in the <em>stats</em> package (our data frame is called <em>vitamin</em>).\r\n\r\n<pre>\r\naov1 &lt;- aov(effort~dose*gender*type, data=vitamin)\r\nsummary(aov1)\r\n</pre>\r\n\r\nIn the formula above the interaction effect is, of course, <em>dose<em>gender</em>type</em>. The ANOVA results can be seen below (we have only kept the line presenting the third-order interaction effect).                                 <br />\r\n <br />\r\n\r\n<pre>\r\n                                  Df Sum Sq Mean Sq F value   Pr(&gt;F)\r\n\r\n\r\ndose:gender:type   2    187    93.4  22.367 3.81e-10\r\n</pre>\r\n\r\nThe interaction effect is statistically significant: F(2)=22.367, p<0.01. In other words, we do have a third-order interaction effect.\r\n\r\nIn this situation, it is not advisable to report and interpret the second-order interaction effects (they could be misleading). Therefore, we are going to compute the <em>simple second-order interaction effects</em>.\r\n\r\n<strong>Simple second-order interaction effects</strong>\r\n\r\nThe simple second-order interaction effects are the effects of each pair of factors at each level of the third factor. Specifically, we have to compute the following effects:\r\n\r\n<ol>\r\n    <li>the interaction effect of dose and type of employee, for each gender category (male and female)</li>\r\n    <li>the interaction effect of gender and type of employee, at each dose level (placebo, low and high)</li>\r\n    <li>the interaction effect of dose and gender, for each type of employee (blue collar and white collar).</li>\r\n</ol>\r\n\r\nThe total number of second-order interaction effect is given by the sum of the factor levels. In our case we have 7 effects (3+2+2). We will not analyze of all them, because this article would become too long. We will only focus on the first set of effects, leaving the others for you as an exercise.\r\n\r\nSo let’s investigate the interaction effect of dose and type of employee for each gender group. First we have to create two separate data frames, for male and female employees. We do that with the <em>filter</em> command in the <em>dplyr</em> package (though you can also use brackets or subsets).\r\n\r\n<pre>\r\nvitamin_male &lt;- filter(vitamin, gender==&quot;male&quot;)\r\nvitamin_female &lt;- filter(vitamin, gender==&quot;female&quot;)\r\n</pre>\r\n\r\nNow we perform a two-way analysis of variance on each data frame (the factors being dose and type, of course).\r\n\r\n\r\naov1 <- aov(effort~dose*type, data=vitamin_male)\r\nsummary(aov1)\r\n\r\naov2 <- aov(effort~dose*type, data=vitamin_female)\r\nsummary(aov2)\r\n\r\n\r\nThe results of the analyses are shown below (we have only retained the lines with the interaction effects).\r\n\r\n<pre>\r\n                Df Sum Sq Mean Sq F value   Pr(&gt;F)   \n\r\n dose:type     2    249   124.7   28.42 3.57e-12            \n\r\n                    Df Sum Sq Mean Sq F value   Pr(&gt;F)   \n\r\n dose:type     2  137.2    68.6   17.31 6.74e-08\n\r\n</pre>\r\n\r\n<br />\r\n\r\nWe can notice that both simple second-order interaction effects are significant (p<0.01). So we are dealing with a combined influence of the factors dose and type of employee in both male and female groups. In this situation, we have to examine the <em>simple main effects</em> for each factor. This is what we are going to do in the next section.\r\n\r\n<strong>Simple main effects</strong>\r\n\r\nLet’s compute the main effect for the factor dose of vitamin, which is the most important (after all, the company wants to demonstrate that the vitamin does affect the resistance to effort). You will be able to compute the other simple main effects yourself, using this as an example.\r\n\r\nNow we must create four separate data frames, for each combination of the factors gender and type of employee: male – blue collar, male – white collar, female – blue collar, female – white collar.\r\n\r\n\r\nvitamin_male_blue <- filter(vitamin, gender==\"male\", type==\"blue collar\")\r\n\r\nvitamin_male_white <- filter(vitamin, gender==\"male\", type==\"white collar\")\r\n\r\nvitamin_female_blue <- filter(vitamin, gender==\"female\", type==\"blue collar\")\r\n\r\nvitamin_female_white <- filter(vitamin, gender==\"female\", type==\"white collar\")\r\n\r\n\r\nNext we perform a one-way ANOVA for each data frame. Let’s do it for the first group, male – blue collar.\r\n\r\n\r\naov1 <- aov(effort~dose, data=vitamin_male_blue)\r\nsummary(aov1)\r\n\r\n\r\n<pre>\r\n\r\n                 Df Sum Sq Mean Sq F value Pr(&gt;F)   \n\r\n dose          2 2943.5  1471.8   349.9 &lt;2e-16\r\n</pre>\r\n\r\n<br />\r\n The simple main effect for the factor dose on this group is statistically significant (p<0.01). In other words, there is a significant difference between placebo, low dose and high dose levels within the male – blue collar employees category, regarding the resistance to effort. To find out how big the differences are, we use the <em>TuckeyHSD</em> function to compute the test with the same name.\r\n\r\n<pre>TukeyHSD(aov1)\r\n\r\n\r\n                                              diff        lwr       upr   p adj\n\r\n low dose-high dose -2.528333  -3.413363 -1.643303     0\n\r\n placebo-high dose  -9.558333 -10.443363 -8.673303     0\n\r\n placebo-low dose   -7.030000  -7.915030 -6.144970     0\r\n\r\n</pre>\r\n\r\n<br />\r\n By inspection of the table we conclude that the differences in effort resistance between the dose groups are significant (p<0.01). The highest difference, in absolute values, is that between low dose and placebo levels: 9.5 points. So the employees who took a high dose present a higher resistance to effort than those who just took a placebo.\r\n\r\nOne more example: the simple main effects of the variable dose of vitamin on the female – blue collar group.\r\n\r\n\r\naov1 <- aov(effort~dose, data=vitamin_female_blue)\r\nsummary(aov1)\r\n\r\n\r\n                 Df Sum Sq Mean Sq F value Pr(>F)   <br />\r\n dose          2  399.6  199.81   45.57 <2e-16\r\n\r\n \r\n\r\n<pre>TukeyHSD(aov1)\r\n\r\n\r\n                                            diff        lwr       upr     p adj\n\r\n low dose-high dose  1.083333  0.1797508  1.986916 0.0141485\n\r\n placebo-high dose  -2.476667 -3.3802492 -1.573084 0.0000000\n\r\n placebo-low dose   -3.560000 -4.4635826 -2.656417 0.0000000\r\n</pre>\r\n\r\n \r\n\r\nThe simple main effect is statistically significant, as it results from the first table. Furthermore, all the differences between dose levels are significant. The highest difference is the difference between low dose and placebo (3.5 points).\r\n\r\nTo learn more on data analysis in R, <a href=\"http://www.statistics-with-r.com/\" rel=\"nofollow\" target=\"_blank\">check the free “Statistics with R” video course here.</a>\r\n\r\n\n<div id='jp-relatedposts' class='jp-relatedposts' >\n\t<h3 class=\"jp-relatedposts-headline\"><em>Related</em></h3>\n</div><aside class=\"mashsb-container mashsb-main mashsb-stretched\"><div class=\"mashsb-box\"><div class=\"mashsb-buttons\"><a class=\"mashicon-facebook mash-large mash-center mashsb-noshadow\" href=\"https://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.r-bloggers.com%2F2017%2F05%2Fthree-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects%2F\" target=\"_blank\" rel=\"nofollow\"><span class=\"icon\"></span><span class=\"text\">Share</span></a><a class=\"mashicon-twitter mash-large mash-center mashsb-noshadow\" href=\"https://twitter.com/intent/tweet?text=Three-Way%20Analysis%20of%20Variance%3A%20Simple%20Second-Order%20Interaction%20Effects%20and%20Simple%20Main%20Effects&url=https://www.r-bloggers.com/2017/05/three-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects/&via=Rbloggers\" target=\"_blank\" rel=\"nofollow\"><span class=\"icon\"></span><span class=\"text\">Tweet</span></a><div class=\"onoffswitch2 mash-large mashsb-noshadow\" style=\"display:none;\"></div></div>\n            </div>\n                <div style=\"clear:both;\"></div></aside>\n\n<p class=\"syndicated-attribution\"><div style=\"border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;\">\r\n<div style=\"text-align: center;\">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href=\"http://r-posts.com/three-way-analysis-of-variance-simple-second-order-interaction-effects-and-simple-main-effects/\"> R-posts.com</a></strong>.</div>\r\n<hr />\r\n<a href=\"https://www.r-bloggers.com/\" rel=\"nofollow\">R-bloggers.com</a> offers <strong><a href=\"https://feedburner.google.com/fb/a/mailverify?uri=RBloggers\" rel=\"nofollow\">daily e-mail updates</a></strong> about <a title=\"The R Project for Statistical Computing\" href=\"https://www.r-project.org/\" rel=\"nofollow\">R</a> news and tutorials about <a title=\"R tutorials\" href=\"https://www.r-bloggers.com/how-to-learn-r-2/\" rel=\"nofollow\">learning R</a> and many other topics. <a title=\"Data science jobs\" href=\"https://www.r-users.com/\" rel=\"nofollow\">Click here if you're looking to post or find an R/data-science job</a>.\r\n\r\n<hr>Want to share your content on R-bloggers?<a href=\"https://www.r-bloggers.com/add-your-blog/\" rel=\"nofollow\"> click here</a> if you have a blog, or <a href=\"http://r-posts.com/\" rel=\"nofollow\"> here</a> if you don't.\r\n</div></p>\t\t\t</div>\n\t</article><nav class=\"post-navigation clearfix\" role=\"navigation\">\n<div class=\"post-nav left\">\n<a href=\"https://www.r-bloggers.com/2017/05/r%e2%81%b6-tracking-wannacry-bitcoin-wallet-payments-with-r/\" rel=\"prev\">&larr; Previous post</a></div>\n<div class=\"post-nav right\">\n<a href=\"https://www.r-bloggers.com/2017/05/visualizing-r-ladies-growth/\" rel=\"next\">Next post &rarr;</a></div>\n</nav>\n\t</div>\n\t<aside class=\"mh-sidebar sb-right\">\n\t<div id=\"custom_html-2\" class=\"widget_text sb-widget widget_custom_html\"><div class=\"textwidget custom-html-widget\">\r\n<div class=\"top-search\" style=\"padding-left: 0px;\">\r\n\t<form id=\"searchform\" action=\"https://www.google.com/cse\" target=\"_blank\">\r\n\t\t<div>\r\n\t\t\t<input type=\"hidden\" name=\"cx\" value=\"005359090438081006639:paz69t-s8ua\" />\r\n\t\t\t<input type=\"hidden\" name=\"ie\" value=\"UTF-8\" />\r\n\t\t\t<input type=\"text\" value=\"\" name=\"q\" id=\"q\" autocomplete=\"on\" style=\"font-size:16px;\" placeholder=\"Search R-bloggers..\" />\r\n\t\t\t<input type=\"submit\" id=\"searchsubmit2\" name=\"sa\" value=\"Go\" style=\"font-size:16px;\" />\r\n\t\t</div>\r\n\t</form>\r\n\r\n</div>\r\n</div></div><div id=\"text-6\" class=\"sb-widget widget_text\">\t\t\t<div class=\"textwidget\"><div style=\"min-height:26px;border:1px solid #ccc;padding:3px;text-align:left; background: none repeat scroll 0 0 #FDEADA;\">\r\n\r\n<form  style=\"width:202px; float:left;\" action=\"https://r-bloggers.com/phplist/?p=subscribe&id=1\" method=\"post\" target=\"popupwindow\">\r\n\r\n<input type=\"text\" style=\"width:110px\" onclick=\"if (!window.__cfRLUnblockHandlers) return false; if (this.value == 'Your e-mail here') this.value = '';\" value='Your e-mail here' name=\"email\" data-cf-modified-ebd39e8ac491a9a765cf5d88-=\"\" />\r\n<input type=\"hidden\" value=\"RBloggers\" name=\"uri\"/><input type=\"hidden\" name=\"loc\" value=\"en_US\"/><input type=\"submit\" value=\"Subscribe\" />\r\n\r\n</form>\r\n\r\n\r\n<div>\r\n<a href=\"https://feeds.feedburner.com/RBloggers\"><img src=\"https://i1.wp.com/www.r-bloggers.com/wp-content/uploads/2020/07/RBloggers_feedburner_count_2020_07_01-e1593671704447.gif?w=578&#038;ssl=1\" style=\"height:17px;min-width:80px;class:skip-lazy;\" alt data-recalc-dims=\"1\" data-lazy-src=\"https://i1.wp.com/www.r-bloggers.com/wp-content/uploads/2020/07/RBloggers_feedburner_count_2020_07_01-e1593671704447.gif?w=578&amp;is-pending-load=1#038;ssl=1\" srcset=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" class=\" jetpack-lazy-image\"><noscript><img src=\"https://i1.wp.com/www.r-bloggers.com/wp-content/uploads/2020/07/RBloggers_feedburner_count_2020_07_01-e1593671704447.gif?w=578&#038;ssl=1\" style=\"height:17px;min-width:80px;class:skip-lazy;\" alt=\"\" data-recalc-dims=\"1\" /></noscript></a>\r\n</div>\r\n\r\n</div>\r\n\r\n<br/>\r\n\r\n<div>\r\n\r\n\r\n<iframe allowtransparency=\"true\" frameborder=\"0\" scrolling=\"no\"\r\nsrc=\"\" data-src=\"//platform.twitter.com/widgets/follow_button.html?screen_name=rbloggers&data-show-count\"\r\n  style=\"width:100%; height:30px;\"></iframe>\r\n\r\n\r\n<div id=\"fb-root\"></div>\r\n\r\n\r\n<div style=\"min-height: 154px;\" class=\"fb-page\" data-href=\"https://www.facebook.com/rbloggers/\" data-tabs=\"\" data-width=\"300\" data-height=\"154\" data-small-header=\"true\" data-adapt-container-width=\"true\" data-hide-cover=\"false\" data-show-facepile=\"true\"><blockquote cite=\"https://www.facebook.com/rbloggers/\" class=\"fb-xfbml-parse-ignore\"><a href=\"https://www.facebook.com/rbloggers/\">R bloggers Facebook page</a></blockquote></div>\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n</div></div>\n\t\t</div><div id=\"wppp-3\" class=\"sb-widget widget_wppp\"><h4 class=\"widget-title\">Most viewed posts (weekly)</h4>\n<ul class='wppp_list'>\n\t<li><a href='https://www.r-bloggers.com/2018/07/pca-vs-autoencoders-for-dimensionality-reduction/' title='PCA vs Autoencoders for Dimensionality Reduction'>PCA vs Autoencoders for Dimensionality Reduction</a></li>\n\t<li><a href='https://www.r-bloggers.com/2016/11/5-ways-to-subset-a-data-frame-in-r/' title='5 Ways to Subset a Data Frame in R'>5 Ways to Subset a Data Frame in R</a></li>\n\t<li><a href='https://www.r-bloggers.com/2015/12/how-to-write-the-first-for-loop-in-r/' title='How to write the first for loop in R'>How to write the first for loop in R</a></li>\n\t<li><a href='https://www.r-bloggers.com/2022/03/how-to-calculate-a-cumulative-average-in-r/' title='How to Calculate a Cumulative Average in R'>How to Calculate a Cumulative Average in R</a></li>\n\t<li><a href='https://www.r-bloggers.com/2013/08/date-formats-in-r/' title='Date Formats in R'>Date Formats in R</a></li>\n\t<li><a href='https://www.r-bloggers.com/2022/03/complete-tutorial-on-using-apply-functions-in-r/' title='Complete tutorial on using &#039;apply&#039; functions in R'>Complete tutorial on using &#039;apply&#039; functions in R</a></li>\n\t<li><a href='https://www.r-bloggers.com/2010/02/r-sorting-a-data-frame-by-the-contents-of-a-column/' title='R – Sorting a data frame by the contents of a column'>R – Sorting a data frame by the contents of a column</a></li>\n</ul>\n</div><div id=\"text-18\" class=\"sb-widget widget_text\"><h4 class=\"widget-title\">Sponsors</h4>\t\t\t<div class=\"textwidget\"><div style=\"min-height: 2055px;\">\r\n\r\n\r\n\r\n</div></div>\n\t\t</div>\n\t\t<div id=\"recent-posts-3\" class=\"sb-widget widget_recent_entries\">\n\t\t<h4 class=\"widget-title\">Recent Posts</h4>\n\t\t<ul>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/something-to-note-when-using-the-merge-function-in-r/\">Something to note when using the merge function in R</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/better-sentiment-analysis-with-sentiment-ai/\">Better Sentiment Analysis with sentiment.ai</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/self-documenting-plots-in-ggplot2/\">Self-documenting plots in ggplot2</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/data-challenges-for-r-users/\">Data Challenges for R Users</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/simplevis-new-improved/\">simplevis: new &amp; improved!</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/checking-the-inputs-of-your-r-functions/\">Checking the inputs of your R functions</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/imputing-missing-values-in-r/\">Imputing missing values in R</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/creating-a-dashboard-framework-with-aws-part-1/\">Creating a Dashboard Framework with AWS (Part 1)</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/bensstatstalks3-5-tips-for-landing-a-data-professional-role/\">BensstatsTalks#3: 5 Tips for Landing a Data Professional Role</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/live-covid-19-swiss-vaccination-analysis/\">Live COVID-19 Swiss vaccination analysis</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/complete-tutorial-on-using-apply-functions-in-r/\">Complete tutorial on using &#8216;apply&#8217; functions in R</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/getting-to-know-julia/\">Getting to know Julia</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/bootstraps-bandings/\">Bootstraps &amp; Bandings</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/how-to-calculate-a-cumulative-average-in-r/\">How to Calculate a Cumulative Average in R</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t<a href=\"https://www.r-bloggers.com/2022/03/some-thoughts-about-the-use-of-cloud-services-and-web-apis-in-social-science-research/\">Some thoughts about the use of cloud services and web APIs in social science research</a>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\n\t\t</div><div id=\"rss-7\" class=\"sb-widget widget_rss\"><h4 class=\"widget-title\"><a class=\"rsswidget\" href=\"https://feeds.feedburner.com/Rjobs\"><img class=\"rss-widget-icon\" style=\"border:0\" width=\"14\" height=\"14\" src=\"https://www.r-bloggers.com/wp-includes/images/rss.png\" alt=\"RSS\" /></a> <a class=\"rsswidget\" href=\"https://www.r-users.com/\">Jobs for R-users</a></h4><ul><li><a class='rsswidget' href='https://www.r-users.com/jobs/junior-data-scientist-quantitative-economist/'>Junior Data Scientist / Quantitative economist</a></li><li><a class='rsswidget' href='https://www.r-users.com/jobs/senior-quantitative-analyst/'>Senior Quantitative Analyst</a></li><li><a class='rsswidget' href='https://www.r-users.com/jobs/r-programmer-4/'>R programmer</a></li><li><a class='rsswidget' href='https://www.r-users.com/jobs/data-scientist-cgiar-excellence-in-agronomy-ref-no-ddg-r4d-ds-1-cg-ea-06-20/'>Data Scientist – CGIAR Excellence in Agronomy (Ref No: DDG-R4D/DS/1/CG/EA/06/20)</a></li><li><a class='rsswidget' href='https://www.r-users.com/jobs/data-analytics-auditor-future-of-audit-lead-london-or-newcastle/'>Data Analytics Auditor, Future of Audit Lead @ London or Newcastle</a></li></ul></div><div id=\"rss-9\" class=\"sb-widget widget_rss\"><h4 class=\"widget-title\"><a class=\"rsswidget\" href=\"https://feeds.feedburner.com/Python-bloggers\"><img class=\"rss-widget-icon\" style=\"border:0\" width=\"14\" height=\"14\" src=\"https://www.r-bloggers.com/wp-includes/images/rss.png\" alt=\"RSS\" /></a> <a class=\"rsswidget\" href=\"https://python-bloggers.com/\">python-bloggers.com (python/data-science news)</a></h4><ul><li><a class='rsswidget' href='https://python-bloggers.com/2022/03/dunn-index-for-k-means-clustering-evaluation/'>Dunn Index for K-Means Clustering Evaluation</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/03/installing-python-and-tensorflow-with-jupyter-notebook-configurations/'>Installing Python and Tensorflow with Jupyter Notebook Configurations</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/03/how-to-get-twitter-data-using-python/'>How to Get Twitter Data using Python</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/02/visualizations-with-altair/'>Visualizations with Altair</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/02/spelling-corrector-program-in-python/'>Spelling Corrector Program in Python</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/02/spelling-checker-program-in-python/'>Spelling Checker Program in Python</a></li><li><a class='rsswidget' href='https://python-bloggers.com/2022/02/streamlit-tutorial-how-to-deploy-streamlit-apps-on-rstudio-connect/'>Streamlit Tutorial: How to Deploy Streamlit Apps on RStudio Connect</a></li></ul></div><div id=\"text-16\" class=\"sb-widget widget_text\">\t\t\t<div class=\"textwidget\"><strong><a href=\"https://www.r-bloggers.com/blogs-list/\">Full list of contributing R-bloggers</a></strong></div>\n\t\t</div><div id=\"archives-3\" class=\"sb-widget widget_archive\"><h4 class=\"widget-title\">Archives</h4>\t\t<label class=\"screen-reader-text\" for=\"archives-dropdown-3\">Archives</label>\n\t\t<select id=\"archives-dropdown-3\" name=\"archive-dropdown\">\n\n\t\t\t<option value=\"\">Select Month</option>\n\t\t\t\t<option value='https://www.r-bloggers.com/2022/03/'> March 2022 &nbsp;(35)</option>\n\t<option value='https://www.r-bloggers.com/2022/02/'> February 2022 &nbsp;(152)</option>\n\t<option value='https://www.r-bloggers.com/2022/01/'> January 2022 &nbsp;(154)</option>\n\t<option value='https://www.r-bloggers.com/2021/12/'> December 2021 &nbsp;(172)</option>\n\t<option value='https://www.r-bloggers.com/2021/11/'> November 2021 &nbsp;(145)</option>\n\t<option value='https://www.r-bloggers.com/2021/10/'> October 2021 &nbsp;(199)</option>\n\t<option value='https://www.r-bloggers.com/2021/09/'> September 2021 &nbsp;(202)</option>\n\t<option value='https://www.r-bloggers.com/2021/08/'> August 2021 &nbsp;(153)</option>\n\t<option value='https://www.r-bloggers.com/2021/07/'> July 2021 &nbsp;(171)</option>\n\t<option value='https://www.r-bloggers.com/2021/06/'> June 2021 &nbsp;(195)</option>\n\t<option value='https://www.r-bloggers.com/2021/05/'> May 2021 &nbsp;(196)</option>\n\t<option value='https://www.r-bloggers.com/2021/04/'> April 2021 &nbsp;(183)</option>\n\t<option value='https://www.r-bloggers.com/2021/03/'> March 2021 &nbsp;(221)</option>\n\t<option value='https://www.r-bloggers.com/2021/02/'> February 2021 &nbsp;(225)</option>\n\t<option value='https://www.r-bloggers.com/2021/01/'> January 2021 &nbsp;(252)</option>\n\t<option value='https://www.r-bloggers.com/2020/12/'> December 2020 &nbsp;(295)</option>\n\t<option value='https://www.r-bloggers.com/2020/11/'> November 2020 &nbsp;(266)</option>\n\t<option value='https://www.r-bloggers.com/2020/10/'> October 2020 &nbsp;(276)</option>\n\t<option value='https://www.r-bloggers.com/2020/09/'> September 2020 &nbsp;(249)</option>\n\t<option value='https://www.r-bloggers.com/2020/08/'> August 2020 &nbsp;(226)</option>\n\t<option value='https://www.r-bloggers.com/2020/07/'> July 2020 &nbsp;(268)</option>\n\t<option value='https://www.r-bloggers.com/2020/06/'> June 2020 &nbsp;(231)</option>\n\t<option value='https://www.r-bloggers.com/2020/05/'> May 2020 &nbsp;(332)</option>\n\t<option value='https://www.r-bloggers.com/2020/04/'> April 2020 &nbsp;(326)</option>\n\t<option value='https://www.r-bloggers.com/2020/03/'> March 2020 &nbsp;(279)</option>\n\t<option value='https://www.r-bloggers.com/2020/02/'> February 2020 &nbsp;(259)</option>\n\t<option value='https://www.r-bloggers.com/2020/01/'> January 2020 &nbsp;(250)</option>\n\t<option value='https://www.r-bloggers.com/2019/12/'> December 2019 &nbsp;(241)</option>\n\t<option value='https://www.r-bloggers.com/2019/11/'> November 2019 &nbsp;(214)</option>\n\t<option value='https://www.r-bloggers.com/2019/10/'> October 2019 &nbsp;(230)</option>\n\t<option value='https://www.r-bloggers.com/2019/09/'> September 2019 &nbsp;(227)</option>\n\t<option value='https://www.r-bloggers.com/2019/08/'> August 2019 &nbsp;(270)</option>\n\t<option value='https://www.r-bloggers.com/2019/07/'> July 2019 &nbsp;(258)</option>\n\t<option value='https://www.r-bloggers.com/2019/06/'> June 2019 &nbsp;(242)</option>\n\t<option value='https://www.r-bloggers.com/2019/05/'> May 2019 &nbsp;(272)</option>\n\t<option value='https://www.r-bloggers.com/2019/04/'> April 2019 &nbsp;(289)</option>\n\t<option value='https://www.r-bloggers.com/2019/03/'> March 2019 &nbsp;(302)</option>\n\t<option value='https://www.r-bloggers.com/2019/02/'> February 2019 &nbsp;(259)</option>\n\t<option value='https://www.r-bloggers.com/2019/01/'> January 2019 &nbsp;(282)</option>\n\t<option value='https://www.r-bloggers.com/2018/12/'> December 2018 &nbsp;(257)</option>\n\t<option value='https://www.r-bloggers.com/2018/11/'> November 2018 &nbsp;(285)</option>\n\t<option value='https://www.r-bloggers.com/2018/10/'> October 2018 &nbsp;(298)</option>\n\t<option value='https://www.r-bloggers.com/2018/09/'> September 2018 &nbsp;(285)</option>\n\t<option value='https://www.r-bloggers.com/2018/08/'> August 2018 &nbsp;(266)</option>\n\t<option value='https://www.r-bloggers.com/2018/07/'> July 2018 &nbsp;(327)</option>\n\t<option value='https://www.r-bloggers.com/2018/06/'> June 2018 &nbsp;(296)</option>\n\t<option value='https://www.r-bloggers.com/2018/05/'> May 2018 &nbsp;(315)</option>\n\t<option value='https://www.r-bloggers.com/2018/04/'> April 2018 &nbsp;(296)</option>\n\t<option value='https://www.r-bloggers.com/2018/03/'> March 2018 &nbsp;(287)</option>\n\t<option value='https://www.r-bloggers.com/2018/02/'> February 2018 &nbsp;(239)</option>\n\t<option value='https://www.r-bloggers.com/2018/01/'> January 2018 &nbsp;(328)</option>\n\t<option value='https://www.r-bloggers.com/2017/12/'> December 2017 &nbsp;(260)</option>\n\t<option value='https://www.r-bloggers.com/2017/11/'> November 2017 &nbsp;(265)</option>\n\t<option value='https://www.r-bloggers.com/2017/10/'> October 2017 &nbsp;(287)</option>\n\t<option value='https://www.r-bloggers.com/2017/09/'> September 2017 &nbsp;(287)</option>\n\t<option value='https://www.r-bloggers.com/2017/08/'> August 2017 &nbsp;(328)</option>\n\t<option value='https://www.r-bloggers.com/2017/07/'> July 2017 &nbsp;(279)</option>\n\t<option value='https://www.r-bloggers.com/2017/06/'> June 2017 &nbsp;(312)</option>\n\t<option value='https://www.r-bloggers.com/2017/05/'> May 2017 &nbsp;(341)</option>\n\t<option value='https://www.r-bloggers.com/2017/04/'> April 2017 &nbsp;(319)</option>\n\t<option value='https://www.r-bloggers.com/2017/03/'> March 2017 &nbsp;(364)</option>\n\t<option value='https://www.r-bloggers.com/2017/02/'> February 2017 &nbsp;(312)</option>\n\t<option value='https://www.r-bloggers.com/2017/01/'> January 2017 &nbsp;(364)</option>\n\t<option value='https://www.r-bloggers.com/2016/12/'> December 2016 &nbsp;(345)</option>\n\t<option value='https://www.r-bloggers.com/2016/11/'> November 2016 &nbsp;(288)</option>\n\t<option value='https://www.r-bloggers.com/2016/10/'> October 2016 &nbsp;(298)</option>\n\t<option value='https://www.r-bloggers.com/2016/09/'> September 2016 &nbsp;(249)</option>\n\t<option value='https://www.r-bloggers.com/2016/08/'> August 2016 &nbsp;(280)</option>\n\t<option value='https://www.r-bloggers.com/2016/07/'> July 2016 &nbsp;(322)</option>\n\t<option value='https://www.r-bloggers.com/2016/06/'> June 2016 &nbsp;(259)</option>\n\t<option value='https://www.r-bloggers.com/2016/05/'> May 2016 &nbsp;(288)</option>\n\t<option value='https://www.r-bloggers.com/2016/04/'> April 2016 &nbsp;(258)</option>\n\t<option value='https://www.r-bloggers.com/2016/03/'> March 2016 &nbsp;(295)</option>\n\t<option value='https://www.r-bloggers.com/2016/02/'> February 2016 &nbsp;(261)</option>\n\t<option value='https://www.r-bloggers.com/2016/01/'> January 2016 &nbsp;(334)</option>\n\t<option value='https://www.r-bloggers.com/2015/12/'> December 2015 &nbsp;(300)</option>\n\t<option value='https://www.r-bloggers.com/2015/11/'> November 2015 &nbsp;(234)</option>\n\t<option value='https://www.r-bloggers.com/2015/10/'> October 2015 &nbsp;(255)</option>\n\t<option value='https://www.r-bloggers.com/2015/09/'> September 2015 &nbsp;(232)</option>\n\t<option value='https://www.r-bloggers.com/2015/08/'> August 2015 &nbsp;(261)</option>\n\t<option value='https://www.r-bloggers.com/2015/07/'> July 2015 &nbsp;(240)</option>\n\t<option value='https://www.r-bloggers.com/2015/06/'> June 2015 &nbsp;(205)</option>\n\t<option value='https://www.r-bloggers.com/2015/05/'> May 2015 &nbsp;(228)</option>\n\t<option value='https://www.r-bloggers.com/2015/04/'> April 2015 &nbsp;(203)</option>\n\t<option value='https://www.r-bloggers.com/2015/03/'> March 2015 &nbsp;(256)</option>\n\t<option value='https://www.r-bloggers.com/2015/02/'> February 2015 &nbsp;(207)</option>\n\t<option value='https://www.r-bloggers.com/2015/01/'> January 2015 &nbsp;(237)</option>\n\t<option value='https://www.r-bloggers.com/2014/12/'> December 2014 &nbsp;(230)</option>\n\t<option value='https://www.r-bloggers.com/2014/11/'> November 2014 &nbsp;(219)</option>\n\t<option value='https://www.r-bloggers.com/2014/10/'> October 2014 &nbsp;(212)</option>\n\t<option value='https://www.r-bloggers.com/2014/09/'> September 2014 &nbsp;(253)</option>\n\t<option value='https://www.r-bloggers.com/2014/08/'> August 2014 &nbsp;(214)</option>\n\t<option value='https://www.r-bloggers.com/2014/07/'> July 2014 &nbsp;(226)</option>\n\t<option value='https://www.r-bloggers.com/2014/06/'> June 2014 &nbsp;(234)</option>\n\t<option value='https://www.r-bloggers.com/2014/05/'> May 2014 &nbsp;(238)</option>\n\t<option value='https://www.r-bloggers.com/2014/04/'> April 2014 &nbsp;(256)</option>\n\t<option value='https://www.r-bloggers.com/2014/03/'> March 2014 &nbsp;(286)</option>\n\t<option value='https://www.r-bloggers.com/2014/02/'> February 2014 &nbsp;(266)</option>\n\t<option value='https://www.r-bloggers.com/2014/01/'> January 2014 &nbsp;(260)</option>\n\t<option value='https://www.r-bloggers.com/2013/12/'> December 2013 &nbsp;(261)</option>\n\t<option value='https://www.r-bloggers.com/2013/11/'> November 2013 &nbsp;(237)</option>\n\t<option value='https://www.r-bloggers.com/2013/10/'> October 2013 &nbsp;(233)</option>\n\t<option value='https://www.r-bloggers.com/2013/09/'> September 2013 &nbsp;(214)</option>\n\t<option value='https://www.r-bloggers.com/2013/08/'> August 2013 &nbsp;(223)</option>\n\t<option value='https://www.r-bloggers.com/2013/07/'> July 2013 &nbsp;(254)</option>\n\t<option value='https://www.r-bloggers.com/2013/06/'> June 2013 &nbsp;(271)</option>\n\t<option value='https://www.r-bloggers.com/2013/05/'> May 2013 &nbsp;(260)</option>\n\t<option value='https://www.r-bloggers.com/2013/04/'> April 2013 &nbsp;(278)</option>\n\t<option value='https://www.r-bloggers.com/2013/03/'> March 2013 &nbsp;(277)</option>\n\t<option value='https://www.r-bloggers.com/2013/02/'> February 2013 &nbsp;(293)</option>\n\t<option value='https://www.r-bloggers.com/2013/01/'> January 2013 &nbsp;(340)</option>\n\t<option value='https://www.r-bloggers.com/2012/12/'> December 2012 &nbsp;(306)</option>\n\t<option value='https://www.r-bloggers.com/2012/11/'> November 2012 &nbsp;(274)</option>\n\t<option value='https://www.r-bloggers.com/2012/10/'> October 2012 &nbsp;(304)</option>\n\t<option value='https://www.r-bloggers.com/2012/09/'> September 2012 &nbsp;(268)</option>\n\t<option value='https://www.r-bloggers.com/2012/08/'> August 2012 &nbsp;(262)</option>\n\t<option value='https://www.r-bloggers.com/2012/07/'> July 2012 &nbsp;(247)</option>\n\t<option value='https://www.r-bloggers.com/2012/06/'> June 2012 &nbsp;(297)</option>\n\t<option value='https://www.r-bloggers.com/2012/05/'> May 2012 &nbsp;(283)</option>\n\t<option value='https://www.r-bloggers.com/2012/04/'> April 2012 &nbsp;(295)</option>\n\t<option value='https://www.r-bloggers.com/2012/03/'> March 2012 &nbsp;(304)</option>\n\t<option value='https://www.r-bloggers.com/2012/02/'> February 2012 &nbsp;(264)</option>\n\t<option value='https://www.r-bloggers.com/2012/01/'> January 2012 &nbsp;(278)</option>\n\t<option value='https://www.r-bloggers.com/2011/12/'> December 2011 &nbsp;(251)</option>\n\t<option value='https://www.r-bloggers.com/2011/11/'> November 2011 &nbsp;(261)</option>\n\t<option value='https://www.r-bloggers.com/2011/10/'> October 2011 &nbsp;(280)</option>\n\t<option value='https://www.r-bloggers.com/2011/09/'> September 2011 &nbsp;(187)</option>\n\t<option value='https://www.r-bloggers.com/2011/08/'> August 2011 &nbsp;(258)</option>\n\t<option value='https://www.r-bloggers.com/2011/07/'> July 2011 &nbsp;(219)</option>\n\t<option value='https://www.r-bloggers.com/2011/06/'> June 2011 &nbsp;(224)</option>\n\t<option value='https://www.r-bloggers.com/2011/05/'> May 2011 &nbsp;(239)</option>\n\t<option value='https://www.r-bloggers.com/2011/04/'> April 2011 &nbsp;(267)</option>\n\t<option value='https://www.r-bloggers.com/2011/03/'> March 2011 &nbsp;(249)</option>\n\t<option value='https://www.r-bloggers.com/2011/02/'> February 2011 &nbsp;(203)</option>\n\t<option value='https://www.r-bloggers.com/2011/01/'> January 2011 &nbsp;(209)</option>\n\t<option value='https://www.r-bloggers.com/2010/12/'> December 2010 &nbsp;(188)</option>\n\t<option value='https://www.r-bloggers.com/2010/11/'> November 2010 &nbsp;(172)</option>\n\t<option value='https://www.r-bloggers.com/2010/10/'> October 2010 &nbsp;(219)</option>\n\t<option value='https://www.r-bloggers.com/2010/09/'> September 2010 &nbsp;(185)</option>\n\t<option value='https://www.r-bloggers.com/2010/08/'> August 2010 &nbsp;(203)</option>\n\t<option value='https://www.r-bloggers.com/2010/07/'> July 2010 &nbsp;(175)</option>\n\t<option value='https://www.r-bloggers.com/2010/06/'> June 2010 &nbsp;(167)</option>\n\t<option value='https://www.r-bloggers.com/2010/05/'> May 2010 &nbsp;(164)</option>\n\t<option value='https://www.r-bloggers.com/2010/04/'> April 2010 &nbsp;(152)</option>\n\t<option value='https://www.r-bloggers.com/2010/03/'> March 2010 &nbsp;(165)</option>\n\t<option value='https://www.r-bloggers.com/2010/02/'> February 2010 &nbsp;(135)</option>\n\t<option value='https://www.r-bloggers.com/2010/01/'> January 2010 &nbsp;(121)</option>\n\t<option value='https://www.r-bloggers.com/2009/12/'> December 2009 &nbsp;(126)</option>\n\t<option value='https://www.r-bloggers.com/2009/11/'> November 2009 &nbsp;(66)</option>\n\t<option value='https://www.r-bloggers.com/2009/10/'> October 2009 &nbsp;(87)</option>\n\t<option value='https://www.r-bloggers.com/2009/09/'> September 2009 &nbsp;(65)</option>\n\t<option value='https://www.r-bloggers.com/2009/08/'> August 2009 &nbsp;(56)</option>\n\t<option value='https://www.r-bloggers.com/2009/07/'> July 2009 &nbsp;(64)</option>\n\t<option value='https://www.r-bloggers.com/2009/06/'> June 2009 &nbsp;(54)</option>\n\t<option value='https://www.r-bloggers.com/2009/05/'> May 2009 &nbsp;(35)</option>\n\t<option value='https://www.r-bloggers.com/2009/04/'> April 2009 &nbsp;(38)</option>\n\t<option value='https://www.r-bloggers.com/2009/03/'> March 2009 &nbsp;(40)</option>\n\t<option value='https://www.r-bloggers.com/2009/02/'> February 2009 &nbsp;(33)</option>\n\t<option value='https://www.r-bloggers.com/2009/01/'> January 2009 &nbsp;(42)</option>\n\t<option value='https://www.r-bloggers.com/2008/12/'> December 2008 &nbsp;(16)</option>\n\t<option value='https://www.r-bloggers.com/2008/11/'> November 2008 &nbsp;(14)</option>\n\t<option value='https://www.r-bloggers.com/2008/10/'> October 2008 &nbsp;(10)</option>\n\t<option value='https://www.r-bloggers.com/2008/09/'> September 2008 &nbsp;(8)</option>\n\t<option value='https://www.r-bloggers.com/2008/08/'> August 2008 &nbsp;(11)</option>\n\t<option value='https://www.r-bloggers.com/2008/07/'> July 2008 &nbsp;(7)</option>\n\t<option value='https://www.r-bloggers.com/2008/06/'> June 2008 &nbsp;(8)</option>\n\t<option value='https://www.r-bloggers.com/2008/05/'> May 2008 &nbsp;(8)</option>\n\t<option value='https://www.r-bloggers.com/2008/04/'> April 2008 &nbsp;(4)</option>\n\t<option value='https://www.r-bloggers.com/2008/03/'> March 2008 &nbsp;(5)</option>\n\t<option value='https://www.r-bloggers.com/2008/02/'> February 2008 &nbsp;(6)</option>\n\t<option value='https://www.r-bloggers.com/2008/01/'> January 2008 &nbsp;(10)</option>\n\t<option value='https://www.r-bloggers.com/2007/12/'> December 2007 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2007/11/'> November 2007 &nbsp;(5)</option>\n\t<option value='https://www.r-bloggers.com/2007/10/'> October 2007 &nbsp;(9)</option>\n\t<option value='https://www.r-bloggers.com/2007/09/'> September 2007 &nbsp;(7)</option>\n\t<option value='https://www.r-bloggers.com/2007/08/'> August 2007 &nbsp;(21)</option>\n\t<option value='https://www.r-bloggers.com/2007/07/'> July 2007 &nbsp;(9)</option>\n\t<option value='https://www.r-bloggers.com/2007/06/'> June 2007 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2007/05/'> May 2007 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2007/04/'> April 2007 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2007/03/'> March 2007 &nbsp;(5)</option>\n\t<option value='https://www.r-bloggers.com/2007/02/'> February 2007 &nbsp;(4)</option>\n\t<option value='https://www.r-bloggers.com/2006/11/'> November 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2006/10/'> October 2006 &nbsp;(2)</option>\n\t<option value='https://www.r-bloggers.com/2006/08/'> August 2006 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2006/07/'> July 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2006/06/'> June 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2006/05/'> May 2006 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2006/04/'> April 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2006/03/'> March 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2006/02/'> February 2006 &nbsp;(5)</option>\n\t<option value='https://www.r-bloggers.com/2006/01/'> January 2006 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2005/10/'> October 2005 &nbsp;(1)</option>\n\t<option value='https://www.r-bloggers.com/2005/09/'> September 2005 &nbsp;(3)</option>\n\t<option value='https://www.r-bloggers.com/2005/05/'> May 2005 &nbsp;(1)</option>\n\n\t\t</select>\n\n\n\t\t\t</div><div id=\"linkcat-3349\" class=\"sb-widget widget_links\"><h4 class=\"widget-title\">Other sites</h4>\n\t<ul class='xoxo blogroll'>\n<li><a href=\"https://www.r-users.com/\">Jobs for R-users</a></li>\n<li><a href=\"http://www.proc-x.com/\" title=\"SAS news gathered from bloggers\">SAS blogs</a></li>\n\n\t</ul>\n</div>\n</aside></div>\n</div>\n<div class=\"copyright-wrap\">\n\t<p class=\"copyright\">Copyright &copy; 2022 | <a href=\"https://www.mhthemes.com/\" rel=\"nofollow\">MH Corporate basic by MH Themes</a></p>\n</div>\n</div>\n\r\n\r\n\r\n\n\n\n\n\n    <div class=\"snp-root\">\n        <input type=\"hidden\" id=\"snp_popup\" value=\"\" />\n        <input type=\"hidden\" id=\"snp_popup_id\" value=\"\" />\n        <input type=\"hidden\" id=\"snp_popup_theme\" value=\"\" />\n        <input type=\"hidden\" id=\"snp_exithref\" value=\"\" />\n        <input type=\"hidden\" id=\"snp_exittarget\" value=\"\" />\n        \t<div id=\"snppopup-welcome\" class=\"snp-pop-109583 snppopup\"><input type=\"hidden\" class=\"snp_open\" value=\"scroll\" /><input type=\"hidden\" class=\"snp_show_on_exit\" value=\"2\" /><input type=\"hidden\" class=\"snp_exit_js_alert_text\" value=\"\" /><input type=\"hidden\" class=\"snp_exit_scroll_down\" value=\"10\" /><input type=\"hidden\" class=\"snp_exit_scroll_up\" value=\"10\" /><input type=\"hidden\" class=\"snp_open_scroll\" value=\"45\" /><input type=\"hidden\" class=\"snp_close_scroll\" value=\"10\" /><input type=\"hidden\" class=\"snp_optin_redirect_url\" value=\"\" /><input type=\"hidden\" class=\"snp_show_cb_button\" value=\"yes\" /><input type=\"hidden\" class=\"snp_popup_id\" value=\"109583\" /><input type=\"hidden\" class=\"snp_popup_theme\" value=\"theme6\" /><input type=\"hidden\" class=\"snp_overlay\" value=\"disabled\" /><input type=\"hidden\" class=\"snp_cookie_conversion\" value=\"30\" /><input type=\"hidden\" class=\"snp_cookie_close\" value=\"180\" /><div class=\"snp-fb snp-theme6\">\n    <div class=\"snp-subscribe-inner\">\n\t<h1 class=\"snp-header\"><i>Never miss an update! </i>\r\n<br/>\r\n<strong>Subscribe to R-bloggers</strong> to receive <br/>e-mails with the latest R posts.<br/>\r\n\r\n<small>(You will not see this message again.)</small></h1>\t<div class=\"snp-form\">\n\t    <form action=\"https://r-bloggers.com/phplist/?p=subscribe&id=1&email=\" method=\"post\" class=\"snp-subscribeform snp_subscribeform\" target=\"_blank\">\n\t\t\t\t<fieldset>\n\t\t    <div class=\"snp-field\">\n\t\t\t<input type=\"text\" name=\"email\" id=\"snp_email\" placeholder=\"Your E-mail...\" class=\"snp-field snp-field-email\" />\n\t\t    </div>\n\t\t    <button type=\"submit\" class=\"snp-submit\">Submit</button>\n\t\t</fieldset>\n\t    </form>\n\t</div>\n\t<a href=\"#\" class=\"snp_nothanks snp-close\">Click here to close (This popup will not appear again)</a>    </div>\n    </div>\n\n</div>\n    </div>\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\t</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/issues/test-nested-simple.html",
    "content": "<ul>\n<li>\n<a href=\"/team-appraisal\">Team Appraisal</a>\n<div>\n<ul>\n<li><a href=\"/team-appraisal/team\">Team</a></li>\n<li><a href=\"/team-appraisal/pending\">Pending</a></li>\n</ul>\n</div>\n</li>\n</ul>\n"
  },
  {
    "path": "test_documents/html/issues/test-nested-simple.md",
    "content": "- [Team Appraisal](/team-appraisal)\n\n  * [Team](/team-appraisal/team)\n  * [Pending](/team-appraisal/pending)\n"
  },
  {
    "path": "test_documents/html/issues/test-with-custom-elements.html",
    "content": "<ul>\n  <!----><!----><!---->\n  <li class=\"first-level-menu\">\n    <a href=\"/team-appraisal\">\n      <span><svg-icon src=\"icon.svg\"></svg-icon></span>\n      <span>Team Appraisal</span>\n    </a>\n    <!----><!----><!---->\n    <div class=\"children-container\">\n      <ul>\n        <!----><!----><!---->\n        <li><a href=\"/team-appraisal/team\">Team</a></li>\n        <!----><!----><!---->\n        <li><a href=\"/team-appraisal/pending\">Pending</a></li>\n        <!----><!----><!---->\n      </ul>\n    </div>\n    <!---->\n  </li>\n</ul>\n"
  },
  {
    "path": "test_documents/html/visitor/baseline.html",
    "content": "<p>Hello <strong>World</strong></p>\n<p>This is a <em>simple</em> paragraph with <strong>formatting</strong>.</p>\n<p>Another paragraph without any formatting.</p>\n<p>Text with <code>inline code</code> and more text.</p>\n<p>Final paragraph with <strong>bold</strong> and <em>italic</em> and <u>underline</u>.</p>\n"
  },
  {
    "path": "test_documents/html/visitor/callbacks.html",
    "content": "<h1>Document with Various Elements</h1>\n<h2>Section One</h2>\n<p>Paragraph with <a href=\"https://example.com\">a link</a> and some text.</p>\n<p>Another paragraph with <a href=\"https://test.org\">multiple</a> <a href=\"https://foo.bar\">links</a>.</p>\n<h3>Images Section</h3>\n<p>Image: <img src=\"https://example.com/image.png\" alt=\"Example Image\" /></p>\n<p>More text with <img src=\"https://example.com/photo.jpg\" alt=\"Photo\" /> and <a href=\"/\">home link</a>.</p>\n<h2>Another Section</h2>\n<p>Heading <strong>bold text</strong> in paragraph.</p>\n<p>Text with <a href=\"https://github.com\">GitHub</a> and <img src=\"https://example.com/icon.svg\" alt=\"Icon\" /> icon.</p>\n<h3>Subsection</h3>\n<p>Final paragraph with <em>emphasis</em> and <code>code</code> elements.</p>\n"
  },
  {
    "path": "test_documents/html/visitor/complex.html",
    "content": "<article>\n  <h1>Complex Document</h1>\n  <section>\n    <h2>Nested Lists</h2>\n    <ul>\n      <li>Item 1 with <a href=\"https://example.com\">link</a></li>\n      <li>Item 2 with nested list:\n        <ul>\n          <li>Nested 1 with <img src=\"icon.png\" alt=\"Icon\" /></li>\n          <li>Nested 2 with <strong>bold</strong> and <a href=\"/\">home</a></li>\n          <li>Nested 3 with <em>italic</em>\n            <ul>\n              <li>Deep 1 with <code>code</code></li>\n              <li>Deep 2 with <img src=\"img.jpg\" alt=\"Image\" /></li>\n              <li>Deep 3 with <a href=\"https://test.com\">test link</a></li>\n            </ul>\n          </li>\n        </ul>\n      </li>\n      <li>Item 3 with <strong>bold</strong> and <img src=\"photo.jpg\" alt=\"Photo\" /></li>\n    </ul>\n  </section>\n  <section>\n    <h2>Complex Table</h2>\n    <table>\n      <thead>\n        <tr>\n          <th>Header 1 with <a href=\"/\">link</a></th>\n          <th>Header 2 with <strong>bold</strong></th>\n          <th>Header 3 with <img src=\"icon.png\" alt=\"Icon\" /></th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr>\n          <td>Cell with <a href=\"https://example.com\">link</a></td>\n          <td>Cell with <em>italic</em> and <code>code</code></td>\n          <td><img src=\"img1.jpg\" alt=\"Img1\" /> and <img src=\"img2.jpg\" alt=\"Img2\" /></td>\n        </tr>\n        <tr>\n          <td><strong>Bold cell</strong></td>\n          <td>Normal <a href=\"/\">cell link</a></td>\n          <td><code>monospace</code> cell</td>\n        </tr>\n        <tr>\n          <td>Cell <img src=\"icon.svg\" alt=\"Icon\" /></td>\n          <td>Cell with <strong>bold</strong> <a href=\"https://test.com\">and link</a></td>\n          <td>Final <em>formatted</em> cell</td>\n        </tr>\n      </tbody>\n    </table>\n  </section>\n  <section>\n    <h2>Mixed Formatting</h2>\n    <p>Paragraph with <strong>bold</strong>, <em>italic</em>, <strong><em>bold italic</em></strong>, <code>code</code>, and <a href=\"https://example.com\">link</a>.</p>\n    <blockquote>\n      <p>Quote with <a href=\"/\">link</a> and <img src=\"quote.png\" alt=\"Quote\" /></p>\n      <p>Quote with <strong>bold</strong> and <em>italic</em>.</p>\n    </blockquote>\n    <p>Text after blockquote with <img src=\"final.jpg\" alt=\"Final\" /> and <a href=\"https://final.com\">final link</a>.</p>\n  </section>\n  <section>\n    <h2>Ordered Lists</h2>\n    <ol>\n      <li>First with <a href=\"https://one.com\">first link</a></li>\n      <li>Second with <img src=\"icon2.png\" alt=\"Icon2\" />\n        <ol>\n          <li>Nested first with <strong>bold</strong></li>\n          <li>Nested second with <a href=\"https://two.com\">second link</a></li>\n        </ol>\n      </li>\n      <li>Third with <code>code</code> and <em>italic</em></li>\n    </ol>\n  </section>\n</article>\n"
  },
  {
    "path": "test_documents/html/visitor/custom.html",
    "content": "<article>\n  <h1>Article Title</h1>\n  <section>\n    <h2>Introduction</h2>\n    <p>This is an introductory paragraph with <strong>important information</strong> and <a href=\"https://example.com\">relevant links</a>.</p>\n    <p>Another paragraph discussing <em>key concepts</em> with <code>technical terms</code> and <a href=\"https://docs.example.com\">documentation</a>.</p>\n  </section>\n  <section>\n    <h2>Main Content</h2>\n    <p>Content section with multiple <a href=\"https://ref1.com\">references</a>, <a href=\"https://ref2.com\">citations</a>, and <a href=\"https://ref3.com\">sources</a>.</p>\n    <p>Image gallery: <img src=\"/image1.jpg\" alt=\"First\" /> <img src=\"/image2.jpg\" alt=\"Second\" /> <img src=\"/image3.jpg\" alt=\"Third\" /></p>\n    <p>Mixed content with <strong>bold</strong>, <em>italic</em>, <code>monospace</code>, and <a href=\"https://example.com\">hyperlinks</a>.</p>\n  </section>\n  <section>\n    <h2>Conclusion</h2>\n    <p>Concluding remarks with <a href=\"https://further-reading.com\">further reading</a> and <img src=\"/conclusion.png\" alt=\"Summary\" /></p>\n    <p>Final thoughts with <strong>emphasis</strong> and <em>style</em>.</p>\n  </section>\n</article>\n"
  },
  {
    "path": "test_documents/html/wikipedia/large_rust.html",
    "content": "<!DOCTYPE html>\n<html class=\"client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\" lang=\"en\" dir=\"ltr\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Rust (programming language) - Wikipedia</title>\n<script>(function(){var className=\"client-js vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\";var cookie=document.cookie.match(/(?:^|; )enwikimwclientpreferences=([^;]+)/);if(cookie){cookie[1].split('%2C').forEach(function(pref){className=className.replace(new RegExp('(^| )'+pref.replace(/-clientpref-\\w+$|[^\\w-]+/g,'')+'-clientpref-\\\\w+( |$)'),'$1'+pref+'$2');});}document.documentElement.className=className;}());RLCONF={\"wgBreakFrames\":false,\"wgSeparatorTransformTable\":[\"\",\"\"],\"wgDigitTransformTable\":[\"\",\"\"],\"wgDefaultDateFormat\":\"dmy\",\"wgMonthNames\":[\"\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"wgRequestId\":\"10bb5389-3589-4760-ab9e-bb34408bce9c\",\"wgCanonicalNamespace\":\"\",\"wgCanonicalSpecialPageName\":false,\"wgNamespaceNumber\":0,\"wgPageName\":\"Rust_(programming_language)\",\"wgTitle\":\"Rust (programming language)\",\"wgCurRevisionId\":1314392250,\"wgRevisionId\":1314392250,\"wgArticleId\":29414838,\"wgIsArticle\":true,\"wgIsRedirect\":false,\"wgAction\":\"view\",\"wgUserName\":null,\"wgUserGroups\":[\"*\"],\"wgCategories\":[\"Articles with short description\",\"Short description is different from Wikidata\",\"Good articles\",\"Use American English from July 2022\",\"All Wikipedia articles written in American English\",\"Use mdy dates from July 2022\",\"Articles with example C++ code\",\"Articles with excerpts\",\"Articles containing potentially dated statements from 2024\",\"All articles containing potentially dated statements\",\"Articles containing potentially dated statements from July 2024\",\"Pages using Sister project links with hidden wikidata\",\"Articles with example Rust code\",\"Rust (programming language)\",\"Compiled programming languages\",\"Concurrent programming languages\",\"Free and open source compilers\",\"Free software projects\",\"Functional languages\",\"High-level programming languages\",\"Mozilla\",\"Multi-paradigm programming languages\",\"Pattern matching programming languages\",\"Procedural programming languages\",\"Programming languages created in 2015\",\"Software using the Apache license\",\"Software using the MIT license\",\"Statically typed programming languages\",\"Systems programming languages\"],\"wgPageViewLanguage\":\"en\",\"wgPageContentLanguage\":\"en\",\"wgPageContentModel\":\"wikitext\",\"wgRelevantPageName\":\"Rust_(programming_language)\",\"wgRelevantArticleId\":29414838,\"wgIsProbablyEditable\":true,\"wgRelevantPageIsProbablyEditable\":true,\"wgRestrictionEdit\":[],\"wgRestrictionMove\":[],\"wgNoticeProject\":\"wikipedia\",\"wgFlaggedRevsParams\":{\"tags\":{\"status\":{\"levels\":1}}},\"wgMediaViewerOnClick\":true,\"wgMediaViewerEnabledByDefault\":true,\"wgPopupsFlags\":0,\"wgVisualEditor\":{\"pageLanguageCode\":\"en\",\"pageLanguageDir\":\"ltr\",\"pageVariantFallbacks\":\"en\"},\"wgMFDisplayWikibaseDescriptions\":{\"search\":true,\"watchlist\":true,\"tagline\":false,\"nearby\":true},\"wgWMESchemaEditAttemptStepOversample\":false,\"wgWMEPageLength\":100000,\"wgMetricsPlatformUserExperiments\":{\"active_experiments\":[],\"overrides\":[],\"enrolled\":[],\"assigned\":[],\"subject_ids\":[],\"sampling_units\":[]},\"wgEditSubmitButtonLabelPublish\":true,\"wgULSPosition\":\"interlanguage\",\"wgULSisCompactLinksEnabled\":false,\"wgVector2022LanguageInHeader\":true,\"wgULSisLanguageSelectorEmpty\":false,\"wgWikibaseItemId\":\"Q575650\",\"wgCheckUserClientHintsHeadersJsApi\":[\"brands\",\"architecture\",\"bitness\",\"fullVersionList\",\"mobile\",\"model\",\"platform\",\"platformVersion\"],\"GEHomepageSuggestedEditsEnableTopics\":true,\"wgGESuggestedEditsTaskTypes\":{\"taskTypes\":[\"copyedit\",\"link-recommendation\"],\"unavailableTaskTypes\":[]},\"wgGETopicsMatchModeEnabled\":false,\"wgGELevelingUpEnabledForUser\":false};\nRLSTATE={\"ext.globalCssJs.user.styles\":\"ready\",\"site.styles\":\"ready\",\"user.styles\":\"ready\",\"ext.globalCssJs.user\":\"ready\",\"user\":\"ready\",\"user.options\":\"loading\",\"ext.cite.styles\":\"ready\",\"ext.pygments\":\"ready\",\"ext.tmh.player.styles\":\"ready\",\"ext.wikimediamessages.styles\":\"ready\",\"skins.vector.search.codex.styles\":\"ready\",\"skins.vector.styles\":\"ready\",\"skins.vector.icons\":\"ready\",\"jquery.makeCollapsible.styles\":\"ready\",\"ext.visualEditor.desktopArticleTarget.noscript\":\"ready\",\"ext.uls.interlanguage\":\"ready\",\"wikibase.client.init\":\"ready\"};RLPAGEMODULES=[\"ext.xLab\",\"ext.cite.ux-enhancements\",\"ext.pygments.view\",\"mediawiki.page.media\",\"ext.tmh.player\",\"ext.scribunto.logs\",\"site\",\"mediawiki.page.ready\",\"jquery.makeCollapsible\",\"mediawiki.toc\",\"skins.vector.js\",\"ext.centralNotice.geoIP\",\"ext.centralNotice.startUp\",\"ext.gadget.ReferenceTooltips\",\"ext.gadget.switcher\",\"ext.urlShortener.toolbar\",\"ext.centralauth.centralautologin\",\"mmv.bootstrap\",\"ext.popups\",\"ext.visualEditor.desktopArticleTarget.init\",\"ext.visualEditor.targetLoader\",\"ext.echo.centralauth\",\"ext.eventLogging\",\"ext.wikimediaEvents\",\"ext.navigationTiming\",\"ext.uls.interface\",\"ext.cx.eventlogging.campaigns\",\"ext.cx.uls.quick.actions\",\"wikibase.client.vector-2022\",\"ext.checkUser.clientHints\",\"ext.quicksurveys.init\",\"ext.growthExperiments.SuggestedEditSession\"];</script>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return[\"user.options@12s5i\",function($,jQuery,require,module){mw.user.tokens.set({\"patrolToken\":\"+\\\\\",\"watchToken\":\"+\\\\\",\"csrfToken\":\"+\\\\\"});\n}];});});</script>\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=ext.cite.styles%7Cext.pygments%7Cext.tmh.player.styles%7Cext.uls.interlanguage%7Cext.visualEditor.desktopArticleTarget.noscript%7Cext.wikimediamessages.styles%7Cjquery.makeCollapsible.styles%7Cskins.vector.icons%2Cstyles%7Cskins.vector.search.codex.styles%7Cwikibase.client.init&amp;only=styles&amp;skin=vector-2022\">\n<script async=\"\" src=\"/w/load.php?lang=en&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;skin=vector-2022\"></script>\n<meta name=\"ResourceLoaderDynamicStyles\" content=\"\">\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=vector-2022\">\n<meta name=\"generator\" content=\"MediaWiki 1.45.0-wmf.20\">\n<meta name=\"referrer\" content=\"origin\">\n<meta name=\"referrer\" content=\"origin-when-cross-origin\">\n<meta name=\"robots\" content=\"max-image-preview:standard\">\n<meta name=\"format-detection\" content=\"telephone=no\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/1200px-Rust_programming_language_black_logo.svg.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"1200\">\n<meta name=\"viewport\" content=\"width=1120\">\n<meta property=\"og:title\" content=\"Rust (programming language) - Wikipedia\">\n<meta property=\"og:type\" content=\"website\">\n<link rel=\"preconnect\" href=\"//upload.wikimedia.org\">\n<link rel=\"alternate\" media=\"only screen and (max-width: 640px)\" href=\"//en.m.wikipedia.org/wiki/Rust_(programming_language)\">\n<link rel=\"alternate\" type=\"application/x-wiki\" title=\"Edit this page\" href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit\">\n<link rel=\"apple-touch-icon\" href=\"/static/apple-touch/wikipedia.png\">\n<link rel=\"icon\" href=\"/static/favicon/wikipedia.ico\">\n<link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"/w/rest.php/v1/search\" title=\"Wikipedia (en)\">\n<link rel=\"EditURI\" type=\"application/rsd+xml\" href=\"//en.wikipedia.org/w/api.php?action=rsd\">\n<link rel=\"canonical\" href=\"https://en.wikipedia.org/wiki/Rust_(programming_language)\">\n<link rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/4.0/deed.en\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Wikipedia Atom feed\" href=\"/w/index.php?title=Special:RecentChanges&amp;feed=atom\">\n<link rel=\"dns-prefetch\" href=\"//meta.wikimedia.org\" />\n<link rel=\"dns-prefetch\" href=\"auth.wikimedia.org\">\n</head>\n<body class=\"skin--responsive skin-vector skin-vector-search-vue mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject mw-editable page-Rust_programming_language rootpage-Rust_programming_language skin-vector-2022 action-view\"><a class=\"mw-jump-link\" href=\"#bodyContent\">Jump to content</a>\n<div class=\"vector-header-container\">\n\t<header class=\"vector-header mw-header no-font-mode-scale\">\n\t\t<div class=\"vector-header-start\">\n\t\t\t<nav class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\n<div id=\"vector-main-menu-dropdown\" class=\"vector-dropdown vector-main-menu-dropdown vector-button-flush-left vector-button-flush-right\"  title=\"Main menu\" >\n\t<input type=\"checkbox\" id=\"vector-main-menu-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-main-menu-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Main menu\"  >\n\t<label id=\"vector-main-menu-dropdown-label\" for=\"vector-main-menu-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu\"></span>\n\n<span class=\"vector-dropdown-label-text\">Main menu</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t<div id=\"vector-main-menu-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-main-menu\" class=\"vector-main-menu vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-main-menu-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"main-menu-pinned\"\n\tdata-pinnable-element-id=\"vector-main-menu\"\n\tdata-pinned-container-id=\"vector-main-menu-pinned-container\"\n\tdata-unpinned-container-id=\"vector-main-menu-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Main menu</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-main-menu.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-main-menu.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-navigation\" class=\"vector-menu mw-portlet mw-portlet-navigation\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tNavigation\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-mainpage-description\" class=\"mw-list-item\"><a href=\"/wiki/Main_Page\" title=\"Visit the main page [z]\" accesskey=\"z\"><span>Main page</span></a></li><li id=\"n-contents\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Contents\" title=\"Guides to browsing Wikipedia\"><span>Contents</span></a></li><li id=\"n-currentevents\" class=\"mw-list-item\"><a href=\"/wiki/Portal:Current_events\" title=\"Articles related to current events\"><span>Current events</span></a></li><li id=\"n-randompage\" class=\"mw-list-item\"><a href=\"/wiki/Special:Random\" title=\"Visit a randomly selected article [x]\" accesskey=\"x\"><span>Random article</span></a></li><li id=\"n-aboutsite\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:About\" title=\"Learn about Wikipedia and how it works\"><span>About Wikipedia</span></a></li><li id=\"n-contactpage\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\" title=\"How to contact Wikipedia\"><span>Contact us</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\n<div id=\"p-interaction\" class=\"vector-menu mw-portlet mw-portlet-interaction\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tContribute\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-help\" class=\"mw-list-item\"><a href=\"/wiki/Help:Contents\" title=\"Guidance on how to use and edit Wikipedia\"><span>Help</span></a></li><li id=\"n-introduction\" class=\"mw-list-item\"><a href=\"/wiki/Help:Introduction\" title=\"Learn how to edit Wikipedia\"><span>Learn to edit</span></a></li><li id=\"n-portal\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Community_portal\" title=\"The hub for editors\"><span>Community portal</span></a></li><li id=\"n-recentchanges\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChanges\" title=\"A list of recent changes to Wikipedia [r]\" accesskey=\"r\"><span>Recent changes</span></a></li><li id=\"n-upload\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:File_upload_wizard\" title=\"Add images or other media for use on Wikipedia\"><span>Upload file</span></a></li><li id=\"n-specialpages\" class=\"mw-list-item\"><a href=\"/wiki/Special:SpecialPages\"><span>Special pages</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t</nav>\n\n<a href=\"/wiki/Main_Page\" class=\"mw-logo\">\n\t<img class=\"mw-logo-icon\" src=\"/static/images/icons/wikipedia.png\" alt=\"\" aria-hidden=\"true\" height=\"50\" width=\"50\">\n\t<span class=\"mw-logo-container skin-invert\">\n\t\t<img class=\"mw-logo-wordmark\" alt=\"Wikipedia\" src=\"/static/images/mobile/copyright/wikipedia-wordmark-en.svg\" style=\"width: 7.5em; height: 1.125em;\">\n\t\t<img class=\"mw-logo-tagline\" alt=\"The Free Encyclopedia\" src=\"/static/images/mobile/copyright/wikipedia-tagline-en.svg\" width=\"117\" height=\"13\" style=\"width: 7.3125em; height: 0.8125em;\">\n\t</span>\n</a>\n\n\t\t</div>\n\t\t<div class=\"vector-header-end\">\n\n<div id=\"p-search\" role=\"search\" class=\"vector-search-box-vue  vector-search-box-collapses vector-search-box-show-thumbnail vector-search-box-auto-expand-width vector-search-box\">\n\t<a href=\"/wiki/Special:Search\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only search-toggle\" title=\"Search Wikipedia [f]\" accesskey=\"f\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t</a>\n\t<div class=\"vector-typeahead-search-container\">\n\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail cdx-typeahead-search--auto-expand-width\">\n\t\t\t<form action=\"/w/index.php\" id=\"searchform\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t<div id=\"simpleSearch\" class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\t\t\t\t\t\t\t type=\"search\" name=\"search\" placeholder=\"Search Wikipedia\" aria-label=\"Search Wikipedia\" autocapitalize=\"sentences\" spellcheck=\"false\" title=\"Search Wikipedia [f]\" accesskey=\"f\" id=\"searchInput\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t</div>\n\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n\t\t\t<nav class=\"vector-user-links vector-user-links-wide\" aria-label=\"Personal tools\">\n\t<div class=\"vector-user-links-main\">\n\n<div id=\"p-vector-user-menu-preferences\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-userpage\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\n<div id=\"vector-appearance-dropdown\" class=\"vector-dropdown \"  title=\"Change the appearance of the page&#039;s font size, width, and color\" >\n\t<input type=\"checkbox\" id=\"vector-appearance-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-appearance-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Appearance\"  >\n\t<label id=\"vector-appearance-dropdown-label\" for=\"vector-appearance-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-appearance mw-ui-icon-wikimedia-appearance\"></span>\n\n<span class=\"vector-dropdown-label-text\">Appearance</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t<div id=\"vector-appearance-unpinned-container\" class=\"vector-unpinned-container\">\n\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t</nav>\n\n<div id=\"p-vector-user-menu-notifications\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-overflow\" class=\"vector-menu mw-portlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\t\t\t<li id=\"pt-sitesupport-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\" class=\"\"><span>Donate</span></a>\n</li>\n<li id=\"pt-createaccount-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Rust+%28programming+language%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\" class=\"\"><span>Create account</span></a>\n</li>\n<li id=\"pt-login-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Rust+%28programming+language%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\" class=\"\"><span>Log in</span></a>\n</li>\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t</div>\n\n<div id=\"vector-user-links-dropdown\" class=\"vector-dropdown vector-user-menu vector-button-flush-right vector-user-menu-logged-out\"  title=\"Log in and more options\" >\n\t<input type=\"checkbox\" id=\"vector-user-links-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-user-links-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Personal tools\"  >\n\t<label id=\"vector-user-links-dropdown-label\" for=\"vector-user-links-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-ellipsis mw-ui-icon-wikimedia-ellipsis\"></span>\n\n<span class=\"vector-dropdown-label-text\">Personal tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-personal\" class=\"vector-menu mw-portlet mw-portlet-personal user-links-collapsible-item\"  title=\"User menu\" >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-sitesupport\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\"><span>Donate</span></a></li><li id=\"pt-createaccount\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Rust+%28programming+language%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\"><span class=\"vector-icon mw-ui-icon-userAdd mw-ui-icon-wikimedia-userAdd\"></span> <span>Create account</span></a></li><li id=\"pt-login\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Rust+%28programming+language%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\"><span class=\"vector-icon mw-ui-icon-logIn mw-ui-icon-wikimedia-logIn\"></span> <span>Log in</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-user-menu-anon-editor\" class=\"vector-menu mw-portlet mw-portlet-user-menu-anon-editor\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPages for logged out editors <a href=\"/wiki/Help:Introduction\" aria-label=\"Learn more about editing\"><span>learn more</span></a>\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-anoncontribs\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyContributions\" title=\"A list of edits made from this IP address [y]\" accesskey=\"y\"><span>Contributions</span></a></li><li id=\"pt-anontalk\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyTalk\" title=\"Discussion about edits from this IP address [n]\" accesskey=\"n\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n</nav>\n\n\t\t</div>\n\t</header>\n</div>\n<div class=\"mw-page-container\">\n\t<div class=\"mw-page-container-inner\">\n\t\t<div class=\"vector-sitenotice-container\">\n\t\t\t<div id=\"siteNotice\"><!-- CentralNotice --></div>\n\t\t</div>\n\t\t<div class=\"vector-column-start\">\n\t\t\t<div class=\"vector-main-menu-container\">\n\t\t<div id=\"mw-navigation\">\n\t\t\t<nav id=\"mw-panel\" class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\t\t\t\t<div id=\"vector-main-menu-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t</div>\n\t\t</nav>\n\t\t</div>\n\t</div>\n\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t<nav id=\"mw-panel-toc\" aria-label=\"Contents\" data-event-name=\"ui.sidebar-toc\" class=\"mw-table-of-contents-container vector-toc-landmark\">\n\t\t\t\t\t<div id=\"vector-toc-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t\t<div id=\"vector-toc\" class=\"vector-toc vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"toc-pinned\"\n\tdata-pinnable-element-id=\"vector-toc\"\n\n\n>\n\t<h2 class=\"vector-pinnable-header-label\">Contents</h2>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-toc.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-toc.unpin\">hide</button>\n</div>\n\n\n\t<ul class=\"vector-toc-contents\" id=\"mw-panel-toc-list\">\n\t\t<li id=\"toc-mw-content-text\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t\t<a href=\"#\" class=\"vector-toc-link\">\n\t\t\t\t<div class=\"vector-toc-text\">(Top)</div>\n\t\t\t</a>\n\t\t</li>\n\t\t<li id=\"toc-Etymology\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Etymology\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">1</span>\n\t\t\t\t<span>Etymology</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Etymology-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-History\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#History\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">2</span>\n\t\t\t\t<span>History</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-History-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle History subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-History-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-2006–2009:_Early_years\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#2006–2009:_Early_years\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1</span>\n\t\t\t\t\t<span>2006–2009: Early years</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-2006–2009:_Early_years-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-2009–2012:_Mozilla_sponsorship\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#2009–2012:_Mozilla_sponsorship\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.2</span>\n\t\t\t\t\t<span>2009–2012: Mozilla sponsorship</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-2009–2012:_Mozilla_sponsorship-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-2012–2015:_Evolution\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#2012–2015:_Evolution\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.3</span>\n\t\t\t\t\t<span>2012–2015: Evolution</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-2012–2015:_Evolution-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-2015–2020:_Servo_and_early_adoption\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#2015–2020:_Servo_and_early_adoption\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.4</span>\n\t\t\t\t\t<span>2015–2020: Servo and early adoption</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-2015–2020:_Servo_and_early_adoption-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-2020–present:_Mozilla_layoffs_and_Rust_Foundation\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#2020–present:_Mozilla_layoffs_and_Rust_Foundation\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.5</span>\n\t\t\t\t\t<span>2020–present: Mozilla layoffs and Rust Foundation</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-2020–present:_Mozilla_layoffs_and_Rust_Foundation-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Syntax_and_features\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Syntax_and_features\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">3</span>\n\t\t\t\t<span>Syntax and features</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Syntax_and_features-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Syntax and features subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Syntax_and_features-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Hello_World_program\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Hello_World_program\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.1</span>\n\t\t\t\t\t<span>Hello World program</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Hello_World_program-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Variables\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Variables\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.2</span>\n\t\t\t\t\t<span>Variables</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Variables-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Block_expressions_and_control_flow\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Block_expressions_and_control_flow\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3</span>\n\t\t\t\t\t<span>Block expressions and control flow</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Block_expressions_and_control_flow-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-if_expressions\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#if_expressions\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3.1</span>\n\t\t\t\t\t<span><span>if</span> expressions</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-if_expressions-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-while_loops\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#while_loops\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3.2</span>\n\t\t\t\t\t<span><span>while</span> loops</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-while_loops-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-for_loops_and_iterators\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#for_loops_and_iterators\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3.3</span>\n\t\t\t\t\t<span><span>for</span> loops and iterators</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-for_loops_and_iterators-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-loop_and_break_statements\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#loop_and_break_statements\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3.4</span>\n\t\t\t\t\t<span><span>loop</span> and <span>break</span> statements</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-loop_and_break_statements-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Pattern_matching\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Pattern_matching\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.4</span>\n\t\t\t\t\t<span>Pattern matching</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Pattern_matching-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Types\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Types\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.5</span>\n\t\t\t\t\t<span>Types</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Types-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Primitive_types\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Primitive_types\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.5.1</span>\n\t\t\t\t\t<span>Primitive types</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Primitive_types-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Compound_types\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Compound_types\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.5.2</span>\n\t\t\t\t\t<span>Compound types</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Compound_types-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Ownership_and_references\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Ownership_and_references\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.6</span>\n\t\t\t\t\t<span>Ownership and references</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Ownership_and_references-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Lifetimes\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Lifetimes\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.6.1</span>\n\t\t\t\t\t<span>Lifetimes</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Lifetimes-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-User-defined_types\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#User-defined_types\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.7</span>\n\t\t\t\t\t<span>User-defined types</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-User-defined_types-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Standard_library\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Standard_library\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.7.1</span>\n\t\t\t\t\t<span>Standard library</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Standard_library-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Pointers\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Pointers\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.8</span>\n\t\t\t\t\t<span>Pointers</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Pointers-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Type_conversion\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Type_conversion\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.9</span>\n\t\t\t\t\t<span>Type conversion</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Type_conversion-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Polymorphism\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Polymorphism\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.10</span>\n\t\t\t\t\t<span>Polymorphism</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Polymorphism-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Trait_objects\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Trait_objects\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.10.1</span>\n\t\t\t\t\t<span>Trait objects</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Trait_objects-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Memory_safety\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Memory_safety\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.11</span>\n\t\t\t\t\t<span>Memory safety</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Memory_safety-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Memory_management\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Memory_management\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.12</span>\n\t\t\t\t\t<span>Memory management</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Memory_management-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Unsafe\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Unsafe\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.13</span>\n\t\t\t\t\t<span>Unsafe</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Unsafe-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Macros\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Macros\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.14</span>\n\t\t\t\t\t<span>Macros</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Macros-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Declarative_macros\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Declarative_macros\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.14.1</span>\n\t\t\t\t\t<span>Declarative macros</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Declarative_macros-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Procedural_macros\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Procedural_macros\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.14.2</span>\n\t\t\t\t\t<span>Procedural macros</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Procedural_macros-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Interface_with_C_and_C++\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Interface_with_C_and_C++\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.15</span>\n\t\t\t\t\t<span>Interface with C and C++</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Interface_with_C_and_C++-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Ecosystem\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Ecosystem\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">4</span>\n\t\t\t\t<span>Ecosystem</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Ecosystem-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Ecosystem subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Ecosystem-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Compiler\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Compiler\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.1</span>\n\t\t\t\t\t<span>Compiler</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Compiler-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Cargo\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Cargo\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.2</span>\n\t\t\t\t\t<span>Cargo</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Cargo-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Rustfmt\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Rustfmt\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.3</span>\n\t\t\t\t\t<span>Rustfmt</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Rustfmt-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Clippy\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Clippy\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.4</span>\n\t\t\t\t\t<span>Clippy</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Clippy-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Versioning_system\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Versioning_system\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.5</span>\n\t\t\t\t\t<span>Versioning system</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Versioning_system-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-IDE_support\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#IDE_support\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.6</span>\n\t\t\t\t\t<span>IDE support</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-IDE_support-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Performance\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Performance\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">5</span>\n\t\t\t\t<span>Performance</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Performance-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Adoption\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Adoption\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">6</span>\n\t\t\t\t<span>Adoption</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Adoption-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-In_academic_research\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#In_academic_research\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">7</span>\n\t\t\t\t<span>In academic research</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-In_academic_research-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Community\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Community\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">8</span>\n\t\t\t\t<span>Community</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Community-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Community subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Community-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Rust_Foundation\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Rust_Foundation\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">8.1</span>\n\t\t\t\t\t<span>Rust Foundation</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Rust_Foundation-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Governance_teams\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Governance_teams\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">8.2</span>\n\t\t\t\t\t<span>Governance teams</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Governance_teams-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-See_also\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#See_also\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">9</span>\n\t\t\t\t<span>See also</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-See_also-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Notes\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Notes\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">10</span>\n\t\t\t\t<span>Notes</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Notes-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-References\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#References\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">11</span>\n\t\t\t\t<span>References</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-References-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle References subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-References-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Book_sources\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Book_sources\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">11.1</span>\n\t\t\t\t\t<span>Book sources</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Book_sources-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Others\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Others\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">11.2</span>\n\t\t\t\t\t<span>Others</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Others-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-External_links\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">12</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n</ul>\n</div>\n\n\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mw-content-container\">\n\t\t\t<main id=\"content\" class=\"mw-body\">\n\t\t\t\t<header class=\"mw-body-header vector-page-titlebar no-font-mode-scale\">\n\t\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n<div id=\"vector-page-titlebar-toc\" class=\"vector-dropdown vector-page-titlebar-toc vector-button-flush-left\"  title=\"Table of Contents\" >\n\t<input type=\"checkbox\" id=\"vector-page-titlebar-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-titlebar-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t<label id=\"vector-page-titlebar-toc-label\" for=\"vector-page-titlebar-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t<div id=\"vector-page-titlebar-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t</nav>\n\t\t\t\t\t<h1 id=\"firstHeading\" class=\"firstHeading mw-first-heading\"><span class=\"mw-page-title-main\">Rust (programming language)</span></h1>\n\n<div id=\"p-lang-btn\" class=\"vector-dropdown mw-portlet mw-portlet-lang\"  >\n\t<input type=\"checkbox\" id=\"p-lang-btn-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-p-lang-btn\" class=\"vector-dropdown-checkbox mw-interlanguage-selector\" aria-label=\"Go to an article in another language. Available in 51 languages\"   >\n\t<label id=\"p-lang-btn-label\" for=\"p-lang-btn-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive mw-portlet-lang-heading-51\" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-language-progressive mw-ui-icon-wikimedia-language-progressive\"></span>\n\n<span class=\"vector-dropdown-label-text\">51 languages</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\t\t<div class=\"vector-menu-content\">\n\n\t\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t\t<li class=\"interlanguage-link interwiki-af mw-list-item\"><a href=\"https://af.wikipedia.org/wiki/Rust_(programmeertaal)\" title=\"Rust (programmeertaal) – Afrikaans\" lang=\"af\" hreflang=\"af\" data-title=\"Rust (programmeertaal)\" data-language-autonym=\"Afrikaans\" data-language-local-name=\"Afrikaans\" class=\"interlanguage-link-target\"><span>Afrikaans</span></a></li><li class=\"interlanguage-link interwiki-ar mw-list-item\"><a href=\"https://ar.wikipedia.org/wiki/%D8%B1%D8%B3%D8%AA_(%D9%84%D8%BA%D8%A9_%D8%A8%D8%B1%D9%85%D8%AC%D8%A9)\" title=\"رست (لغة برمجة) – Arabic\" lang=\"ar\" hreflang=\"ar\" data-title=\"رست (لغة برمجة)\" data-language-autonym=\"العربية\" data-language-local-name=\"Arabic\" class=\"interlanguage-link-target\"><span>العربية</span></a></li><li class=\"interlanguage-link interwiki-az mw-list-item\"><a href=\"https://az.wikipedia.org/wiki/Rust_(proqramla%C5%9Fd%C4%B1rma_dili)\" title=\"Rust (proqramlaşdırma dili) – Azerbaijani\" lang=\"az\" hreflang=\"az\" data-title=\"Rust (proqramlaşdırma dili)\" data-language-autonym=\"Azərbaycanca\" data-language-local-name=\"Azerbaijani\" class=\"interlanguage-link-target\"><span>Azərbaycanca</span></a></li><li class=\"interlanguage-link interwiki-bn mw-list-item\"><a href=\"https://bn.wikipedia.org/wiki/%E0%A6%B0%E0%A6%BE%E0%A6%B8%E0%A7%8D%E0%A6%9F_(%E0%A6%AA%E0%A7%8D%E0%A6%B0%E0%A7%8B%E0%A6%97%E0%A7%8D%E0%A6%B0%E0%A6%BE%E0%A6%AE%E0%A6%BF%E0%A6%82_%E0%A6%AD%E0%A6%BE%E0%A6%B7%E0%A6%BE)\" title=\"রাস্ট (প্রোগ্রামিং ভাষা) – Bangla\" lang=\"bn\" hreflang=\"bn\" data-title=\"রাস্ট (প্রোগ্রামিং ভাষা)\" data-language-autonym=\"বাংলা\" data-language-local-name=\"Bangla\" class=\"interlanguage-link-target\"><span>বাংলা</span></a></li><li class=\"interlanguage-link interwiki-zh-min-nan mw-list-item\"><a href=\"https://zh-min-nan.wikipedia.org/wiki/Rust_(pian-t%C3%AAng_g%C3%BA-gi%C3%A2n)\" title=\"Rust (pian-têng gú-giân) – Minnan\" lang=\"nan\" hreflang=\"nan\" data-title=\"Rust (pian-têng gú-giân)\" data-language-autonym=\"閩南語 / Bân-lâm-gí\" data-language-local-name=\"Minnan\" class=\"interlanguage-link-target\"><span>閩南語 / Bân-lâm-gí</span></a></li><li class=\"interlanguage-link interwiki-be mw-list-item\"><a href=\"https://be.wikipedia.org/wiki/Rust\" title=\"Rust – Belarusian\" lang=\"be\" hreflang=\"be\" data-title=\"Rust\" data-language-autonym=\"Беларуская\" data-language-local-name=\"Belarusian\" class=\"interlanguage-link-target\"><span>Беларуская</span></a></li><li class=\"interlanguage-link interwiki-ca mw-list-item\"><a href=\"https://ca.wikipedia.org/wiki/Rust_(llenguatge_de_programaci%C3%B3)\" title=\"Rust (llenguatge de programació) – Catalan\" lang=\"ca\" hreflang=\"ca\" data-title=\"Rust (llenguatge de programació)\" data-language-autonym=\"Català\" data-language-local-name=\"Catalan\" class=\"interlanguage-link-target\"><span>Català</span></a></li><li class=\"interlanguage-link interwiki-cs mw-list-item\"><a href=\"https://cs.wikipedia.org/wiki/Rust_(programovac%C3%AD_jazyk)\" title=\"Rust (programovací jazyk) – Czech\" lang=\"cs\" hreflang=\"cs\" data-title=\"Rust (programovací jazyk)\" data-language-autonym=\"Čeština\" data-language-local-name=\"Czech\" class=\"interlanguage-link-target\"><span>Čeština</span></a></li><li class=\"interlanguage-link interwiki-da mw-list-item\"><a href=\"https://da.wikipedia.org/wiki/Rust_(programmeringssprog)\" title=\"Rust (programmeringssprog) – Danish\" lang=\"da\" hreflang=\"da\" data-title=\"Rust (programmeringssprog)\" data-language-autonym=\"Dansk\" data-language-local-name=\"Danish\" class=\"interlanguage-link-target\"><span>Dansk</span></a></li><li class=\"interlanguage-link interwiki-de mw-list-item\"><a href=\"https://de.wikipedia.org/wiki/Rust_(Programmiersprache)\" title=\"Rust (Programmiersprache) – German\" lang=\"de\" hreflang=\"de\" data-title=\"Rust (Programmiersprache)\" data-language-autonym=\"Deutsch\" data-language-local-name=\"German\" class=\"interlanguage-link-target\"><span>Deutsch</span></a></li><li class=\"interlanguage-link interwiki-et mw-list-item\"><a href=\"https://et.wikipedia.org/wiki/Rust\" title=\"Rust – Estonian\" lang=\"et\" hreflang=\"et\" data-title=\"Rust\" data-language-autonym=\"Eesti\" data-language-local-name=\"Estonian\" class=\"interlanguage-link-target\"><span>Eesti</span></a></li><li class=\"interlanguage-link interwiki-es mw-list-item\"><a href=\"https://es.wikipedia.org/wiki/Rust_(lenguaje_de_programaci%C3%B3n)\" title=\"Rust (lenguaje de programación) – Spanish\" lang=\"es\" hreflang=\"es\" data-title=\"Rust (lenguaje de programación)\" data-language-autonym=\"Español\" data-language-local-name=\"Spanish\" class=\"interlanguage-link-target\"><span>Español</span></a></li><li class=\"interlanguage-link interwiki-eo mw-list-item\"><a href=\"https://eo.wikipedia.org/wiki/Rust_(programlingvo)\" title=\"Rust (programlingvo) – Esperanto\" lang=\"eo\" hreflang=\"eo\" data-title=\"Rust (programlingvo)\" data-language-autonym=\"Esperanto\" data-language-local-name=\"Esperanto\" class=\"interlanguage-link-target\"><span>Esperanto</span></a></li><li class=\"interlanguage-link interwiki-eu mw-list-item\"><a href=\"https://eu.wikipedia.org/wiki/Rust_(programazio-lengoaia)\" title=\"Rust (programazio-lengoaia) – Basque\" lang=\"eu\" hreflang=\"eu\" data-title=\"Rust (programazio-lengoaia)\" data-language-autonym=\"Euskara\" data-language-local-name=\"Basque\" class=\"interlanguage-link-target\"><span>Euskara</span></a></li><li class=\"interlanguage-link interwiki-fa mw-list-item\"><a href=\"https://fa.wikipedia.org/wiki/%D8%B1%D8%A7%D8%B3%D8%AA_(%D8%B2%D8%A8%D8%A7%D9%86_%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%E2%80%8C%D9%86%D9%88%DB%8C%D8%B3%DB%8C)\" title=\"راست (زبان برنامه‌نویسی) – Persian\" lang=\"fa\" hreflang=\"fa\" data-title=\"راست (زبان برنامه‌نویسی)\" data-language-autonym=\"فارسی\" data-language-local-name=\"Persian\" class=\"interlanguage-link-target\"><span>فارسی</span></a></li><li class=\"interlanguage-link interwiki-fr mw-list-item\"><a href=\"https://fr.wikipedia.org/wiki/Rust_(langage)\" title=\"Rust (langage) – French\" lang=\"fr\" hreflang=\"fr\" data-title=\"Rust (langage)\" data-language-autonym=\"Français\" data-language-local-name=\"French\" class=\"interlanguage-link-target\"><span>Français</span></a></li><li class=\"interlanguage-link interwiki-gl mw-list-item\"><a href=\"https://gl.wikipedia.org/wiki/Rust_(linguaxe_de_programaci%C3%B3n)\" title=\"Rust (linguaxe de programación) – Galician\" lang=\"gl\" hreflang=\"gl\" data-title=\"Rust (linguaxe de programación)\" data-language-autonym=\"Galego\" data-language-local-name=\"Galician\" class=\"interlanguage-link-target\"><span>Galego</span></a></li><li class=\"interlanguage-link interwiki-ko mw-list-item\"><a href=\"https://ko.wikipedia.org/wiki/%EB%9F%AC%EC%8A%A4%ED%8A%B8_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%EC%96%B8%EC%96%B4)\" title=\"러스트 (프로그래밍 언어) – Korean\" lang=\"ko\" hreflang=\"ko\" data-title=\"러스트 (프로그래밍 언어)\" data-language-autonym=\"한국어\" data-language-local-name=\"Korean\" class=\"interlanguage-link-target\"><span>한국어</span></a></li><li class=\"interlanguage-link interwiki-hr mw-list-item\"><a href=\"https://hr.wikipedia.org/wiki/Rust_(programski_jezik)\" title=\"Rust (programski jezik) – Croatian\" lang=\"hr\" hreflang=\"hr\" data-title=\"Rust (programski jezik)\" data-language-autonym=\"Hrvatski\" data-language-local-name=\"Croatian\" class=\"interlanguage-link-target\"><span>Hrvatski</span></a></li><li class=\"interlanguage-link interwiki-id mw-list-item\"><a href=\"https://id.wikipedia.org/wiki/Rust_(bahasa_pemrograman)\" title=\"Rust (bahasa pemrograman) – Indonesian\" lang=\"id\" hreflang=\"id\" data-title=\"Rust (bahasa pemrograman)\" data-language-autonym=\"Bahasa Indonesia\" data-language-local-name=\"Indonesian\" class=\"interlanguage-link-target\"><span>Bahasa Indonesia</span></a></li><li class=\"interlanguage-link interwiki-is mw-list-item\"><a href=\"https://is.wikipedia.org/wiki/Rust_(forritunarm%C3%A1l)\" title=\"Rust (forritunarmál) – Icelandic\" lang=\"is\" hreflang=\"is\" data-title=\"Rust (forritunarmál)\" data-language-autonym=\"Íslenska\" data-language-local-name=\"Icelandic\" class=\"interlanguage-link-target\"><span>Íslenska</span></a></li><li class=\"interlanguage-link interwiki-it mw-list-item\"><a href=\"https://it.wikipedia.org/wiki/Rust_(linguaggio_di_programmazione)\" title=\"Rust (linguaggio di programmazione) – Italian\" lang=\"it\" hreflang=\"it\" data-title=\"Rust (linguaggio di programmazione)\" data-language-autonym=\"Italiano\" data-language-local-name=\"Italian\" class=\"interlanguage-link-target\"><span>Italiano</span></a></li><li class=\"interlanguage-link interwiki-he mw-list-item\"><a href=\"https://he.wikipedia.org/wiki/%D7%A8%D7%90%D7%A1%D7%98_(%D7%A9%D7%A4%D7%AA_%D7%AA%D7%9B%D7%A0%D7%95%D7%AA)\" title=\"ראסט (שפת תכנות) – Hebrew\" lang=\"he\" hreflang=\"he\" data-title=\"ראסט (שפת תכנות)\" data-language-autonym=\"עברית\" data-language-local-name=\"Hebrew\" class=\"interlanguage-link-target\"><span>עברית</span></a></li><li class=\"interlanguage-link interwiki-sw mw-list-item\"><a href=\"https://sw.wikipedia.org/wiki/Rust_(lugha_ya_kompyuta)\" title=\"Rust (lugha ya kompyuta) – Swahili\" lang=\"sw\" hreflang=\"sw\" data-title=\"Rust (lugha ya kompyuta)\" data-language-autonym=\"Kiswahili\" data-language-local-name=\"Swahili\" class=\"interlanguage-link-target\"><span>Kiswahili</span></a></li><li class=\"interlanguage-link interwiki-lv mw-list-item\"><a href=\"https://lv.wikipedia.org/wiki/Rust\" title=\"Rust – Latvian\" lang=\"lv\" hreflang=\"lv\" data-title=\"Rust\" data-language-autonym=\"Latviešu\" data-language-local-name=\"Latvian\" class=\"interlanguage-link-target\"><span>Latviešu</span></a></li><li class=\"interlanguage-link interwiki-lmo mw-list-item\"><a href=\"https://lmo.wikipedia.org/wiki/Rust_(lenguagg_de_programazzion)\" title=\"Rust (lenguagg de programazzion) – Lombard\" lang=\"lmo\" hreflang=\"lmo\" data-title=\"Rust (lenguagg de programazzion)\" data-language-autonym=\"Lombard\" data-language-local-name=\"Lombard\" class=\"interlanguage-link-target\"><span>Lombard</span></a></li><li class=\"interlanguage-link interwiki-hu mw-list-item\"><a href=\"https://hu.wikipedia.org/wiki/Rust_(programoz%C3%A1si_nyelv)\" title=\"Rust (programozási nyelv) – Hungarian\" lang=\"hu\" hreflang=\"hu\" data-title=\"Rust (programozási nyelv)\" data-language-autonym=\"Magyar\" data-language-local-name=\"Hungarian\" class=\"interlanguage-link-target\"><span>Magyar</span></a></li><li class=\"interlanguage-link interwiki-ml mw-list-item\"><a href=\"https://ml.wikipedia.org/wiki/%E0%B4%B1%E0%B4%B8%E0%B5%8D%E0%B4%B1%E0%B5%8D%E0%B4%B1%E0%B5%8D_(%E0%B4%AA%E0%B5%8D%E0%B4%B0%E0%B5%8B%E0%B4%97%E0%B5%8D%E0%B4%B0%E0%B4%BE%E0%B4%AE%E0%B4%BF%E0%B4%82%E0%B4%97%E0%B5%8D_%E0%B4%AD%E0%B4%BE%E0%B4%B7)\" title=\"റസ്റ്റ് (പ്രോഗ്രാമിംഗ് ഭാഷ) – Malayalam\" lang=\"ml\" hreflang=\"ml\" data-title=\"റസ്റ്റ് (പ്രോഗ്രാമിംഗ് ഭാഷ)\" data-language-autonym=\"മലയാളം\" data-language-local-name=\"Malayalam\" class=\"interlanguage-link-target\"><span>മലയാളം</span></a></li><li class=\"interlanguage-link interwiki-ms mw-list-item\"><a href=\"https://ms.wikipedia.org/wiki/Rust_(bahasa_pengaturcaraan)\" title=\"Rust (bahasa pengaturcaraan) – Malay\" lang=\"ms\" hreflang=\"ms\" data-title=\"Rust (bahasa pengaturcaraan)\" data-language-autonym=\"Bahasa Melayu\" data-language-local-name=\"Malay\" class=\"interlanguage-link-target\"><span>Bahasa Melayu</span></a></li><li class=\"interlanguage-link interwiki-nl mw-list-item\"><a href=\"https://nl.wikipedia.org/wiki/Rust_(programmeertaal)\" title=\"Rust (programmeertaal) – Dutch\" lang=\"nl\" hreflang=\"nl\" data-title=\"Rust (programmeertaal)\" data-language-autonym=\"Nederlands\" data-language-local-name=\"Dutch\" class=\"interlanguage-link-target\"><span>Nederlands</span></a></li><li class=\"interlanguage-link interwiki-ja mw-list-item\"><a href=\"https://ja.wikipedia.org/wiki/Rust_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)\" title=\"Rust (プログラミング言語) – Japanese\" lang=\"ja\" hreflang=\"ja\" data-title=\"Rust (プログラミング言語)\" data-language-autonym=\"日本語\" data-language-local-name=\"Japanese\" class=\"interlanguage-link-target\"><span>日本語</span></a></li><li class=\"interlanguage-link interwiki-no mw-list-item\"><a href=\"https://no.wikipedia.org/wiki/Rust_(programmeringsspr%C3%A5k)\" title=\"Rust (programmeringsspråk) – Norwegian Bokmål\" lang=\"nb\" hreflang=\"nb\" data-title=\"Rust (programmeringsspråk)\" data-language-autonym=\"Norsk bokmål\" data-language-local-name=\"Norwegian Bokmål\" class=\"interlanguage-link-target\"><span>Norsk bokmål</span></a></li><li class=\"interlanguage-link interwiki-pl mw-list-item\"><a href=\"https://pl.wikipedia.org/wiki/Rust_(j%C4%99zyk_programowania)\" title=\"Rust (język programowania) – Polish\" lang=\"pl\" hreflang=\"pl\" data-title=\"Rust (język programowania)\" data-language-autonym=\"Polski\" data-language-local-name=\"Polish\" class=\"interlanguage-link-target\"><span>Polski</span></a></li><li class=\"interlanguage-link interwiki-pt mw-list-item\"><a href=\"https://pt.wikipedia.org/wiki/Rust_(linguagem_de_programa%C3%A7%C3%A3o)\" title=\"Rust (linguagem de programação) – Portuguese\" lang=\"pt\" hreflang=\"pt\" data-title=\"Rust (linguagem de programação)\" data-language-autonym=\"Português\" data-language-local-name=\"Portuguese\" class=\"interlanguage-link-target\"><span>Português</span></a></li><li class=\"interlanguage-link interwiki-ro mw-list-item\"><a href=\"https://ro.wikipedia.org/wiki/Rust_(limbaj_de_programare)\" title=\"Rust (limbaj de programare) – Romanian\" lang=\"ro\" hreflang=\"ro\" data-title=\"Rust (limbaj de programare)\" data-language-autonym=\"Română\" data-language-local-name=\"Romanian\" class=\"interlanguage-link-target\"><span>Română</span></a></li><li class=\"interlanguage-link interwiki-qu mw-list-item\"><a href=\"https://qu.wikipedia.org/wiki/Rust\" title=\"Rust – Quechua\" lang=\"qu\" hreflang=\"qu\" data-title=\"Rust\" data-language-autonym=\"Runa Simi\" data-language-local-name=\"Quechua\" class=\"interlanguage-link-target\"><span>Runa Simi</span></a></li><li class=\"interlanguage-link interwiki-ru mw-list-item\"><a href=\"https://ru.wikipedia.org/wiki/Rust_(%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)\" title=\"Rust (язык программирования) – Russian\" lang=\"ru\" hreflang=\"ru\" data-title=\"Rust (язык программирования)\" data-language-autonym=\"Русский\" data-language-local-name=\"Russian\" class=\"interlanguage-link-target\"><span>Русский</span></a></li><li class=\"interlanguage-link interwiki-sq mw-list-item\"><a href=\"https://sq.wikipedia.org/wiki/Rust_(gjuh%C3%AB_programimi)\" title=\"Rust (gjuhë programimi) – Albanian\" lang=\"sq\" hreflang=\"sq\" data-title=\"Rust (gjuhë programimi)\" data-language-autonym=\"Shqip\" data-language-local-name=\"Albanian\" class=\"interlanguage-link-target\"><span>Shqip</span></a></li><li class=\"interlanguage-link interwiki-simple mw-list-item\"><a href=\"https://simple.wikipedia.org/wiki/Rust_(programming_language)\" title=\"Rust (programming language) – Simple English\" lang=\"en-simple\" hreflang=\"en-simple\" data-title=\"Rust (programming language)\" data-language-autonym=\"Simple English\" data-language-local-name=\"Simple English\" class=\"interlanguage-link-target\"><span>Simple English</span></a></li><li class=\"interlanguage-link interwiki-sk mw-list-item\"><a href=\"https://sk.wikipedia.org/wiki/Rust_(programovac%C3%AD_jazyk)\" title=\"Rust (programovací jazyk) – Slovak\" lang=\"sk\" hreflang=\"sk\" data-title=\"Rust (programovací jazyk)\" data-language-autonym=\"Slovenčina\" data-language-local-name=\"Slovak\" class=\"interlanguage-link-target\"><span>Slovenčina</span></a></li><li class=\"interlanguage-link interwiki-sr mw-list-item\"><a href=\"https://sr.wikipedia.org/wiki/Rust_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D1%81%D0%BA%D0%B8_%D1%98%D0%B5%D0%B7%D0%B8%D0%BA)\" title=\"Rust (програмски језик) – Serbian\" lang=\"sr\" hreflang=\"sr\" data-title=\"Rust (програмски језик)\" data-language-autonym=\"Српски / srpski\" data-language-local-name=\"Serbian\" class=\"interlanguage-link-target\"><span>Српски / srpski</span></a></li><li class=\"interlanguage-link interwiki-fi mw-list-item\"><a href=\"https://fi.wikipedia.org/wiki/Rust_(ohjelmointikieli)\" title=\"Rust (ohjelmointikieli) – Finnish\" lang=\"fi\" hreflang=\"fi\" data-title=\"Rust (ohjelmointikieli)\" data-language-autonym=\"Suomi\" data-language-local-name=\"Finnish\" class=\"interlanguage-link-target\"><span>Suomi</span></a></li><li class=\"interlanguage-link interwiki-sv mw-list-item\"><a href=\"https://sv.wikipedia.org/wiki/Rust_(programspr%C3%A5k)\" title=\"Rust (programspråk) – Swedish\" lang=\"sv\" hreflang=\"sv\" data-title=\"Rust (programspråk)\" data-language-autonym=\"Svenska\" data-language-local-name=\"Swedish\" class=\"interlanguage-link-target\"><span>Svenska</span></a></li><li class=\"interlanguage-link interwiki-te mw-list-item\"><a href=\"https://te.wikipedia.org/wiki/%E0%B0%B0%E0%B0%B8%E0%B1%8D%E0%B0%9F%E0%B1%8D_(%E0%B0%AA%E0%B1%8D%E0%B0%B0%E0%B1%8B%E0%B0%97%E0%B1%8D%E0%B0%B0%E0%B0%BE%E0%B0%AE%E0%B0%BF%E0%B0%82%E0%B0%97%E0%B1%8D_%E0%B0%AD%E0%B0%BE%E0%B0%B7)\" title=\"రస్ట్ (ప్రోగ్రామింగ్ భాష) – Telugu\" lang=\"te\" hreflang=\"te\" data-title=\"రస్ట్ (ప్రోగ్రామింగ్ భాష)\" data-language-autonym=\"తెలుగు\" data-language-local-name=\"Telugu\" class=\"interlanguage-link-target\"><span>తెలుగు</span></a></li><li class=\"interlanguage-link interwiki-th mw-list-item\"><a href=\"https://th.wikipedia.org/wiki/%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B8%A3%E0%B8%B1%E0%B8%AA%E0%B8%95%E0%B9%8C\" title=\"ภาษารัสต์ – Thai\" lang=\"th\" hreflang=\"th\" data-title=\"ภาษารัสต์\" data-language-autonym=\"ไทย\" data-language-local-name=\"Thai\" class=\"interlanguage-link-target\"><span>ไทย</span></a></li><li class=\"interlanguage-link interwiki-tr mw-list-item\"><a href=\"https://tr.wikipedia.org/wiki/Rust_(programlama_dili)\" title=\"Rust (programlama dili) – Turkish\" lang=\"tr\" hreflang=\"tr\" data-title=\"Rust (programlama dili)\" data-language-autonym=\"Türkçe\" data-language-local-name=\"Turkish\" class=\"interlanguage-link-target\"><span>Türkçe</span></a></li><li class=\"interlanguage-link interwiki-uk mw-list-item\"><a href=\"https://uk.wikipedia.org/wiki/Rust_(%D0%BC%D0%BE%D0%B2%D0%B0_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D1%83%D0%B2%D0%B0%D0%BD%D0%BD%D1%8F)\" title=\"Rust (мова програмування) – Ukrainian\" lang=\"uk\" hreflang=\"uk\" data-title=\"Rust (мова програмування)\" data-language-autonym=\"Українська\" data-language-local-name=\"Ukrainian\" class=\"interlanguage-link-target\"><span>Українська</span></a></li><li class=\"interlanguage-link interwiki-vi mw-list-item\"><a href=\"https://vi.wikipedia.org/wiki/Rust_(ng%C3%B4n_ng%E1%BB%AF_l%E1%BA%ADp_tr%C3%ACnh)\" title=\"Rust (ngôn ngữ lập trình) – Vietnamese\" lang=\"vi\" hreflang=\"vi\" data-title=\"Rust (ngôn ngữ lập trình)\" data-language-autonym=\"Tiếng Việt\" data-language-local-name=\"Vietnamese\" class=\"interlanguage-link-target\"><span>Tiếng Việt</span></a></li><li class=\"interlanguage-link interwiki-zh-yue mw-list-item\"><a href=\"https://zh-yue.wikipedia.org/wiki/Rust\" title=\"Rust – Cantonese\" lang=\"yue\" hreflang=\"yue\" data-title=\"Rust\" data-language-autonym=\"粵語\" data-language-local-name=\"Cantonese\" class=\"interlanguage-link-target\"><span>粵語</span></a></li><li class=\"interlanguage-link interwiki-zh mw-list-item\"><a href=\"https://zh.wikipedia.org/wiki/Rust_(%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80)\" title=\"Rust (编程语言) – Chinese\" lang=\"zh\" hreflang=\"zh\" data-title=\"Rust (编程语言)\" data-language-autonym=\"中文\" data-language-local-name=\"Chinese\" class=\"interlanguage-link-target\"><span>中文</span></a></li><li class=\"interlanguage-link interwiki-dtp mw-list-item\"><a href=\"https://dtp.wikipedia.org/wiki/Rust_(boros_tokud)\" title=\"Rust (boros tokud) – Central Dusun\" lang=\"dtp\" hreflang=\"dtp\" data-title=\"Rust (boros tokud)\" data-language-autonym=\"Kadazandusun\" data-language-local-name=\"Central Dusun\" class=\"interlanguage-link-target\"><span>Kadazandusun</span></a></li>\n\t\t\t</ul>\n\t\t\t<div class=\"after-portlet after-portlet-lang\"><span class=\"wb-langlinks-edit wb-langlinks-link\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q575650#sitelinks-wikipedia\" title=\"Edit interlanguage links\" class=\"wbc-editpage\">Edit links</a></span></div>\n\t\t</div>\n\n\t</div>\n</div>\n</header>\n\t\t\t\t<div class=\"vector-page-toolbar vector-feature-custom-font-size-clientpref--excluded\">\n\t\t\t\t\t<div class=\"vector-page-toolbar-container\">\n\t\t\t\t\t\t<div id=\"left-navigation\">\n\t\t\t\t\t\t\t<nav aria-label=\"Namespaces\">\n\n<div id=\"p-associated-pages\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-associated-pages\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-nstab-main\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Rust_(programming_language)\" title=\"View the content page [c]\" accesskey=\"c\"><span>Article</span></a></li><li id=\"ca-talk\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/wiki/Talk:Rust_(programming_language)\" rel=\"discussion\" title=\"Discuss improvements to the content page [t]\" accesskey=\"t\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"vector-variants-dropdown\" class=\"vector-dropdown emptyPortlet\"  >\n\t<input type=\"checkbox\" id=\"vector-variants-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-variants-dropdown\" class=\"vector-dropdown-checkbox \" aria-label=\"Change language variant\"   >\n\t<label id=\"vector-variants-dropdown-label\" for=\"vector-variants-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">English</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-variants\" class=\"vector-menu mw-portlet mw-portlet-variants emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div id=\"right-navigation\" class=\"vector-collapsible\">\n\t\t\t\t\t\t\t<nav aria-label=\"Views\">\n\n<div id=\"p-views\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-views\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-view\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Rust_(programming_language)\"><span>Read</span></a></li><li id=\"ca-edit\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-history\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=history\" title=\"Past revisions of this page [h]\" accesskey=\"h\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\n<div id=\"vector-page-tools-dropdown\" class=\"vector-dropdown vector-page-tools-dropdown\"  >\n\t<input type=\"checkbox\" id=\"vector-page-tools-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-tools-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Tools\"  >\n\t<label id=\"vector-page-tools-dropdown-label\" for=\"vector-page-tools-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">Tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t\t\t<div id=\"vector-page-tools-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-page-tools\" class=\"vector-page-tools vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-page-tools-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"page-tools-pinned\"\n\tdata-pinnable-element-id=\"vector-page-tools\"\n\tdata-pinned-container-id=\"vector-page-tools-pinned-container\"\n\tdata-unpinned-container-id=\"vector-page-tools-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Tools</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-page-tools.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-page-tools.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-cactions\" class=\"vector-menu mw-portlet mw-portlet-cactions emptyPortlet vector-has-collapsible-items\"  title=\"More options\" >\n\t<div class=\"vector-menu-heading\">\n\t\tActions\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-more-view\" class=\"selected vector-more-collapsible-item mw-list-item\"><a href=\"/wiki/Rust_(programming_language)\"><span>Read</span></a></li><li id=\"ca-more-edit\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-more-history\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=history\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-tb\" class=\"vector-menu mw-portlet mw-portlet-tb\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tGeneral\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-whatlinkshere\" class=\"mw-list-item\"><a href=\"/wiki/Special:WhatLinksHere/Rust_(programming_language)\" title=\"List of all English Wikipedia pages containing links to this page [j]\" accesskey=\"j\"><span>What links here</span></a></li><li id=\"t-recentchangeslinked\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChangesLinked/Rust_(programming_language)\" rel=\"nofollow\" title=\"Recent changes in pages linked from this page [k]\" accesskey=\"k\"><span>Related changes</span></a></li><li id=\"t-upload\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:File_Upload_Wizard\" title=\"Upload files [u]\" accesskey=\"u\"><span>Upload file</span></a></li><li id=\"t-permalink\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;oldid=1314392250\" title=\"Permanent link to this revision of this page\"><span>Permanent link</span></a></li><li id=\"t-info\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=info\" title=\"More information about this page\"><span>Page information</span></a></li><li id=\"t-cite\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:CiteThisPage&amp;page=Rust_%28programming_language%29&amp;id=1314392250&amp;wpFormIdentifier=titleform\" title=\"Information on how to cite this page\"><span>Cite this page</span></a></li><li id=\"t-urlshortener\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:UrlShortener&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FRust_%28programming_language%29\"><span>Get shortened URL</span></a></li><li id=\"t-urlshortener-qrcode\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:QrCode&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FRust_%28programming_language%29\"><span>Download QR code</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-coll-print_export\" class=\"vector-menu mw-portlet mw-portlet-coll-print_export\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPrint/export\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"coll-download-as-rl\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:DownloadAsPdf&amp;page=Rust_%28programming_language%29&amp;action=show-download-screen\" title=\"Download this page as a PDF file\"><span>Download as PDF</span></a></li><li id=\"t-print\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Rust_(programming_language)&amp;printable=yes\" title=\"Printable version of this page [p]\" accesskey=\"p\"><span>Printable version</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-wikibase-otherprojects\" class=\"vector-menu mw-portlet mw-portlet-wikibase-otherprojects\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tIn other projects\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li class=\"wb-otherproject-link wb-otherproject-commons mw-list-item\"><a href=\"https://commons.wikimedia.org/wiki/Category:Rust_(programming_language)\" hreflang=\"en\"><span>Wikimedia Commons</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikiversity mw-list-item\"><a href=\"https://en.wikiversity.org/wiki/Rust\" hreflang=\"en\"><span>Wikiversity</span></a></li><li id=\"t-wikibase\" class=\"wb-otherproject-link wb-otherproject-wikibase-dataitem mw-list-item\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q575650\" title=\"Structured data on this page hosted by Wikidata [g]\" accesskey=\"g\"><span>Wikidata item</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"vector-column-end no-font-mode-scale\">\n\t\t\t\t\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\t\t\t\t\t\t\t<div id=\"vector-page-tools-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\t\t\t\t\t\t\t<div id=\"vector-appearance-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t<div id=\"vector-appearance\" class=\"vector-appearance vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-appearance-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"appearance-pinned\"\n\tdata-pinnable-element-id=\"vector-appearance\"\n\tdata-pinned-container-id=\"vector-appearance-pinned-container\"\n\tdata-unpinned-container-id=\"vector-appearance-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Appearance</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-appearance.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-appearance.unpin\">hide</button>\n</div>\n\n\n</div>\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"bodyContent\" class=\"vector-body\" aria-labelledby=\"firstHeading\" data-mw-ve-target-container>\n\t\t\t\t\t<div class=\"vector-body-before-content\">\n\t\t\t\t\t\t\t<div class=\"mw-indicators\">\n\t\t<div id=\"mw-indicator-good-star\" class=\"mw-indicator\"><div class=\"mw-parser-output\"><span typeof=\"mw:File\"><a href=\"/wiki/Wikipedia:Good_articles*\" title=\"This is a good article. Click here for more information.\"><img alt=\"This is a good article. Click here for more information.\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/94/Symbol_support_vote.svg/20px-Symbol_support_vote.svg.png\" decoding=\"async\" width=\"19\" height=\"20\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/94/Symbol_support_vote.svg/40px-Symbol_support_vote.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></a></span></div></div>\n\t\t</div>\n\n\t\t\t\t\t\t<div id=\"siteSub\" class=\"noprint\">From Wikipedia, the free encyclopedia</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"contentSub\"><div id=\"mw-content-subtitle\"></div></div>\n\n\n\t\t\t\t\t<div id=\"mw-content-text\" class=\"mw-body-content\"><div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><div class=\"shortdescription nomobile noexcerpt noprint searchaux\" style=\"display:none\">General-purpose programming language</div>\n<p class=\"mw-empty-elt\">\n\n\n</p>\n<style data-mw-deduplicate=\"TemplateStyles:r1295905060\">.mw-parser-output .infobox-subbox{padding:0;border:none;margin:-3px;width:auto;min-width:100%;font-size:100%;clear:none;float:none;background-color:transparent}.mw-parser-output .infobox-3cols-child{margin:auto}.mw-parser-output .infobox .navbar{font-size:100%}@media screen{html.skin-theme-clientpref-night .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media(min-width:640px){body.skin--responsive .mw-parser-output .infobox-table{display:table!important}body.skin--responsive .mw-parser-output .infobox-table>caption{display:table-caption!important}body.skin--responsive .mw-parser-output .infobox-table>tbody{display:table-row-group}body.skin--responsive .mw-parser-output .infobox-table th,body.skin--responsive .mw-parser-output .infobox-table td{padding-left:inherit;padding-right:inherit}}</style><table class=\"infobox vevent\"><tbody><tr><th colspan=\"2\" class=\"infobox-above\" style=\"background-color:#e0e0e0;\">Rust</th></tr><tr><td colspan=\"2\" class=\"infobox-image\"><span class=\"skin-invert\" typeof=\"mw:File\"><a href=\"/wiki/File:Rust_programming_language_black_logo.svg\" class=\"mw-file-description\"><img alt=\"Rust logo; a capital letter R set into a sprocket\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/150px-Rust_programming_language_black_logo.svg.png\" decoding=\"async\" width=\"150\" height=\"150\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/225px-Rust_programming_language_black_logo.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/300px-Rust_programming_language_black_logo.svg.png 2x\" data-file-width=\"106\" data-file-height=\"106\" /></a></span></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Programming_paradigm\" title=\"Programming paradigm\">Paradigms</a></th><td class=\"infobox-data\"><style data-mw-deduplicate=\"TemplateStyles:r1288773161\">.mw-parser-output ul.cslist,.mw-parser-output ul.sslist,.mw-parser-output ul.andlist,.mw-parser-output ul.andlistoxford{margin:0;padding:0;display:inline-block;list-style:none}.mw-parser-output ul.cslist-embedded{display:inline}.mw-parser-output .cslist li,.mw-parser-output .sslist li,.mw-parser-output .andlist li,.mw-parser-output .andlistoxford li{margin:0;padding:0 0.25em 0 0;display:inline-block}.mw-parser-output .cslist li:after,.mw-parser-output .andlistoxford li:after{content:\", \"}.mw-parser-output .sslist li:after{content:\"; \"}.mw-parser-output .cslist li:last-child:after,.mw-parser-output .sslist li:last-child:after,.mw-parser-output .andlist li:last-child:after,.mw-parser-output .andlistoxford li:last-child:after{content:none}.mw-parser-output .andlist li:nth-last-child(2):after{content:\" and \"}.mw-parser-output .andlistoxford li:nth-last-child(2):after{content:\", and \"}</style><ul class=\"cslist\"><li><a href=\"/wiki/Concurrent_computing\" title=\"Concurrent computing\">Concurrent</a></li><li><a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional</a></li><li><a href=\"/wiki/Generic_programming\" title=\"Generic programming\">generic</a></li><li><a href=\"/wiki/Imperative_programming\" title=\"Imperative programming\">imperative</a></li><li><a href=\"/wiki/Structured_programming\" title=\"Structured programming\">structured</a></li></ul></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Software_developer\" class=\"mw-redirect\" title=\"Software developer\">Developer</a></th><td class=\"infobox-data organiser\">The Rust Team</td></tr><tr><th scope=\"row\" class=\"infobox-label\">First&#160;appeared</th><td class=\"infobox-data\">January&#160;19, 2012<span class=\"noprint\">&#59;&#32;13 years ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">2012-01-19</span>)</span></td></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1295905060\" /></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"white-space: nowrap;\"><a href=\"/wiki/Software_release_life_cycle\" title=\"Software release life cycle\">Stable release</a></th><td class=\"infobox-data\"><div style=\"margin:0px;\">1.90.0<sup id=\"cite_ref-wikidata-625dfa02256b12affe5cdb18bbe05fea7b2cb7a3-v20_1-0\" class=\"reference\"><a href=\"#cite_note-wikidata-625dfa02256b12affe5cdb18bbe05fea7b2cb7a3-v20-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup>&#160;<span class=\"mw-valign-text-top\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q575650?uselang=en#P348\" title=\"Edit this on Wikidata\"><img alt=\"Edit this on Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span>\n   / September 18, 2025<span class=\"noprint\">&#59;&#32;13 days ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">September 18, 2025</span>)</span></div></td></tr><tr style=\"display:none\"><td colspan=\"2\">\n</td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Type_system\" title=\"Type system\">Typing discipline</a></th><td class=\"infobox-data\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1288773161\" /><ul class=\"cslist\"><li><a href=\"/wiki/Affine_type_system\" class=\"mw-redirect\" title=\"Affine type system\">Affine</a></li><li><a href=\"/wiki/Type_inference\" title=\"Type inference\">inferred</a></li><li><a href=\"/wiki/Nominal_type_system\" title=\"Nominal type system\">nominal</a></li><li><a href=\"/wiki/Static_typing\" class=\"mw-redirect\" title=\"Static typing\">static</a></li><li><a href=\"/wiki/Strong_and_weak_typing\" title=\"Strong and weak typing\">strong</a></li></ul></td></tr><tr><th scope=\"row\" class=\"infobox-label\">Implementation language</th><td class=\"infobox-data\"><a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a> (2006–2011)<br />Rust (2012–present)</td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Computing_platform\" title=\"Computing platform\">Platform</a></th><td class=\"infobox-data\"><a href=\"/wiki/Cross-platform_software\" title=\"Cross-platform software\">Cross-platform</a><sup id=\"cite_ref-3\" class=\"reference\"><a href=\"#cite_note-3\"><span class=\"cite-bracket\">&#91;</span>note 1<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Operating_system\" title=\"Operating system\">OS</a></th><td class=\"infobox-data\"><a href=\"/wiki/Cross-platform_software\" title=\"Cross-platform software\">Cross-platform</a><sup id=\"cite_ref-4\" class=\"reference\"><a href=\"#cite_note-4\"><span class=\"cite-bracket\">&#91;</span>note 2<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Software_license\" title=\"Software license\">License</a></th><td class=\"infobox-data\"><a href=\"/wiki/MIT_License\" title=\"MIT License\">MIT</a>, <a href=\"/wiki/Apache_License\" title=\"Apache License\">Apache 2.0</a><sup id=\"cite_ref-7\" class=\"reference\"><a href=\"#cite_note-7\"><span class=\"cite-bracket\">&#91;</span>note 3<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Filename_extension\" title=\"Filename extension\">Filename extensions</a></th><td class=\"infobox-data\"><code>.rs</code>, <code>.rlib</code></td></tr><tr><th scope=\"row\" class=\"infobox-label\">Website</th><td class=\"infobox-data\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.rust-lang.org/\">rust-lang.org</a></span></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\">Influenced by</th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1288773161\" /><ul class=\"cslist\"><li><a href=\"/wiki/Alef_(programming_language)\" title=\"Alef (programming language)\">Alef</a></li><li><a href=\"/wiki/BETA_(programming_language)\" title=\"BETA (programming language)\">BETA</a></li><li><a href=\"/wiki/CLU_(programming_language)\" title=\"CLU (programming language)\">CLU</a></li><li><a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">C#</a></li><li><a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a></li><li><a href=\"/wiki/Cyclone_(programming_language)\" title=\"Cyclone (programming language)\">Cyclone</a></li><li><a href=\"/wiki/Elm_(programming_language)\" title=\"Elm (programming language)\">Elm</a></li><li><a href=\"/wiki/Erlang_(programming_language)\" title=\"Erlang (programming language)\">Erlang</a></li><li><a href=\"/wiki/Haskell\" title=\"Haskell\">Haskell</a></li><li><a href=\"/wiki/Hermes_(programming_language)\" title=\"Hermes (programming language)\">Hermes</a></li><li><a href=\"/wiki/Limbo_(programming_language)\" title=\"Limbo (programming language)\">Limbo</a></li><li><a href=\"/wiki/Mesa_(programming_language)\" title=\"Mesa (programming language)\">Mesa</a></li><li><a href=\"/wiki/Napier88\" title=\"Napier88\">Napier</a></li><li><a href=\"/wiki/Newsqueak\" title=\"Newsqueak\">Newsqueak</a></li><li><a href=\"/wiki/Typestate_analysis\" title=\"Typestate analysis\">NIL</a><sup id=\"cite_ref-10\" class=\"reference\"><a href=\"#cite_note-10\"><span class=\"cite-bracket\">&#91;</span>note 4<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a></li><li><a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a></li><li><a href=\"/wiki/Sather\" title=\"Sather\">Sather</a></li><li><a href=\"/wiki/Scheme_(programming_language)\" title=\"Scheme (programming language)\">Scheme</a></li><li><a href=\"/wiki/Standard_ML\" title=\"Standard ML\">Standard ML</a></li><li><a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a><sup id=\"cite_ref-11\" class=\"reference\"><a href=\"#cite_note-11\"><span class=\"cite-bracket\">&#91;</span>7<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-influences_12-0\" class=\"reference\"><a href=\"#cite_note-influences-12\"><span class=\"cite-bracket\">&#91;</span>8<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\">Influenced</th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1288773161\" /><ul class=\"cslist\"><li><a href=\"/wiki/Idris_(programming_language)\" title=\"Idris (programming language)\">Idris</a><sup id=\"cite_ref-13\" class=\"reference\"><a href=\"#cite_note-13\"><span class=\"cite-bracket\">&#91;</span>9<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/Project_Verona\" title=\"Project Verona\">Project Verona</a><sup id=\"cite_ref-Project_Verona_14-0\" class=\"reference\"><a href=\"#cite_note-Project_Verona-14\"><span class=\"cite-bracket\">&#91;</span>10<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/SPARK_(programming_language)\" title=\"SPARK (programming language)\">SPARK</a><sup id=\"cite_ref-Jaloyan_15-0\" class=\"reference\"><a href=\"#cite_note-Jaloyan-15\"><span class=\"cite-bracket\">&#91;</span>11<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a><sup id=\"cite_ref-Lattner_16-0\" class=\"reference\"><a href=\"#cite_note-Lattner-16\"><span class=\"cite-bracket\">&#91;</span>12<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/V_(programming_language)\" title=\"V (programming language)\">V</a><sup id=\"cite_ref-17\" class=\"reference\"><a href=\"#cite_note-17\"><span class=\"cite-bracket\">&#91;</span>13<span class=\"cite-bracket\">&#93;</span></a></sup></li><li><a href=\"/wiki/Zig_(programming_language)\" title=\"Zig (programming language)\">Zig</a><sup id=\"cite_ref-18\" class=\"reference\"><a href=\"#cite_note-18\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul></td></tr></tbody></table>\n<p><b>Rust</b> is a <a href=\"/wiki/General-purpose_programming_language\" title=\"General-purpose programming language\">general-purpose</a> <a href=\"/wiki/Programming_language\" title=\"Programming language\">programming language</a>. It is noted for its emphasis on <a href=\"/wiki/Computer_performance\" title=\"Computer performance\">performance</a>, <a href=\"/wiki/Type_safety\" title=\"Type safety\">type safety</a>, <a href=\"/wiki/Concurrency_(computer_science)\" title=\"Concurrency (computer science)\">concurrency</a>, and <a href=\"/wiki/Memory_safety\" title=\"Memory safety\">memory safety</a>.\n</p><p>Rust supports multiple <a href=\"/wiki/Programming_paradigm\" title=\"Programming paradigm\">programming paradigms</a>. It was influenced by ideas from <a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional programming</a>, including <a href=\"/wiki/Immutable_object\" title=\"Immutable object\">immutability</a>, <a href=\"/wiki/Higher-order_function\" title=\"Higher-order function\">higher-order functions</a>, <a href=\"/wiki/Algebraic_data_type\" title=\"Algebraic data type\">algebraic data types</a>, and <a href=\"/wiki/Pattern_matching\" title=\"Pattern matching\">pattern matching</a>. It also supports <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented programming</a> via structs, <a href=\"/wiki/Union_type\" title=\"Union type\">enums</a>, traits, and methods. Rust is noted for enforcing memory safety (i.e., that all <a href=\"/wiki/Reference_(computer_science)\" title=\"Reference (computer science)\">references</a> point to valid memory) without a conventional <a href=\"/wiki/Garbage_collection_(computer_science)\" title=\"Garbage collection (computer science)\">garbage collector</a>; instead, memory safety errors and <a href=\"/wiki/Data_race\" class=\"mw-redirect\" title=\"Data race\">data races</a> are prevented by the \"borrow checker\", which tracks the <a href=\"/wiki/Object_lifetime\" title=\"Object lifetime\">object lifetime</a> of references <a href=\"/wiki/Compiler\" title=\"Compiler\">at compile time</a>.\n</p><p>Software developer Graydon Hoare created Rust in 2006 while working at <a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a> Research, which officially sponsored the project in 2009. The first stable release, Rust 1.0, was published in May 2015. Following a layoff of Mozilla employees in August 2020, four other companies joined Mozilla in sponsoring Rust through the creation of the <a href=\"#Rust_Foundation\">Rust Foundation</a> in February 2021.\n</p><p>Rust has been adopted by many software projects, especially <a href=\"/wiki/Web_service\" title=\"Web service\">web services</a> and <a href=\"/wiki/System_software\" title=\"System software\">system software</a>, and is the first language other than <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a> and <a href=\"/wiki/Assembly_language\" title=\"Assembly language\">assembly</a> to be supported in the development of the <a href=\"/wiki/Linux_kernel\" title=\"Linux kernel\">Linux kernel</a>. It has been studied academically and has a growing community of developers.\n</p>\n<meta property=\"mw:PageProp/toc\" />\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Etymology\">Etymology</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=1\" title=\"Edit section: Etymology\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust was named for the <a href=\"/wiki/Rust_(fungus)\" title=\"Rust (fungus)\">group of fungi</a> that are \"over-engineered for survival\".<sup id=\"cite_ref-MITTechReview_19-0\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\nThe Rust logo was developed in 2011 based on a bicycle <a href=\"/wiki/Crankset#Chainring\" title=\"Crankset\">chainring</a>.<sup id=\"cite_ref-20\" class=\"reference\"><a href=\"#cite_note-20\"><span class=\"cite-bracket\">&#91;</span>16<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"History\">History</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=2\" title=\"Edit section: History\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"2006–2009:_Early_years\"><span id=\"2006.E2.80.932009:_Early_years\"></span>2006–2009: Early years</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=3\" title=\"Edit section: 2006–2009: Early years\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:MozillaCaliforniaHeadquarters.JPG\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/MozillaCaliforniaHeadquarters.JPG/250px-MozillaCaliforniaHeadquarters.JPG\" decoding=\"async\" width=\"250\" height=\"188\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/MozillaCaliforniaHeadquarters.JPG/500px-MozillaCaliforniaHeadquarters.JPG 1.5x\" data-file-width=\"2560\" data-file-height=\"1920\" /></a><figcaption>Mozilla Foundation headquarters, 650 Castro Street in <a href=\"/wiki/Mountain_View,_California\" title=\"Mountain View, California\">Mountain View, California</a>, June 2009</figcaption></figure>\n<p>Rust began as a personal project by <a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a> employee Graydon Hoare in 2006.<sup id=\"cite_ref-MITTechReview_19-1\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> Hoare started the project due to his frustration with a broken elevator in his apartment building.<sup id=\"cite_ref-MITTechReview_19-2\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>  During the time period between 2006 and 2009, Rust was not publicized to others at Mozilla and was written in Hoare's free time;<sup id=\"cite_ref-Klabnik2016ACMHistory_21-0\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 7:50\">&#58;&#8202;7:50&#8202;</span></sup> Hoare began speaking about the language around 2009 after a small group at Mozilla became interested in the project.<sup id=\"cite_ref-Hoare2010_22-0\" class=\"reference\"><a href=\"#cite_note-Hoare2010-22\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> Hoare emphasized prioritizing good ideas from old languages over new development, citing languages including <a href=\"/wiki/CLU_(programming_language)\" title=\"CLU (programming language)\">CLU</a> (1974), <a href=\"/wiki/BETA_(programming_language)\" title=\"BETA (programming language)\">BETA</a> (1975), <a href=\"/wiki/Mesa_(programming_language)\" title=\"Mesa (programming language)\">Mesa</a> (1977), NIL (1983),<sup id=\"cite_ref-23\" class=\"reference\"><a href=\"#cite_note-23\"><span class=\"cite-bracket\">&#91;</span>note 5<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Erlang_(programming_language)\" title=\"Erlang (programming language)\">Erlang</a> (1987), <a href=\"/wiki/Newsqueak\" title=\"Newsqueak\">Newsqueak</a> (1988), <a href=\"/wiki/Napier88\" title=\"Napier88\">Napier</a> (1988), <a href=\"/wiki/Hermes_(programming_language)\" title=\"Hermes (programming language)\">Hermes</a> (1990), <a href=\"/wiki/Sather\" title=\"Sather\">Sather</a> (1990), <a href=\"/wiki/Alef_(programming_language)\" title=\"Alef (programming language)\">Alef</a> (1992), and <a href=\"/wiki/Limbo_(programming_language)\" title=\"Limbo (programming language)\">Limbo</a> (1996) as influences, stating \"many older languages [are] better than new ones\", and describing the language as \"technology from the past come to save the future from itself.\"<sup id=\"cite_ref-Klabnik2016ACMHistory_21-1\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 8:17\">&#58;&#8202;8:17&#8202;</span></sup><sup id=\"cite_ref-Hoare2010_22-1\" class=\"reference\"><a href=\"#cite_note-Hoare2010-22\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> Early Rust developer Manish Goregaokar similarly described Rust as being based on \"mostly decades-old research.\"<sup id=\"cite_ref-MITTechReview_19-3\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>During the early years, the Rust <a href=\"/wiki/Compiler\" title=\"Compiler\">compiler</a> was written in about 38,000 lines of <a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a>.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-2\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 15:34\">&#58;&#8202;15:34&#8202;</span></sup><sup id=\"cite_ref-OCamlCompiler_24-0\" class=\"reference\"><a href=\"#cite_note-OCamlCompiler-24\"><span class=\"cite-bracket\">&#91;</span>19<span class=\"cite-bracket\">&#93;</span></a></sup> Early Rust contained features such as explicit <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented programming</a> via an <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">obj</code> keyword (later removed),<sup id=\"cite_ref-Klabnik2016ACMHistory_21-3\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 10:08\">&#58;&#8202;10:08&#8202;</span></sup> and a <a href=\"/wiki/Typestate_analysis\" title=\"Typestate analysis\">typestates</a> system that would allow variables of a type to be tracked along with state changes (such as going from uninitialized to initialized, also removed).<sup id=\"cite_ref-Klabnik2016ACMHistory_21-4\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 13:12\">&#58;&#8202;13:12&#8202;</span></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"2009–2012:_Mozilla_sponsorship\"><span id=\"2009.E2.80.932012:_Mozilla_sponsorship\"></span>2009–2012: Mozilla sponsorship</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=4\" title=\"Edit section: 2009–2012: Mozilla sponsorship\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Mozilla officially sponsored the Rust project in 2009.<sup id=\"cite_ref-MITTechReview_19-4\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Brendan_Eich\" title=\"Brendan Eich\">Brendan Eich</a> and other executives, intrigued by the possibility of using Rust for a safe <a href=\"/wiki/Web_browser\" title=\"Web browser\">web browser</a> <a href=\"/wiki/Browser_engine\" title=\"Browser engine\">engine</a>, placed engineers on the project including Patrick Walton, Niko Matsakis, Felix Klock, and Manish Goregaokar.<sup id=\"cite_ref-MITTechReview_19-5\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> A conference room taken by the project developers was dubbed \"the nerd cave,\" with a sign placed outside the door.<sup id=\"cite_ref-MITTechReview_19-6\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>During this time period, work had shifted from the initial OCaml compiler to a <a href=\"/wiki/Self-hosting_(compilers)\" title=\"Self-hosting (compilers)\">self-hosting compiler</a>, <i>i.e.</i>, written in Rust, based on <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a>.<sup id=\"cite_ref-Rust0.1_25-0\" class=\"reference\"><a href=\"#cite_note-Rust0.1-25\"><span class=\"cite-bracket\">&#91;</span>20<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-27\" class=\"reference\"><a href=\"#cite_note-27\"><span class=\"cite-bracket\">&#91;</span>note 6<span class=\"cite-bracket\">&#93;</span></a></sup> The Rust ownership system was also in place by 2010.<sup id=\"cite_ref-MITTechReview_19-7\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The first public release, Rust 0.1, was released on January 20, 2012<sup id=\"cite_ref-Rust0.1a_28-0\" class=\"reference\"><a href=\"#cite_note-Rust0.1a-28\"><span class=\"cite-bracket\">&#91;</span>22<span class=\"cite-bracket\">&#93;</span></a></sup> for Windows, Linux, and MacOS.<sup id=\"cite_ref-ExtremeTechRust0.1_29-0\" class=\"reference\"><a href=\"#cite_note-ExtremeTechRust0.1-29\"><span class=\"cite-bracket\">&#91;</span>23<span class=\"cite-bracket\">&#93;</span></a></sup> The early 2010s saw increasing involvement from open source volunteers outside of Mozilla and outside of the United States. At Mozilla, executives would eventually employ over a dozen engineers to work on Rust full time over the next decade.<sup id=\"cite_ref-MITTechReview_19-8\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"2012–2015:_Evolution\"><span id=\"2012.E2.80.932015:_Evolution\"></span>2012–2015: Evolution</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=5\" title=\"Edit section: 2012–2015: Evolution\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>The years from 2012 to 2015 were marked by substantial changes to the Rust <a href=\"/wiki/Type_system\" title=\"Type system\">type system</a>, including the removal of the typestate system and <a href=\"/wiki/Garbage_collection_(computer_science)\" title=\"Garbage collection (computer science)\">garbage collector</a>.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-5\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 18:36\">&#58;&#8202;18:36&#8202;</span></sup><sup id=\"cite_ref-MITTechReview_19-9\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> Memory management through the ownership system was gradually consolidated and expanded to prevent memory-related bugs. By 2013, the garbage collector feature was rarely used, and was removed by the team in favor of the ownership system.<sup id=\"cite_ref-MITTechReview_19-10\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> Other changes during this time included the removal of <a href=\"/wiki/Pure_function\" title=\"Pure function\">pure functions</a>, which were declared by an explicit <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">pure</code> annotation, in March 2013.<sup id=\"cite_ref-30\" class=\"reference\"><a href=\"#cite_note-30\"><span class=\"cite-bracket\">&#91;</span>24<span class=\"cite-bracket\">&#93;</span></a></sup> Specialized syntax support for <a href=\"/wiki/Channel_(programming)\" title=\"Channel (programming)\">channels</a> and various pointer types were removed to simplify the language.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-6\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 22:32\">&#58;&#8202;22:32&#8202;</span></sup>\n</p><p>According to Rust developer Steve Klabnik, Rust was influenced during this period by developers coming from <a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a> (e.g., low-level performance of features), <a href=\"/wiki/Scripting_language\" title=\"Scripting language\">scripting languages</a> (e.g., Cargo and package management), and <a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional programming</a> (e.g., type systems development).<sup id=\"cite_ref-Klabnik2016ACMHistory_21-7\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 30:50\">&#58;&#8202;30:50&#8202;</span></sup>\n</p><p>Graydon Hoare stepped down from Rust in 2013.<sup id=\"cite_ref-MITTechReview_19-11\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> After Hoare's departure, it evolved organically under a federated governance structure, with a \"core team\" of initially six people,<sup id=\"cite_ref-Klabnik2016ACMHistory_21-8\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 21:45\">&#58;&#8202;21:45&#8202;</span></sup> around 30-40 developers total across various other teams,<sup id=\"cite_ref-Klabnik2016ACMHistory_21-9\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 22:22\">&#58;&#8202;22:22&#8202;</span></sup> and a Request for Comments (RFC) process for new language features added in March 2014.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-10\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 33:47\">&#58;&#8202;33:47&#8202;</span></sup> The core team would grow to nine people by 2016<sup id=\"cite_ref-Klabnik2016ACMHistory_21-11\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 21:45\">&#58;&#8202;21:45&#8202;</span></sup> with over 1600 proposed RFCs.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-12\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 34:08\">&#58;&#8202;34:08&#8202;</span></sup>\n</p><p>According to Andrew Binstock writing for <i><a href=\"/wiki/Dr._Dobb%27s_Journal\" title=\"Dr. Dobb&#39;s Journal\">Dr. Dobb's Journal</a></i> in January 2014, while Rust was \"widely viewed as a remarkably elegant language\", adoption slowed because it radically changed from version to version.<sup id=\"cite_ref-31\" class=\"reference\"><a href=\"#cite_note-31\"><span class=\"cite-bracket\">&#91;</span>25<span class=\"cite-bracket\">&#93;</span></a></sup> Rust development at this time was focused on finalizing the language features and moving towards 1.0 so it could begin promising <a href=\"/wiki/Backward_compatibility\" title=\"Backward compatibility\">backward compatibility</a>.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-13\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 41:26\">&#58;&#8202;41:26&#8202;</span></sup>\n</p><p>Six years after Mozilla sponsored its development, the first <a href=\"/wiki/Stable_release\" class=\"mw-redirect\" title=\"Stable release\">stable release</a>, Rust 1.0, was published on May 15, 2015.<sup id=\"cite_ref-MITTechReview_19-12\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> A year after the release, the Rust compiler had accumulated over 1,400 contributors and there were over 5,000 third-party libraries published on the Rust package management website Crates.io.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-14\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 43:15\">&#58;&#8202;43:15&#8202;</span></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"2015–2020:_Servo_and_early_adoption\"><span id=\"2015.E2.80.932020:_Servo_and_early_adoption\"></span>2015–2020: Servo and early adoption</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=6\" title=\"Edit section: 2015–2020: Servo and early adoption\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Home_page_servo_v0.01.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/39/Home_page_servo_v0.01.png/250px-Home_page_servo_v0.01.png\" decoding=\"async\" width=\"250\" height=\"142\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/39/Home_page_servo_v0.01.png/500px-Home_page_servo_v0.01.png 1.5x\" data-file-width=\"1855\" data-file-height=\"1056\" /></a><figcaption>Early homepage of Mozilla's <a href=\"/wiki/Servo_(software)\" title=\"Servo (software)\">Servo browser engine</a></figcaption></figure>\n<p>The development of the <a href=\"/wiki/Servo_(software)\" title=\"Servo (software)\">Servo browser engine</a> continued in parallel with Rust, jointly funded by Mozilla and <a href=\"/wiki/Samsung\" title=\"Samsung\">Samsung</a>.<sup id=\"cite_ref-32\" class=\"reference\"><a href=\"#cite_note-32\"><span class=\"cite-bracket\">&#91;</span>26<span class=\"cite-bracket\">&#93;</span></a></sup> The teams behind the two projects worked in close collaboration; new features in Rust were tested out by the Servo team, and new features in Servo were used to give feedback back to the Rust team.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-15\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 5:41\">&#58;&#8202;5:41&#8202;</span></sup> The first version of Servo was released in 2016.<sup id=\"cite_ref-MITTechReview_19-13\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> The <a href=\"/wiki/Firefox\" title=\"Firefox\">Firefox</a> web browser shipped with Rust code as of 2016 (version 45),<sup id=\"cite_ref-Klabnik2016ACMHistory_21-16\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 53:30\">&#58;&#8202;53:30&#8202;</span></sup><sup id=\"cite_ref-33\" class=\"reference\"><a href=\"#cite_note-33\"><span class=\"cite-bracket\">&#91;</span>27<span class=\"cite-bracket\">&#93;</span></a></sup> but components of Servo did not appear in Firefox until September 2017 (version 57) as part of the <a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a> and <a href=\"/wiki/Gecko_(software)#Quantum\" title=\"Gecko (software)\">Quantum</a> projects.<sup id=\"cite_ref-34\" class=\"reference\"><a href=\"#cite_note-34\"><span class=\"cite-bracket\">&#91;</span>28<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Improvements were made to the Rust toolchain ecosystem during the years following 1.0 including <a href=\"#Rustfmt\">Rustfmt</a>, <a href=\"/wiki/Integrated_development_environment\" title=\"Integrated development environment\">integrated development environment</a> integration,<sup id=\"cite_ref-Klabnik2016ACMHistory_21-17\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 44:56\">&#58;&#8202;44:56&#8202;</span></sup> and a regular compiler testing and release cycle.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-18\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 46:48\">&#58;&#8202;46:48&#8202;</span></sup> Rust also gained a community <a href=\"/wiki/Code_of_conduct\" title=\"Code of conduct\">code of conduct</a> and an <a href=\"/wiki/IRC\" title=\"IRC\">IRC</a> chat for community discussion.<sup id=\"cite_ref-Klabnik2016ACMHistory_21-19\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 50:36\">&#58;&#8202;50:36&#8202;</span></sup>\n</p><p>The earliest known adoption outside of Mozilla was by individual projects at Samsung, <a href=\"/wiki/Facebook\" title=\"Facebook\">Facebook</a> (now <a href=\"/wiki/Meta_Platforms\" title=\"Meta Platforms\">Meta Platforms</a>), <a href=\"/wiki/Dropbox\" title=\"Dropbox\">Dropbox</a>, and Tilde, Inc. (the company behind <a href=\"/wiki/Ember.js\" title=\"Ember.js\">ember.js</a>).<sup id=\"cite_ref-Klabnik2016ACMHistory_21-20\" class=\"reference\"><a href=\"#cite_note-Klabnik2016ACMHistory-21\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"reference nowrap\"><span title=\"Location: 55:44\">&#58;&#8202;55:44&#8202;</span></sup><sup id=\"cite_ref-MITTechReview_19-14\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Amazon_Web_Services\" title=\"Amazon Web Services\">Amazon Web Services</a> followed in 2020.<sup id=\"cite_ref-MITTechReview_19-15\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> Engineers acknowledged the risks of adopting a new technology; they cited performance, lack of a garbage collector, safety, and pleasantness of working in the language as reasons for the adoption. Amazon developers cited a finding by Portuguese researchers that Rust code used <a href=\"/wiki/Energy_efficiency_in_computing\" class=\"mw-redirect\" title=\"Energy efficiency in computing\">less energy</a> compared to similar code written in <a href=\"/wiki/Java_(programming_language)\" title=\"Java (programming language)\">Java</a> and <a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a> (behind only <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a>).<sup id=\"cite_ref-MITTechReview_19-16\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-2017PortugalEnergyStudy_35-0\" class=\"reference\"><a href=\"#cite_note-2017PortugalEnergyStudy-35\"><span class=\"cite-bracket\">&#91;</span>29<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-36\" class=\"reference\"><a href=\"#cite_note-36\"><span class=\"cite-bracket\">&#91;</span>note 7<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"2020–present:_Mozilla_layoffs_and_Rust_Foundation\"><span id=\"2020.E2.80.93present:_Mozilla_layoffs_and_Rust_Foundation\"></span>2020–present: Mozilla layoffs and Rust Foundation</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=7\" title=\"Edit section: 2020–present: Mozilla layoffs and Rust Foundation\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>In August 2020, Mozilla laid off 250 of its 1,000 employees worldwide, as part of a corporate restructuring caused by the <a href=\"/wiki/COVID-19_pandemic\" title=\"COVID-19 pandemic\">COVID-19 pandemic</a>.<sup id=\"cite_ref-37\" class=\"reference\"><a href=\"#cite_note-37\"><span class=\"cite-bracket\">&#91;</span>30<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-38\" class=\"reference\"><a href=\"#cite_note-38\"><span class=\"cite-bracket\">&#91;</span>31<span class=\"cite-bracket\">&#93;</span></a></sup> The team behind Servo was disbanded. The event raised concerns about the future of Rust.<sup id=\"cite_ref-39\" class=\"reference\"><a href=\"#cite_note-39\"><span class=\"cite-bracket\">&#91;</span>32<span class=\"cite-bracket\">&#93;</span></a></sup> In the following week, the Rust Core Team acknowledged the severe impact of the layoffs and announced that plans for a Rust foundation were underway. The first goal of the foundation would be to take ownership of all <a href=\"/wiki/Trademark\" title=\"Trademark\">trademarks</a> and <a href=\"/wiki/Domain_name\" title=\"Domain name\">domain names</a> and to take financial responsibility for their costs.<sup id=\"cite_ref-40\" class=\"reference\"><a href=\"#cite_note-40\"><span class=\"cite-bracket\">&#91;</span>33<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>On February 8, 2021, the formation of the <a href=\"#Rust_Foundation\">Rust Foundation</a> was announced by five founding companies: <a href=\"/wiki/Amazon_Web_Services\" title=\"Amazon Web Services\">Amazon Web Services</a>, <a href=\"/wiki/Google\" title=\"Google\">Google</a>, <a href=\"/wiki/Huawei\" title=\"Huawei\">Huawei</a>, <a href=\"/wiki/Microsoft\" title=\"Microsoft\">Microsoft</a>, and <a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a>.<sup id=\"cite_ref-41\" class=\"reference\"><a href=\"#cite_note-41\"><span class=\"cite-bracket\">&#91;</span>34<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-42\" class=\"reference\"><a href=\"#cite_note-42\"><span class=\"cite-bracket\">&#91;</span>35<span class=\"cite-bracket\">&#93;</span></a></sup> The foundation, led by Shane Miller for its first two years, offered $20,000 grants and other support for programmers working on major Rust features.<sup id=\"cite_ref-MITTechReview_19-17\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> In a <a href=\"/wiki/Blog\" title=\"Blog\">blog</a> post published on April 6, 2021, Google announced support for Rust within the <a href=\"/wiki/Android_Open_Source_Project\" class=\"mw-redirect\" title=\"Android Open Source Project\">Android Open Source Project</a> as an alternative to C/C++.<sup id=\"cite_ref-43\" class=\"reference\"><a href=\"#cite_note-43\"><span class=\"cite-bracket\">&#91;</span>36<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>On November 22, 2021, the Moderation Team, which was responsible for enforcing the community code of conduct, announced their resignation \"in protest of the Core Team placing themselves unaccountable to anyone but themselves\".<sup id=\"cite_ref-moderation_44-0\" class=\"reference\"><a href=\"#cite_note-moderation-44\"><span class=\"cite-bracket\">&#91;</span>37<span class=\"cite-bracket\">&#93;</span></a></sup> In May 2022, the Rust Core Team, other lead programmers, and members of the Rust Foundation board implemented governance reforms in response to the incident.<sup id=\"cite_ref-45\" class=\"reference\"><a href=\"#cite_note-45\"><span class=\"cite-bracket\">&#91;</span>38<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The Rust Foundation posted a draft for a new trademark policy on April 6, 2023, which resulted in widespread negative reactions from Rust users and contributors.<sup id=\"cite_ref-ApologizesTrademarkPolicy_46-0\" class=\"reference\"><a href=\"#cite_note-ApologizesTrademarkPolicy-46\"><span class=\"cite-bracket\">&#91;</span>39<span class=\"cite-bracket\">&#93;</span></a></sup> The trademark policy included rules for how the Rust logo and name could be used.<sup id=\"cite_ref-ApologizesTrademarkPolicy_46-1\" class=\"reference\"><a href=\"#cite_note-ApologizesTrademarkPolicy-46\"><span class=\"cite-bracket\">&#91;</span>39<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>On February 26, 2024, the U.S. <a href=\"/wiki/White_House\" title=\"White House\">White House</a> <a href=\"/wiki/Office_of_the_National_Cyber_Director\" title=\"Office of the National Cyber Director\">Office of the National Cyber Director</a> released a 19-page press report urging software development to move away from C and C++ to memory-safe languages like C#, Go, Java, Ruby, Swift, and Rust.<sup id=\"cite_ref-WhiteHouse1_47-0\" class=\"reference\"><a href=\"#cite_note-WhiteHouse1-47\"><span class=\"cite-bracket\">&#91;</span>40<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-WhiteHouse2_48-0\" class=\"reference\"><a href=\"#cite_note-WhiteHouse2-48\"><span class=\"cite-bracket\">&#91;</span>41<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-WhiteHouseFullReport_49-0\" class=\"reference\"><a href=\"#cite_note-WhiteHouseFullReport-49\"><span class=\"cite-bracket\">&#91;</span>42<span class=\"cite-bracket\">&#93;</span></a></sup> News coverage of Rust increased in light of the report.<sup id=\"cite_ref-WhiteHouse3_50-0\" class=\"reference\"><a href=\"#cite_note-WhiteHouse3-50\"><span class=\"cite-bracket\">&#91;</span>43<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-WhiteHouse4_51-0\" class=\"reference\"><a href=\"#cite_note-WhiteHouse4-51\"><span class=\"cite-bracket\">&#91;</span>44<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Syntax_and_features\">Syntax and features</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=8\" title=\"Edit section: Syntax and features\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1236090951\">.mw-parser-output .hatnote{font-style:italic}.mw-parser-output div.hatnote{padding-left:1.6em;margin-bottom:0.5em}.mw-parser-output .hatnote i{font-style:normal}.mw-parser-output .hatnote+link+.hatnote{margin-top:-0.5em}@media print{body.ns-0 .mw-parser-output .hatnote{display:none!important}}</style><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/Rust_syntax\" title=\"Rust syntax\">Rust syntax</a></div>\n<p>Rust's <a href=\"/wiki/Syntax_(programming_languages)\" title=\"Syntax (programming languages)\">syntax</a> is similar to that of <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a> and C++,<sup id=\"cite_ref-52\" class=\"reference\"><a href=\"#cite_note-52\"><span class=\"cite-bracket\">&#91;</span>45<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-:4_53-0\" class=\"reference\"><a href=\"#cite_note-:4-53\"><span class=\"cite-bracket\">&#91;</span>46<span class=\"cite-bracket\">&#93;</span></a></sup> although many of its features were influenced by <a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional programming</a> languages such as <a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019263_54-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019263-54\"><span class=\"cite-bracket\">&#91;</span>47<span class=\"cite-bracket\">&#93;</span></a></sup> Hoare has described Rust as targeted at frustrated C++ developers and emphasized features such as safety, control of <a href=\"/wiki/Memory_layout\" class=\"mw-redirect\" title=\"Memory layout\">memory layout</a>, and <a href=\"/wiki/Concurrency_(computer_science)\" title=\"Concurrency (computer science)\">concurrency</a>.<sup id=\"cite_ref-Hoare2010_22-2\" class=\"reference\"><a href=\"#cite_note-Hoare2010-22\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> Safety in Rust includes the guarantees of memory safety, type safety, and lack of data races.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Hello_World_program\">Hello World program</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=9\" title=\"Edit section: Hello World program\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Below is a <a href=\"/wiki/%22Hello,_World!%22_program\" title=\"&quot;Hello, World!&quot; program\">\"Hello, World!\" program</a> in Rust. The <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">fn</span></code> keyword denotes a <a href=\"/wiki/Function_(computer_programming)\" title=\"Function (computer programming)\">function</a>, and the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"fm\">println!</span></code> <a href=\"/wiki/Macro_(computer_science)\" title=\"Macro (computer science)\">macro</a> (see <a href=\"#Macros\">§&#160;Macros</a>) prints the message to <a href=\"/wiki/Standard_output\" class=\"mw-redirect\" title=\"Standard output\">standard output</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols20195–6_55-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols20195–6-55\"><span class=\"cite-bracket\">&#91;</span>48<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Statement_(computer_science)\" title=\"Statement (computer science)\">Statements</a> in Rust are separated by <a href=\"/wiki/Semicolon#Programming\" title=\"Semicolon\">semicolons</a>.\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello, World!&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Variables\">Variables</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=10\" title=\"Edit section: Variables\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Variable_(computer_science)\" title=\"Variable (computer science)\">Variables</a> in Rust are defined through the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kd\">let</span></code> keyword.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202332_56-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202332-56\"><span class=\"cite-bracket\">&#91;</span>49<span class=\"cite-bracket\">&#93;</span></a></sup> The example below assigns a value to the variable with name <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">foo</span></code> of type <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">i32</span></code> and outputs its value.\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;The value of foo is {foo}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Variables are <a href=\"/wiki/Immutable_object\" title=\"Immutable object\">immutable</a> by default, but adding the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">mut</span></code> keyword allows the variable to be mutated.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202332–33_57-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202332–33-57\"><span class=\"cite-bracket\">&#91;</span>50<span class=\"cite-bracket\">&#93;</span></a></sup> The following example uses <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"c1\">//</span></code>, which denotes the start of a <a href=\"/wiki/Comment_(computer_programming)\" title=\"Comment (computer programming)\">comment</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202349–50_58-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202349–50-58\"><span class=\"cite-bracket\">&#91;</span>51<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"c1\">// This code would not compile without adding &quot;mut&quot;.</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span><span class=\"w\"> </span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;The value of foo is {foo}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">20</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;The value of foo is {foo}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Multiple <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kd\">let</span></code> expressions can define multiple variables with the same name, known as <a href=\"/wiki/Variable_shadowing\" title=\"Variable shadowing\">variable shadowing</a>. Variable shadowing allows transforming variables without having to name the variables differently.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202334–36_59-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202334–36-59\"><span class=\"cite-bracket\">&#91;</span>52<span class=\"cite-bracket\">&#93;</span></a></sup> The example below declares a new variable with the same name that is double the original value:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"c1\">// This will output &quot;The value of foo is 10&quot;</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;The value of foo is {foo}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">foo</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"c1\">// This will output &quot;The value of foo is 20&quot;</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;The value of foo is {foo}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Variable shadowing is also possible for values of different types. For example, going from a string to its length:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">letters</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s\">&quot;abc&quot;</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">letters</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">letters</span><span class=\"p\">.</span><span class=\"n\">len</span><span class=\"p\">();</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Block_expressions_and_control_flow\">Block expressions and control flow</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=11\" title=\"Edit section: Block expressions and control flow\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>A <i>block expression</i> is delimited by <a href=\"/wiki/Bracket#Curly_brackets\" title=\"Bracket\">curly brackets</a>. When the last expression inside a block does not end with a semicolon, the block evaluates to the value of that trailing expression:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols20236,_47,_53_60-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols20236,_47,_53-60\"><span class=\"cite-bracket\">&#91;</span>53<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;this is inside the block&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">        </span><span class=\"mi\">1</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"mi\">2</span>\n<span class=\"w\">    </span><span class=\"p\">};</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;1 + 2 = {x}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Trailing expressions of function bodies are used as the return value:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202347–48_61-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202347–48-61\"><span class=\"cite-bracket\">&#91;</span>54<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">add_two</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"mi\">2</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"if_expressions\"><code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">if</span></code> expressions</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=12\" title=\"Edit section: if expressions\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>An <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">if</span></code> <a href=\"/wiki/Conditional_expression\" class=\"mw-redirect\" title=\"Conditional expression\">conditional expression</a> executes code based on whether the given value is <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kc\">true</span></code>. <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">else</span></code> can be used for when the value evaluates to <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kc\">false</span></code>, and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">else</span><span class=\"w\"> </span><span class=\"k\">if</span></code> can be used for combining multiple expressions.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202350–53_62-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202350–53-62\"><span class=\"cite-bracket\">&#91;</span>55<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value is greater than five&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">%</span><span class=\"w\"> </span><span class=\"mi\">7</span><span class=\"w\"> </span><span class=\"o\">==</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value is divisible by 7&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span><span class=\"w\"> </span><span class=\"k\">else</span><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">%</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"w\"> </span><span class=\"o\">==</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value is divisible by 5&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span><span class=\"w\"> </span><span class=\"k\">else</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value is not divisible by 7 or 5&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p><code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">if</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">else</span></code> blocks can evaluate to a value, which can then be assigned to a variable:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202350–53_62-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202350–53-62\"><span class=\"cite-bracket\">&#91;</span>55<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">new_x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">%</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"w\"> </span><span class=\"o\">==</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">/</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\"> </span><span class=\"k\">else</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"mi\">3</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"w\"> </span><span class=\"p\">};</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{new_x}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"while_loops\"><code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">while</span></code> loops</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=13\" title=\"Edit section: while loops\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><code><a href=\"/wiki/While_loop\" title=\"While loop\">while</a></code> can be used to repeat a block of code while a condition is met.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202356_63-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202356-63\"><span class=\"cite-bracket\">&#91;</span>56<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"c1\">// Iterate over all integers from 4 to 10</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">4</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"k\">while</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">&lt;=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">         </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value = {value}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">         </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">+=</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"for_loops_and_iterators\"><code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">for</span></code> loops and iterators</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=14\" title=\"Edit section: for loops and iterators\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/For_loop\" title=\"For loop\">For loops</a> in Rust loop over elements of a collection.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202357–58_64-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202357–58-64\"><span class=\"cite-bracket\">&#91;</span>57<span class=\"cite-bracket\">&#93;</span></a></sup>\n<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">for</span></code> expressions work over any <a href=\"/wiki/Iterator\" title=\"Iterator\">iterator</a> type.\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"c1\">// Using `for` with range syntax for the same functionality as above</span>\n<span class=\"w\">    </span><span class=\"c1\">// The syntax 4..=10 means the range from 4 to 10, up to and including 10.</span>\n<span class=\"w\">    </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"k\">in</span><span class=\"w\"> </span><span class=\"mi\">4</span><span class=\"o\">..=</span><span class=\"mi\">10</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;value = {value}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>In the above code, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"mi\">4</span><span class=\"o\">..=</span><span class=\"mi\">10</span></code> is a value of type <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">Range</span></code> which implements the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">Iterator</span></code> trait. The code within the curly braces is applied to each element returned by the iterator.\n</p><p>Iterators can be combined with functions over iterators like <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">map</span></code>, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">filter</span></code>, and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">sum</span></code>. For example, the following adds up all numbers between 1 and 100 that are multiples of 3:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"o\">..=</span><span class=\"mi\">100</span><span class=\"p\">).</span><span class=\"n\">filter</span><span class=\"p\">(</span><span class=\"o\">|&amp;</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">i8</span><span class=\"o\">|</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">bool</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">%</span><span class=\"w\"> </span><span class=\"mi\">3</span><span class=\"w\"> </span><span class=\"o\">==</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"w\"> </span><span class=\"p\">}).</span><span class=\"n\">sum</span><span class=\"p\">()</span>\n</pre></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"loop_and_break_statements\"><code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">loop</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">break</span></code> statements</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=15\" title=\"Edit section: loop and break statements\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>More generally, the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">loop</span></code> keyword allows repeating a portion of code until a <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">break</span></code> occurs. <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">break</span></code> may optionally exit the loop with a value. In the case of nested loops, labels denoted by <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">label_name</span></code> can be used to break an outer loop rather than the innermost loop.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202354–56_65-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202354–56-65\"><span class=\"cite-bracket\">&#91;</span>58<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">456</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">y</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">loop</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">*=</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">        </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">            </span><span class=\"k\">break</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">/</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span>\n<span class=\"w\">        </span><span class=\"p\">}</span>\n<span class=\"w\">    </span><span class=\"p\">};</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;largest power of ten that is smaller than or equal to value: {y}&quot;</span><span class=\"p\">);</span>\n\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">up</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"o\">&#39;</span><span class=\"na\">outer</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nc\">loop</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">down</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">120</span><span class=\"p\">;</span>\n<span class=\"w\">        </span><span class=\"k\">loop</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">            </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">up</span><span class=\"w\"> </span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"mi\">100</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">                </span><span class=\"k\">break</span><span class=\"w\"> </span><span class=\"nl\">&#39;outer</span><span class=\"p\">;</span>\n<span class=\"w\">            </span><span class=\"p\">}</span>\n\n<span class=\"w\">            </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">down</span><span class=\"w\"> </span><span class=\"o\">&lt;</span><span class=\"w\"> </span><span class=\"mi\">4</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">                </span><span class=\"k\">break</span><span class=\"p\">;</span>\n<span class=\"w\">            </span><span class=\"p\">}</span>\n\n<span class=\"w\">            </span><span class=\"n\">down</span><span class=\"w\"> </span><span class=\"o\">/=</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">;</span>\n<span class=\"w\">            </span><span class=\"n\">up</span><span class=\"w\"> </span><span class=\"o\">+=</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">;</span>\n<span class=\"w\">            </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;up: {up}, down: {down}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">        </span><span class=\"p\">}</span>\n<span class=\"w\">        </span><span class=\"n\">up</span><span class=\"w\"> </span><span class=\"o\">*=</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Pattern_matching\">Pattern matching</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=16\" title=\"Edit section: Pattern matching\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>The <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">match</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"kd\">let</span></code> expressions can be used for <a href=\"/wiki/Pattern_matching\" title=\"Pattern matching\">pattern matching</a>. For example, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">match</span></code> can be used to double an optional integer value if present, and return zero otherwise:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019104–109_66-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019104–109-66\"><span class=\"cite-bracket\">&#91;</span>59<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">double</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Option</span><span class=\"o\">&lt;</span><span class=\"kt\">u64</span><span class=\"o\">&gt;</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">u64</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">match</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"nb\">Some</span><span class=\"p\">(</span><span class=\"n\">y</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=&gt;</span><span class=\"w\"> </span><span class=\"n\">y</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">,</span>\n<span class=\"w\">        </span><span class=\"nb\">None</span><span class=\"w\"> </span><span class=\"o\">=&gt;</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">,</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Equivalently, this can be written with <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"kd\">let</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">else</span></code>:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">double</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Option</span><span class=\"o\">&lt;</span><span class=\"kt\">u64</span><span class=\"o\">&gt;</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">u64</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"nb\">Some</span><span class=\"p\">(</span><span class=\"n\">y</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"n\">y</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"w\"> </span><span class=\"mi\">2</span>\n<span class=\"w\">    </span><span class=\"p\">}</span><span class=\"w\"> </span><span class=\"k\">else</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"mi\">0</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Types\">Types</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=17\" title=\"Edit section: Types\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust is <a href=\"/wiki/Strongly_typed\" class=\"mw-redirect\" title=\"Strongly typed\">strongly typed</a> and <a href=\"/wiki/Statically_typed\" class=\"mw-redirect\" title=\"Statically typed\">statically typed</a>, meaning that the types of all variables must be known at compilation time. Assigning a value of a particular type to a differently typed variable causes a <a href=\"/wiki/Compilation_error\" title=\"Compilation error\">compilation error</a>. <a href=\"/wiki/Type_inference\" title=\"Type inference\">Type inference</a> is used to determine the type of variables if unspecified.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201924_67-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201924-67\"><span class=\"cite-bracket\">&#91;</span>60<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The type <code>()</code>, called the \"unit type\" in Rust, is a concrete type that has exactly one value. It occupies no memory (as it represents the absence of value). All functions that do not have an indicated return type implicitly return <code>()</code>. It is similar to <code class=\"mw-highlight mw-highlight-lang-cpp mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">void</span></code> in other C-style languages, however <code class=\"mw-highlight mw-highlight-lang-cpp mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">void</span></code> denotes the absence of a type and cannot have any value.\n</p><p>The default integer type is <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">i32</span></code>, and the default <a href=\"/wiki/Floating_point\" class=\"mw-redirect\" title=\"Floating point\">floating point</a> type is <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">f64</span></code>. If the type of a <a href=\"/wiki/Literal_(computer_programming)\" title=\"Literal (computer programming)\">literal</a> number is not explicitly provided, it is either inferred from the context or the default type is used.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201936–38_68-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201936–38-68\"><span class=\"cite-bracket\">&#91;</span>61<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Primitive_types\">Primitive types</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=18\" title=\"Edit section: Primitive types\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Integer_type\" class=\"mw-redirect\" title=\"Integer type\">Integer types</a> in Rust are named based on the <a href=\"/wiki/Signedness\" title=\"Signedness\">signedness</a> and the number of bits the type takes. For example, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">i32</span></code> is a signed integer that takes 32 bits of storage, whereas <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">u8</span></code> is unsigned and only takes 8 bits of storage. <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">isize</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">usize</span></code> take storage depending on the <a href=\"/wiki/Bus_(computing)#Address_bus\" title=\"Bus (computing)\">memory address bus width</a> of the compilation target. For example, when building for <a href=\"/wiki/32-bit_computing\" title=\"32-bit computing\">32-bit targets</a>, both types will take up 32 bits of space.<sup id=\"cite_ref-69\" class=\"reference\"><a href=\"#cite_note-69\"><span class=\"cite-bracket\">&#91;</span>62<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-70\" class=\"reference\"><a href=\"#cite_note-70\"><span class=\"cite-bracket\">&#91;</span>63<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>By default, integer literals are in base-10, but different <a href=\"/wiki/Radix\" title=\"Radix\">radices</a> are supported with prefixes, for example, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"mb\">0b11</span></code> for <a href=\"/wiki/Binary_number\" title=\"Binary number\">binary numbers</a>, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"mo\">0o567</span></code> for <a href=\"/wiki/Octal\" title=\"Octal\">octals</a>, and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"mh\">0xDB</span></code> for <a href=\"/wiki/Hexadecimal\" title=\"Hexadecimal\">hexadecimals</a>. By default, integer literals default to <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">i32</span></code> as its type. Suffixes such as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"mi\">4</span><span class=\"k\">u32</span></code> can be used to explicitly set the type of a literal.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202336–38_71-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202336–38-71\"><span class=\"cite-bracket\">&#91;</span>64<span class=\"cite-bracket\">&#93;</span></a></sup> Byte literals such as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"sc\">b&#39;X&#39;</span></code> are available to represent the <a href=\"/wiki/ASCII\" title=\"ASCII\">ASCII</a> value (as a <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">u8</span></code>) of a specific character.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023502_72-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023502-72\"><span class=\"cite-bracket\">&#91;</span>65<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The <a href=\"/wiki/Boolean_type\" class=\"mw-redirect\" title=\"Boolean type\">Boolean type</a> is referred to as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">bool</span></code> which can take a value of either <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kc\">true</span></code> or <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kc\">false</span></code>. A <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">char</span></code> takes up 32 bits of space and represents a Unicode scalar value:<sup id=\"cite_ref-73\" class=\"reference\"><a href=\"#cite_note-73\"><span class=\"cite-bracket\">&#91;</span>66<span class=\"cite-bracket\">&#93;</span></a></sup> a <a href=\"/wiki/Unicode_codepoint\" class=\"mw-redirect\" title=\"Unicode codepoint\">Unicode codepoint</a> that is not a <a href=\"/wiki/Universal_Character_Set_characters#Surrogates\" title=\"Universal Character Set characters\">surrogate</a>.<sup id=\"cite_ref-74\" class=\"reference\"><a href=\"#cite_note-74\"><span class=\"cite-bracket\">&#91;</span>67<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/IEEE_754\" title=\"IEEE 754\">IEEE 754</a> floating point numbers are supported with <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">f32</span></code> for <a href=\"/wiki/Single_precision_float\" class=\"mw-redirect\" title=\"Single precision float\">single precision floats</a> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"kt\">f64</span></code> for <a href=\"/wiki/Double_precision_float\" class=\"mw-redirect\" title=\"Double precision float\">double precision floats</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201938–40_75-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201938–40-75\"><span class=\"cite-bracket\">&#91;</span>68<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Compound_types\">Compound types</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=19\" title=\"Edit section: Compound types\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Compound types can contain multiple values. Tuples are fixed-size lists that can contain values whose types can be different. Arrays are fixed-size lists whose values are of the same type. Expressions of the tuple and array types can be written through listing the values, and can be accessed with <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"p\">.</span><span class=\"n\">index</span></code> or <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"p\">[</span><span class=\"n\">index</span><span class=\"p\">]</span></code>:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202340–42_76-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202340–42-76\"><span class=\"cite-bracket\">&#91;</span>69<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">tuple</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"kt\">u32</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kt\">bool</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"mi\">3</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"p\">);</span>\n<span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">array</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"kt\">i8</span><span class=\"p\">;</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"p\">]</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"mi\">1</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">3</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">4</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"p\">];</span>\n<span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">tuple</span><span class=\"p\">.</span><span class=\"mi\">1</span><span class=\"p\">;</span><span class=\"w\"> </span><span class=\"c1\">// true</span>\n<span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">value</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"mi\">2</span><span class=\"p\">];</span><span class=\"w\"> </span><span class=\"c1\">// 3</span>\n</pre></div>\n<p>Arrays can also be constructed through copying a single value a number of times:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202342_77-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202342-77\"><span class=\"cite-bracket\">&#91;</span>70<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">array2</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"kt\">char</span><span class=\"p\">;</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">]</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"sc\">&#39; &#39;</span><span class=\"p\">;</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">];</span>\n</pre></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Ownership_and_references\">Ownership and references</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=20\" title=\"Edit section: Ownership and references\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust's ownership system consists of rules that ensure memory safety without using a garbage collector. At compile time, each value must be attached to a variable called the <i>owner</i> of that value, and every value must have exactly one owner.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201959–61_78-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201959–61-78\"><span class=\"cite-bracket\">&#91;</span>71<span class=\"cite-bracket\">&#93;</span></a></sup> Values are moved between different owners through assignment or passing a value as a function parameter. Values can also be <i>borrowed,</i> meaning they are temporarily passed to a different function before being returned to the owner.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201963–68_79-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201963–68-79\"><span class=\"cite-bracket\">&#91;</span>72<span class=\"cite-bracket\">&#93;</span></a></sup> With these rules, Rust can prevent the creation and use of <a href=\"/wiki/Dangling_pointers\" class=\"mw-redirect\" title=\"Dangling pointers\">dangling pointers</a>:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201963–68_79-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201963–68-79\"><span class=\"cite-bracket\">&#91;</span>72<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-FOOTNOTEKlabnikNichols201974–75_80-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201974–75-80\"><span class=\"cite-bracket\">&#91;</span>73<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">print_string</span><span class=\"p\">(</span><span class=\"n\">s</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">String</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">s</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">s</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">String</span><span class=\"p\">::</span><span class=\"n\">from</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello, World&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"n\">print_string</span><span class=\"p\">(</span><span class=\"n\">s</span><span class=\"p\">);</span><span class=\"w\"> </span><span class=\"c1\">// s consumed by print_string</span>\n<span class=\"w\">    </span><span class=\"c1\">// s has been moved, so cannot be used any more</span>\n<span class=\"w\">    </span><span class=\"c1\">// another print_string(s); would result in a compile error</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>The function <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">print_string</span></code> takes ownership over the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">String</span></code> value passed in; Alternatively, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&amp;</span></code> can be used to indicate a <a href=\"/wiki/Reference_(computer_science)\" title=\"Reference (computer science)\">reference</a> type (in <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&amp;</span><span class=\"nb\">String</span></code>) and to create a reference (in <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&amp;</span><span class=\"n\">s</span></code>):<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202371–72_81-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202371–72-81\"><span class=\"cite-bracket\">&#91;</span>74<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">print_string</span><span class=\"p\">(</span><span class=\"n\">s</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kp\">&amp;</span><span class=\"nb\">String</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">s</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">s</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">String</span><span class=\"p\">::</span><span class=\"n\">from</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello, World&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"n\">print_string</span><span class=\"p\">(</span><span class=\"o\">&amp;</span><span class=\"n\">s</span><span class=\"p\">);</span><span class=\"w\"> </span><span class=\"c1\">// s borrowed by print_string</span>\n<span class=\"w\">    </span><span class=\"n\">print_string</span><span class=\"p\">(</span><span class=\"o\">&amp;</span><span class=\"n\">s</span><span class=\"p\">);</span><span class=\"w\"> </span><span class=\"c1\">// s has not been consumed; we can call the function many times</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Because of these ownership rules, Rust types are known as <i><a href=\"/wiki/Linear_types\" class=\"mw-redirect\" title=\"Linear types\">linear</a></i> or <i>affine</i> types, meaning each value can be used exactly once. This enforces a form of <a href=\"/wiki/Software_fault_isolation\" class=\"mw-redirect\" title=\"Software fault isolation\">software fault isolation</a> as the owner of a value is solely responsible for its correctness and deallocation.<sup id=\"cite_ref-BeyondSafety_82-0\" class=\"reference\"><a href=\"#cite_note-BeyondSafety-82\"><span class=\"cite-bracket\">&#91;</span>75<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>When a value goes out of scope, it is <i>dropped</i> by running its <a href=\"/wiki/Destructor_(computer_programming)\" title=\"Destructor (computer programming)\">destructor</a>. The destructor may be programmatically defined through implementing the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">Drop</span></code> <a href=\"#Traits\">trait</a>. This helps manage resources such as file handles, network sockets, and <a href=\"/wiki/Lock_(computer_science)\" title=\"Lock (computer science)\">locks</a>, since when objects are dropped, the resources associated with them are closed or released automatically.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023327–30_83-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023327–30-83\"><span class=\"cite-bracket\">&#91;</span>76<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Lifetimes\">Lifetimes</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=21\" title=\"Edit section: Lifetimes\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Object_lifetime\" title=\"Object lifetime\">Object lifetime</a> refers to the period of time during which a reference is valid; that is, the time between the object creation and destruction.<sup id=\"cite_ref-84\" class=\"reference\"><a href=\"#cite_note-84\"><span class=\"cite-bracket\">&#91;</span>77<span class=\"cite-bracket\">&#93;</span></a></sup> These <i>lifetimes</i> are implicitly associated with all Rust reference types. While often inferred, they can also be indicated explicitly with named lifetime parameters (often denoted <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">a</span></code>, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">b</span></code>, and so on).<sup id=\"cite_ref-85\" class=\"reference\"><a href=\"#cite_note-85\"><span class=\"cite-bracket\">&#91;</span>78<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Lifetimes in Rust can be thought of as <a href=\"/wiki/Scope_(computer_science)\" title=\"Scope (computer science)\">lexically scoped</a>, meaning that the duration of an object lifetime is inferred from the set of locations in the source code (i.e., function, line, and column numbers) for which a variable is valid.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019194_86-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019194-86\"><span class=\"cite-bracket\">&#91;</span>79<span class=\"cite-bracket\">&#93;</span></a></sup> For example, a reference to a local variable has a lifetime corresponding to the block it is defined in:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019194_86-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019194-86\"><span class=\"cite-bracket\">&#91;</span>79<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"p\">;</span><span class=\"w\">                </span><span class=\"c1\">// ------------------+- Lifetime &#39;a</span>\n<span class=\"w\">                              </span><span class=\"c1\">//                   |</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"o\">&amp;</span><span class=\"n\">x</span><span class=\"p\">;</span><span class=\"w\">               </span><span class=\"c1\">// -+-- Lifetime &#39;b  |</span>\n<span class=\"w\">                              </span><span class=\"c1\">//  |                |</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;r: {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"p\">);</span><span class=\"w\">     </span><span class=\"c1\">//  |                |</span>\n<span class=\"w\">                              </span><span class=\"c1\">//  |                |</span>\n<span class=\"w\">                              </span><span class=\"c1\">// -+                |</span>\n<span class=\"p\">}</span><span class=\"w\">                             </span><span class=\"c1\">// ------------------+</span>\n</pre></div>\n<p>The borrow checker in the Rust compiler then enforces that references are only used in the locations of the source code where the associated lifetime is valid.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201975,_134_87-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201975,_134-87\"><span class=\"cite-bracket\">&#91;</span>80<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-88\" class=\"reference\"><a href=\"#cite_note-88\"><span class=\"cite-bracket\">&#91;</span>81<span class=\"cite-bracket\">&#93;</span></a></sup> In the example above, storing a reference to variable <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">x</span></code> in <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">r</span></code> is valid, as variable <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">x</span></code> has a longer lifetime (<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">a</span></code>) than variable <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">r</span></code> (<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">b</span></code>). However, when <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">x</span></code> has a shorter lifetime, the borrow checker would reject the program:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"p\">;</span><span class=\"w\">                    </span><span class=\"c1\">// ------------------+- Lifetime &#39;a</span>\n<span class=\"w\">                              </span><span class=\"c1\">//                   |</span>\n<span class=\"w\">    </span><span class=\"p\">{</span><span class=\"w\">                         </span><span class=\"c1\">//                   |</span>\n<span class=\"w\">        </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">5</span><span class=\"p\">;</span><span class=\"w\">            </span><span class=\"c1\">// -+-- Lifetime &#39;b  |</span>\n<span class=\"w\">        </span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"o\">&amp;</span><span class=\"n\">x</span><span class=\"p\">;</span><span class=\"w\"> </span><span class=\"c1\">// ERROR: x does  |                |</span>\n<span class=\"w\">    </span><span class=\"p\">}</span><span class=\"w\">           </span><span class=\"c1\">// not live long -|                |</span>\n<span class=\"w\">                </span><span class=\"c1\">// enough                          |</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;r: {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"p\">);</span><span class=\"w\">     </span><span class=\"c1\">//                   |</span>\n<span class=\"p\">}</span><span class=\"w\">                             </span><span class=\"c1\">// ------------------+</span>\n</pre></div>\n<p>Since the lifetime of the referenced variable (<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">b</span></code>) is shorter than the lifetime of the variable holding the reference (<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&#39;</span><span class=\"na\">a</span></code>), the borrow checker errors, preventing <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">x</span></code> from being used from outside its scope.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019194–195_89-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019194–195-89\"><span class=\"cite-bracket\">&#91;</span>82<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Lifetimes can be indicated using explicit <i>lifetime parameters</i> on function arguments. For example, the following code specifies that the reference returned by the function has the same lifetime as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">original</span></code> (and <i>not</i> necessarily the same lifetime as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">prefix</span></code>):<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023208–12_90-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023208–12-90\"><span class=\"cite-bracket\">&#91;</span>83<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">remove_prefix</span><span class=\"o\">&lt;&#39;</span><span class=\"na\">a</span><span class=\"o\">&gt;</span><span class=\"p\">(</span><span class=\"k\">mut</span><span class=\"w\"> </span><span class=\"n\">original</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kp\">&amp;</span><span class=\"o\">&#39;</span><span class=\"na\">a</span><span class=\"w\"> </span><span class=\"kt\">str</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">prefix</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kp\">&amp;</span><span class=\"kt\">str</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kp\">&amp;</span><span class=\"o\">&#39;</span><span class=\"na\">a</span><span class=\"w\"> </span><span class=\"kt\">str</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"n\">original</span><span class=\"p\">.</span><span class=\"n\">starts_with</span><span class=\"p\">(</span><span class=\"n\">prefix</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"n\">original</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">original</span><span class=\"p\">[</span><span class=\"n\">prefix</span><span class=\"p\">.</span><span class=\"n\">len</span><span class=\"p\">()</span><span class=\"o\">..</span><span class=\"p\">];</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"w\">    </span><span class=\"n\">original</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>In the compiler, ownership and lifetimes work together to prevent memory safety issues such as dangling pointers.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023&#91;httpsdocrust-langorgbookch04-02-references-and-borrowinghtml_4.2._References_and_Borrowing&#93;_91-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023[httpsdocrust-langorgbookch04-02-references-and-borrowinghtml_4.2._References_and_Borrowing]-91\"><span class=\"cite-bracket\">&#91;</span>84<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-Pearce_92-0\" class=\"reference\"><a href=\"#cite_note-Pearce-92\"><span class=\"cite-bracket\">&#91;</span>85<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"User-defined_types\">User-defined types</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=22\" title=\"Edit section: User-defined types\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>User-defined types are created with the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">struct</span></code> or <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">enum</span></code> keywords. The <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">struct</span></code> keyword is used to denote a <a href=\"/wiki/Record_(computer_science)\" title=\"Record (computer science)\">record type</a> that groups multiple related values.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201983_93-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201983-93\"><span class=\"cite-bracket\">&#91;</span>86<span class=\"cite-bracket\">&#93;</span></a></sup> <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">enum</span></code>s can take on different variants at runtime, with its capabilities similar to <a href=\"/wiki/Algebraic_data_types\" class=\"mw-redirect\" title=\"Algebraic data types\">algebraic data types</a> found in functional programming languages.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201997_94-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201997-94\"><span class=\"cite-bracket\">&#91;</span>87<span class=\"cite-bracket\">&#93;</span></a></sup> Both records and enum variants can contain <a href=\"/wiki/Field_(computer_science)\" title=\"Field (computer science)\">fields</a> with different types.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201998–101_95-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201998–101-95\"><span class=\"cite-bracket\">&#91;</span>88<span class=\"cite-bracket\">&#93;</span></a></sup> Alternative names, or aliases, for the same type can be defined with the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">type</span></code> keyword.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019438–440_96-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019438–440-96\"><span class=\"cite-bracket\">&#91;</span>89<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">impl</span></code> keyword can define methods for a user-defined type. Data and functions are defined separately. Implementations fulfill a role similar to that of <a href=\"/wiki/Class_(computer_programming)\" title=\"Class (computer programming)\">classes</a> within other languages.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201993_97-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201993-97\"><span class=\"cite-bracket\">&#91;</span>90<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Standard_library\">Standard library</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=23\" title=\"Edit section: Standard library\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Rust_standard_libraries.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/af/Rust_standard_libraries.svg/250px-Rust_standard_libraries.svg.png\" decoding=\"async\" width=\"250\" height=\"314\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/af/Rust_standard_libraries.svg/375px-Rust_standard_libraries.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/a/af/Rust_standard_libraries.svg/500px-Rust_standard_libraries.svg.png 2x\" data-file-width=\"259\" data-file-height=\"325\" /></a><figcaption>A diagram of the dependencies between the standard library modules of Rust.</figcaption></figure>\n<p>The Rust <a href=\"/wiki/Standard_library\" title=\"Standard library\">standard library</a> defines and implements many widely used custom data types, including core data structures such as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">Vec</span></code>, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">Option</span></code>, and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">HashMap</span></code>, as well as <a href=\"/wiki/Smart_pointer\" title=\"Smart pointer\">smart pointer</a> types. Rust also provides a way to exclude most of the standard library using the attribute <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"cp\">#![no_std]</span></code>, for applications such as embedded devices. Internally, the standard library is divided into three parts, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">core</span></code>, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">alloc</span></code>, and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">std</span></code>, where <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">std</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">alloc</span></code> are excluded by <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"cp\">#![no_std]</span></code>.<sup id=\"cite_ref-FOOTNOTEGjengset2021213–215_98-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021213–215-98\"><span class=\"cite-bracket\">&#91;</span>91<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Rust uses the <a href=\"/wiki/Option_type\" title=\"Option type\">option type</a> <code>Option&lt;T&gt;</code> to define optional values, which can be matched using <code>if let</code> or <code>match</code> to access the inner value:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023108–110,_113–114,_116–117_99-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023108–110,_113–114,_116–117-99\"><span class=\"cite-bracket\">&#91;</span>92<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">name1</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Option</span><span class=\"o\">&lt;&amp;</span><span class=\"kt\">str</span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">None</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"c1\">// In this case, nothing will be printed out</span>\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"nb\">Some</span><span class=\"p\">(</span><span class=\"n\">name</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">name1</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{name}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">name2</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Option</span><span class=\"o\">&lt;&amp;</span><span class=\"kt\">str</span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">Some</span><span class=\"p\">(</span><span class=\"s\">&quot;Matthew&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"c1\">// In this case, the word &quot;Matthew&quot; will be printed out</span>\n<span class=\"w\">    </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"nb\">Some</span><span class=\"p\">(</span><span class=\"n\">name</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">name2</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{name}&quot;</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>Similarly, Rust's <a href=\"/wiki/Result_type\" title=\"Result type\">result type</a> <code>Result&lt;T, E&gt;</code> holds either a successfully computed value (the <code>Ok</code> variant) or an error (the <code>Err</code> variant)<sup id=\"cite_ref-Rust_error_handling_100-0\" class=\"reference\"><a href=\"#cite_note-Rust_error_handling-100\"><span class=\"cite-bracket\">&#91;</span>93<span class=\"cite-bracket\">&#93;</span></a></sup>. Like <code>Option</code>, the use of <code>Result</code> means that the inner value cannot be used directly; programmers must use a <code>match</code> expression, syntactic sugar such as <code>?</code> (the “try” operator), or an explicit <code>unwrap</code> assertion to access it. Both <code>Option</code> and <code>Result</code> are used throughout the standard library and are a fundamental part of Rust's explicit approach to handling errors and missing data.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Pointers\">Pointers</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=24\" title=\"Edit section: Pointers\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>The <code>&amp;</code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&amp;</span><span class=\"k\">mut</span></code> reference types are guaranteed to not be null and point to valid memory.<sup id=\"cite_ref-FOOTNOTEGjengset2021155-156_101-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021155-156-101\"><span class=\"cite-bracket\">&#91;</span>94<span class=\"cite-bracket\">&#93;</span></a></sup> The raw pointer types <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">*</span><span class=\"k\">const</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">*</span><span class=\"k\">mut</span></code> opt out of the safety guarantees, thus they may be null or invalid; however, it is impossible to dereference them unless the code is explicitly declared unsafe through the use of an <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">unsafe</span></code> block.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023421–423_102-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023421–423-102\"><span class=\"cite-bracket\">&#91;</span>95<span class=\"cite-bracket\">&#93;</span></a></sup> Unlike dereferencing, the creation of raw pointers is allowed inside safe Rust code.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019418–427_103-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019418–427-103\"><span class=\"cite-bracket\">&#91;</span>96<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Type_conversion\">Type conversion</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=25\" title=\"Edit section: Type conversion\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<div class=\"excerpt-block\"><style data-mw-deduplicate=\"TemplateStyles:r1066933788\">.mw-parser-output .excerpt-hat .mw-editsection-like{font-style:normal}</style><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable dablink excerpt-hat selfref\">This section is an excerpt from <a href=\"/wiki/Type_conversion#Rust\" title=\"Type conversion\">Type conversion § Rust</a>.<span class=\"mw-editsection-like plainlinks\"><span class=\"mw-editsection-bracket\">[</span><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Type_conversion&amp;action=edit\">edit</a><span class=\"mw-editsection-bracket\">]</span></span></div><div class=\"excerpt\">\n<p>Rust provides no implicit type conversion (coercion) between most primitive types. But, explicit type conversion (casting) can be performed using the <code>as</code> keyword.<sup id=\"cite_ref-104\" class=\"reference\"><a href=\"#cite_note-104\"><span class=\"cite-bracket\">&#91;</span>97<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">1000</span><span class=\"p\">;</span>\n<span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;1000 as a u16 is: {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"k\">as</span><span class=\"w\"> </span><span class=\"kt\">u16</span><span class=\"p\">);</span>\n</pre></div></div></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><span><video id=\"mwe_player_0\" poster=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Rust_101.webm/250px--Rust_101.webm.jpg\" controls=\"\" preload=\"none\" data-mw-tmh=\"\" class=\"mw-file-element\" width=\"250\" height=\"141\" data-durationhint=\"5448\" data-mwtitle=\"Rust_101.webm\" data-mwprovider=\"wikimediacommons\" resource=\"/wiki/File:Rust_101.webm\"><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.480p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"480p.vp9.webm\" data-width=\"854\" data-height=\"480\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.720p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"720p.vp9.webm\" data-width=\"1280\" data-height=\"720\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/5/5c/Rust_101.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-width=\"1280\" data-height=\"720\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.144p.mjpeg.mov\" type=\"video/quicktime\" data-transcodekey=\"144p.mjpeg.mov\" data-width=\"256\" data-height=\"144\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.240p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"240p.vp9.webm\" data-width=\"426\" data-height=\"240\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.360p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"360p.vp9.webm\" data-width=\"640\" data-height=\"360\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/5c/Rust_101.webm/Rust_101.webm.360p.webm\" type=\"video/webm; codecs=&quot;vp8, vorbis&quot;\" data-transcodekey=\"360p.webm\" data-width=\"640\" data-height=\"360\" /></video></span><figcaption>A presentation on Rust by Emily Dunham from <a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a>'s Rust team (<a href=\"/wiki/Linux.conf.au\" title=\"Linux.conf.au\">linux.conf.au</a> conference, Hobart, 2017)</figcaption></figure>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Polymorphism\">Polymorphism</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=26\" title=\"Edit section: Polymorphism\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust supports <a href=\"/wiki/Bounded_parametric_polymorphism\" class=\"mw-redirect\" title=\"Bounded parametric polymorphism\">bounded parametric polymorphism</a> through <a href=\"/wiki/Trait_(computer_programming)\" title=\"Trait (computer programming)\">traits</a> and <a href=\"/wiki/Generic_function\" title=\"Generic function\">generic functions</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023378_105-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023378-105\"><span class=\"cite-bracket\">&#91;</span>98<span class=\"cite-bracket\">&#93;</span></a></sup> Common behavior between types may be declared using traits and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">impl</span></code>s:<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023192–198-106\"><span class=\"cite-bracket\">&#91;</span>99<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">trait</span><span class=\"w\"> </span><span class=\"n\">Zero</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Sized</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">zero</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"nc\">Self</span><span class=\"p\">;</span>\n<span class=\"w\">    </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">is_zero</span><span class=\"p\">(</span><span class=\"o\">&amp;</span><span class=\"bp\">self</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">bool</span>\n<span class=\"w\">    </span><span class=\"nc\">where</span>\n<span class=\"w\">        </span><span class=\"bp\">Self</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">PartialEq</span><span class=\"p\">,</span>\n<span class=\"w\">    </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"bp\">self</span><span class=\"w\"> </span><span class=\"o\">==</span><span class=\"w\"> </span><span class=\"o\">&amp;</span><span class=\"n\">Zero</span><span class=\"p\">::</span><span class=\"n\">zero</span><span class=\"p\">()</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">impl</span><span class=\"w\"> </span><span class=\"n\">Zero</span><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"kt\">u32</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">zero</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">u32</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"w\"> </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">impl</span><span class=\"w\"> </span><span class=\"n\">Zero</span><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"kt\">f32</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">zero</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"nc\">Self</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"mf\">0.0</span><span class=\"w\"> </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>The example above also includes a method <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">is_zero</span></code> which provides a default implementation that is not required when implementing the trait.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023192–198-106\"><span class=\"cite-bracket\">&#91;</span>99<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>A function can then be made generic by adding type parameters inside angle brackets (<code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"o\">&lt;</span><span class=\"n\">Num</span><span class=\"o\">&gt;</span></code>), which only allow types that implement the trait:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"c1\">// zero is a generic function with one type parameter, Num</span>\n<span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">zero</span><span class=\"o\">&lt;</span><span class=\"n\">Num</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nc\">Zero</span><span class=\"o\">&gt;</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"nc\">Num</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"n\">Num</span><span class=\"p\">::</span><span class=\"n\">zero</span><span class=\"p\">()</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">a</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">u32</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">zero</span><span class=\"p\">();</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">b</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">f32</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">zero</span><span class=\"p\">();</span>\n<span class=\"w\">    </span><span class=\"fm\">assert!</span><span class=\"p\">(</span><span class=\"n\">a</span><span class=\"p\">.</span><span class=\"n\">is_zero</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">&amp;&amp;</span><span class=\"w\"> </span><span class=\"n\">b</span><span class=\"p\">.</span><span class=\"n\">is_zero</span><span class=\"p\">());</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>In the examples above, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">Num</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nc\">Zero</span></code> as well as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">where</span><span class=\"w\"> </span><span class=\"bp\">Self</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">PartialEq</span></code> are trait bounds that constrain the type to only allow types that implement <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">Zero</span></code> or <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"nb\">PartialEq</span></code>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-2\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023192–198-106\"><span class=\"cite-bracket\">&#91;</span>99<span class=\"cite-bracket\">&#93;</span></a></sup> Within a trait or impl, <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"bp\">Self</span></code> refers to the type that the code is implementing.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols202398_107-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols202398-107\"><span class=\"cite-bracket\">&#91;</span>100<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Generics can be used in functions to allow implementing a behavior for different types without repeating the same code. Generic functions can be written in relation to other generics, without knowing the actual type.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019171–172,_205_108-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019171–172,_205-108\"><span class=\"cite-bracket\">&#91;</span>101<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Trait_objects\">Trait objects</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=27\" title=\"Edit section: Trait objects\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Generic functions use <a href=\"/wiki/Static_dispatch\" title=\"Static dispatch\">static dispatch</a>, meaning that the type of all parameters that end up being used for the function must be known at compile time. Generic functions generate separate copies of the code for each combination of generic parameters used in a process called <a href=\"/wiki/Monomorphization\" title=\"Monomorphization\">monomorphization</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023191–192_109-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023191–192-109\"><span class=\"cite-bracket\">&#91;</span>102<span class=\"cite-bracket\">&#93;</span></a></sup> Because monomorphization duplicates the code for each type used, it is as performant as writing functions using concrete types,<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023191–192_109-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023191–192-109\"><span class=\"cite-bracket\">&#91;</span>102<span class=\"cite-bracket\">&#93;</span></a></sup> but compile time and size of the output binary could be increased.<sup id=\"cite_ref-FOOTNOTEGjengset202125_110-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset202125-110\"><span class=\"cite-bracket\">&#91;</span>103<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>However, Rust also uses a feature known as <i>trait objects</i> to accomplish <a href=\"/wiki/Dynamic_dispatch\" title=\"Dynamic dispatch\">dynamic dispatch</a>, a type of polymorphism where the implementation of a polymorphic operation is chosen at <a href=\"/wiki/Runtime_(program_lifecycle_phase)\" class=\"mw-redirect\" title=\"Runtime (program lifecycle phase)\">runtime</a>. This allows for behavior similar to <a href=\"/wiki/Duck_typing\" title=\"Duck typing\">duck typing</a>, where all data types that implement a given trait can be treated as functionally equivalent.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023&#91;httpsdocrust-langorgbookch18-02-trait-objectshtml_18.2._Using_Trait_Objects_That_Allow_for_Values_of_Different_Types&#93;_111-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023[httpsdocrust-langorgbookch18-02-trait-objectshtml_18.2._Using_Trait_Objects_That_Allow_for_Values_of_Different_Types]-111\"><span class=\"cite-bracket\">&#91;</span>104<span class=\"cite-bracket\">&#93;</span></a></sup> Trait objects are declared using the syntax <code>dyn Tr</code> where <code>Tr</code> is a trait. Trait objects are dynamically sized, therefore they must be put behind a pointer, such as <code>Box</code>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019441–442_112-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019441–442-112\"><span class=\"cite-bracket\">&#91;</span>105<span class=\"cite-bracket\">&#93;</span></a></sup> The following example creates a list of objects where each object can be printed out using the <code>Display</code> trait:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">use</span><span class=\"w\"> </span><span class=\"n\">std</span><span class=\"p\">::</span><span class=\"n\">fmt</span><span class=\"p\">::</span><span class=\"n\">Display</span><span class=\"p\">;</span>\n\n<span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">v</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nb\">Vec</span><span class=\"o\">&lt;</span><span class=\"nb\">Box</span><span class=\"o\">&lt;</span><span class=\"k\">dyn</span><span class=\"w\"> </span><span class=\"n\">Display</span><span class=\"o\">&gt;&gt;</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"fm\">vec!</span><span class=\"p\">[</span>\n<span class=\"w\">    </span><span class=\"nb\">Box</span><span class=\"p\">::</span><span class=\"n\">new</span><span class=\"p\">(</span><span class=\"mi\">3</span><span class=\"p\">),</span>\n<span class=\"w\">    </span><span class=\"nb\">Box</span><span class=\"p\">::</span><span class=\"n\">new</span><span class=\"p\">(</span><span class=\"mf\">5.0</span><span class=\"p\">),</span>\n<span class=\"w\">    </span><span class=\"nb\">Box</span><span class=\"p\">::</span><span class=\"n\">new</span><span class=\"p\">(</span><span class=\"s\">&quot;hi&quot;</span><span class=\"p\">),</span>\n<span class=\"p\">];</span>\n\n<span class=\"k\">for</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"k\">in</span><span class=\"w\"> </span><span class=\"n\">v</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{x}&quot;</span><span class=\"p\">);</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>If an element in the list does not implement the <code>Display</code> trait, it will cause a compile-time error.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019379–380_113-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019379–380-113\"><span class=\"cite-bracket\">&#91;</span>106<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Memory_safety\">Memory safety</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=28\" title=\"Edit section: Memory safety\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust is designed to be <a href=\"/wiki/Memory_safe\" class=\"mw-redirect\" title=\"Memory safe\">memory safe</a>. It does not permit null pointers, <a href=\"/wiki/Dangling_pointer\" title=\"Dangling pointer\">dangling pointers</a>, or <a href=\"/wiki/Data_race\" class=\"mw-redirect\" title=\"Data race\">data races</a>.<sup id=\"cite_ref-cnet_114-0\" class=\"reference\"><a href=\"#cite_note-cnet-114\"><span class=\"cite-bracket\">&#91;</span>107<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-lwn_115-0\" class=\"reference\"><a href=\"#cite_note-lwn-115\"><span class=\"cite-bracket\">&#91;</span>108<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-The_Rustonomicon_116-0\" class=\"reference\"><a href=\"#cite_note-The_Rustonomicon-116\"><span class=\"cite-bracket\">&#91;</span>109<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-Sensors_117-0\" class=\"reference\"><a href=\"#cite_note-Sensors-117\"><span class=\"cite-bracket\">&#91;</span>110<span class=\"cite-bracket\">&#93;</span></a></sup> Data values can be initialized only through a fixed set of forms, all of which require their inputs to be already initialized.<sup id=\"cite_ref-lang-faq_118-0\" class=\"reference\"><a href=\"#cite_note-lang-faq-118\"><span class=\"cite-bracket\">&#91;</span>111<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Memory_management\">Memory management</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=29\" title=\"Edit section: Memory management\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust does not use <a href=\"/wiki/Garbage_collection_(computer_science)\" title=\"Garbage collection (computer science)\">garbage collection</a>. Memory and other resources are instead managed through the \"resource acquisition is initialization\" convention,<sup id=\"cite_ref-119\" class=\"reference\"><a href=\"#cite_note-119\"><span class=\"cite-bracket\">&#91;</span>112<span class=\"cite-bracket\">&#93;</span></a></sup> with optional <a href=\"/wiki/Reference_counting\" title=\"Reference counting\">reference counting</a>. Rust provides deterministic management of resources, with very low <a href=\"/wiki/Overhead_(computing)\" title=\"Overhead (computing)\">overhead</a>.<sup id=\"cite_ref-120\" class=\"reference\"><a href=\"#cite_note-120\"><span class=\"cite-bracket\">&#91;</span>113<span class=\"cite-bracket\">&#93;</span></a></sup> Values are <a href=\"/wiki/Stack-based_memory_allocation\" title=\"Stack-based memory allocation\">allocated on the stack</a> by default, and all <a href=\"/wiki/Dynamic_allocation\" class=\"mw-redirect\" title=\"Dynamic allocation\">dynamic allocations</a> must be explicit.<sup id=\"cite_ref-121\" class=\"reference\"><a href=\"#cite_note-121\"><span class=\"cite-bracket\">&#91;</span>114<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The built-in reference types using the <code>&amp;</code> symbol do not involve run-time reference counting. The safety and validity of the underlying pointers is verified at compile time, preventing <a href=\"/wiki/Dangling_pointers\" class=\"mw-redirect\" title=\"Dangling pointers\">dangling pointers</a> and other forms of <a href=\"/wiki/Undefined_behavior\" title=\"Undefined behavior\">undefined behavior</a>.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols201970–75_122-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols201970–75-122\"><span class=\"cite-bracket\">&#91;</span>115<span class=\"cite-bracket\">&#93;</span></a></sup> Rust's type system separates shared, <a href=\"/wiki/Immutable_object\" title=\"Immutable object\">immutable</a> references of the form <code>&amp;T</code> from unique, mutable references of the form <code>&amp;mut T</code>. A mutable reference can be coerced to an immutable reference, but not vice versa.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019323_123-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019323-123\"><span class=\"cite-bracket\">&#91;</span>116<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Unsafe\">Unsafe</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=30\" title=\"Edit section: Unsafe\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust's memory safety checks may be circumvented through the use of <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">unsafe</span></code> blocks. This allows programmers to dereference arbitrary raw pointers, call external code, or perform other low-level functionality not allowed by safe Rust.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023420–429_124-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023420–429-124\"><span class=\"cite-bracket\">&#91;</span>117<span class=\"cite-bracket\">&#93;</span></a></sup> Some low-level functionality enabled in this way includes <a href=\"/wiki/Volatile_(computer_programming)\" title=\"Volatile (computer programming)\">volatile memory access</a>, architecture-specific intrinsics, <a href=\"/wiki/Type_punning\" title=\"Type punning\">type punning</a>, and inline assembly.<sup id=\"cite_ref-FOOTNOTEMcNamara2021139,_376–379,_395_125-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEMcNamara2021139,_376–379,_395-125\"><span class=\"cite-bracket\">&#91;</span>118<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Unsafe code is sometimes needed to implement complex data structures.<sup id=\"cite_ref-UnsafeRustUse_126-0\" class=\"reference\"><a href=\"#cite_note-UnsafeRustUse-126\"><span class=\"cite-bracket\">&#91;</span>119<span class=\"cite-bracket\">&#93;</span></a></sup> A frequently cited example is that it is difficult or impossible to implement <a href=\"/wiki/Doubly_linked_list\" title=\"Doubly linked list\">doubly linked lists</a> in safe Rust.<sup id=\"cite_ref-127\" class=\"reference\"><a href=\"#cite_note-127\"><span class=\"cite-bracket\">&#91;</span>120<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-128\" class=\"reference\"><a href=\"#cite_note-128\"><span class=\"cite-bracket\">&#91;</span>121<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-129\" class=\"reference\"><a href=\"#cite_note-129\"><span class=\"cite-bracket\">&#91;</span>122<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-130\" class=\"reference\"><a href=\"#cite_note-130\"><span class=\"cite-bracket\">&#91;</span>123<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Programmers using unsafe Rust are considered responsible for upholding Rust's memory and type safety requirements, for example, that no two mutable references exist pointing to the same location.<sup id=\"cite_ref-IsRustSafely_131-0\" class=\"reference\"><a href=\"#cite_note-IsRustSafely-131\"><span class=\"cite-bracket\">&#91;</span>124<span class=\"cite-bracket\">&#93;</span></a></sup> If programmers write code which violates these requirements, this results in <a href=\"/wiki/Undefined_behavior\" title=\"Undefined behavior\">undefined behavior</a>.<sup id=\"cite_ref-IsRustSafely_131-1\" class=\"reference\"><a href=\"#cite_note-IsRustSafely-131\"><span class=\"cite-bracket\">&#91;</span>124<span class=\"cite-bracket\">&#93;</span></a></sup> The Rust documentation includes a list of behavior considered undefined, including accessing dangling or misaligned pointers, or breaking the aliasing rules for references.<sup id=\"cite_ref-132\" class=\"reference\"><a href=\"#cite_note-132\"><span class=\"cite-bracket\">&#91;</span>125<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Macros\">Macros</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=31\" title=\"Edit section: Macros\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Macros allow generation and transformation of Rust code to reduce repetition. Macros come in two forms, with <i>declarative macros</i> defined through <code>macro_rules!</code>, and <i>procedural macros</i>, which are defined in separate crates.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023449–455_133-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023449–455-133\"><span class=\"cite-bracket\">&#91;</span>126<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-FOOTNOTEGjengset2021101–102_134-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021101–102-134\"><span class=\"cite-bracket\">&#91;</span>127<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Declarative_macros\">Declarative macros</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=32\" title=\"Edit section: Declarative macros\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>A declarative macro (also called a \"macro by example\") is a macro, defined using the <code>macro_rules!</code> keyword, that uses pattern matching to determine its expansion.<sup id=\"cite_ref-Rust_Ref._–_Macros_By_Example_135-0\" class=\"reference\"><a href=\"#cite_note-Rust_Ref._–_Macros_By_Example-135\"><span class=\"cite-bracket\">&#91;</span>128<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019446–448_136-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019446–448-136\"><span class=\"cite-bracket\">&#91;</span>129<span class=\"cite-bracket\">&#93;</span></a></sup> Below is an example that sums over all its arguments:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"fm\">macro_rules!</span><span class=\"w\"> </span><span class=\"n\">sum</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"p\">(</span><span class=\"w\"> </span><span class=\"cp\">$initial</span><span class=\"p\">:</span><span class=\"nc\">expr</span><span class=\"w\"> </span><span class=\"cp\">$(,</span><span class=\"w\"> </span><span class=\"cp\">$expr</span><span class=\"p\">:</span><span class=\"nc\">expr</span><span class=\"w\"> </span><span class=\"p\">)</span><span class=\"o\">*</span><span class=\"w\"> </span><span class=\"cp\">$(,</span><span class=\"p\">)</span><span class=\"o\">?</span><span class=\"w\"> </span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">=&gt;</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">        </span><span class=\"cp\">$initial</span><span class=\"w\"> </span><span class=\"cp\">$(</span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"cp\">$expr</span><span class=\"p\">)</span><span class=\"o\">*</span>\n<span class=\"w\">    </span><span class=\"p\">}</span>\n<span class=\"p\">}</span>\n\n<span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">sum</span><span class=\"o\">!</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">2</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">3</span><span class=\"p\">);</span>\n<span class=\"w\">    </span><span class=\"fm\">println!</span><span class=\"p\">(</span><span class=\"s\">&quot;{x}&quot;</span><span class=\"p\">);</span><span class=\"w\"> </span><span class=\"c1\">// prints 6</span>\n<span class=\"p\">}</span>\n</pre></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Procedural_macros\">Procedural macros</h4><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=33\" title=\"Edit section: Procedural macros\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Procedural macros are Rust functions that run and modify the compiler's input <a href=\"/wiki/Token_(parser)\" class=\"mw-redirect\" title=\"Token (parser)\">token</a> stream, before any other components are compiled. They are generally more flexible than declarative macros, but are more difficult to maintain due to their complexity.<sup id=\"cite_ref-rust-procedural-macros_137-0\" class=\"reference\"><a href=\"#cite_note-rust-procedural-macros-137\"><span class=\"cite-bracket\">&#91;</span>130<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019449–455_138-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019449–455-138\"><span class=\"cite-bracket\">&#91;</span>131<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Procedural macros come in three flavors:\n</p>\n<ul><li>Function-like macros <code>custom!(...)</code></li>\n<li>Derive macros <code>#[derive(CustomDerive)]</code></li>\n<li>Attribute macros <code>#[custom_attribute]</code></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Interface_with_C_and_C++\"><span id=\"Interface_with_C_and_C.2B.2B\"></span>Interface with C and C++</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=34\" title=\"Edit section: Interface with C and C++\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust supports the creation of <a href=\"/wiki/Foreign_function_interface\" title=\"Foreign function interface\">foreign function interfaces</a> (FFI) through the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">extern</span></code> keyword. A function that uses the C <a href=\"/wiki/Calling_convention\" title=\"Calling convention\">calling convention</a> can be written using <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">extern</span><span class=\"w\"> </span><span class=\"s\">&quot;C&quot;</span><span class=\"w\"> </span><span class=\"k\">fn</span></code>. Symbols can be exported from Rust to other languages through the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"cp\">#[no_mangle]</span></code> attribute, and symbols can be imported into Rust through <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">extern</span></code> blocks:<sup id=\"cite_ref-140\" class=\"reference\"><a href=\"#cite_note-140\"><span class=\"cite-bracket\">&#91;</span>note 8<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-FOOTNOTEGjengset2021193–209_141-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021193–209-141\"><span class=\"cite-bracket\">&#91;</span>133<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"cp\">#[no_mangle]</span>\n<span class=\"k\">pub</span><span class=\"w\"> </span><span class=\"k\">extern</span><span class=\"w\"> </span><span class=\"s\">&quot;C&quot;</span><span class=\"w\"> </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">exported_from_rust</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\"> </span><span class=\"n\">x</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"w\"> </span><span class=\"p\">}</span>\n<span class=\"k\">unsafe</span><span class=\"w\"> </span><span class=\"k\">extern</span><span class=\"w\"> </span><span class=\"s\">&quot;C&quot;</span><span class=\"w\"> </span><span class=\"p\">{</span>\n<span class=\"w\">    </span><span class=\"k\">fn</span><span class=\"w\"> </span><span class=\"nf\">imported_into_rust</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">-&gt;</span><span class=\"w\"> </span><span class=\"kt\">i32</span><span class=\"p\">;</span>\n<span class=\"p\">}</span>\n</pre></div>\n<p>The <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"cp\">#[repr(C)]</span></code> attribute enables deterministic memory layouts for <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">struct</span></code>s and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">enum</span></code>s for use across FFI boundaries.<sup id=\"cite_ref-FOOTNOTEGjengset2021193–209_141-1\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021193–209-141\"><span class=\"cite-bracket\">&#91;</span>133<span class=\"cite-bracket\">&#93;</span></a></sup> External libraries such as <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">bindgen</span></code> and <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"n\">cxx</span></code> can generate Rust bindings for C/C++.<sup id=\"cite_ref-FOOTNOTEGjengset2021193–209_141-2\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset2021193–209-141\"><span class=\"cite-bracket\">&#91;</span>133<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-142\" class=\"reference\"><a href=\"#cite_note-142\"><span class=\"cite-bracket\">&#91;</span>134<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Ecosystem\">Ecosystem</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=35\" title=\"Edit section: Ecosystem\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><span><video id=\"mwe_player_1\" poster=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/52/Cargo_compiling.webm/250px--Cargo_compiling.webm.jpg\" controls=\"\" preload=\"none\" data-mw-tmh=\"\" class=\"mw-file-element\" width=\"250\" height=\"140\" data-durationhint=\"20\" data-mwtitle=\"Cargo_compiling.webm\" data-mwprovider=\"wikimediacommons\" resource=\"/wiki/File:Cargo_compiling.webm\"><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.480p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"480p.vp9.webm\" data-width=\"854\" data-height=\"478\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.720p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"720p.vp9.webm\" data-width=\"1280\" data-height=\"716\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/5/52/Cargo_compiling.webm\" type=\"video/webm; codecs=&quot;vp9&quot;\" data-width=\"1508\" data-height=\"844\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.144p.mjpeg.mov\" type=\"video/quicktime\" data-transcodekey=\"144p.mjpeg.mov\" data-width=\"256\" data-height=\"144\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.240p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"240p.vp9.webm\" data-width=\"426\" data-height=\"238\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.360p.webm\" type=\"video/webm; codecs=&quot;vp8, vorbis&quot;\" data-transcodekey=\"360p.webm\" data-width=\"640\" data-height=\"358\" /><source src=\"//upload.wikimedia.org/wikipedia/commons/transcoded/5/52/Cargo_compiling.webm/Cargo_compiling.webm.360p.vp9.webm\" type=\"video/webm; codecs=&quot;vp9, opus&quot;\" data-transcodekey=\"360p.vp9.webm\" data-width=\"640\" data-height=\"358\" /></video></span><figcaption>Compiling a Rust program with Cargo</figcaption></figure>\n<p>The Rust ecosystem includes its compiler, its <a href=\"#Standard_library\">standard library</a>, and additional components for software development. Component installation is typically managed by <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">rustup</code>, a Rust <a href=\"/wiki/Toolchain\" title=\"Toolchain\">toolchain</a> installer developed by the Rust project.<sup id=\"cite_ref-FOOTNOTEBlandyOrendorffTindall20216–8_143-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEBlandyOrendorffTindall20216–8-143\"><span class=\"cite-bracket\">&#91;</span>135<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Compiler\">Compiler</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=36\" title=\"Edit section: Compiler\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>The Rust compiler, <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">rustc</code>, compiles Rust code into <a href=\"/wiki/Executable\" title=\"Executable\">binaries</a>. First, the compiler parses the source code into an <a href=\"/wiki/Abstract_syntax_tree\" title=\"Abstract syntax tree\">AST</a>. Next, this AST is lowered to <a href=\"/wiki/Intermediate_representation\" title=\"Intermediate representation\">IR</a>. The compiler backend is then invoked as a subcomponent to apply <a href=\"/wiki/Optimizing_compiler\" title=\"Optimizing compiler\">optimizations</a> and translate the resulting IR into <a href=\"/wiki/Object_code\" title=\"Object code\">object code</a>. Finally, a <a href=\"/wiki/Linker_(computing)\" title=\"Linker (computing)\">linker</a> is used to combine the object(s) into a single executable image.<sup id=\"cite_ref-144\" class=\"reference\"><a href=\"#cite_note-144\"><span class=\"cite-bracket\">&#91;</span>136<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>rustc uses <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a> as its compiler backend by default, but it also supports using alternative backends such as <a href=\"/wiki/GNU_Compiler_Collection\" title=\"GNU Compiler Collection\">GCC</a> and <a href=\"/wiki/Cranelift\" title=\"Cranelift\">Cranelift</a>.<sup id=\"cite_ref-145\" class=\"reference\"><a href=\"#cite_note-145\"><span class=\"cite-bracket\">&#91;</span>137<span class=\"cite-bracket\">&#93;</span></a></sup> The intention of those alternative backends is to increase platform coverage of Rust or to improve compilation times.<sup id=\"cite_ref-146\" class=\"reference\"><a href=\"#cite_note-146\"><span class=\"cite-bracket\">&#91;</span>138<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-147\" class=\"reference\"><a href=\"#cite_note-147\"><span class=\"cite-bracket\">&#91;</span>139<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Cargo\">Cargo</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=37\" title=\"Edit section: Cargo\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Crates.io_website.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/88/Crates.io_website.png/250px-Crates.io_website.png\" decoding=\"async\" width=\"250\" height=\"143\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/88/Crates.io_website.png/500px-Crates.io_website.png 1.5x\" data-file-width=\"2102\" data-file-height=\"1202\" /></a><figcaption>Screenshot of crates.io in June 2022</figcaption></figure>\n<p>Cargo is Rust's <a href=\"/wiki/Build_system_(software_development)\" title=\"Build system (software development)\">build system</a> and <a href=\"/wiki/Package_manager\" title=\"Package manager\">package manager</a>. It downloads, compiles, distributes, and uploads packages—called <i>crates</i>—that are maintained in an official registry. It also acts as a front-end for Clippy and other Rust components.<sup id=\"cite_ref-Nature_148-0\" class=\"reference\"><a href=\"#cite_note-Nature-148\"><span class=\"cite-bracket\">&#91;</span>140<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>By default, Cargo sources its dependencies from the user-contributed registry <i>crates.io</i>, but <a href=\"/wiki/Git\" title=\"Git\">Git</a> repositories, crates in the local filesystem, and other external sources can also be specified as dependencies.<sup id=\"cite_ref-149\" class=\"reference\"><a href=\"#cite_note-149\"><span class=\"cite-bracket\">&#91;</span>141<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Rustfmt\">Rustfmt</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=38\" title=\"Edit section: Rustfmt\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rustfmt is a <a href=\"/wiki/Code_formatter\" class=\"mw-redirect\" title=\"Code formatter\">code formatter</a> for Rust. It formats whitespace and <a href=\"/wiki/Indentation_style\" title=\"Indentation style\">indentation</a> to produce code in accordance with a common <a href=\"/wiki/Programming_style\" title=\"Programming style\">style</a>, unless otherwise specified. It can be invoked as a standalone program, or from a Rust project through Cargo.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2019511–512_150-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2019511–512-150\"><span class=\"cite-bracket\">&#91;</span>142<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Clippy\">Clippy</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=39\" title=\"Edit section: Clippy\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Cargo_clippy_hello_world_example.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Cargo_clippy_hello_world_example.png/250px-Cargo_clippy_hello_world_example.png\" decoding=\"async\" width=\"250\" height=\"193\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Cargo_clippy_hello_world_example.png/500px-Cargo_clippy_hello_world_example.png 1.5x\" data-file-width=\"912\" data-file-height=\"704\" /></a><figcaption>Example output of Clippy on a hello world Rust program</figcaption></figure>\n<p>Clippy is Rust's built-in <a href=\"/wiki/Linting\" class=\"mw-redirect\" title=\"Linting\">linting</a> tool to improve the correctness, performance, and readability of Rust code. As of 2024<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup>, it has more than 700 rules.<sup id=\"cite_ref-151\" class=\"reference\"><a href=\"#cite_note-151\"><span class=\"cite-bracket\">&#91;</span>143<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-152\" class=\"reference\"><a href=\"#cite_note-152\"><span class=\"cite-bracket\">&#91;</span>144<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Versioning_system\">Versioning system</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=40\" title=\"Edit section: Versioning system\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Following Rust 1.0, new features are developed in <i>nightly</i> versions which are released daily. During each six-week release cycle, changes to nightly versions are released to beta, while changes from the previous beta version are released to a new stable version.<sup id=\"cite_ref-Rust_Book_G_153-0\" class=\"reference\"><a href=\"#cite_note-Rust_Book_G-153\"><span class=\"cite-bracket\">&#91;</span>145<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Every two or three years, a new \"edition\" is produced. Editions are released to allow making limited <a href=\"/wiki/Breaking_changes\" class=\"mw-redirect\" title=\"Breaking changes\">breaking changes</a>, such as promoting <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">await</span></code> to a keyword to support <a href=\"/wiki/Async/await\" title=\"Async/await\">async/await</a> features. Crates targeting different editions can interoperate with each other, so a crate can upgrade to a new edition even if its callers or its dependencies still target older editions. Migration to a new edition can be assisted with automated tooling.<sup id=\"cite_ref-FOOTNOTEBlandyOrendorffTindall2021176–177_154-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEBlandyOrendorffTindall2021176–177-154\"><span class=\"cite-bracket\">&#91;</span>146<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"IDE_support\">IDE support</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=41\" title=\"Edit section: IDE support\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><i>rust-analyzer</i> is a set of <a href=\"/wiki/Utility_software\" title=\"Utility software\">utilities</a> that provides <a href=\"/wiki/Integrated_development_environment\" title=\"Integrated development environment\">integrated development environments</a> (IDEs) and <a href=\"/wiki/Text_editor\" title=\"Text editor\">text editors</a> with information about a Rust project through the <a href=\"/wiki/Language_Server_Protocol\" title=\"Language Server Protocol\">Language Server Protocol</a>. This enables features including <a href=\"/wiki/Autocomplete\" title=\"Autocomplete\">autocomplete</a>, and <a href=\"/wiki/Compilation_error\" title=\"Compilation error\">compilation error</a> display, while editing code.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols2023623_155-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols2023623-155\"><span class=\"cite-bracket\">&#91;</span>147<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Performance\">Performance</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=42\" title=\"Edit section: Performance\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Since it performs no garbage collection, Rust is often faster than other memory-safe languages.<sup id=\"cite_ref-156\" class=\"reference\"><a href=\"#cite_note-156\"><span class=\"cite-bracket\">&#91;</span>148<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-BeyondSafety_82-1\" class=\"reference\"><a href=\"#cite_note-BeyondSafety-82\"><span class=\"cite-bracket\">&#91;</span>75<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-157\" class=\"reference\"><a href=\"#cite_note-157\"><span class=\"cite-bracket\">&#91;</span>149<span class=\"cite-bracket\">&#93;</span></a></sup> Most of Rust's memory safety guarantees impose no runtime overhead,<sup id=\"cite_ref-FOOTNOTEMcNamara202111_158-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEMcNamara202111-158\"><span class=\"cite-bracket\">&#91;</span>150<span class=\"cite-bracket\">&#93;</span></a></sup> with the exception of <a href=\"/wiki/Array_(data_structure)\" title=\"Array (data structure)\">array indexing</a> which is checked at runtime by default.<sup id=\"cite_ref-SaferAtAnySpeed_159-0\" class=\"reference\"><a href=\"#cite_note-SaferAtAnySpeed-159\"><span class=\"cite-bracket\">&#91;</span>151<span class=\"cite-bracket\">&#93;</span></a></sup>  The performance impact of array indexing bounds checks varies, but can be significant in some cases.<sup id=\"cite_ref-SaferAtAnySpeed_159-1\" class=\"reference\"><a href=\"#cite_note-SaferAtAnySpeed-159\"><span class=\"cite-bracket\">&#91;</span>151<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Many of Rust's features are so-called <i>zero-cost abstractions</i>, meaning they are optimized away at compile time and incur no runtime penalty.<sup id=\"cite_ref-FOOTNOTEMcNamara202119,_27_160-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEMcNamara202119,_27-160\"><span class=\"cite-bracket\">&#91;</span>152<span class=\"cite-bracket\">&#93;</span></a></sup> The ownership and borrowing system permits <a href=\"/wiki/Zero-copy\" title=\"Zero-copy\">zero-copy</a> implementations for some performance-sensitive tasks, such as <a href=\"/wiki/Parsing\" title=\"Parsing\">parsing</a>.<sup id=\"cite_ref-161\" class=\"reference\"><a href=\"#cite_note-161\"><span class=\"cite-bracket\">&#91;</span>153<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Static_dispatch\" title=\"Static dispatch\">Static dispatch</a> is used by default to eliminate <a href=\"/wiki/Method_call\" class=\"mw-redirect\" title=\"Method call\">method calls</a>, except for methods called on dynamic trait objects.<sup id=\"cite_ref-FOOTNOTEMcNamara202120_162-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEMcNamara202120-162\"><span class=\"cite-bracket\">&#91;</span>154<span class=\"cite-bracket\">&#93;</span></a></sup> The compiler also uses <a href=\"/wiki/Inline_expansion\" title=\"Inline expansion\">inline expansion</a> to eliminate <a href=\"/wiki/Function_call\" class=\"mw-redirect\" title=\"Function call\">function calls</a> and statically-dispatched method invocations.<sup id=\"cite_ref-163\" class=\"reference\"><a href=\"#cite_note-163\"><span class=\"cite-bracket\">&#91;</span>155<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Since Rust uses <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a>, all performance improvements in LLVM apply to Rust also.<sup id=\"cite_ref-how-fast-is-rust_164-0\" class=\"reference\"><a href=\"#cite_note-how-fast-is-rust-164\"><span class=\"cite-bracket\">&#91;</span>156<span class=\"cite-bracket\">&#93;</span></a></sup> Unlike C and C++, Rust allows the compiler to reorder struct and enum elements unless a <code>#[repr(C)]</code> representation attribute is applied.<sup id=\"cite_ref-165\" class=\"reference\"><a href=\"#cite_note-165\"><span class=\"cite-bracket\">&#91;</span>157<span class=\"cite-bracket\">&#93;</span></a></sup> This allows the compiler to optimize for memory footprint, alignment, and padding, which can be used to produce more efficient code in some cases.<sup id=\"cite_ref-FOOTNOTEGjengset202122_166-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEGjengset202122-166\"><span class=\"cite-bracket\">&#91;</span>158<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Adoption\">Adoption</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=43\" title=\"Edit section: Adoption\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/Category:Rust_(programming_language)_software\" title=\"Category:Rust (programming language) software\">Category:Rust (programming language) software</a></div>\n<figure class=\"mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Firefox_logo,_2019.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Firefox_logo%2C_2019.svg/250px-Firefox_logo%2C_2019.svg.png\" decoding=\"async\" width=\"150\" height=\"150\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Firefox_logo%2C_2019.svg/330px-Firefox_logo%2C_2019.svg.png 2x\" data-file-width=\"512\" data-file-height=\"512\" /></a><figcaption><a href=\"/wiki/Firefox\" title=\"Firefox\">Firefox</a> has components written in Rust as part of the underlying <a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a> browser engine.</figcaption></figure>\n<p>Rust is used in software across different domains. Components from the Servo browser engine (funded by <a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a> and <a href=\"/wiki/Samsung\" title=\"Samsung\">Samsung</a>) were incorporated in the <a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a> browser engine underlying <a href=\"/wiki/Firefox\" title=\"Firefox\">Firefox</a>.<sup id=\"cite_ref-167\" class=\"reference\"><a href=\"#cite_note-167\"><span class=\"cite-bracket\">&#91;</span>159<span class=\"cite-bracket\">&#93;</span></a></sup> In January 2023, Google (<a href=\"/wiki/Alphabet_Inc.\" title=\"Alphabet Inc.\">Alphabet</a>) announced support for using third party Rust libraries in <a href=\"/wiki/Chromium_(web_browser)\" title=\"Chromium (web browser)\">Chromium</a>.<sup id=\"cite_ref-168\" class=\"reference\"><a href=\"#cite_note-168\"><span class=\"cite-bracket\">&#91;</span>160<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-169\" class=\"reference\"><a href=\"#cite_note-169\"><span class=\"cite-bracket\">&#91;</span>161<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Rust is used in several <a href=\"/wiki/Frontend_and_backend\" class=\"mw-redirect\" title=\"Frontend and backend\">backend</a> software projects of large <a href=\"/wiki/Web_service\" title=\"Web service\">web services</a>. <a href=\"/wiki/OpenDNS\" title=\"OpenDNS\">OpenDNS</a>, a <a href=\"/wiki/Domain_Name_System\" title=\"Domain Name System\">DNS</a> resolution service owned by <a href=\"/wiki/Cisco\" title=\"Cisco\">Cisco</a>, uses Rust internally.<sup id=\"cite_ref-170\" class=\"reference\"><a href=\"#cite_note-170\"><span class=\"cite-bracket\">&#91;</span>162<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-171\" class=\"reference\"><a href=\"#cite_note-171\"><span class=\"cite-bracket\">&#91;</span>163<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Amazon_Web_Services\" title=\"Amazon Web Services\">Amazon Web Services</a> uses Rust in \"performance-sensitive components\" of its several services. In 2019, AWS <a href=\"/wiki/Open_sourced\" class=\"mw-redirect\" title=\"Open sourced\">open-sourced</a> <a href=\"/wiki/Firecracker_(software)\" title=\"Firecracker (software)\">Firecracker</a>, a virtualization solution primarily written in Rust.<sup id=\"cite_ref-172\" class=\"reference\"><a href=\"#cite_note-172\"><span class=\"cite-bracket\">&#91;</span>164<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Microsoft_Azure\" title=\"Microsoft Azure\">Microsoft Azure</a> IoT Edge, a platform used to run Azure services on <a href=\"/wiki/Internet_of_things\" title=\"Internet of things\">IoT</a> devices, has components implemented in Rust.<sup id=\"cite_ref-173\" class=\"reference\"><a href=\"#cite_note-173\"><span class=\"cite-bracket\">&#91;</span>165<span class=\"cite-bracket\">&#93;</span></a></sup> Microsoft also uses Rust to run containerized modules with <a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a> and <a href=\"/wiki/Kubernetes\" title=\"Kubernetes\">Kubernetes</a>.<sup id=\"cite_ref-174\" class=\"reference\"><a href=\"#cite_note-174\"><span class=\"cite-bracket\">&#91;</span>166<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Cloudflare\" title=\"Cloudflare\">Cloudflare</a>, a company providing <a href=\"/wiki/Content_delivery_network\" title=\"Content delivery network\">content delivery network</a> services, used Rust to build a new <a href=\"/wiki/Web_proxy\" class=\"mw-redirect\" title=\"Web proxy\">web proxy</a> named Pingora for increased performance and efficiency.<sup id=\"cite_ref-175\" class=\"reference\"><a href=\"#cite_note-175\"><span class=\"cite-bracket\">&#91;</span>167<span class=\"cite-bracket\">&#93;</span></a></sup> The <a href=\"/wiki/Npm\" title=\"Npm\">npm package manager</a> used Rust for its production authentication service in 2019.<sup id=\"cite_ref-176\" class=\"reference\"><a href=\"#cite_note-176\"><span class=\"cite-bracket\">&#91;</span>168<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-177\" class=\"reference\"><a href=\"#cite_note-177\"><span class=\"cite-bracket\">&#91;</span>169<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-178\" class=\"reference\"><a href=\"#cite_note-178\"><span class=\"cite-bracket\">&#91;</span>170<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<figure typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Rust_for_Linux_logo.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Rust_for_Linux_logo.svg/150px-Rust_for_Linux_logo.svg.png\" decoding=\"async\" width=\"150\" height=\"150\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Rust_for_Linux_logo.svg/225px-Rust_for_Linux_logo.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Rust_for_Linux_logo.svg/300px-Rust_for_Linux_logo.svg.png 2x\" data-file-width=\"106\" data-file-height=\"106\" /></a><figcaption>The <a href=\"/wiki/Rust_for_Linux\" title=\"Rust for Linux\">Rust for Linux</a> project has been supported in the <a href=\"/wiki/Linux_kernel\" title=\"Linux kernel\">Linux kernel</a> since 2022.</figcaption></figure>\n<p>In operating systems, the <a href=\"/wiki/Rust_for_Linux\" title=\"Rust for Linux\">Rust for Linux</a> project, launched in 2020, merged initial support into the <a href=\"/wiki/Linux_kernel\" title=\"Linux kernel\">Linux kernel</a> version 6.1 in late 2022.<sup id=\"cite_ref-UsenixRustForLinux_179-0\" class=\"reference\"><a href=\"#cite_note-UsenixRustForLinux-179\"><span class=\"cite-bracket\">&#91;</span>171<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-180\" class=\"reference\"><a href=\"#cite_note-180\"><span class=\"cite-bracket\">&#91;</span>172<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-181\" class=\"reference\"><a href=\"#cite_note-181\"><span class=\"cite-bracket\">&#91;</span>173<span class=\"cite-bracket\">&#93;</span></a></sup> The project is active with a team of 6–7 developers, and has added more Rust code with kernel releases from 2022 to 2024,<sup id=\"cite_ref-182\" class=\"reference\"><a href=\"#cite_note-182\"><span class=\"cite-bracket\">&#91;</span>174<span class=\"cite-bracket\">&#93;</span></a></sup> aiming to demonstrate the <a href=\"/wiki/Minimum_viable_product\" title=\"Minimum viable product\">minimum viability</a> of the project and resolve key compatibility blockers.<sup id=\"cite_ref-UsenixRustForLinux_179-1\" class=\"reference\"><a href=\"#cite_note-UsenixRustForLinux-179\"><span class=\"cite-bracket\">&#91;</span>171<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-183\" class=\"reference\"><a href=\"#cite_note-183\"><span class=\"cite-bracket\">&#91;</span>175<span class=\"cite-bracket\">&#93;</span></a></sup> The first drivers written in Rust were merged into the kernel for version 6.8.<sup id=\"cite_ref-UsenixRustForLinux_179-2\" class=\"reference\"><a href=\"#cite_note-UsenixRustForLinux-179\"><span class=\"cite-bracket\">&#91;</span>171<span class=\"cite-bracket\">&#93;</span></a></sup> The <a href=\"/wiki/Android_(operating_system)\" title=\"Android (operating system)\">Android</a> developers used Rust in 2021 to rewrite existing components.<sup id=\"cite_ref-184\" class=\"reference\"><a href=\"#cite_note-184\"><span class=\"cite-bracket\">&#91;</span>176<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-185\" class=\"reference\"><a href=\"#cite_note-185\"><span class=\"cite-bracket\">&#91;</span>177<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Microsoft\" title=\"Microsoft\">Microsoft</a> has rewritten parts of <a href=\"/wiki/Windows\" class=\"mw-redirect\" title=\"Windows\">Windows</a> in Rust.<sup id=\"cite_ref-186\" class=\"reference\"><a href=\"#cite_note-186\"><span class=\"cite-bracket\">&#91;</span>178<span class=\"cite-bracket\">&#93;</span></a></sup> The r9 project aims to re-implement <a href=\"/wiki/Plan_9_from_Bell_Labs\" title=\"Plan 9 from Bell Labs\">Plan 9 from Bell Labs</a> in Rust.<sup id=\"cite_ref-187\" class=\"reference\"><a href=\"#cite_note-187\"><span class=\"cite-bracket\">&#91;</span>179<span class=\"cite-bracket\">&#93;</span></a></sup> Rust has been used in the development of new operating systems such as <a href=\"/wiki/Redox_(operating_system)\" class=\"mw-redirect\" title=\"Redox (operating system)\">Redox</a>, a \"Unix-like\" operating system and <a href=\"/wiki/Microkernel\" title=\"Microkernel\">microkernel</a>,<sup id=\"cite_ref-188\" class=\"reference\"><a href=\"#cite_note-188\"><span class=\"cite-bracket\">&#91;</span>180<span class=\"cite-bracket\">&#93;</span></a></sup> Theseus, an experimental operating system with modular state management,<sup id=\"cite_ref-189\" class=\"reference\"><a href=\"#cite_note-189\"><span class=\"cite-bracket\">&#91;</span>181<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-190\" class=\"reference\"><a href=\"#cite_note-190\"><span class=\"cite-bracket\">&#91;</span>182<span class=\"cite-bracket\">&#93;</span></a></sup> and most of <a href=\"/wiki/Fuchsia_(operating_system)\" title=\"Fuchsia (operating system)\">Fuchsia</a>.<sup id=\"cite_ref-rustmag-1_191-0\" class=\"reference\"><a href=\"#cite_note-rustmag-1-191\"><span class=\"cite-bracket\">&#91;</span>183<span class=\"cite-bracket\">&#93;</span></a></sup> Rust is also used for command-line tools and operating system components, including <a href=\"/wiki/Stratis_(configuration_daemon)\" title=\"Stratis (configuration daemon)\">stratisd</a>, a <a href=\"/wiki/File_system\" title=\"File system\">file system</a> manager<sup id=\"cite_ref-192\" class=\"reference\"><a href=\"#cite_note-192\"><span class=\"cite-bracket\">&#91;</span>184<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-193\" class=\"reference\"><a href=\"#cite_note-193\"><span class=\"cite-bracket\">&#91;</span>185<span class=\"cite-bracket\">&#93;</span></a></sup> and COSMIC, a <a href=\"/wiki/Desktop_environment\" title=\"Desktop environment\">desktop environment</a> by <a href=\"/wiki/System76\" title=\"System76\">System76</a>.<sup id=\"cite_ref-194\" class=\"reference\"><a href=\"#cite_note-194\"><span class=\"cite-bracket\">&#91;</span>186<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>In web development, <a href=\"/wiki/Deno_(software)\" title=\"Deno (software)\">Deno</a>, a secure runtime for <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a> and <a href=\"/wiki/TypeScript\" title=\"TypeScript\">TypeScript</a>, is built on top of <a href=\"/wiki/V8_(JavaScript_engine)\" title=\"V8 (JavaScript engine)\">V8</a> using Rust and Tokio.<sup id=\"cite_ref-195\" class=\"reference\"><a href=\"#cite_note-195\"><span class=\"cite-bracket\">&#91;</span>187<span class=\"cite-bracket\">&#93;</span></a></sup> Other notable adoptions in this space include <a href=\"/wiki/Ruffle_(software)\" title=\"Ruffle (software)\">Ruffle</a>, an open-source <a href=\"/wiki/SWF\" title=\"SWF\">SWF</a> emulator,<sup id=\"cite_ref-196\" class=\"reference\"><a href=\"#cite_note-196\"><span class=\"cite-bracket\">&#91;</span>188<span class=\"cite-bracket\">&#93;</span></a></sup> and <a href=\"/wiki/Polkadot_(cryptocurrency)\" class=\"mw-redirect\" title=\"Polkadot (cryptocurrency)\">Polkadot</a>, an open source <a href=\"/wiki/Blockchain\" title=\"Blockchain\">blockchain</a> and <a href=\"/wiki/Cryptocurrency\" title=\"Cryptocurrency\">cryptocurrency</a> platform.<sup id=\"cite_ref-197\" class=\"reference\"><a href=\"#cite_note-197\"><span class=\"cite-bracket\">&#91;</span>189<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p><a href=\"/wiki/Discord\" title=\"Discord\">Discord</a>, an <a href=\"/wiki/Instant_messaging\" title=\"Instant messaging\">instant messaging</a> software company, rewrote parts of its system in Rust for increased performance in 2020. In the same year, Dropbox announced that its <a href=\"/wiki/File_synchronization\" title=\"File synchronization\">file synchronization</a> had been rewritten in Rust. <a href=\"/wiki/Facebook\" title=\"Facebook\">Facebook</a> (<a href=\"/wiki/Meta_Platforms\" title=\"Meta Platforms\">Meta</a>) used Rust to redesign its system that manages source code for internal projects.<sup id=\"cite_ref-MITTechReview_19-18\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>In the 2025 <a href=\"/wiki/Stack_Overflow\" title=\"Stack Overflow\">Stack Overflow</a> Developer Survey, 14.8% of respondents had recently done extensive development in Rust.<sup id=\"cite_ref-SO-2025-survey_198-0\" class=\"reference\"><a href=\"#cite_note-SO-2025-survey-198\"><span class=\"cite-bracket\">&#91;</span>190<span class=\"cite-bracket\">&#93;</span></a></sup> The survey named Rust the \"most admired programming language\" annually from 2016 to 2025 (inclusive), as measured by the number of existing developers interested in continuing to work in the language.<sup id=\"cite_ref-199\" class=\"reference\"><a href=\"#cite_note-199\"><span class=\"cite-bracket\">&#91;</span>191<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-200\" class=\"reference\"><a href=\"#cite_note-200\"><span class=\"cite-bracket\">&#91;</span>note 9<span class=\"cite-bracket\">&#93;</span></a></sup> In 2025, 29.2% of developers not currently working in Rust expressed an interest in doing so.<sup id=\"cite_ref-SO-2025-survey_198-2\" class=\"reference\"><a href=\"#cite_note-SO-2025-survey-198\"><span class=\"cite-bracket\">&#91;</span>190<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p><a href=\"/wiki/DARPA\" title=\"DARPA\">DARPA</a> has a project TRACTOR (Translating All C to Rust) automatically translating C to Rust using techniques such as static analysis, dynamic analysis, and large language models.<sup id=\"cite_ref-201\" class=\"reference\"><a href=\"#cite_note-201\"><span class=\"cite-bracket\">&#91;</span>192<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"In_academic_research\">In academic research</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=44\" title=\"Edit section: In academic research\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Rust's safety and performance have been investigated in <a href=\"/wiki/Programming_language_theory\" title=\"Programming language theory\">programming language theory</a> research.<sup id=\"cite_ref-202\" class=\"reference\"><a href=\"#cite_note-202\"><span class=\"cite-bracket\">&#91;</span>193<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-UnsafeRustUse_126-1\" class=\"reference\"><a href=\"#cite_note-UnsafeRustUse-126\"><span class=\"cite-bracket\">&#91;</span>119<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-203\" class=\"reference\"><a href=\"#cite_note-203\"><span class=\"cite-bracket\">&#91;</span>194<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Rust's applicability to writing research software has been examined in other fields. A journal article published to <i><a href=\"/wiki/Proceedings_of_the_International_Astronomical_Union\" class=\"mw-redirect\" title=\"Proceedings of the International Astronomical Union\">Proceedings of the International Astronomical Union</a></i> used Rust to simulate multi-planet systems.<sup id=\"cite_ref-ResearchSoftware1_204-0\" class=\"reference\"><a href=\"#cite_note-ResearchSoftware1-204\"><span class=\"cite-bracket\">&#91;</span>195<span class=\"cite-bracket\">&#93;</span></a></sup> An article published in <i><a href=\"/wiki/Nature_(journal)\" title=\"Nature (journal)\">Nature</a></i> shared stories of bioinformaticians using Rust.<sup id=\"cite_ref-Nature_148-1\" class=\"reference\"><a href=\"#cite_note-Nature-148\"><span class=\"cite-bracket\">&#91;</span>140<span class=\"cite-bracket\">&#93;</span></a></sup> Both articles found that Rust has advantages for its performance and safety, and cited the <a href=\"/wiki/Learning_curve\" title=\"Learning curve\">learning curve</a> as being a primary drawback to its adoption.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Community\">Community</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=45\" title=\"Edit section: Community\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Rustacean-orig-noshadow.svg\" class=\"mw-file-description\"><img alt=\"A bright orange crab icon\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Rustacean-orig-noshadow.svg/250px-Rustacean-orig-noshadow.svg.png\" decoding=\"async\" width=\"250\" height=\"167\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Rustacean-orig-noshadow.svg/500px-Rustacean-orig-noshadow.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"341\" /></a><figcaption>Some Rust users refer to themselves as Rustaceans (similar to the word <a href=\"/wiki/Crustacean\" title=\"Crustacean\">crustacean</a>) and have adopted an orange crab, Ferris, as their unofficial mascot.<sup id=\"cite_ref-FOOTNOTEKlabnikNichols20194_205-0\" class=\"reference\"><a href=\"#cite_note-FOOTNOTEKlabnikNichols20194-205\"><span class=\"cite-bracket\">&#91;</span>196<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-206\" class=\"reference\"><a href=\"#cite_note-206\"><span class=\"cite-bracket\">&#91;</span>197<span class=\"cite-bracket\">&#93;</span></a></sup></figcaption></figure>\n<p>According to the <i><a href=\"/wiki/MIT_Technology_Review\" title=\"MIT Technology Review\">MIT Technology Review</a></i>, the Rust community has been seen as \"unusually friendly\" to newcomers and particularly attracted people from the <a href=\"/wiki/Queer_community\" class=\"mw-redirect\" title=\"Queer community\">queer community</a>, partly due to its <a href=\"/wiki/Code_of_conduct\" title=\"Code of conduct\">code of conduct</a> which outlined a set of expectations for Rust community members to follow.<sup id=\"cite_ref-MITTechReview_19-19\" class=\"reference\"><a href=\"#cite_note-MITTechReview-19\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> Inclusiveness of the community has been cited as an important factor for some Rust developers.<sup id=\"cite_ref-Nature_148-2\" class=\"reference\"><a href=\"#cite_note-Nature-148\"><span class=\"cite-bracket\">&#91;</span>140<span class=\"cite-bracket\">&#93;</span></a></sup> Demographic data on the community has been collected and published by the Rust official blog.<sup id=\"cite_ref-StateOfRustSurvey2024_207-0\" class=\"reference\"><a href=\"#cite_note-StateOfRustSurvey2024-207\"><span class=\"cite-bracket\">&#91;</span>198<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>According to <a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a>'s <i>State of the Octoverse</i> project, the Rust community grew by 50.5% in 2022, making it one of the fastest growing communities,<sup id=\"cite_ref-208\" class=\"reference\"><a href=\"#cite_note-208\"><span class=\"cite-bracket\">&#91;</span>199<span class=\"cite-bracket\">&#93;</span></a></sup> though not one of the 10 largest communities as of 2024.<sup id=\"cite_ref-209\" class=\"reference\"><a href=\"#cite_note-209\"><span class=\"cite-bracket\">&#91;</span>200<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Rust_Foundation\">Rust Foundation</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=46\" title=\"Edit section: Rust Foundation\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1295905060\" /><table class=\"infobox vcard\"><caption class=\"infobox-title fn org\">Rust Foundation</caption><tbody><tr><td colspan=\"2\" class=\"infobox-image logo\"><span class=\"mw-default-size\" typeof=\"mw:File/Frameless\"><a href=\"/wiki/File:Rust_Foundation_logo.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Rust_Foundation_logo.png/250px-Rust_Foundation_logo.png\" decoding=\"async\" width=\"250\" height=\"80\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Rust_Foundation_logo.png/500px-Rust_Foundation_logo.png 1.5x\" data-file-width=\"687\" data-file-height=\"219\" /></a></span></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\">Formation</th><td class=\"infobox-data note\">February&#160;8, 2021<span class=\"noprint\">&#59;&#32;4 years ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">2021-02-08</span>)</span></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\">Founders</th><td class=\"infobox-data\"><style data-mw-deduplicate=\"TemplateStyles:r1126788409\">.mw-parser-output .plainlist ol,.mw-parser-output .plainlist ul{line-height:inherit;list-style:none;margin:0;padding:0}.mw-parser-output .plainlist ol li,.mw-parser-output .plainlist ul li{margin-bottom:0}</style><div class=\"plainlist\"><ul><li><a href=\"/wiki/Amazon_Web_Services\" title=\"Amazon Web Services\">Amazon Web Services</a></li><li><a href=\"/wiki/Google\" title=\"Google\">Google</a></li><li><a href=\"/wiki/Huawei\" title=\"Huawei\">Huawei</a></li><li><a href=\"/wiki/Microsoft\" title=\"Microsoft\">Microsoft</a></li><li><a href=\"/wiki/Mozilla_Foundation\" title=\"Mozilla Foundation\">Mozilla Foundation</a></li></ul></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\">Type</th><td class=\"infobox-data\"><a href=\"/wiki/Nonprofit_organization\" title=\"Nonprofit organization\">Nonprofit organization</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\">Location</th><td class=\"infobox-data label\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" /><div class=\"plainlist\"><ul><li><span class=\"country-name\"><a href=\"/wiki/United_States\" title=\"United States\">United States</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\"><div style=\"display: inline-block; line-height: 1.2em; padding: .1em 0;\"><a href=\"/wiki/Chairperson\" class=\"mw-redirect\" title=\"Chairperson\">Chairperson</a></div></th><td class=\"infobox-data\">Shane Miller</td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\"><div style=\"display: inline-block; line-height: 1.2em; padding: .1em 0;\"><a href=\"/wiki/Executive_Director\" class=\"mw-redirect\" title=\"Executive Director\">Executive Director</a></div></th><td class=\"infobox-data\">Rebecca Rumbul</td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"padding-right:0.6em;\">Website</th><td class=\"infobox-data\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"http://foundation.rust-lang.org\">foundation<wbr />.rust-lang<wbr />.org</a></span></td></tr></tbody></table>\n<p>The <b>Rust Foundation</b> is a <a href=\"/wiki/Nonprofit_organization\" title=\"Nonprofit organization\">non-profit</a> <a href=\"/wiki/Membership_organization\" title=\"Membership organization\">membership organization</a> incorporated in <a href=\"/wiki/United_States\" title=\"United States\">United States</a>, with the primary purposes of backing the technical project as a <a href=\"/wiki/Legal_entity\" class=\"mw-redirect\" title=\"Legal entity\">legal entity</a> and helping to manage the trademark and infrastructure assets.<sup id=\"cite_ref-210\" class=\"reference\"><a href=\"#cite_note-210\"><span class=\"cite-bracket\">&#91;</span>201<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-:4_53-1\" class=\"reference\"><a href=\"#cite_note-:4-53\"><span class=\"cite-bracket\">&#91;</span>46<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>It was established on February 8, 2021, with five founding corporate members (Amazon Web Services, Huawei, Google, Microsoft, and Mozilla).<sup id=\"cite_ref-211\" class=\"reference\"><a href=\"#cite_note-211\"><span class=\"cite-bracket\">&#91;</span>202<span class=\"cite-bracket\">&#93;</span></a></sup> The foundation's board is chaired by Shane Miller.<sup id=\"cite_ref-212\" class=\"reference\"><a href=\"#cite_note-212\"><span class=\"cite-bracket\">&#91;</span>203<span class=\"cite-bracket\">&#93;</span></a></sup> Starting in late 2021, its Executive Director and CEO is Rebecca Rumbul.<sup id=\"cite_ref-213\" class=\"reference\"><a href=\"#cite_note-213\"><span class=\"cite-bracket\">&#91;</span>204<span class=\"cite-bracket\">&#93;</span></a></sup> Prior to this, Ashley Williams was interim executive director.<sup id=\"cite_ref-:4_53-2\" class=\"reference\"><a href=\"#cite_note-:4-53\"><span class=\"cite-bracket\">&#91;</span>46<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Governance_teams\">Governance teams</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=47\" title=\"Edit section: Governance teams\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>The Rust project is composed of <i>teams</i> that are responsible for different subareas of the development. The compiler team develops, manages, and optimizes compiler internals; and the language team designs new language features and helps implement them. The Rust project website lists 6 top-level teams as of July 2024<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup>.<sup id=\"cite_ref-214\" class=\"reference\"><a href=\"#cite_note-214\"><span class=\"cite-bracket\">&#91;</span>205<span class=\"cite-bracket\">&#93;</span></a></sup> Representatives among teams form the Leadership council, which oversees the Rust project as a whole.<sup id=\"cite_ref-215\" class=\"reference\"><a href=\"#cite_note-215\"><span class=\"cite-bracket\">&#91;</span>206<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"See_also\">See also</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=48\" title=\"Edit section: See also\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1308029216\">.mw-parser-output .side-box{margin:4px 0;box-sizing:border-box;border:1px solid #aaa;font-size:88%;line-height:1.25em;background-color:var(--background-color-interactive-subtle,#f8f9fa);display:flow-root}.mw-parser-output .infobox .side-box{font-size:100%}.mw-parser-output .side-box-abovebelow,.mw-parser-output .side-box-text{padding:0.25em 0.9em}.mw-parser-output .side-box-image{padding:2px 0 2px 0.9em;text-align:center}.mw-parser-output .side-box-imageright{padding:2px 0.9em 2px 0;text-align:center}@media(min-width:500px){.mw-parser-output .side-box-flex{display:flex;align-items:center}.mw-parser-output .side-box-text{flex:1;min-width:0}}@media(min-width:640px){.mw-parser-output .side-box{width:238px}.mw-parser-output .side-box-right{clear:right;float:right;margin-left:1em}.mw-parser-output .side-box-left{margin-right:1em}}</style><style data-mw-deduplicate=\"TemplateStyles:r1311551236\">@media print{body.ns-0 .mw-parser-output .sistersitebox{display:none!important}}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}</style><div class=\"side-box side-box-right plainlinks sistersitebox\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" />\n<div class=\"side-box-flex\">\n<div class=\"side-box-image\"><span class=\"noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo-en-noslogan.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/40px-Wikibooks-logo-en-noslogan.svg.png\" decoding=\"async\" width=\"40\" height=\"40\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/60px-Wikibooks-logo-en-noslogan.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/120px-Wikibooks-logo-en-noslogan.svg.png 2x\" data-file-width=\"400\" data-file-height=\"400\" /></a></span></div>\n<div class=\"side-box-text plainlist\">Wikibooks has a book on the topic of: <i><b><a href=\"https://en.wikibooks.org/wiki/Rust_for_the_Novice_Programmer\" class=\"extiw\" title=\"wikibooks:Rust for the Novice Programmer\">Rust for the Novice Programmer</a></b></i></div></div>\n</div>\n<ul><li><a href=\"/wiki/Comparison_of_programming_languages\" title=\"Comparison of programming languages\">Comparison of programming languages</a></li>\n<li><a href=\"/wiki/History_of_programming_languages\" title=\"History of programming languages\">History of programming languages</a></li>\n<li><a href=\"/wiki/List_of_programming_languages\" title=\"List of programming languages\">List of programming languages</a></li>\n<li><a href=\"/wiki/List_of_programming_languages_by_type\" title=\"List of programming languages by type\">List of programming languages by type</a></li>\n<li><a href=\"/wiki/List_of_Rust_software_and_tools\" title=\"List of Rust software and tools\">List of Rust software and tools</a></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Notes\">Notes</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=49\" title=\"Edit section: Notes\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1239543626\">.mw-parser-output .reflist{margin-bottom:0.5em;list-style-type:decimal}@media screen{.mw-parser-output .reflist{font-size:90%}}.mw-parser-output .reflist .references{font-size:100%;margin-bottom:0;list-style-type:inherit}.mw-parser-output .reflist-columns-2{column-width:30em}.mw-parser-output .reflist-columns-3{column-width:25em}.mw-parser-output .reflist-columns{margin-top:0.3em}.mw-parser-output .reflist-columns ol{margin-top:0}.mw-parser-output .reflist-columns li{page-break-inside:avoid;break-inside:avoid-column}.mw-parser-output .reflist-upper-alpha{list-style-type:upper-alpha}.mw-parser-output .reflist-upper-roman{list-style-type:upper-roman}.mw-parser-output .reflist-lower-alpha{list-style-type:lower-alpha}.mw-parser-output .reflist-lower-greek{list-style-type:lower-greek}.mw-parser-output .reflist-lower-roman{list-style-type:lower-roman}</style><div class=\"reflist\">\n<div class=\"mw-references-wrap\"><ol class=\"references\" data-mw-group=\"note\">\n<li id=\"cite_note-3\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-3\">^</a></b></span> <span class=\"reference-text\">Including build tools, host tools, and standard library support for <a href=\"/wiki/X86-64\" title=\"X86-64\">x86-64</a>, <a href=\"/wiki/ARM_architecture_family\" title=\"ARM architecture family\">ARM</a>, <a href=\"/wiki/MIPS_architecture\" title=\"MIPS architecture\">MIPS</a>, <a href=\"/wiki/RISC-V\" title=\"RISC-V\">RISC-V</a>, <a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a>, <a href=\"/wiki/P6_(microarchitecture)\" title=\"P6 (microarchitecture)\">i686</a>, <a href=\"/wiki/AArch64\" title=\"AArch64\">AArch64</a>, <a href=\"/wiki/PowerPC\" title=\"PowerPC\">PowerPC</a>, and <a href=\"/wiki/Linux_on_IBM_Z\" title=\"Linux on IBM Z\">s390x</a>.<sup id=\"cite_ref-CrossPlatform_2-0\" class=\"reference\"><a href=\"#cite_note-CrossPlatform-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-4\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-4\">^</a></b></span> <span class=\"reference-text\">Including <a href=\"/wiki/Windows\" class=\"mw-redirect\" title=\"Windows\">Windows</a>, <a href=\"/wiki/Linux\" title=\"Linux\">Linux</a>, <a href=\"/wiki/MacOS\" title=\"MacOS\">macOS</a>, <a href=\"/wiki/FreeBSD\" title=\"FreeBSD\">FreeBSD</a>, <a href=\"/wiki/NetBSD\" title=\"NetBSD\">NetBSD</a>, and <a href=\"/wiki/Illumos\" title=\"Illumos\">Illumos</a>. Host build tools on <a href=\"/wiki/Android_(operating_system)\" title=\"Android (operating system)\">Android</a>, <a href=\"/wiki/IOS\" title=\"IOS\">iOS</a>, <a href=\"/wiki/Haiku_(operating_system)\" title=\"Haiku (operating system)\">Haiku</a>, <a href=\"/wiki/Redox_(operating_system)\" class=\"mw-redirect\" title=\"Redox (operating system)\">Redox</a>, and <a href=\"/wiki/Fuchsia_(operating_system)\" title=\"Fuchsia (operating system)\">Fuchsia</a> are not officially shipped; these operating systems are supported as targets.<sup id=\"cite_ref-CrossPlatform_2-1\" class=\"reference\"><a href=\"#cite_note-CrossPlatform-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-7\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-7\">^</a></b></span> <span class=\"reference-text\">Third-party dependencies, e.g., <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a> or <a href=\"/wiki/MSVC\" class=\"mw-redirect\" title=\"MSVC\">MSVC</a>, are subject to their own licenses.<sup id=\"cite_ref-5\" class=\"reference\"><a href=\"#cite_note-5\"><span class=\"cite-bracket\">&#91;</span>3<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-licenses_6-0\" class=\"reference\"><a href=\"#cite_note-licenses-6\"><span class=\"cite-bracket\">&#91;</span>4<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-10\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-10\">^</a></b></span> <span class=\"reference-text\">NIL is cited as an influence for Rust in multiple sources; this likely refers to Network Implementation Language developed by Robert Strom and others at <a href=\"/wiki/IBM\" title=\"IBM\">IBM</a>, which pioneered <a href=\"/wiki/Typestate_analysis\" title=\"Typestate analysis\">typestate analysis</a>,<sup id=\"cite_ref-Strom1983_8-0\" class=\"reference\"><a href=\"#cite_note-Strom1983-8\"><span class=\"cite-bracket\">&#91;</span>5<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-9\" class=\"reference\"><a href=\"#cite_note-9\"><span class=\"cite-bracket\">&#91;</span>6<span class=\"cite-bracket\">&#93;</span></a></sup> not to be confused with <a href=\"/wiki/NIL_(programming_language)\" title=\"NIL (programming language)\">New Implementation of LISP</a>.</span>\n</li>\n<li id=\"cite_note-23\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-23\">^</a></b></span> <span class=\"reference-text\">See previous footnote on the referent of NIL.</span>\n</li>\n<li id=\"cite_note-27\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-27\">^</a></b></span> <span class=\"reference-text\">The list of Rust compiler versions (referred to as a bootstrapping chain) has history going back to 2012.<sup id=\"cite_ref-Nelson2022RustConf_26-0\" class=\"reference\"><a href=\"#cite_note-Nelson2022RustConf-26\"><span class=\"cite-bracket\">&#91;</span>21<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-36\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-36\">^</a></b></span> <span class=\"reference-text\">Energy compared to C was 3% more for Rust and 34% more for C++; time was 4% more and 56% more, respectively.</span>\n</li>\n<li id=\"cite_note-140\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-140\">^</a></b></span> <span class=\"reference-text\">wrapping <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">no_mangle</code> with <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">unsafe</span></code> as well as prefacing the <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">extern</span><span class=\"w\"> </span><span class=\"s\">&quot;C&quot;</span></code> block with <code class=\"mw-highlight mw-highlight-lang-rust mw-content-ltr\" dir=\"ltr\"><span class=\"k\">unsafe</span></code> are required in the 2024 edition or later.<sup id=\"cite_ref-139\" class=\"reference\"><a href=\"#cite_note-139\"><span class=\"cite-bracket\">&#91;</span>132<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-200\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-200\">^</a></b></span> <span class=\"reference-text\">That is, among respondents who have done \"extensive development work [with Rust] in over the past year\" (14.8%), Rust had the largest percentage who also expressed interest to \"work in [Rust] over the next year\" (72.4%).<sup id=\"cite_ref-SO-2025-survey_198-1\" class=\"reference\"><a href=\"#cite_note-SO-2025-survey-198\"><span class=\"cite-bracket\">&#91;</span>190<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n</ol></div></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"References\">References</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=50\" title=\"Edit section: References\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Book_sources\">Book sources</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=51\" title=\"Edit section: Book sources\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1239549316\">.mw-parser-output .refbegin{margin-bottom:0.5em}.mw-parser-output .refbegin-hanging-indents>ul{margin-left:0}.mw-parser-output .refbegin-hanging-indents>ul>li{margin-left:0;padding-left:3.2em;text-indent:-3.2em}.mw-parser-output .refbegin-hanging-indents ul,.mw-parser-output .refbegin-hanging-indents ul li{list-style:none}@media(max-width:720px){.mw-parser-output .refbegin-hanging-indents>ul>li{padding-left:1.6em;text-indent:-1.6em}}.mw-parser-output .refbegin-columns{margin-top:0.3em}.mw-parser-output .refbegin-columns ul{margin-top:0}.mw-parser-output .refbegin-columns li{page-break-inside:avoid;break-inside:avoid-column}@media screen{.mw-parser-output .refbegin{font-size:90%}}</style><div class=\"refbegin\" style=\"\">\n<ul><li><style data-mw-deduplicate=\"TemplateStyles:r1238218222\">.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}.mw-parser-output .citation q{quotes:\"\\\"\"\"\\\"\"\"'\"\"'\"}.mw-parser-output .citation:target{background-color:rgba(0,127,255,0.133)}.mw-parser-output .id-lock-free.id-lock-free a{background:url(\"//upload.wikimedia.org/wikipedia/commons/6/65/Lock-green.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-limited.id-lock-limited a,.mw-parser-output .id-lock-registration.id-lock-registration a{background:url(\"//upload.wikimedia.org/wikipedia/commons/d/d6/Lock-gray-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-subscription.id-lock-subscription a{background:url(\"//upload.wikimedia.org/wikipedia/commons/a/aa/Lock-red-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .cs1-ws-icon a{background:url(\"//upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg\")right 0.1em center/12px no-repeat}body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-free a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-limited a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-registration a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-subscription a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .cs1-ws-icon a{background-size:contain;padding:0 1em 0 0}.mw-parser-output .cs1-code{color:inherit;background:inherit;border:none;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;color:var(--color-error,#d33)}.mw-parser-output .cs1-visible-error{color:var(--color-error,#d33)}.mw-parser-output .cs1-maint{display:none;color:#085;margin-left:0.3em}.mw-parser-output .cs1-kern-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right{padding-right:0.2em}.mw-parser-output .citation .mw-selflink{font-weight:inherit}@media screen{.mw-parser-output .cs1-format{font-size:95%}html.skin-theme-clientpref-night .mw-parser-output .cs1-maint{color:#18911f}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .cs1-maint{color:#18911f}}</style><cite id=\"CITEREFGjengset2021\" class=\"citation book cs1\">Gjengset, Jon (2021). <i>Rust for Rustaceans</i> (1st&#160;ed.). No Starch Press. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/9781718501850\" title=\"Special:BookSources/9781718501850\"><bdi>9781718501850</bdi></a>. <a href=\"/wiki/OCLC_(identifier)\" class=\"mw-redirect\" title=\"OCLC (identifier)\">OCLC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/oclc/1277511986\">1277511986</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Rust+for+Rustaceans&amp;rft.edition=1st&amp;rft.pub=No+Starch+Press&amp;rft.date=2021&amp;rft_id=info%3Aoclcnum%2F1277511986&amp;rft.isbn=9781718501850&amp;rft.aulast=Gjengset&amp;rft.aufirst=Jon&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKlabnikNichols2019\" class=\"citation book cs1\">Klabnik, Steve; Nichols, Carol (2019-08-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://books.google.com/books?id=0Vv6DwAAQBAJ\"><i>The Rust Programming Language (Covers Rust 2018)</i></a>. No Starch Press. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-7185-0044-0\" title=\"Special:BookSources/978-1-7185-0044-0\"><bdi>978-1-7185-0044-0</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=The+Rust+Programming+Language+%28Covers+Rust+2018%29&amp;rft.pub=No+Starch+Press&amp;rft.date=2019-08-12&amp;rft.isbn=978-1-7185-0044-0&amp;rft.aulast=Klabnik&amp;rft.aufirst=Steve&amp;rft.au=Nichols%2C+Carol&amp;rft_id=https%3A%2F%2Fbooks.google.com%2Fbooks%3Fid%3D0Vv6DwAAQBAJ&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBlandyOrendorffTindall2021\" class=\"citation book cs1\">Blandy, Jim; Orendorff, Jason; Tindall, Leonora F. S. (2021). <i>Programming Rust: Fast, Safe Systems Development</i> (2nd&#160;ed.). O'Reilly Media. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4920-5254-8\" title=\"Special:BookSources/978-1-4920-5254-8\"><bdi>978-1-4920-5254-8</bdi></a>. <a href=\"/wiki/OCLC_(identifier)\" class=\"mw-redirect\" title=\"OCLC (identifier)\">OCLC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/oclc/1289839504\">1289839504</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Programming+Rust%3A+Fast%2C+Safe+Systems+Development&amp;rft.edition=2nd&amp;rft.pub=O%27Reilly+Media&amp;rft.date=2021&amp;rft_id=info%3Aoclcnum%2F1289839504&amp;rft.isbn=978-1-4920-5254-8&amp;rft.aulast=Blandy&amp;rft.aufirst=Jim&amp;rft.au=Orendorff%2C+Jason&amp;rft.au=Tindall%2C+Leonora+F.+S.&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFMcNamara2021\" class=\"citation book cs1\">McNamara, Tim (2021). <i>Rust in Action</i>. Manning Publications. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-6172-9455-6\" title=\"Special:BookSources/978-1-6172-9455-6\"><bdi>978-1-6172-9455-6</bdi></a>. <a href=\"/wiki/OCLC_(identifier)\" class=\"mw-redirect\" title=\"OCLC (identifier)\">OCLC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/oclc/1153044639\">1153044639</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Rust+in+Action&amp;rft.pub=Manning+Publications&amp;rft.date=2021&amp;rft_id=info%3Aoclcnum%2F1153044639&amp;rft.isbn=978-1-6172-9455-6&amp;rft.aulast=McNamara&amp;rft.aufirst=Tim&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKlabnikNichols2023\" class=\"citation book cs1\">Klabnik, Steve; Nichols, Carol (2023). <i>The Rust programming language</i> (2nd&#160;ed.). No Starch Press. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-7185-0310-6\" title=\"Special:BookSources/978-1-7185-0310-6\"><bdi>978-1-7185-0310-6</bdi></a>. <a href=\"/wiki/OCLC_(identifier)\" class=\"mw-redirect\" title=\"OCLC (identifier)\">OCLC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/oclc/1363816350\">1363816350</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=The+Rust+programming+language&amp;rft.edition=2nd&amp;rft.pub=No+Starch+Press&amp;rft.date=2023&amp;rft_id=info%3Aoclcnum%2F1363816350&amp;rft.isbn=978-1-7185-0310-6&amp;rft.aulast=Klabnik&amp;rft.aufirst=Steve&amp;rft.au=Nichols%2C+Carol&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></li></ul>\n</div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Others\">Others</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=52\" title=\"Edit section: Others\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239543626\" /><div class=\"reflist\">\n<div class=\"mw-references-wrap mw-references-columns\"><ol class=\"references\">\n<li id=\"cite_note-wikidata-625dfa02256b12affe5cdb18bbe05fea7b2cb7a3-v20-1\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-wikidata-625dfa02256b12affe5cdb18bbe05fea7b2cb7a3-v20_1-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2025/09/18/Rust-1.90.0/\">\"Announcing Rust 1.90.0\"</a>. 2025-09-18<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-18</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Announcing+Rust+1.90.0&amp;rft.date=2025-09-18&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2025%2F09%2F18%2FRust-1.90.0%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-CrossPlatform-2\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-CrossPlatform_2-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-CrossPlatform_2-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/rustc/platform-support.html\">\"Platform Support\"</a>. <i>The rustc book</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220630164523/https://doc.rust-lang.org/rustc/platform-support.html\">Archived</a> from the original on 2022-06-30<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-27</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+rustc+book&amp;rft.atitle=Platform+Support&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Frustc%2Fplatform-support.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-5\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-5\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rust/blob/master/COPYRIGHT\">\"Copyright\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. The Rust Programming Language. 2022-10-19. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230722190056/http://github.com/rust-lang/rust/blob/master/COPYRIGHT\">Archived</a> from the original on 2023-07-22<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-10-19</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=Copyright&amp;rft.date=2022-10-19&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust%2Fblob%2Fmaster%2FCOPYRIGHT&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-licenses-6\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-licenses_6-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.rust-lang.org/policies/licenses\">\"Licenses\"</a>. <i>The Rust Programming Language</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20250223193908/https://www.rust-lang.org/policies/licenses\">Archived</a> from the original on 2025-02-23<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-03-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language&amp;rft.atitle=Licenses&amp;rft_id=https%3A%2F%2Fwww.rust-lang.org%2Fpolicies%2Flicenses&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Strom1983-8\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Strom1983_8-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFStrom1983\" class=\"citation book cs1\">Strom, Robert E. (1983). \"Mechanisms for compile-time enforcement of security\". <i>Proceedings of the 10th ACM SIGACT-SIGPLAN symposium on Principles of programming languages - POPL '83</i>. pp.&#160;<span class=\"nowrap\">276–</span>284. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F567067.567093\">10.1145/567067.567093</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/0897910907\" title=\"Special:BookSources/0897910907\"><bdi>0897910907</bdi></a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:6630704\">6630704</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Mechanisms+for+compile-time+enforcement+of+security&amp;rft.btitle=Proceedings+of+the+10th+ACM+SIGACT-SIGPLAN+symposium+on+Principles+of+programming+languages+-+POPL+%2783&amp;rft.pages=276-284&amp;rft.date=1983&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A6630704%23id-name%3DS2CID&amp;rft_id=info%3Adoi%2F10.1145%2F567067.567093&amp;rft.isbn=0897910907&amp;rft.aulast=Strom&amp;rft.aufirst=Robert+E.&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-9\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-9\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFStromYemini,_Shaula1986\" class=\"citation journal cs1\">Strom, Robert E.; Yemini, Shaula (1986). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.cs.cmu.edu/~aldrich/papers/classic/tse12-typestate.pdf\">\"Typestate: A programming language concept for enhancing software reliability\"</a> <span class=\"cs1-format\">(PDF)</span>. <i>IEEE Transactions on Software Engineering</i>. <b>12</b>. IEEE: <span class=\"nowrap\">157–</span>171. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1109%2Ftse.1986.6312929\">10.1109/tse.1986.6312929</a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:15575346\">15575346</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=IEEE+Transactions+on+Software+Engineering&amp;rft.atitle=Typestate%3A+A+programming+language+concept+for+enhancing+software+reliability&amp;rft.volume=12&amp;rft.pages=157-171&amp;rft.date=1986&amp;rft_id=info%3Adoi%2F10.1109%2Ftse.1986.6312929&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A15575346%23id-name%3DS2CID&amp;rft.aulast=Strom&amp;rft.aufirst=Robert+E.&amp;rft.au=Yemini%2C+Shaula&amp;rft_id=https%3A%2F%2Fwww.cs.cmu.edu%2F~aldrich%2Fpapers%2Fclassic%2Ftse12-typestate.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-11\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-11\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2016/08/10/Shape-of-errors-to-come.html\">\"Uniqueness Types\"</a>. <i>Rust Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160915133745/https://blog.rust-lang.org/2016/08/10/Shape-of-errors-to-come.html\">Archived</a> from the original on 2016-09-15<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2016-10-08</span></span>. <q>Those of you familiar with the Elm style may recognize that the updated <style data-mw-deduplicate=\"TemplateStyles:r886049734\">.mw-parser-output .monospaced{font-family:monospace,monospace}</style><span class=\"monospaced\">--explain</span> messages draw heavy inspiration from the Elm approach.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Blog&amp;rft.atitle=Uniqueness+Types&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2016%2F08%2F10%2FShape-of-errors-to-come.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-influences-12\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-influences_12-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/reference/influences.html\">\"Influences\"</a>. <i>The Rust Reference</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231126231034/https://doc.rust-lang.org/reference/influences.html\">Archived</a> from the original on 2023-11-26<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-12-31</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Reference&amp;rft.atitle=Influences&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Freference%2Finfluences.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-13\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-13\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://docs.idris-lang.org/en/latest/reference/uniqueness-types.html\">\"Uniqueness Types\"</a>. <i>Idris 1.3.3 documentation</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181121072557/http://docs.idris-lang.org/en/latest/reference/uniqueness-types.html\">Archived</a> from the original on 2018-11-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>. <q>They are inspired by ... ownership types and borrowed pointers in the Rust programming language.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Idris+1.3.3+documentation&amp;rft.atitle=Uniqueness+Types&amp;rft_id=http%3A%2F%2Fdocs.idris-lang.org%2Fen%2Flatest%2Freference%2Funiqueness-types.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Project_Verona-14\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Project_Verona_14-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTung\" class=\"citation web cs1\">Tung, Liam. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/microsoft-opens-up-rust-inspired-project-verona-programming-language-on-github/\">\"Microsoft opens up Rust-inspired Project Verona programming language on GitHub\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200117143852/https://www.zdnet.com/article/microsoft-opens-up-rust-inspired-project-verona-programming-language-on-github/\">Archived</a> from the original on 2020-01-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-01-17</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=Microsoft+opens+up+Rust-inspired+Project+Verona+programming+language+on+GitHub&amp;rft.aulast=Tung&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fmicrosoft-opens-up-rust-inspired-project-verona-programming-language-on-github%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Jaloyan-15\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Jaloyan_15-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJaloyan2017\" class=\"citation arxiv cs1\">Jaloyan, Georges-Axel (2017-10-19). \"Safe Pointers in SPARK 2014\". <a href=\"/wiki/ArXiv_(identifier)\" class=\"mw-redirect\" title=\"ArXiv (identifier)\">arXiv</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://arxiv.org/abs/1710.07047\">1710.07047</a></span> [<a rel=\"nofollow\" class=\"external text\" href=\"https://arxiv.org/archive/cs.PL\">cs.PL</a>].</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=preprint&amp;rft.jtitle=arXiv&amp;rft.atitle=Safe+Pointers+in+SPARK+2014&amp;rft.date=2017-10-19&amp;rft_id=info%3Aarxiv%2F1710.07047&amp;rft.aulast=Jaloyan&amp;rft.aufirst=Georges-Axel&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Lattner-16\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Lattner_16-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLattner\" class=\"citation web cs1\">Lattner, Chris. <a rel=\"nofollow\" class=\"external text\" href=\"http://nondot.org/sabre/\">\"Chris Lattner's Homepage\"</a>. <i>Nondot</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181225175312/http://nondot.org/sabre/\">Archived</a> from the original on 2018-12-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-05-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Nondot&amp;rft.atitle=Chris+Lattner%27s+Homepage&amp;rft.aulast=Lattner&amp;rft.aufirst=Chris&amp;rft_id=http%3A%2F%2Fnondot.org%2Fsabre%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-17\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-17\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/vlang/v/blob/master/doc/docs.md#introduction\">\"V documentation (Introduction)\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. The V Programming Language<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-04</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=V+documentation+%28Introduction%29&amp;rft_id=https%3A%2F%2Fgithub.com%2Fvlang%2Fv%2Fblob%2Fmaster%2Fdoc%2Fdocs.md%23introduction&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-18\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-18\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFYegulalp2016\" class=\"citation web cs1\">Yegulalp, Serdar (2016-08-29). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3113083/new-challenger-joins-rust-to-upend-c-language.html\">\"New challenger joins Rust to topple C language\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20211125104022/https://www.infoworld.com/article/3113083/new-challenger-joins-rust-to-upend-c-language.html\">Archived</a> from the original on 2021-11-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-10-19</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoWorld&amp;rft.atitle=New+challenger+joins+Rust+to+topple+C+language&amp;rft.date=2016-08-29&amp;rft.aulast=Yegulalp&amp;rft.aufirst=Serdar&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3113083%2Fnew-challenger-joins-rust-to-upend-c-language.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-MITTechReview-19\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-MITTechReview_19-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-2\"><sup><i><b>c</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-3\"><sup><i><b>d</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-4\"><sup><i><b>e</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-5\"><sup><i><b>f</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-6\"><sup><i><b>g</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-7\"><sup><i><b>h</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-8\"><sup><i><b>i</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-9\"><sup><i><b>j</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-10\"><sup><i><b>k</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-11\"><sup><i><b>l</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-12\"><sup><i><b>m</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-13\"><sup><i><b>n</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-14\"><sup><i><b>o</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-15\"><sup><i><b>p</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-16\"><sup><i><b>q</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-17\"><sup><i><b>r</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-18\"><sup><i><b>s</b></i></sup></a> <a href=\"#cite_ref-MITTechReview_19-19\"><sup><i><b>t</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFThompson2023\" class=\"citation web cs1\">Thompson, Clive (2023-02-14). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language/\">\"How Rust went from a side project to the world's most-loved programming language\"</a>. <i>MIT Technology Review</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240919102849/https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language/\">Archived</a> from the original on 2024-09-19<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-02-23</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=MIT+Technology+Review&amp;rft.atitle=How+Rust+went+from+a+side+project+to+the+world%27s+most-loved+programming+language&amp;rft.date=2023-02-14&amp;rft.aulast=Thompson&amp;rft.aufirst=Clive&amp;rft_id=https%3A%2F%2Fwww.technologyreview.com%2F2023%2F02%2F14%2F1067869%2Frust-worlds-fastest-growing-programming-language%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-20\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-20\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=680521\">\"Rust logo\"</a>. <i><a href=\"/wiki/Bugzilla\" title=\"Bugzilla\">Bugzilla</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240202045212/https://bugzilla.mozilla.org/show_bug.cgi?id=680521\">Archived</a> from the original on 2024-02-02<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-02-02</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Bugzilla&amp;rft.atitle=Rust+logo&amp;rft_id=https%3A%2F%2Fbugzilla.mozilla.org%2Fshow_bug.cgi%3Fid%3D680521&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Klabnik2016ACMHistory-21\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-Klabnik2016ACMHistory_21-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-2\"><sup><i><b>c</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-3\"><sup><i><b>d</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-4\"><sup><i><b>e</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-5\"><sup><i><b>f</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-6\"><sup><i><b>g</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-7\"><sup><i><b>h</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-8\"><sup><i><b>i</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-9\"><sup><i><b>j</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-10\"><sup><i><b>k</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-11\"><sup><i><b>l</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-12\"><sup><i><b>m</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-13\"><sup><i><b>n</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-14\"><sup><i><b>o</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-15\"><sup><i><b>p</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-16\"><sup><i><b>q</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-17\"><sup><i><b>r</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-18\"><sup><i><b>s</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-19\"><sup><i><b>t</b></i></sup></a> <a href=\"#cite_ref-Klabnik2016ACMHistory_21-20\"><sup><i><b>u</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKlabnik2016\" class=\"citation book cs1\">Klabnik, Steve (2016-06-02). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/2959689.2960081\">\"The History of Rust\"</a>. <i>Applicative 2016</i>. New York, NY, USA: Association for Computing Machinery. p.&#160;80. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F2959689.2960081\">10.1145/2959689.2960081</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-4464-7\" title=\"Special:BookSources/978-1-4503-4464-7\"><bdi>978-1-4503-4464-7</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=The+History+of+Rust&amp;rft.btitle=Applicative+2016&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.pages=80&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2016-06-02&amp;rft_id=info%3Adoi%2F10.1145%2F2959689.2960081&amp;rft.isbn=978-1-4503-4464-7&amp;rft.aulast=Klabnik&amp;rft.aufirst=Steve&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F2959689.2960081&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Hoare2010-22\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-Hoare2010_22-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-Hoare2010_22-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-Hoare2010_22-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHoare2010\" class=\"citation conference cs1\">Hoare, Graydon (July 2010). <a rel=\"nofollow\" class=\"external text\" href=\"https://archive.today/20211226213836/http://venge.net/graydon/talks/intro-talk-2.pdf\"><i>Project Servo: Technology from the past come to save the future from itself</i></a> <span class=\"cs1-format\">(PDF)</span>. Mozilla Annual Summit. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://venge.net/graydon/talks/intro-talk-2.pdf\">the original</a> <span class=\"cs1-format\">(PDF)</span> on 2021-12-26<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=conference&amp;rft.btitle=Project+Servo%3A+Technology+from+the+past+come+to+save+the+future+from+itself&amp;rft.pub=Mozilla+Annual+Summit&amp;rft.date=2010-07&amp;rft.aulast=Hoare&amp;rft.aufirst=Graydon&amp;rft_id=http%3A%2F%2Fvenge.net%2Fgraydon%2Ftalks%2Fintro-talk-2.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-OCamlCompiler-24\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-OCamlCompiler_24-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHoare2016\" class=\"citation web cs1\">Hoare, Graydon (November 2016). <a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/graydon/rust-prehistory/tree/master\">\"Rust Prehistory (Archive of the original Rust OCaml compiler source code)\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=Rust+Prehistory+%28Archive+of+the+original+Rust+OCaml+compiler+source+code%29&amp;rft.date=2016-11&amp;rft.aulast=Hoare&amp;rft.aufirst=Graydon&amp;rft_id=https%3A%2F%2Fgithub.com%2Fgraydon%2Frust-prehistory%2Ftree%2Fmaster&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Rust0.1-25\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Rust0.1_25-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rust/milestone/3?closed=1\">\"0.1 first supported public release Milestone · rust-lang/rust\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=0.1+first+supported+public+release+Milestone+%C2%B7+rust-lang%2Frust&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust%2Fmilestone%2F3%3Fclosed%3D1&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Nelson2022RustConf-26\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Nelson2022RustConf_26-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFNelson2022\" class=\"citation audio-visual cs1\">Nelson, Jynn (2022-08-05). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.youtube.com/watch?v=oUIjG-y4zaA\"><i>RustConf 2022 - Bootstrapping: The once and future compiler</i></a>. Portland, Oregon: Rust Team<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span> &#8211; via YouTube.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=RustConf+2022+-+Bootstrapping%3A+The+once+and+future+compiler&amp;rft.place=Portland%2C+Oregon&amp;rft.pub=Rust+Team&amp;rft.date=2022-08-05&amp;rft.aulast=Nelson&amp;rft.aufirst=Jynn&amp;rft_id=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DoUIjG-y4zaA&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Rust0.1a-28\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Rust0.1a_28-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAnderson2012\" class=\"citation mailinglist cs1\">Anderson, Brian (2012-01-24). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120124160628/https://mail.mozilla.org/pipermail/rust-dev/2012-January/001256.html\">\"&#91;rust-dev&#93; The Rust compiler 0.1 is unleashed\"</a>. <i>rust-dev</i> (Mailing list). Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://mail.mozilla.org/pipermail/rust-dev/2012-January/001256.html\">the original</a> on 2012-01-24<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=%5Brust-dev%5D+The+Rust+compiler+0.1+is+unleashed&amp;rft.date=2012-01-24&amp;rft.aulast=Anderson&amp;rft.aufirst=Brian&amp;rft_id=https%3A%2F%2Fmail.mozilla.org%2Fpipermail%2Frust-dev%2F2012-January%2F001256.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ExtremeTechRust0.1-29\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-ExtremeTechRust0.1_29-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAnthony2012\" class=\"citation web cs1\">Anthony, Sebastian (2012-01-24). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.extremetech.com/internet/115207-mozilla-releases-rust-0-1-the-language-that-will-eventually-usurp-firefoxs-c\">\"Mozilla releases Rust 0.1, the language that will eventually usurp Firefox's C++\"</a>. <i>ExtremeTech</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ExtremeTech&amp;rft.atitle=Mozilla+releases+Rust+0.1%2C+the+language+that+will+eventually+usurp+Firefox%27s+C%2B%2B&amp;rft.date=2012-01-24&amp;rft.aulast=Anthony&amp;rft.aufirst=Sebastian&amp;rft_id=https%3A%2F%2Fwww.extremetech.com%2Finternet%2F115207-mozilla-releases-rust-0-1-the-language-that-will-eventually-usurp-firefoxs-c&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-30\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-30\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rust/pull/5412\">\"Purity by pcwalton · Pull Request #5412 · rust-lang/rust\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=Purity+by+pcwalton+%C2%B7+Pull+Request+%235412+%C2%B7+rust-lang%2Frust&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust%2Fpull%2F5412&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-31\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-31\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBinstock2014\" class=\"citation news cs1\">Binstock, Andrew (2014-01-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160807075745/http://www.drdobbs.com/jvm/the-rise-and-fall-of-languages-in-2013/240165192\">\"The Rise And Fall of Languages in 2013\"</a>. <i><a href=\"/wiki/Dr._Dobb%27s_Journal\" title=\"Dr. Dobb&#39;s Journal\">Dr. Dobb's Journal</a></i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.drdobbs.com/jvm/the-rise-and-fall-of-languages-in-2013/240165192\">the original</a> on 2016-08-07<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-11-20</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Dr.+Dobb%27s+Journal&amp;rft.atitle=The+Rise+And+Fall+of+Languages+in+2013&amp;rft.date=2014-01-07&amp;rft.aulast=Binstock&amp;rft.aufirst=Andrew&amp;rft_id=https%3A%2F%2Fwww.drdobbs.com%2Fjvm%2Fthe-rise-and-fall-of-languages-in-2013%2F240165192&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-32\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-32\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLardinois2015\" class=\"citation news cs1\">Lardinois, Frederic (2015-04-03). <a rel=\"nofollow\" class=\"external text\" href=\"https://techcrunch.com/2013/04/03/mozilla-and-samsung-collaborate-on-servo-mozillas-next-gen-browser-engine-for-tomorrows-multicore-processors/\">\"Mozilla And Samsung Team Up To Develop Servo, Mozilla's Next-Gen Browser Engine For Multicore Processors\"</a>. <i><a href=\"/wiki/TechCrunch\" title=\"TechCrunch\">TechCrunch</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160910211537/https://techcrunch.com/2013/04/03/mozilla-and-samsung-collaborate-on-servo-mozillas-next-gen-browser-engine-for-tomorrows-multicore-processors/\">Archived</a> from the original on 2016-09-10<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2017-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=TechCrunch&amp;rft.atitle=Mozilla+And+Samsung+Team+Up+To+Develop+Servo%2C+Mozilla%27s+Next-Gen+Browser+Engine+For+Multicore+Processors&amp;rft.date=2015-04-03&amp;rft.aulast=Lardinois&amp;rft.aufirst=Frederic&amp;rft_id=https%3A%2F%2Ftechcrunch.com%2F2013%2F04%2F03%2Fmozilla-and-samsung-collaborate-on-servo-mozillas-next-gen-browser-engine-for-tomorrows-multicore-processors%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-33\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-33\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.mozilla.org/en-US/firefox/45.0/releasenotes/\">\"Firefox 45.0, See All New Features, Updates and Fixes\"</a>. <i>Mozilla</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160317215950/https://www.mozilla.org/en-US/firefox/45.0/releasenotes/\">Archived</a> from the original on 2016-03-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-31</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Mozilla&amp;rft.atitle=Firefox+45.0%2C+See+All+New+Features%2C+Updates+and+Fixes&amp;rft_id=https%3A%2F%2Fwww.mozilla.org%2Fen-US%2Ffirefox%2F45.0%2Freleasenotes%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-34\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-34\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLardinois2017\" class=\"citation web cs1\">Lardinois, Frederic (2017-09-29). <a rel=\"nofollow\" class=\"external text\" href=\"https://techcrunch.com/2017/09/29/its-time-to-give-firefox-another-chance/\">\"It's time to give Firefox another chance\"</a>. <i><a href=\"/wiki/TechCrunch\" title=\"TechCrunch\">TechCrunch</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230815025149/https://techcrunch.com/2017/09/29/its-time-to-give-firefox-another-chance/\">Archived</a> from the original on 2023-08-15<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-08-15</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=TechCrunch&amp;rft.atitle=It%27s+time+to+give+Firefox+another+chance&amp;rft.date=2017-09-29&amp;rft.aulast=Lardinois&amp;rft.aufirst=Frederic&amp;rft_id=https%3A%2F%2Ftechcrunch.com%2F2017%2F09%2F29%2Fits-time-to-give-firefox-another-chance%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-2017PortugalEnergyStudy-35\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-2017PortugalEnergyStudy_35-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPereiraCoutoRibeiroRua2017\" class=\"citation book cs1\">Pereira, Rui; Couto, Marco; Ribeiro, Francisco; Rua, Rui; Cunha, Jácome; Fernandes, João Paulo; Saraiva, João (2017-10-23). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/3136014.3136031\">\"Energy efficiency across programming languages: How do energy, time, and memory relate?\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"http://repositorio.inesctec.pt/handle/123456789/5492\"><i>Proceedings of the 10th ACM SIGPLAN International Conference on Software Language Engineering</i></a>. SLE 2017. New York, NY, USA: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">256–</span>267. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3136014.3136031\">10.1145/3136014.3136031</a>. <a href=\"/wiki/Hdl_(identifier)\" class=\"mw-redirect\" title=\"Hdl (identifier)\">hdl</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://hdl.handle.net/1822%2F65359\">1822/65359</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-5525-4\" title=\"Special:BookSources/978-1-4503-5525-4\"><bdi>978-1-4503-5525-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Energy+efficiency+across+programming+languages%3A+How+do+energy%2C+time%2C+and+memory+relate%3F&amp;rft.btitle=Proceedings+of+the+10th+ACM+SIGPLAN+International+Conference+on+Software+Language+Engineering&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.series=SLE+2017&amp;rft.pages=256-267&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2017-10-23&amp;rft_id=info%3Ahdl%2F1822%2F65359&amp;rft_id=info%3Adoi%2F10.1145%2F3136014.3136031&amp;rft.isbn=978-1-4503-5525-4&amp;rft.aulast=Pereira&amp;rft.aufirst=Rui&amp;rft.au=Couto%2C+Marco&amp;rft.au=Ribeiro%2C+Francisco&amp;rft.au=Rua%2C+Rui&amp;rft.au=Cunha%2C+J%C3%A1come&amp;rft.au=Fernandes%2C+Jo%C3%A3o+Paulo&amp;rft.au=Saraiva%2C+Jo%C3%A3o&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F3136014.3136031&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-37\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-37\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCimpanu2020\" class=\"citation web cs1\">Cimpanu, Catalin (2020-08-11). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/mozilla-lays-off-250-employees-while-it-refocuses-on-commercial-products/\">\"Mozilla lays off 250 employees while it refocuses on commercial products\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220318025804/https://www.zdnet.com/article/mozilla-lays-off-250-employees-while-it-refocuses-on-commercial-products/\">Archived</a> from the original on 2022-03-18<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-12-02</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=Mozilla+lays+off+250+employees+while+it+refocuses+on+commercial+products&amp;rft.date=2020-08-11&amp;rft.aulast=Cimpanu&amp;rft.aufirst=Catalin&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fmozilla-lays-off-250-employees-while-it-refocuses-on-commercial-products%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-38\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-38\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCooper2020\" class=\"citation web cs1\">Cooper, Daniel (2020-08-11). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.engadget.com/mozilla-firefox-250-employees-layoffs-151324924.html\">\"Mozilla lays off 250 employees due to the pandemic\"</a>. <i><a href=\"/wiki/Engadget\" title=\"Engadget\">Engadget</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201213020220/https://www.engadget.com/mozilla-firefox-250-employees-layoffs-151324924.html\">Archived</a> from the original on 2020-12-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-12-02</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Engadget&amp;rft.atitle=Mozilla+lays+off+250+employees+due+to+the+pandemic&amp;rft.date=2020-08-11&amp;rft.aulast=Cooper&amp;rft.aufirst=Daniel&amp;rft_id=https%3A%2F%2Fwww.engadget.com%2Fmozilla-firefox-250-employees-layoffs-151324924.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-39\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-39\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTung2020\" class=\"citation web cs1\">Tung, Liam (2020-08-21). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/programming-language-rust-mozilla-job-cuts-have-hit-us-badly-but-heres-how-well-survive/\">\"Programming language Rust: Mozilla job cuts have hit us badly but here's how we'll survive\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220421083509/https://www.zdnet.com/article/programming-language-rust-mozilla-job-cuts-have-hit-us-badly-but-heres-how-well-survive/\">Archived</a> from the original on 2022-04-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-04-21</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=Programming+language+Rust%3A+Mozilla+job+cuts+have+hit+us+badly+but+here%27s+how+we%27ll+survive&amp;rft.date=2020-08-21&amp;rft.aulast=Tung&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fprogramming-language-rust-mozilla-job-cuts-have-hit-us-badly-but-heres-how-well-survive%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-40\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-40\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2020/08/18/laying-the-foundation-for-rusts-future.html\">\"Laying the foundation for Rust's future\"</a>. <i>Rust Blog</i>. 2020-08-18. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201202022933/https://blog.rust-lang.org/2020/08/18/laying-the-foundation-for-rusts-future.html\">Archived</a> from the original on 2020-12-02<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-12-02</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Blog&amp;rft.atitle=Laying+the+foundation+for+Rust%27s+future&amp;rft.date=2020-08-18&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2020%2F08%2F18%2Flaying-the-foundation-for-rusts-future.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-41\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-41\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://foundation.rust-lang.org/news/2021-02-08-hello-world/\">\"Hello World!\"</a>. <i>Rust Foundation</i>. 2020-02-08. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220419124635/https://foundation.rust-lang.org/news/2021-02-08-hello-world/\">Archived</a> from the original on 2022-04-19<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-04</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Foundation&amp;rft.atitle=Hello+World%21&amp;rft.date=2020-02-08&amp;rft_id=https%3A%2F%2Ffoundation.rust-lang.org%2Fnews%2F2021-02-08-hello-world%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-42\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-42\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.mozilla.org/blog/2021/02/08/mozilla-welcomes-the-rust-foundation\">\"Mozilla Welcomes the Rust Foundation\"</a>. <i>Mozilla Blog</i>. 2021-02-09. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210208212031/https://blog.mozilla.org/blog/2021/02/08/mozilla-welcomes-the-rust-foundation/\">Archived</a> from the original on 2021-02-08<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-02-09</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Mozilla+Blog&amp;rft.atitle=Mozilla+Welcomes+the+Rust+Foundation&amp;rft.date=2021-02-09&amp;rft_id=https%3A%2F%2Fblog.mozilla.org%2Fblog%2F2021%2F02%2F08%2Fmozilla-welcomes-the-rust-foundation&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-43\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-43\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAmadeo2021\" class=\"citation web cs1\">Amadeo, Ron (2021-04-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://arstechnica.com/gadgets/2021/04/google-is-now-writing-low-level-android-code-in-rust/\">\"Google is now writing low-level Android code in Rust\"</a>. <i>Ars Technica</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210408001446/https://arstechnica.com/gadgets/2021/04/google-is-now-writing-low-level-android-code-in-rust/\">Archived</a> from the original on 2021-04-08<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-04-08</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Ars+Technica&amp;rft.atitle=Google+is+now+writing+low-level+Android+code+in+Rust&amp;rft.date=2021-04-07&amp;rft.aulast=Amadeo&amp;rft.aufirst=Ron&amp;rft_id=https%3A%2F%2Farstechnica.com%2Fgadgets%2F2021%2F04%2Fgoogle-is-now-writing-low-level-android-code-in-rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-moderation-44\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-moderation_44-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAnderson2021\" class=\"citation news cs1\">Anderson, Tim (2021-11-23). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2021/11/23/rust_moderation_team_quits/\">\"Entire Rust moderation team resigns\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714093245/https://www.theregister.com/2021/11/23/rust_moderation_team_quits/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-08-04</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Register&amp;rft.atitle=Entire+Rust+moderation+team+resigns&amp;rft.date=2021-11-23&amp;rft.aulast=Anderson&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2021%2F11%2F23%2Frust_moderation_team_quits%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-45\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-45\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLevickBos\" class=\"citation web cs1\">Levick, Ryan; Bos, Mara. <a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/inside-rust/2022/05/19/governance-update.html\">\"Governance Update\"</a>. <i>Inside Rust Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20221027030926/https://blog.rust-lang.org/inside-rust/2022/05/19/governance-update.html\">Archived</a> from the original on 2022-10-27<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-10-27</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Inside+Rust+Blog&amp;rft.atitle=Governance+Update&amp;rft.aulast=Levick&amp;rft.aufirst=Ryan&amp;rft.au=Bos%2C+Mara&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2Finside-rust%2F2022%2F05%2F19%2Fgovernance-update.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ApologizesTrademarkPolicy-46\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-ApologizesTrademarkPolicy_46-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-ApologizesTrademarkPolicy_46-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFClaburn2023\" class=\"citation news cs1\">Claburn, Thomas (2023-04-17). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2023/04/17/rust_foundation_apologizes_trademark_policy/\">\"Rust Foundation apologizes for trademark policy confusion\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230507053637/https://www.theregister.com/2023/04/17/rust_foundation_apologizes_trademark_policy/\">Archived</a> from the original on 2023-05-07<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-05-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Register&amp;rft.atitle=Rust+Foundation+apologizes+for+trademark+policy+confusion&amp;rft.date=2023-04-17&amp;rft.aulast=Claburn&amp;rft.aufirst=Thomas&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2023%2F04%2F17%2Frust_foundation_apologizes_trademark_policy%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-WhiteHouse1-47\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-WhiteHouse1_47-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGross2024\" class=\"citation web cs1\">Gross, Grant (2024-02-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/2336216/white-house-urges-developers-to-dump-c-and-c.html\">\"White House urges developers to dump C and C++\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-26</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoWorld&amp;rft.atitle=White+House+urges+developers+to+dump+C+and+C%2B%2B&amp;rft.date=2024-02-27&amp;rft.aulast=Gross&amp;rft.aufirst=Grant&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F2336216%2Fwhite-house-urges-developers-to-dump-c-and-c.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-WhiteHouse2-48\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-WhiteHouse2_48-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFWarminsky2024\" class=\"citation web cs1\">Warminsky, Joe (2024-02-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://therecord.media/memory-related-software-bugs-white-house-code-report-oncd\">\"After decades of memory-related software bugs, White House calls on industry to act\"</a>. <i>The Record</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-26</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Record&amp;rft.atitle=After+decades+of+memory-related+software+bugs%2C+White+House+calls+on+industry+to+act&amp;rft.date=2024-02-27&amp;rft.aulast=Warminsky&amp;rft.aufirst=Joe&amp;rft_id=https%3A%2F%2Ftherecord.media%2Fmemory-related-software-bugs-white-house-code-report-oncd&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-WhiteHouseFullReport-49\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-WhiteHouseFullReport_49-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20250118013136/https://www.whitehouse.gov/oncd/briefing-room/2024/02/26/press-release-technical-report/\">\"Press Release: Future Software Should Be Memory Safe\"</a>. <a href=\"/wiki/White_House\" title=\"White House\">The White House</a>. 2024-02-26. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.whitehouse.gov/oncd/briefing-room/2024/02/26/press-release-technical-report/\">the original</a> on 2025-01-18<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-26</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Press+Release%3A+Future+Software+Should+Be+Memory+Safe&amp;rft.pub=The+White+House&amp;rft.date=2024-02-26&amp;rft_id=https%3A%2F%2Fwww.whitehouse.gov%2Foncd%2Fbriefing-room%2F2024%2F02%2F26%2Fpress-release-technical-report%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-WhiteHouse3-50\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-WhiteHouse3_50-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJack2024\" class=\"citation web cs1\">Jack, Bobby (2024-02-29). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.makeuseof.com/memory-safe-programming-white-house-wants/\">\"The White House Wants Memory-Safe Programming, but What Is That?\"</a>. <i>MakeUseOf</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-26</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=MakeUseOf&amp;rft.atitle=The+White+House+Wants+Memory-Safe+Programming%2C+but+What+Is+That%3F&amp;rft.date=2024-02-29&amp;rft.aulast=Jack&amp;rft.aufirst=Bobby&amp;rft_id=https%3A%2F%2Fwww.makeuseof.com%2Fmemory-safe-programming-white-house-wants%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-WhiteHouse4-51\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-WhiteHouse4_51-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFDonovan2024\" class=\"citation web cs1\">Donovan, Ryan (2024-12-30). <a rel=\"nofollow\" class=\"external text\" href=\"https://stackoverflow.blog/2024/12/30/in-rust-we-trust-white-house-office-urges-memory-safety/\">\"In Rust we trust? White House Office urges memory safety\"</a>. <i><a href=\"/wiki/Stack_Overflow\" title=\"Stack Overflow\">The Stack Overflow Blog</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-01-26</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Stack+Overflow+Blog&amp;rft.atitle=In+Rust+we+trust%3F+White+House+Office+urges+memory+safety&amp;rft.date=2024-12-30&amp;rft.aulast=Donovan&amp;rft.aufirst=Ryan&amp;rft_id=https%3A%2F%2Fstackoverflow.blog%2F2024%2F12%2F30%2Fin-rust-we-trust-white-house-office-urges-memory-safety%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-52\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-52\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFProven2019\" class=\"citation news cs1\">Proven, Liam (2019-11-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2021/11/19/rust_foundation_ceo/\">\"Rebecca Rumbul named new CEO of The Rust Foundation\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714110957/https://www.theregister.com/2021/11/19/rust_foundation_ceo/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>. <q>Both are curly bracket languages, with C-like syntax that makes them unintimidating for C programmers.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Register&amp;rft.atitle=Rebecca+Rumbul+named+new+CEO+of+The+Rust+Foundation&amp;rft.date=2019-11-27&amp;rft.aulast=Proven&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2021%2F11%2F19%2Frust_foundation_ceo%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-:4-53\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-:4_53-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-:4_53-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-:4_53-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVigliarolo2021\" class=\"citation web cs1\">Vigliarolo, Brandon (2021-02-10). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230320172900/https://www.techrepublic.com/article/the-rust-programming-language-now-has-its-own-independent-foundation/\">\"The Rust programming language now has its own independent foundation\"</a>. <i><a href=\"/wiki/TechRepublic\" title=\"TechRepublic\">TechRepublic</a></i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.techrepublic.com/article/the-rust-programming-language-now-has-its-own-independent-foundation/\">the original</a> on 2023-03-20<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=TechRepublic&amp;rft.atitle=The+Rust+programming+language+now+has+its+own+independent+foundation&amp;rft.date=2021-02-10&amp;rft.aulast=Vigliarolo&amp;rft.aufirst=Brandon&amp;rft_id=https%3A%2F%2Fwww.techrepublic.com%2Farticle%2Fthe-rust-programming-language-now-has-its-own-independent-foundation%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019263-54\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019263_54-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;263.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols20195–6-55\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols20195–6_55-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;5–6.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202332-56\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202332_56-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;32.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202332–33-57\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202332–33_57-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;32–33.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202349–50-58\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202349–50_58-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;49–50.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202334–36-59\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202334–36_59-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;34–36.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols20236,_47,_53-60\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols20236,_47,_53_60-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;6, 47, 53.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202347–48-61\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202347–48_61-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;47–48.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202350–53-62\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEKlabnikNichols202350–53_62-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols202350–53_62-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;50–53.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202356-63\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202356_63-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;56.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202357–58-64\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202357–58_64-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;57–58.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202354–56-65\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202354–56_65-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;54–56.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019104–109-66\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019104–109_66-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;104–109.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201924-67\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201924_67-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;24.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201936–38-68\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201936–38_68-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;36–38.</span>\n</li>\n<li id=\"cite_note-69\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-69\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/stable/std/primitive.isize.html\">\"isize\"</a>. <i>doc.rust-lang.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=doc.rust-lang.org&amp;rft.atitle=isize&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Fstable%2Fstd%2Fprimitive.isize.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-70\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-70\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/stable/std/primitive.usize.html\">\"usize\"</a>. <i>doc.rust-lang.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=doc.rust-lang.org&amp;rft.atitle=usize&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Fstable%2Fstd%2Fprimitive.usize.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202336–38-71\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202336–38_71-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;36–38.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023502-72\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023502_72-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;502.</span>\n</li>\n<li id=\"cite_note-73\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-73\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/std/primitive.char.html\">\"Primitive Type char\"</a>. <i>The Rust Standard Library documentation</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Standard+Library+documentation&amp;rft.atitle=Primitive+Type+char&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Fstd%2Fprimitive.char.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-74\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-74\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.unicode.org/glossary/\">\"Glossary of Unicode Terms\"</a>. <i><a href=\"/wiki/Unicode_Consortium\" title=\"Unicode Consortium\">Unicode Consortium</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180924092749/http://www.unicode.org/glossary/\">Archived</a> from the original on 2018-09-24<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-30</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Unicode+Consortium&amp;rft.atitle=Glossary+of+Unicode+Terms&amp;rft_id=https%3A%2F%2Fwww.unicode.org%2Fglossary%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201938–40-75\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201938–40_75-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;38–40.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202340–42-76\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202340–42_76-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;40–42.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202342-77\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202342_77-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;42.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201959–61-78\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201959–61_78-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;59–61.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201963–68-79\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEKlabnikNichols201963–68_79-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols201963–68_79-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;63–68.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201974–75-80\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201974–75_80-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;74–75.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202371–72-81\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202371–72_81-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;71–72.</span>\n</li>\n<li id=\"cite_note-BeyondSafety-82\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-BeyondSafety_82-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-BeyondSafety_82-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBalasubramanianBaranowskiBurtsevPanda2017\" class=\"citation book cs1\">Balasubramanian, Abhiram; Baranowski, Marek S.; Burtsev, Anton; Panda, Aurojit; Rakamarić, Zvonimir; Ryzhyk, Leonid (2017-05-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145/3102980.3103006\">\"System Programming in Rust\"</a>. <i>Proceedings of the 16th Workshop on Hot Topics in Operating Systems</i>. HotOS '17. New York, NY, US: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">156–</span>161. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3102980.3103006\">10.1145/3102980.3103006</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-5068-6\" title=\"Special:BookSources/978-1-4503-5068-6\"><bdi>978-1-4503-5068-6</bdi></a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:24100599\">24100599</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220611034046/https://dl.acm.org/doi/10.1145/3102980.3103006\">Archived</a> from the original on 2022-06-11<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-01</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=System+Programming+in+Rust&amp;rft.btitle=Proceedings+of+the+16th+Workshop+on+Hot+Topics+in+Operating+Systems&amp;rft.place=New+York%2C+NY%2C+US&amp;rft.series=HotOS+%2717&amp;rft.pages=156-161&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2017-05-07&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A24100599%23id-name%3DS2CID&amp;rft_id=info%3Adoi%2F10.1145%2F3102980.3103006&amp;rft.isbn=978-1-4503-5068-6&amp;rft.aulast=Balasubramanian&amp;rft.aufirst=Abhiram&amp;rft.au=Baranowski%2C+Marek+S.&amp;rft.au=Burtsev%2C+Anton&amp;rft.au=Panda%2C+Aurojit&amp;rft.au=Rakamari%C4%87%2C+Zvonimir&amp;rft.au=Ryzhyk%2C+Leonid&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%2F3102980.3103006&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023327–30-83\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023327–30_83-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;327–30.</span>\n</li>\n<li id=\"cite_note-84\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-84\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/rust-by-example/scope/lifetime.html\">\"Lifetimes\"</a>. <i>Rust by Example</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20241116192422/https://doc.rust-lang.org/rust-by-example/scope/lifetime.html\">Archived</a> from the original on 2024-11-16<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+by+Example&amp;rft.atitle=Lifetimes&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Frust-by-example%2Fscope%2Flifetime.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-85\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-85\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html\">\"Explicit annotation\"</a>. <i>Rust by Example</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+by+Example&amp;rft.atitle=Explicit+annotation&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Frust-by-example%2Fscope%2Flifetime%2Fexplicit.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019194-86\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019194_86-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019194_86-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;194.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201975,_134-87\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201975,_134_87-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;75, 134.</span>\n</li>\n<li id=\"cite_note-88\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-88\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFShamrell-Harrington2022\" class=\"citation web cs1\">Shamrell-Harrington, Nell (2022-04-15). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/presentations/rust-borrow-checker/\">\"The Rust Borrow Checker – a Deep Dive\"</a>. <i>InfoQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220625140128/https://www.infoq.com/presentations/rust-borrow-checker/\">Archived</a> from the original on 2022-06-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=The+Rust+Borrow+Checker+%E2%80%93+a+Deep+Dive&amp;rft.date=2022-04-15&amp;rft.aulast=Shamrell-Harrington&amp;rft.aufirst=Nell&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Fpresentations%2Frust-borrow-checker%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019194–195-89\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019194–195_89-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;194–195.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023208–12-90\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023208–12_90-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;208–12.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023&#91;httpsdocrust-langorgbookch04-02-references-and-borrowinghtml_4.2._References_and_Borrowing&#93;-91\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023[httpsdocrust-langorgbookch04-02-references-and-borrowinghtml_4.2._References_and_Borrowing]_91-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, <a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html\">4.2. References and Borrowing</a>.</span>\n</li>\n<li id=\"cite_note-Pearce-92\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Pearce_92-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPearce2021\" class=\"citation journal cs1\">Pearce, David (2021-04-17). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/3443420\">\"A Lightweight Formalism for Reference Lifetimes and Borrowing in Rust\"</a>. <i>ACM Transactions on Programming Languages and Systems</i>. <b>43</b>: <span class=\"nowrap\">1–</span>73. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3443420\">10.1145/3443420</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240415053627/https://dl.acm.org/doi/10.1145/3443420\">Archived</a> from the original on 2024-04-15<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-12-11</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=ACM+Transactions+on+Programming+Languages+and+Systems&amp;rft.atitle=A+Lightweight+Formalism+for+Reference+Lifetimes+and+Borrowing+in+Rust&amp;rft.volume=43&amp;rft.pages=1-73&amp;rft.date=2021-04-17&amp;rft_id=info%3Adoi%2F10.1145%2F3443420&amp;rft.aulast=Pearce&amp;rft.aufirst=David&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F3443420&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201983-93\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201983_93-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;83.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201997-94\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201997_94-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;97.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201998–101-95\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201998–101_95-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;98–101.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019438–440-96\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019438–440_96-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;438–440.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201993-97\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201993_97-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;93.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset2021213–215-98\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEGjengset2021213–215_98-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, pp.&#160;213–215.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023108–110,_113–114,_116–117-99\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023108–110,_113–114,_116–117_99-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;108–110, 113–114, 116–117.</span>\n</li>\n<li id=\"cite_note-Rust_error_handling-100\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Rust_error_handling_100-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://bitfieldconsulting.com/posts/rust-errors-option-result\">\"Rust error handling is perfect actually\"</a>. <i>Bitfield Consulting</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20250807061432/https://bitfieldconsulting.com/posts/rust-errors-option-result\">Archived</a> from the original on 2025-08-07<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-15</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Bitfield+Consulting&amp;rft.atitle=Rust+error+handling+is+perfect+actually&amp;rft_id=https%3A%2F%2Fbitfieldconsulting.com%2Fposts%2Frust-errors-option-result&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset2021155-156-101\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEGjengset2021155-156_101-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, p.&#160;155-156.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023421–423-102\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023421–423_102-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;421–423.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019418–427-103\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019418–427_103-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;418–427.</span>\n</li>\n<li id=\"cite_note-104\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-104\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/rust-by-example/types/cast.html\">\"Casting\"</a>. <i>Rust by Example</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-04-01</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+by+Example&amp;rft.atitle=Casting&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Frust-by-example%2Ftypes%2Fcast.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023378-105\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023378_105-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;378.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023192–198-106\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023192–198_106-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;192–198.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols202398-107\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols202398_107-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;98.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019171–172,_205-108\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019171–172,_205_108-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;171–172, 205.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023191–192-109\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023191–192_109-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023191–192_109-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;191–192.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset202125-110\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEGjengset202125_110-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, p.&#160;25.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023&#91;httpsdocrust-langorgbookch18-02-trait-objectshtml_18.2._Using_Trait_Objects_That_Allow_for_Values_of_Different_Types&#93;-111\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023[httpsdocrust-langorgbookch18-02-trait-objectshtml_18.2._Using_Trait_Objects_That_Allow_for_Values_of_Different_Types]_111-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, <a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/book/ch18-02-trait-objects.html\">18.2. Using Trait Objects That Allow for Values of Different Types</a>.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019441–442-112\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019441–442_112-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;441–442.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019379–380-113\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019379–380_113-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;379–380.</span>\n</li>\n<li id=\"cite_note-cnet-114\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-cnet_114-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRosenblatt2013\" class=\"citation web cs1\">Rosenblatt, Seth (2013-04-03). <a rel=\"nofollow\" class=\"external text\" href=\"http://reviews.cnet.com/8301-3514_7-57577639/samsung-joins-mozillas-quest-for-rust/\">\"Samsung joins Mozilla's quest for Rust\"</a>. <a href=\"/wiki/CNET\" title=\"CNET\">CNET</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20130404142333/http://reviews.cnet.com/8301-3514_7-57577639/samsung-joins-mozillas-quest-for-rust/\">Archived</a> from the original on 2013-04-04<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2013-04-05</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Samsung+joins+Mozilla%27s+quest+for+Rust&amp;rft.pub=CNET&amp;rft.date=2013-04-03&amp;rft.aulast=Rosenblatt&amp;rft.aufirst=Seth&amp;rft_id=http%3A%2F%2Freviews.cnet.com%2F8301-3514_7-57577639%2Fsamsung-joins-mozillas-quest-for-rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-lwn-115\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-lwn_115-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBrown2013\" class=\"citation web cs1\">Brown, Neil (2013-04-17). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/547145/\">\"A taste of Rust\"</a>. <i><a href=\"/wiki/LWN.net\" title=\"LWN.net\">LWN.net</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20130426010754/http://lwn.net/Articles/547145/\">Archived</a> from the original on 2013-04-26<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2013-04-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LWN.net&amp;rft.atitle=A+taste+of+Rust&amp;rft.date=2013-04-17&amp;rft.aulast=Brown&amp;rft.aufirst=Neil&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F547145%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-The_Rustonomicon-116\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-The_Rustonomicon_116-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/nomicon/races.html\">\"Races\"</a>. <i>The Rustonomicon</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170710194643/https://doc.rust-lang.org/nomicon/races.html\">Archived</a> from the original on 2017-07-10<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2017-07-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rustonomicon&amp;rft.atitle=Races&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Fnomicon%2Fraces.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Sensors-117\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Sensors_117-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVanderveldenDe_SmetDeacSteenhaut2024\" class=\"citation journal cs1\">Vandervelden, Thibaut; De Smet, Ruben; Deac, Diana; Steenhaut, Kris; Braeken, An (2024-09-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC11398098\">\"Overview of Embedded Rust Operating Systems and Frameworks\"</a>. <i>Sensors</i>. <b>24</b> (17): 5818. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2024Senso..24.5818V\">2024Senso..24.5818V</a>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.3390%2Fs24175818\">10.3390/s24175818</a></span>. <a href=\"/wiki/PMC_(identifier)\" class=\"mw-redirect\" title=\"PMC (identifier)\">PMC</a>&#160;<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC11398098\">11398098</a></span>. <a href=\"/wiki/PMID_(identifier)\" class=\"mw-redirect\" title=\"PMID (identifier)\">PMID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://pubmed.ncbi.nlm.nih.gov/39275729\">39275729</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Sensors&amp;rft.atitle=Overview+of+Embedded+Rust+Operating+Systems+and+Frameworks&amp;rft.volume=24&amp;rft.issue=17&amp;rft.pages=5818&amp;rft.date=2024-09-07&amp;rft_id=https%3A%2F%2Fwww.ncbi.nlm.nih.gov%2Fpmc%2Farticles%2FPMC11398098%23id-name%3DPMC&amp;rft_id=info%3Apmid%2F39275729&amp;rft_id=info%3Adoi%2F10.3390%2Fs24175818&amp;rft_id=info%3Abibcode%2F2024Senso..24.5818V&amp;rft.aulast=Vandervelden&amp;rft.aufirst=Thibaut&amp;rft.au=De+Smet%2C+Ruben&amp;rft.au=Deac%2C+Diana&amp;rft.au=Steenhaut%2C+Kris&amp;rft.au=Braeken%2C+An&amp;rft_id=https%3A%2F%2Fwww.ncbi.nlm.nih.gov%2Fpmc%2Farticles%2FPMC11398098&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-lang-faq-118\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-lang-faq_118-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20150420104147/http://static.rust-lang.org/doc/master/complement-lang-faq.html\">\"The Rust Language FAQ\"</a>. The Rust Programming Language. 2015. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://static.rust-lang.org/doc/master/complement-lang-faq.html\">the original</a> on 2015-04-20<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2017-04-24</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Rust+Language+FAQ&amp;rft.pub=The+Rust+Programming+Language&amp;rft.date=2015&amp;rft_id=http%3A%2F%2Fstatic.rust-lang.org%2Fdoc%2Fmaster%2Fcomplement-lang-faq.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-119\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-119\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/rust-by-example/scope/raii.html\">\"RAII\"</a>. <i>Rust by Example</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190421131142/https://doc.rust-lang.org/rust-by-example/scope/raii.html\">Archived</a> from the original on 2019-04-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-11-22</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+by+Example&amp;rft.atitle=RAII&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Frust-by-example%2Fscope%2Fraii.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-120\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-120\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2015/05/11/traits.html\">\"Abstraction without overhead: traits in Rust\"</a>. <i>Rust Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210923101530/https://blog.rust-lang.org/2015/05/11/traits.html\">Archived</a> from the original on 2021-09-23<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-10-19</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Blog&amp;rft.atitle=Abstraction+without+overhead%3A+traits+in+Rust&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2015%2F05%2F11%2Ftraits.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-121\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-121\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/stable/rust-by-example/std/box.html\">\"Box, stack and heap\"</a>. <i>Rust by Example</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220531114141/https://doc.rust-lang.org/stable/rust-by-example/std/box.html\">Archived</a> from the original on 2022-05-31<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+by+Example&amp;rft.atitle=Box%2C+stack+and+heap&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Fstable%2Frust-by-example%2Fstd%2Fbox.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols201970–75-122\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols201970–75_122-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;70–75.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019323-123\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019323_123-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;323.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023420–429-124\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023420–429_124-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;420–429.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEMcNamara2021139,_376–379,_395-125\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEMcNamara2021139,_376–379,_395_125-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFMcNamara2021\">McNamara 2021</a>, p.&#160;139, 376–379, 395.</span>\n</li>\n<li id=\"cite_note-UnsafeRustUse-126\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-UnsafeRustUse_126-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-UnsafeRustUse_126-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAstrauskasMathejaPoliMüller2020\" class=\"citation journal cs1\">Astrauskas, Vytautas; Matheja, Christoph; Poli, Federico; Müller, Peter; Summers, Alexander J. (2020-11-13). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/3428204\">\"How do programmers use unsafe rust?\"</a>. <i>Proceedings of the ACM on Programming Languages</i>. <b>4</b>: <span class=\"nowrap\">1–</span>27. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3428204\">10.1145/3428204</a>. <a href=\"/wiki/Hdl_(identifier)\" class=\"mw-redirect\" title=\"Hdl (identifier)\">hdl</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://hdl.handle.net/20.500.11850%2F465785\">20.500.11850/465785</a></span>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/2475-1421\">2475-1421</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+ACM+on+Programming+Languages&amp;rft.atitle=How+do+programmers+use+unsafe+rust%3F&amp;rft.volume=4&amp;rft.pages=1-27&amp;rft.date=2020-11-13&amp;rft_id=info%3Ahdl%2F20.500.11850%2F465785&amp;rft.issn=2475-1421&amp;rft_id=info%3Adoi%2F10.1145%2F3428204&amp;rft.aulast=Astrauskas&amp;rft.aufirst=Vytautas&amp;rft.au=Matheja%2C+Christoph&amp;rft.au=Poli%2C+Federico&amp;rft.au=M%C3%BCller%2C+Peter&amp;rft.au=Summers%2C+Alexander+J.&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F3428204&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-127\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-127\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLattuadaHanceChoBrun2023\" class=\"citation journal cs1\">Lattuada, Andrea; Hance, Travis; Cho, Chanhee; Brun, Matthias; Subasinghe, Isitha; Zhou, Yi; Howell, Jon; Parno, Bryan; Hawblitzel, Chris (2023-04-06). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/3586037\">\"Verus: Verifying Rust Programs using Linear Ghost Types\"</a>. <i>Proceedings of the ACM on Programming Languages</i>. <b>7</b>: 85:286–85:315. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3586037\">10.1145/3586037</a>. <a href=\"/wiki/Hdl_(identifier)\" class=\"mw-redirect\" title=\"Hdl (identifier)\">hdl</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://hdl.handle.net/20.500.11850%2F610518\">20.500.11850/610518</a></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+ACM+on+Programming+Languages&amp;rft.atitle=Verus%3A+Verifying+Rust+Programs+using+Linear+Ghost+Types&amp;rft.volume=7&amp;rft.pages=85%3A286-85%3A315&amp;rft.date=2023-04-06&amp;rft_id=info%3Ahdl%2F20.500.11850%2F610518&amp;rft_id=info%3Adoi%2F10.1145%2F3586037&amp;rft.aulast=Lattuada&amp;rft.aufirst=Andrea&amp;rft.au=Hance%2C+Travis&amp;rft.au=Cho%2C+Chanhee&amp;rft.au=Brun%2C+Matthias&amp;rft.au=Subasinghe%2C+Isitha&amp;rft.au=Zhou%2C+Yi&amp;rft.au=Howell%2C+Jon&amp;rft.au=Parno%2C+Bryan&amp;rft.au=Hawblitzel%2C+Chris&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F3586037&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-128\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-128\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFMilanoTurcottiMyers2022\" class=\"citation book cs1\">Milano, Mae; Turcotti, Julia; Myers, Andrew C. (2022-06-09). \"A flexible type system for fearless concurrency\". <i>Proceedings of the 43rd ACM SIGPLAN International Conference on Programming Language Design and Implementation</i>. New York, NY, USA: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">458–</span>473. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3519939.3523443\">10.1145/3519939.3523443</a></span>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-9265-5\" title=\"Special:BookSources/978-1-4503-9265-5\"><bdi>978-1-4503-9265-5</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=A+flexible+type+system+for+fearless+concurrency&amp;rft.btitle=Proceedings+of+the+43rd+ACM+SIGPLAN+International+Conference+on+Programming+Language+Design+and+Implementation&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.pages=458-473&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2022-06-09&amp;rft_id=info%3Adoi%2F10.1145%2F3519939.3523443&amp;rft.isbn=978-1-4503-9265-5&amp;rft.aulast=Milano&amp;rft.aufirst=Mae&amp;rft.au=Turcotti%2C+Julia&amp;rft.au=Myers%2C+Andrew+C.&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-129\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-129\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://rust-unofficial.github.io/too-many-lists/\">\"Introduction - Learning Rust With Entirely Too Many Linked Lists\"</a>. <i>rust-unofficial.github.io</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-08-06</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=rust-unofficial.github.io&amp;rft.atitle=Introduction+-+Learning+Rust+With+Entirely+Too+Many+Linked+Lists&amp;rft_id=https%3A%2F%2Frust-unofficial.github.io%2Ftoo-many-lists%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-130\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-130\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFNobleMackayWrigstad2023\" class=\"citation book cs1\">Noble, James; Mackay, Julian; Wrigstad, Tobias (2023-10-16). <span class=\"id-lock-subscription\" title=\"Paid subscription required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145/3611096.3611097\">\"Rusty Links in Local Chains✱\"</a></span>. <i>Proceedings of the 24th ACM International Workshop on Formal Techniques for Java-like Programs</i>. New York, NY, USA: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">1–</span>3. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3611096.3611097\">10.1145/3611096.3611097</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/979-8-4007-0784-1\" title=\"Special:BookSources/979-8-4007-0784-1\"><bdi>979-8-4007-0784-1</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Rusty+Links+in+Local+Chains%E2%9C%B1&amp;rft.btitle=Proceedings+of+the+24th+ACM+International+Workshop+on+Formal+Techniques+for+Java-like+Programs&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.pages=1-3&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2023-10-16&amp;rft_id=info%3Adoi%2F10.1145%2F3611096.3611097&amp;rft.isbn=979-8-4007-0784-1&amp;rft.aulast=Noble&amp;rft.aufirst=James&amp;rft.au=Mackay%2C+Julian&amp;rft.au=Wrigstad%2C+Tobias&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%2F3611096.3611097&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-IsRustSafely-131\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-IsRustSafely_131-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-IsRustSafely_131-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEvansCampbellSoffa2020\" class=\"citation book cs1\">Evans, Ana Nora; Campbell, Bradford; Soffa, Mary Lou (2020-10-01). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145/3377811.3380413\">\"Is rust used safely by software developers?\"</a>. <i>Proceedings of the ACM/IEEE 42nd International Conference on Software Engineering</i>. New York, NY, USA: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">246–</span>257. <a href=\"/wiki/ArXiv_(identifier)\" class=\"mw-redirect\" title=\"ArXiv (identifier)\">arXiv</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://arxiv.org/abs/2007.00752\">2007.00752</a></span>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3377811.3380413\">10.1145/3377811.3380413</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-7121-6\" title=\"Special:BookSources/978-1-4503-7121-6\"><bdi>978-1-4503-7121-6</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Is+rust+used+safely+by+software+developers%3F&amp;rft.btitle=Proceedings+of+the+ACM%2FIEEE+42nd+International+Conference+on+Software+Engineering&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.pages=246-257&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2020-10-01&amp;rft_id=info%3Aarxiv%2F2007.00752&amp;rft_id=info%3Adoi%2F10.1145%2F3377811.3380413&amp;rft.isbn=978-1-4503-7121-6&amp;rft.aulast=Evans&amp;rft.aufirst=Ana+Nora&amp;rft.au=Campbell%2C+Bradford&amp;rft.au=Soffa%2C+Mary+Lou&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%2F3377811.3380413&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-132\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-132\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/reference/behavior-considered-undefined.html\">\"Behavior considered undefined\"</a>. <i>The Rust Reference</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-08-06</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Reference&amp;rft.atitle=Behavior+considered+undefined&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Freference%2Fbehavior-considered-undefined.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023449–455-133\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023449–455_133-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, pp.&#160;449–455.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset2021101–102-134\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEGjengset2021101–102_134-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, pp.&#160;101–102.</span>\n</li>\n<li id=\"cite_note-Rust_Ref._–_Macros_By_Example-135\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Rust_Ref._–_Macros_By_Example_135-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/reference/macros-by-example.html\">\"Macros By Example\"</a>. <i>The Rust Reference</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230421052332/https://doc.rust-lang.org/reference/macros-by-example.html\">Archived</a> from the original on 2023-04-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-04-21</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Reference&amp;rft.atitle=Macros+By+Example&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Freference%2Fmacros-by-example.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019446–448-136\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019446–448_136-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;446–448.</span>\n</li>\n<li id=\"cite_note-rust-procedural-macros-137\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-rust-procedural-macros_137-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/reference/procedural-macros.html\">\"Procedural Macros\"</a>. <i>The Rust Programming Language Reference</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201107233444/https://doc.rust-lang.org/reference/procedural-macros.html\">Archived</a> from the original on 2020-11-07<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-03-23</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language+Reference&amp;rft.atitle=Procedural+Macros&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Freference%2Fprocedural-macros.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019449–455-138\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019449–455_138-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;449–455.</span>\n</li>\n<li id=\"cite_note-139\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-139\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBaumgartner2025\" class=\"citation web cs1\">Baumgartner, Stefan (2025-05-23). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.heise.de/en/background/Programming-language-Rust-2024-is-the-most-comprehensive-edition-to-date-10393917.html\">\"Programming language: Rust 2024 is the most comprehensive edition to date\"</a>. <i><a href=\"/wiki/Heise_online\" class=\"mw-redirect\" title=\"Heise online\">heise online</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-06-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=heise+online&amp;rft.atitle=Programming+language%3A+Rust+2024+is+the+most+comprehensive+edition+to+date&amp;rft.date=2025-05-23&amp;rft.aulast=Baumgartner&amp;rft.aufirst=Stefan&amp;rft_id=https%3A%2F%2Fwww.heise.de%2Fen%2Fbackground%2FProgramming-language-Rust-2024-is-the-most-comprehensive-edition-to-date-10393917.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset2021193–209-141\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-FOOTNOTEGjengset2021193–209_141-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEGjengset2021193–209_141-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-FOOTNOTEGjengset2021193–209_141-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, pp.&#160;193–209.</span>\n</li>\n<li id=\"cite_note-142\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-142\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/news/2020/12/cpp-rust-interop-cxx/\">\"Safe Interoperability between Rust and C++ with CXX\"</a>. <i>InfoQ</i>. 2020-12-06. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210122142035/https://www.infoq.com/news/2020/12/cpp-rust-interop-cxx/\">Archived</a> from the original on 2021-01-22<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-01-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=Safe+Interoperability+between+Rust+and+C%2B%2B+with+CXX&amp;rft.date=2020-12-06&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Fnews%2F2020%2F12%2Fcpp-rust-interop-cxx%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEBlandyOrendorffTindall20216–8-143\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEBlandyOrendorffTindall20216–8_143-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFBlandyOrendorffTindall2021\">Blandy, Orendorff &amp; Tindall 2021</a>, pp.&#160;6–8.</span>\n</li>\n<li id=\"cite_note-144\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-144\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://rustc-dev-guide.rust-lang.org/overview.html\">\"Overview of the compiler\"</a>. <i>Rust Compiler Development Guide</i>. Rust project contributors. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230531035222/https://rustc-dev-guide.rust-lang.org/overview.html\">Archived</a> from the original on 2023-05-31<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-11-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Compiler+Development+Guide&amp;rft.atitle=Overview+of+the+compiler&amp;rft_id=https%3A%2F%2Frustc-dev-guide.rust-lang.org%2Foverview.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-145\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-145\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://rustc-dev-guide.rust-lang.org/backend/codegen.html\">\"Code Generation\"</a>. <i>Rust Compiler Development Guide</i>. Rust project contributors<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-03-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Compiler+Development+Guide&amp;rft.atitle=Code+Generation&amp;rft_id=https%3A%2F%2Frustc-dev-guide.rust-lang.org%2Fbackend%2Fcodegen.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-146\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-146\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rustc_codegen_gcc#Motivation\">\"rust-lang/rustc_codegen_gcc\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. The Rust Programming Language. 2024-03-02<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-03-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=rust-lang%2Frustc_codegen_gcc&amp;rft.date=2024-03-02&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frustc_codegen_gcc%23Motivation&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-147\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-147\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rustc_codegen_cranelift\">\"rust-lang/rustc_codegen_cranelift\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. The Rust Programming Language. 2024-03-02<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-03-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=rust-lang%2Frustc_codegen_cranelift&amp;rft.date=2024-03-02&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frustc_codegen_cranelift&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Nature-148\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-Nature_148-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-Nature_148-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-Nature_148-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPerkel2020\" class=\"citation journal cs1\">Perkel, Jeffrey M. (2020-12-01). <span class=\"id-lock-subscription\" title=\"Paid subscription required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.nature.com/articles/d41586-020-03382-2\">\"Why scientists are turning to Rust\"</a></span>. <i><a href=\"/wiki/Nature_(journal)\" title=\"Nature (journal)\">Nature</a></i>. <b>588</b> (7836): <span class=\"nowrap\">185–</span>186. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2020Natur.588..185P\">2020Natur.588..185P</a>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1038%2Fd41586-020-03382-2\">10.1038/d41586-020-03382-2</a>. <a href=\"/wiki/PMID_(identifier)\" class=\"mw-redirect\" title=\"PMID (identifier)\">PMID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://pubmed.ncbi.nlm.nih.gov/33262490\">33262490</a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:227251258\">227251258</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220506040523/https://www.nature.com/articles/d41586-020-03382-2\">Archived</a> from the original on 2022-05-06<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-05-15</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Nature&amp;rft.atitle=Why+scientists+are+turning+to+Rust&amp;rft.volume=588&amp;rft.issue=7836&amp;rft.pages=185-186&amp;rft.date=2020-12-01&amp;rft_id=info%3Adoi%2F10.1038%2Fd41586-020-03382-2&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A227251258%23id-name%3DS2CID&amp;rft_id=info%3Apmid%2F33262490&amp;rft_id=info%3Abibcode%2F2020Natur.588..185P&amp;rft.aulast=Perkel&amp;rft.aufirst=Jeffrey+M.&amp;rft_id=https%3A%2F%2Fwww.nature.com%2Farticles%2Fd41586-020-03382-2&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-149\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-149\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSimone2019\" class=\"citation web cs1\">Simone, Sergio De (2019-04-18). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/news/2019/04/rust-1.34-additional-registries\">\"Rust 1.34 Introduces Alternative Registries for Non-Public Crates\"</a>. <i>InfoQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714164454/https://www.infoq.com/news/2019/04/rust-1.34-additional-registries\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=Rust+1.34+Introduces+Alternative+Registries+for+Non-Public+Crates&amp;rft.date=2019-04-18&amp;rft.aulast=Simone&amp;rft.aufirst=Sergio+De&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Fnews%2F2019%2F04%2Frust-1.34-additional-registries&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2019511–512-150\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2019511–512_150-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, pp.&#160;511–512.</span>\n</li>\n<li id=\"cite_note-151\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-151\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rust-clippy\">\"Clippy\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. The Rust Programming Language. 2023-11-30. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210523042004/https://github.com/rust-lang/rust-clippy\">Archived</a> from the original on 2021-05-23<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-30</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=Clippy&amp;rft.date=2023-11-30&amp;rft_id=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust-clippy&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-152\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-152\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://rust-lang.github.io/rust-clippy/master/index.html\">\"Clippy Lints\"</a>. <i>The Rust Programming Language</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-30</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language&amp;rft.atitle=Clippy+Lints&amp;rft_id=https%3A%2F%2Frust-lang.github.io%2Frust-clippy%2Fmaster%2Findex.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Rust_Book_G-153\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Rust_Book_G_153-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, Appendix G – How Rust is Made and \"Nightly Rust\"</span>\n</li>\n<li id=\"cite_note-FOOTNOTEBlandyOrendorffTindall2021176–177-154\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEBlandyOrendorffTindall2021176–177_154-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFBlandyOrendorffTindall2021\">Blandy, Orendorff &amp; Tindall 2021</a>, pp.&#160;176–177.</span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols2023623-155\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols2023623_155-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2023\">Klabnik &amp; Nichols 2023</a>, p.&#160;623.</span>\n</li>\n<li id=\"cite_note-156\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-156\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAnderson2021\" class=\"citation web cs1\">Anderson, Tim (2021-11-30). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2021/11/30/aws_reinvent_rust/\">\"Can Rust save the planet? Why, and why not\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220711001629/https://www.theregister.com/2021/11/30/aws_reinvent_rust/\">Archived</a> from the original on 2022-07-11<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-11</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Can+Rust+save+the+planet%3F+Why%2C+and+why+not&amp;rft.date=2021-11-30&amp;rft.aulast=Anderson&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2021%2F11%2F30%2Faws_reinvent_rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-157\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-157\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFYegulalp2021\" class=\"citation web cs1\">Yegulalp, Serdar (2021-10-06). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3218074/what-is-rust-safe-fast-and-easy-software-development.html\">\"What is the Rust language? Safe, fast, and easy software development\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220624101013/https://www.infoworld.com/article/3218074/what-is-rust-safe-fast-and-easy-software-development.html\">Archived</a> from the original on 2022-06-24<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoWorld&amp;rft.atitle=What+is+the+Rust+language%3F+Safe%2C+fast%2C+and+easy+software+development&amp;rft.date=2021-10-06&amp;rft.aulast=Yegulalp&amp;rft.aufirst=Serdar&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3218074%2Fwhat-is-rust-safe-fast-and-easy-software-development.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEMcNamara202111-158\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEMcNamara202111_158-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFMcNamara2021\">McNamara 2021</a>, p.&#160;11.</span>\n</li>\n<li id=\"cite_note-SaferAtAnySpeed-159\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-SaferAtAnySpeed_159-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-SaferAtAnySpeed_159-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPopescuXuApostolakisAugust2021\" class=\"citation journal cs1\">Popescu, Natalie; Xu, Ziyang; Apostolakis, Sotiris; August, David I.; Levy, Amit (2021-10-15). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3485480\">\"Safer at any speed: automatic context-aware safety enhancement for Rust\"</a>. <i>Proceedings of the ACM on Programming Languages</i>. <b>5</b> (OOPSLA). Section 2. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3485480\">10.1145/3485480</a></span>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:238212612\">238212612</a>. p.&#160;5: <q>We observe a large variance in the overheads of checked indexing: 23.6% of benchmarks do report significant performance hits from checked indexing, but 64.5% report little-to-no impact and, surprisingly, 11.8% report improved performance ... Ultimately, while unchecked indexing can improve performance, most of the time it does not.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+ACM+on+Programming+Languages&amp;rft.atitle=Safer+at+any+speed%3A+automatic+context-aware+safety+enhancement+for+Rust&amp;rft.volume=5&amp;rft.issue=OOPSLA&amp;rft.pages=Section+2&amp;rft.date=2021-10-15&amp;rft_id=info%3Adoi%2F10.1145%2F3485480&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A238212612%23id-name%3DS2CID&amp;rft.aulast=Popescu&amp;rft.aufirst=Natalie&amp;rft.au=Xu%2C+Ziyang&amp;rft.au=Apostolakis%2C+Sotiris&amp;rft.au=August%2C+David+I.&amp;rft.au=Levy%2C+Amit&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%252F3485480&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEMcNamara202119,_27-160\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEMcNamara202119,_27_160-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFMcNamara2021\">McNamara 2021</a>, p.&#160;19, 27.</span>\n</li>\n<li id=\"cite_note-161\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-161\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCouprie2015\" class=\"citation book cs1\">Couprie, Geoffroy (2015). \"Nom, A Byte oriented, streaming, Zero copy, Parser Combinators Library in Rust\". <i>2015 IEEE Security and Privacy Workshops</i>. pp.&#160;<span class=\"nowrap\">142–</span>148. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1109%2FSPW.2015.31\">10.1109/SPW.2015.31</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4799-9933-0\" title=\"Special:BookSources/978-1-4799-9933-0\"><bdi>978-1-4799-9933-0</bdi></a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:16608844\">16608844</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Nom%2C+A+Byte+oriented%2C+streaming%2C+Zero+copy%2C+Parser+Combinators+Library+in+Rust&amp;rft.btitle=2015+IEEE+Security+and+Privacy+Workshops&amp;rft.pages=142-148&amp;rft.date=2015&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A16608844%23id-name%3DS2CID&amp;rft_id=info%3Adoi%2F10.1109%2FSPW.2015.31&amp;rft.isbn=978-1-4799-9933-0&amp;rft.aulast=Couprie&amp;rft.aufirst=Geoffroy&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEMcNamara202120-162\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEMcNamara202120_162-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFMcNamara2021\">McNamara 2021</a>, p.&#160;20.</span>\n</li>\n<li id=\"cite_note-163\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-163\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/reference/attributes/codegen.html\">\"Code generation\"</a>. <i>The Rust Reference</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20221009202615/https://doc.rust-lang.org/reference/attributes/codegen.html\">Archived</a> from the original on 2022-10-09<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-10-09</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Reference&amp;rft.atitle=Code+generation&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2Freference%2Fattributes%2Fcodegen.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-how-fast-is-rust-164\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-how-fast-is-rust_164-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/1.0.0/complement-lang-faq.html#how-fast-is-rust?\">\"How Fast Is Rust?\"</a>. <i>The Rust Programming Language FAQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201028102013/https://doc.rust-lang.org/1.0.0/complement-lang-faq.html#how-fast-is-rust?\">Archived</a> from the original on 2020-10-28<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-04-11</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language+FAQ&amp;rft.atitle=How+Fast+Is+Rust%3F&amp;rft_id=https%3A%2F%2Fdoc.rust-lang.org%2F1.0.0%2Fcomplement-lang-faq.html%23how-fast-is-rust%3F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-165\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-165\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFFarshinBarbetteRoozbehMaguire2021\" class=\"citation book cs1\">Farshin, Alireza; Barbette, Tom; Roozbeh, Amir; Maguire, Gerald Q. Jr; Kostić, Dejan (2021). \"PacketMill: Toward per-Core 100-GBPS networking\". <a rel=\"nofollow\" class=\"external text\" href=\"https://dlnext.acm.org/doi/abs/10.1145/3445814.3446724\"><i>Proceedings of the 26th ACM International Conference on Architectural Support for Programming Languages and Operating Systems</i></a>. pp.&#160;<span class=\"nowrap\">1–</span>17. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3445814.3446724\">10.1145/3445814.3446724</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/9781450383172\" title=\"Special:BookSources/9781450383172\"><bdi>9781450383172</bdi></a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:231949599\">231949599</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220712060927/https://dlnext.acm.org/doi/abs/10.1145/3445814.3446724\">Archived</a> from the original on 2022-07-12<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-12</span></span>. <q>... While some compilers (e.g., Rust) support structure reordering [82], C &amp; C++ compilers are forbidden to reorder data structures (e.g., struct or class) [74] ...</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=PacketMill%3A+Toward+per-Core+100-GBPS+networking&amp;rft.btitle=Proceedings+of+the+26th+ACM+International+Conference+on+Architectural+Support+for+Programming+Languages+and+Operating+Systems&amp;rft.pages=1-17&amp;rft.date=2021&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A231949599%23id-name%3DS2CID&amp;rft_id=info%3Adoi%2F10.1145%2F3445814.3446724&amp;rft.isbn=9781450383172&amp;rft.aulast=Farshin&amp;rft.aufirst=Alireza&amp;rft.au=Barbette%2C+Tom&amp;rft.au=Roozbeh%2C+Amir&amp;rft.au=Maguire%2C+Gerald+Q.+Jr&amp;rft.au=Kosti%C4%87%2C+Dejan&amp;rft_id=https%3A%2F%2Fdlnext.acm.org%2Fdoi%2Fabs%2F10.1145%2F3445814.3446724&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEGjengset202122-166\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEGjengset202122_166-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFGjengset2021\">Gjengset 2021</a>, p.&#160;22.</span>\n</li>\n<li id=\"cite_note-167\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-167\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKeizer2016\" class=\"citation web cs1\">Keizer, Gregg (2016-10-31). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.computerworld.com/article/3137050/mozilla-plans-to-rejuvenate-firefox-in-2017.html\">\"Mozilla plans to rejuvenate Firefox in 2017\"</a>. <i><a href=\"/wiki/Computerworld\" title=\"Computerworld\">Computerworld</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230513020437/https://www.computerworld.com/article/3137050/mozilla-plans-to-rejuvenate-firefox-in-2017.html\">Archived</a> from the original on 2023-05-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-05-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Computerworld&amp;rft.atitle=Mozilla+plans+to+rejuvenate+Firefox+in+2017&amp;rft.date=2016-10-31&amp;rft.aulast=Keizer&amp;rft.aufirst=Gregg&amp;rft_id=https%3A%2F%2Fwww.computerworld.com%2Farticle%2F3137050%2Fmozilla-plans-to-rejuvenate-firefox-in-2017.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-168\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-168\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFClaburn2023\" class=\"citation web cs1\">Claburn, Thomas (2023-01-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2023/01/12/google_chromium_rust/\">\"Google polishes Chromium code with a layer of Rust\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-17</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Google+polishes+Chromium+code+with+a+layer+of+Rust&amp;rft.date=2023-01-12&amp;rft.aulast=Claburn&amp;rft.aufirst=Thomas&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2023%2F01%2F12%2Fgoogle_chromium_rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-169\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-169\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJansens2023\" class=\"citation web cs1\">Jansens, Dana (2023-01-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://security.googleblog.com/2023/01/supporting-use-of-rust-in-chromium.html\">\"Supporting the Use of Rust in the Chromium Project\"</a>. <i>Google Online Security Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230113004438/https://security.googleblog.com/2023/01/supporting-use-of-rust-in-chromium.html\">Archived</a> from the original on 2023-01-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-12</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Google+Online+Security+Blog&amp;rft.atitle=Supporting+the+Use+of+Rust+in+the+Chromium+Project&amp;rft.date=2023-01-12&amp;rft.aulast=Jansens&amp;rft.aufirst=Dana&amp;rft_id=https%3A%2F%2Fsecurity.googleblog.com%2F2023%2F01%2Fsupporting-use-of-rust-in-chromium.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-170\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-170\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFShankland2016\" class=\"citation web cs1\">Shankland, Stephen (2016-07-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.cnet.com/tech/services-and-software/firefox-mozilla-gets-overhaul-in-a-bid-to-get-you-interested-again/\">\"Firefox will get overhaul in bid to get you interested again\"</a>. <a href=\"/wiki/CNET\" title=\"CNET\">CNET</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714172029/https://www.cnet.com/tech/services-and-software/firefox-mozilla-gets-overhaul-in-a-bid-to-get-you-interested-again/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Firefox+will+get+overhaul+in+bid+to+get+you+interested+again&amp;rft.pub=CNET&amp;rft.date=2016-07-12&amp;rft.aulast=Shankland&amp;rft.aufirst=Stephen&amp;rft_id=https%3A%2F%2Fwww.cnet.com%2Ftech%2Fservices-and-software%2Ffirefox-mozilla-gets-overhaul-in-a-bid-to-get-you-interested-again%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-171\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-171\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSecurity_Research_Team2013\" class=\"citation web cs1\">Security Research Team (2013-10-04). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230513161542/https://umbrella.cisco.com/blog/zeromq-helping-us-block-malicious-domains\">\"ZeroMQ: Helping us Block Malicious Domains in Real Time\"</a>. <i>Cisco Umbrella</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://umbrella.cisco.com/blog/zeromq-helping-us-block-malicious-domains\">the original</a> on 2023-05-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-05-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Cisco+Umbrella&amp;rft.atitle=ZeroMQ%3A+Helping+us+Block+Malicious+Domains+in+Real+Time&amp;rft.date=2013-10-04&amp;rft.au=Security+Research+Team&amp;rft_id=https%3A%2F%2Fumbrella.cisco.com%2Fblog%2Fzeromq-helping-us-block-malicious-domains&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-172\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-172\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCimpanu2019\" class=\"citation web cs1\">Cimpanu, Catalin (2019-10-15). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/aws-to-sponsor-rust-project/\">\"AWS to sponsor Rust project\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-17</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=AWS+to+sponsor+Rust+project&amp;rft.date=2019-10-15&amp;rft.aulast=Cimpanu&amp;rft.aufirst=Catalin&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Faws-to-sponsor-rust-project%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-173\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-173\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFNichols2018\" class=\"citation web cs1\">Nichols, Shaun (2018-06-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.co.uk/2018/06/27/microsofts_next_cloud_trick_kicking_things_out_of_the_cloud_to_azure_iot_edge/\">\"Microsoft's next trick? Kicking things out of the cloud to Azure IoT Edge\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190927092433/https://www.theregister.co.uk/2018/06/27/microsofts_next_cloud_trick_kicking_things_out_of_the_cloud_to_azure_iot_edge/\">Archived</a> from the original on 2019-09-27<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-09-27</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Microsoft%27s+next+trick%3F+Kicking+things+out+of+the+cloud+to+Azure+IoT+Edge&amp;rft.date=2018-06-27&amp;rft.aulast=Nichols&amp;rft.aufirst=Shaun&amp;rft_id=https%3A%2F%2Fwww.theregister.co.uk%2F2018%2F06%2F27%2Fmicrosofts_next_cloud_trick_kicking_things_out_of_the_cloud_to_azure_iot_edge%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-174\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-174\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTung2020\" class=\"citation web cs1\">Tung, Liam (2020-04-30). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/microsoft-why-we-used-programming-language-rust-over-go-for-webassembly-on-kubernetes-app/\">\"Microsoft: Why we used programming language Rust over Go for WebAssembly on Kubernetes app\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220421043549/https://www.zdnet.com/article/microsoft-why-we-used-programming-language-rust-over-go-for-webassembly-on-kubernetes-app/\">Archived</a> from the original on 2022-04-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-04-21</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=Microsoft%3A+Why+we+used+programming+language+Rust+over+Go+for+WebAssembly+on+Kubernetes+app&amp;rft.date=2020-04-30&amp;rft.aulast=Tung&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fmicrosoft-why-we-used-programming-language-rust-over-go-for-webassembly-on-kubernetes-app%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-175\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-175\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFClaburn2022\" class=\"citation web cs1\">Claburn, Thomas (2022-09-20). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2022/09/20/rust_microsoft_c/\">\"In Rust We Trust: Microsoft Azure CTO shuns C and C++\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=In+Rust+We+Trust%3A+Microsoft+Azure+CTO+shuns+C+and+C%2B%2B&amp;rft.date=2022-09-20&amp;rft.aulast=Claburn&amp;rft.aufirst=Thomas&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2022%2F09%2F20%2Frust_microsoft_c%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-176\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-176\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSimone2019\" class=\"citation web cs1\">Simone, Sergio De (2019-03-10). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/news/2019/03/rust-npm-performance/\">\"NPM Adopted Rust to Remove Performance Bottlenecks\"</a>. <i>InfoQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231119135434/https://www.infoq.com/news/2019/03/rust-npm-performance/\">Archived</a> from the original on 2023-11-19<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-20</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=NPM+Adopted+Rust+to+Remove+Performance+Bottlenecks&amp;rft.date=2019-03-10&amp;rft.aulast=Simone&amp;rft.aufirst=Sergio+De&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Fnews%2F2019%2F03%2Frust-npm-performance%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-177\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-177\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLyu2020\" class=\"citation book cs1\">Lyu, Shing (2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007/978-1-4842-5599-5_1\">\"Welcome to the World of Rust\"</a>. In Lyu, Shing (ed.). <i>Practical Rust Projects: Building Game, Physical Computing, and Machine Learning Applications</i>. Berkeley, CA: Apress. pp.&#160;<span class=\"nowrap\">1–</span>8. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007%2F978-1-4842-5599-5_1\">10.1007/978-1-4842-5599-5_1</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4842-5599-5\" title=\"Special:BookSources/978-1-4842-5599-5\"><bdi>978-1-4842-5599-5</bdi></a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Welcome+to+the+World+of+Rust&amp;rft.btitle=Practical+Rust+Projects%3A+Building+Game%2C+Physical+Computing%2C+and+Machine+Learning+Applications&amp;rft.place=Berkeley%2C+CA&amp;rft.pages=1-8&amp;rft.pub=Apress&amp;rft.date=2020&amp;rft_id=info%3Adoi%2F10.1007%2F978-1-4842-5599-5_1&amp;rft.isbn=978-1-4842-5599-5&amp;rft.aulast=Lyu&amp;rft.aufirst=Shing&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1007%2F978-1-4842-5599-5_1&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-178\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-178\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLyu2021\" class=\"citation book cs1\">Lyu, Shing (2021). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007/978-1-4842-6589-5_1\">\"Rust in the Web World\"</a>. In Lyu, Shing (ed.). <i>Practical Rust Web Projects: Building Cloud and Web-Based Applications</i>. Berkeley, CA: Apress. pp.&#160;<span class=\"nowrap\">1–</span>7. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007%2F978-1-4842-6589-5_1\">10.1007/978-1-4842-6589-5_1</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4842-6589-5\" title=\"Special:BookSources/978-1-4842-6589-5\"><bdi>978-1-4842-6589-5</bdi></a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Rust+in+the+Web+World&amp;rft.btitle=Practical+Rust+Web+Projects%3A+Building+Cloud+and+Web-Based+Applications&amp;rft.place=Berkeley%2C+CA&amp;rft.pages=1-7&amp;rft.pub=Apress&amp;rft.date=2021&amp;rft_id=info%3Adoi%2F10.1007%2F978-1-4842-6589-5_1&amp;rft.isbn=978-1-4842-6589-5&amp;rft.aulast=Lyu&amp;rft.aufirst=Shing&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1007%2F978-1-4842-6589-5_1&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-UsenixRustForLinux-179\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-UsenixRustForLinux_179-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-UsenixRustForLinux_179-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-UsenixRustForLinux_179-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLiGuoYangWang2024\" class=\"citation web cs1\">Li, Hongyu; Guo, Liwei; Yang, Yexuan; Wang, Shangguang; Xu, Mengwei (2024-06-30). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.usenix.org/publications/loginonline/empirical-study-rust-linux-success-dissatisfaction-and-compromise\">\"An Empirical Study of Rust-for-Linux: The Success, Dissatisfaction, and Compromise\"</a>. <i><a href=\"/wiki/USENIX\" title=\"USENIX\">USENIX</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-11-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=USENIX&amp;rft.atitle=An+Empirical+Study+of+Rust-for-Linux%3A+The+Success%2C+Dissatisfaction%2C+and+Compromise&amp;rft.date=2024-06-30&amp;rft.aulast=Li&amp;rft.aufirst=Hongyu&amp;rft.au=Guo%2C+Liwei&amp;rft.au=Yang%2C+Yexuan&amp;rft.au=Wang%2C+Shangguang&amp;rft.au=Xu%2C+Mengwei&amp;rft_id=https%3A%2F%2Fwww.usenix.org%2Fpublications%2Floginonline%2Fempirical-study-rust-linux-success-dissatisfaction-and-compromise&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-180\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-180\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCorbet2022\" class=\"citation web cs1\">Corbet, Jonathan (2022-10-13). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/910762/\">\"A first look at Rust in the 6.1 kernel\"</a>. <i><a href=\"/wiki/LWN.net\" title=\"LWN.net\">LWN.net</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231117141103/https://lwn.net/Articles/910762/\">Archived</a> from the original on 2023-11-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-11-11</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LWN.net&amp;rft.atitle=A+first+look+at+Rust+in+the+6.1+kernel&amp;rft.date=2022-10-13&amp;rft.aulast=Corbet&amp;rft.aufirst=Jonathan&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F910762%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-181\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-181\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVaughan-Nichols2021\" class=\"citation news cs1\">Vaughan-Nichols, Steven (2021-12-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/rust-takes-a-major-step-forward-as-linuxs-second-official-language/\">\"Rust takes a major step forward as Linux's second official language\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-11-27</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=ZDNET&amp;rft.atitle=Rust+takes+a+major+step+forward+as+Linux%27s+second+official+language&amp;rft.date=2021-12-07&amp;rft.aulast=Vaughan-Nichols&amp;rft.aufirst=Steven&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Frust-takes-a-major-step-forward-as-linuxs-second-official-language%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-182\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-182\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCorbet2022\" class=\"citation web cs1\">Corbet, Jonathan (2022-11-17). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/914458/\">\"Rust in the 6.2 kernel\"</a>. <i><a href=\"/wiki/LWN.net\" title=\"LWN.net\">LWN.net</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-11-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LWN.net&amp;rft.atitle=Rust+in+the+6.2+kernel&amp;rft.date=2022-11-17&amp;rft.aulast=Corbet&amp;rft.aufirst=Jonathan&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F914458%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-183\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-183\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCorbet2024\" class=\"citation web cs1\">Corbet, Jonathan (2024-09-24). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/991062/\">\"Committing to Rust in the kernel\"</a>. <i><a href=\"/wiki/LWN.net\" title=\"LWN.net\">LWN.net</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-11-28</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LWN.net&amp;rft.atitle=Committing+to+Rust+in+the+kernel&amp;rft.date=2024-09-24&amp;rft.aulast=Corbet&amp;rft.aufirst=Jonathan&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F991062%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-184\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-184\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAmadeo2021\" class=\"citation web cs1\">Amadeo, Ron (2021-04-07). <a rel=\"nofollow\" class=\"external text\" href=\"https://arstechnica.com/gadgets/2021/04/google-is-now-writing-low-level-android-code-in-rust/\">\"Google is now writing low-level Android code in Rust\"</a>. <i>Ars Technica</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210408001446/https://arstechnica.com/gadgets/2021/04/google-is-now-writing-low-level-android-code-in-rust/\">Archived</a> from the original on 2021-04-08<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-04-21</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Ars+Technica&amp;rft.atitle=Google+is+now+writing+low-level+Android+code+in+Rust&amp;rft.date=2021-04-07&amp;rft.aulast=Amadeo&amp;rft.aufirst=Ron&amp;rft_id=https%3A%2F%2Farstechnica.com%2Fgadgets%2F2021%2F04%2Fgoogle-is-now-writing-low-level-android-code-in-rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-185\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-185\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFDarkcrizt2021\" class=\"citation web cs1\">Darkcrizt (2021-04-02). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210825165930/https://blog.desdelinux.net/en/google-develops-a-new-bluetooth-stack-for-android-written-in-rust/\">\"Google Develops New Bluetooth Stack for Android, Written in Rust\"</a>. <i>Desde Linux</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://blog.desdelinux.net/en/google-develops-a-new-bluetooth-stack-for-android-written-in-rust/\">the original</a> on 2021-08-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-08-31</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Desde+Linux&amp;rft.atitle=Google+Develops+New+Bluetooth+Stack+for+Android%2C+Written+in+Rust&amp;rft.date=2021-04-02&amp;rft.au=Darkcrizt&amp;rft_id=https%3A%2F%2Fblog.desdelinux.net%2Fen%2Fgoogle-develops-a-new-bluetooth-stack-for-android-written-in-rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-186\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-186\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFClaburn2023\" class=\"citation web cs1\">Claburn, Thomas (2023-04-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2023/04/27/microsoft_windows_rust/\">\"Microsoft is rewriting core Windows libraries in Rust\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230513082735/https://www.theregister.com/2023/04/27/microsoft_windows_rust/\">Archived</a> from the original on 2023-05-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-05-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Microsoft+is+rewriting+core+Windows+libraries+in+Rust&amp;rft.date=2023-04-27&amp;rft.aulast=Claburn&amp;rft.aufirst=Thomas&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2023%2F04%2F27%2Fmicrosoft_windows_rust%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-187\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-187\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFProven2023\" class=\"citation web cs1\">Proven, Liam (2023-12-01). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2023/12/01/9front_humanbiologics/\">\"Small but mighty, 9Front's 'Humanbiologics' is here for the truly curious\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-03-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Small+but+mighty%2C+9Front%27s+%27Humanbiologics%27+is+here+for+the+truly+curious&amp;rft.date=2023-12-01&amp;rft.aulast=Proven&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2023%2F12%2F01%2F9front_humanbiologics%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-188\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-188\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFYegulalp2016\" class=\"citation news cs1\">Yegulalp, Serdar (2016-03-21). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160321192838/http://www.infoworld.com/article/3046100/open-source-tools/rusts-redox-os-could-show-linux-a-few-new-tricks.html\">\"Rust's Redox OS could show Linux a few new tricks\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.infoworld.com/article/3046100/open-source-tools/rusts-redox-os-could-show-linux-a-few-new-tricks.html\">the original</a> on 2016-03-21<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2016-03-21</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=InfoWorld&amp;rft.atitle=Rust%27s+Redox+OS+could+show+Linux+a+few+new+tricks&amp;rft.date=2016-03-21&amp;rft.aulast=Yegulalp&amp;rft.aufirst=Serdar&amp;rft_id=http%3A%2F%2Fwww.infoworld.com%2Farticle%2F3046100%2Fopen-source-tools%2Frusts-redox-os-could-show-linux-a-few-new-tricks.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-189\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-189\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAnderson2021\" class=\"citation web cs1\">Anderson, Tim (2021-01-14). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2021/01/14/rust_os_theseus/\">\"Another Rust-y OS: Theseus joins Redox in pursuit of safer, more resilient systems\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714112619/https://www.theregister.com/2021/01/14/rust_os_theseus/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Another+Rust-y+OS%3A+Theseus+joins+Redox+in+pursuit+of+safer%2C+more+resilient+systems&amp;rft.date=2021-01-14&amp;rft.aulast=Anderson&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2021%2F01%2F14%2Frust_os_theseus%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-190\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-190\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBoosLiyanageIjazZhong2020\" class=\"citation book cs1\">Boos, Kevin; Liyanage, Namitha; Ijaz, Ramla; Zhong, Lin (2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.usenix.org/conference/osdi20/presentation/boos\"><i>Theseus: an Experiment in Operating System Structure and State Management</i></a>. pp.&#160;<span class=\"nowrap\">1–</span>19. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-939133-19-9\" title=\"Special:BookSources/978-1-939133-19-9\"><bdi>978-1-939133-19-9</bdi></a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230513164135/https://www.usenix.org/conference/osdi20/presentation/boos\">Archived</a> from the original on 2023-05-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-05-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Theseus%3A+an+Experiment+in+Operating+System+Structure+and+State+Management&amp;rft.pages=1-19&amp;rft.date=2020&amp;rft.isbn=978-1-939133-19-9&amp;rft.aulast=Boos&amp;rft.aufirst=Kevin&amp;rft.au=Liyanage%2C+Namitha&amp;rft.au=Ijaz%2C+Ramla&amp;rft.au=Zhong%2C+Lin&amp;rft_id=https%3A%2F%2Fwww.usenix.org%2Fconference%2Fosdi20%2Fpresentation%2Fboos&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-rustmag-1-191\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-rustmag-1_191-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFZhang2023\" class=\"citation web cs1\">Zhang, HanDong (2023-01-31). <a rel=\"nofollow\" class=\"external text\" href=\"https://rustmagazine.org/issue-1/2022-review-the-adoption-of-rust-in-business/\">\"2022 Review | The adoption of Rust in Business\"</a>. <i>Rust Magazine</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2023-02-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Magazine&amp;rft.atitle=2022+Review+%7C+The+adoption+of+Rust+in+Business&amp;rft.date=2023-01-31&amp;rft.aulast=Zhang&amp;rft.aufirst=HanDong&amp;rft_id=https%3A%2F%2Frustmagazine.org%2Fissue-1%2F2022-review-the-adoption-of-rust-in-business%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-192\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-192\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSei2018\" class=\"citation web cs1\">Sei, Mark (2018-10-10). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.marksei.com/fedora-29-new-features-startis/\">\"Fedora 29 new features: Startis now officially in Fedora\"</a>. <i>Marksei, Weekly sysadmin pills</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190413075055/https://www.marksei.com/fedora-29-new-features-startis/\">Archived</a> from the original on 2019-04-13<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-05-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Marksei%2C+Weekly+sysadmin+pills&amp;rft.atitle=Fedora+29+new+features%3A+Startis+now+officially+in+Fedora&amp;rft.date=2018-10-10&amp;rft.aulast=Sei&amp;rft.aufirst=Mark&amp;rft_id=https%3A%2F%2Fwww.marksei.com%2Ffedora-29-new-features-startis%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-193\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-193\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFProven2022\" class=\"citation web cs1\">Proven, Liam (2022-07-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2022/07/12/oracle_linux_9/\">\"Oracle Linux 9 released, with some interesting additions\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714073400/https://www.theregister.com/2022/07/12/oracle_linux_9/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Oracle+Linux+9+released%2C+with+some+interesting+additions&amp;rft.date=2022-07-12&amp;rft.aulast=Proven&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2022%2F07%2F12%2Foracle_linux_9%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-194\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-194\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFProven2023\" class=\"citation web cs1\">Proven, Liam (2023-02-02). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2023/02/02/system76_cosmic_xfce_updates/\">\"System76 teases features coming in homegrown Rust-based desktop COSMIC\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240717145511/https://www.theregister.com/2023/02/02/system76_cosmic_xfce_updates/\">Archived</a> from the original on 2024-07-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-17</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=System76+teases+features+coming+in+homegrown+Rust-based+desktop+COSMIC&amp;rft.date=2023-02-02&amp;rft.aulast=Proven&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2023%2F02%2F02%2Fsystem76_cosmic_xfce_updates%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-195\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-195\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHu2020\" class=\"citation web cs1\">Hu, Vivian (2020-06-12). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/news/2020/06/deno-1-ready-production/\">\"Deno Is Ready for Production\"</a>. <i>InfoQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200701105007/https://www.infoq.com/news/2020/06/deno-1-ready-production/\">Archived</a> from the original on 2020-07-01<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=Deno+Is+Ready+for+Production&amp;rft.date=2020-06-12&amp;rft.aulast=Hu&amp;rft.aufirst=Vivian&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Fnews%2F2020%2F06%2Fdeno-1-ready-production%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-196\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-196\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAbrams2021\" class=\"citation web cs1\">Abrams, Lawrence (2021-02-06). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.bleepingcomputer.com/news/software/this-flash-player-emulator-lets-you-securely-play-your-old-games/\">\"This Flash Player emulator lets you securely play your old games\"</a>. <i><a href=\"/wiki/Bleeping_Computer\" title=\"Bleeping Computer\">Bleeping Computer</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20211225124131/https://www.bleepingcomputer.com/news/software/this-flash-player-emulator-lets-you-securely-play-your-old-games/\">Archived</a> from the original on 2021-12-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-12-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Bleeping+Computer&amp;rft.atitle=This+Flash+Player+emulator+lets+you+securely+play+your+old+games&amp;rft.date=2021-02-06&amp;rft.aulast=Abrams&amp;rft.aufirst=Lawrence&amp;rft_id=https%3A%2F%2Fwww.bleepingcomputer.com%2Fnews%2Fsoftware%2Fthis-flash-player-emulator-lets-you-securely-play-your-old-games%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-197\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-197\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKharif2020\" class=\"citation web cs1\">Kharif, Olga (2020-10-17). <span class=\"id-lock-subscription\" title=\"Paid subscription required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.bloomberg.com/news/articles/2020-10-17/ethereum-blockchain-killer-goes-by-unassuming-name-of-polkadot\">\"Ethereum Blockchain Killer Goes By Unassuming Name of Polkadot\"</a></span>. <i><a href=\"/wiki/Bloomberg_News\" title=\"Bloomberg News\">Bloomberg News</a></i>. <a href=\"/wiki/Bloomberg_L.P.\" title=\"Bloomberg L.P.\">Bloomberg L.P.</a> <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201017192915/https://www.bloomberg.com/news/articles/2020-10-17/ethereum-blockchain-killer-goes-by-unassuming-name-of-polkadot\">Archived</a> from the original on 2020-10-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Bloomberg+News&amp;rft.atitle=Ethereum+Blockchain+Killer+Goes+By+Unassuming+Name+of+Polkadot&amp;rft.date=2020-10-17&amp;rft.aulast=Kharif&amp;rft.aufirst=Olga&amp;rft_id=https%3A%2F%2Fwww.bloomberg.com%2Fnews%2Farticles%2F2020-10-17%2Fethereum-blockchain-killer-goes-by-unassuming-name-of-polkadot&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-SO-2025-survey-198\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-SO-2025-survey_198-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-SO-2025-survey_198-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-SO-2025-survey_198-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://survey.stackoverflow.co/2025/technology\">\"2025 Stack Overflow Developer Survey – Technology\"</a>. <i><a href=\"/wiki/Stack_Overflow\" title=\"Stack Overflow\">Stack Overflow</a></i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-08-09</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Stack+Overflow&amp;rft.atitle=2025+Stack+Overflow+Developer+Survey+%E2%80%93+Technology&amp;rft_id=https%3A%2F%2Fsurvey.stackoverflow.co%2F2025%2Ftechnology&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-199\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-199\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFClaburn2022\" class=\"citation web cs1\">Claburn, Thomas (2022-06-23). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2022/06/23/linus_torvalds_rust_linux_kernel/\">\"Linus Torvalds says Rust is coming to the Linux kernel\"</a>. <i><a href=\"/wiki/The_Register\" title=\"The Register\">The Register</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220728221531/https://www.theregister.com/2022/06/23/linus_torvalds_rust_linux_kernel/\">Archived</a> from the original on 2022-07-28<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-15</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Register&amp;rft.atitle=Linus+Torvalds+says+Rust+is+coming+to+the+Linux+kernel&amp;rft.date=2022-06-23&amp;rft.aulast=Claburn&amp;rft.aufirst=Thomas&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2022%2F06%2F23%2Flinus_torvalds_rust_linux_kernel%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-201\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-201\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFWallach\" class=\"citation web cs1\">Wallach, Dan. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.darpa.mil/research/programs/translating-all-c-to-rust\">\"TRACTOR: Translating All C to Rust\"</a>. <a href=\"/wiki/DARPA\" title=\"DARPA\">DARPA</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-08-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=TRACTOR%3A+Translating+All+C+to+Rust&amp;rft.pub=DARPA&amp;rft.aulast=Wallach&amp;rft.aufirst=Dan&amp;rft_id=https%3A%2F%2Fwww.darpa.mil%2Fresearch%2Fprograms%2Ftranslating-all-c-to-rust&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-202\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-202\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJungJourdanKrebbersDreyer2017\" class=\"citation journal cs1\">Jung, Ralf; Jourdan, Jacques-Henri; Krebbers, Robbert; Dreyer, Derek (2017-12-27). <a rel=\"nofollow\" class=\"external text\" href=\"https://dl.acm.org/doi/10.1145/3158154\">\"RustBelt: securing the foundations of the Rust programming language\"</a>. <i>Proceedings of the ACM on Programming Languages</i>. <b>2</b> (POPL): <span class=\"nowrap\">1–</span>34. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3158154\">10.1145/3158154</a>. <a href=\"/wiki/Hdl_(identifier)\" class=\"mw-redirect\" title=\"Hdl (identifier)\">hdl</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://hdl.handle.net/21.11116%2F0000-0003-34C6-3\">21.11116/0000-0003-34C6-3</a></span>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/2475-1421\">2475-1421</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+ACM+on+Programming+Languages&amp;rft.atitle=RustBelt%3A+securing+the+foundations+of+the+Rust+programming+language&amp;rft.volume=2&amp;rft.issue=POPL&amp;rft.pages=1-34&amp;rft.date=2017-12-27&amp;rft_id=info%3Ahdl%2F21.11116%2F0000-0003-34C6-3&amp;rft.issn=2475-1421&amp;rft_id=info%3Adoi%2F10.1145%2F3158154&amp;rft.aulast=Jung&amp;rft.aufirst=Ralf&amp;rft.au=Jourdan%2C+Jacques-Henri&amp;rft.au=Krebbers%2C+Robbert&amp;rft.au=Dreyer%2C+Derek&amp;rft_id=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F3158154&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-203\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-203\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPopescuXuApostolakisAugust2021\" class=\"citation journal cs1\">Popescu, Natalie; Xu, Ziyang; Apostolakis, Sotiris; August, David I.; Levy, Amit (2021-10-20). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3485480\">\"Safer at any speed: automatic context-aware safety enhancement for Rust\"</a>. <i>Proceedings of the ACM on Programming Languages</i>. <b>5</b> (OOPSLA): <span class=\"nowrap\">1–</span>23. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3485480\">10.1145/3485480</a></span>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/2475-1421\">2475-1421</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+ACM+on+Programming+Languages&amp;rft.atitle=Safer+at+any+speed%3A+automatic+context-aware+safety+enhancement+for+Rust&amp;rft.volume=5&amp;rft.issue=OOPSLA&amp;rft.pages=1-23&amp;rft.date=2021-10-20&amp;rft_id=info%3Adoi%2F10.1145%2F3485480&amp;rft.issn=2475-1421&amp;rft.aulast=Popescu&amp;rft.aufirst=Natalie&amp;rft.au=Xu%2C+Ziyang&amp;rft.au=Apostolakis%2C+Sotiris&amp;rft.au=August%2C+David+I.&amp;rft.au=Levy%2C+Amit&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%252F3485480&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ResearchSoftware1-204\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-ResearchSoftware1_204-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBlanco-CuaresmaBolmont2017\" class=\"citation journal cs1\">Blanco-Cuaresma, Sergi; Bolmont, Emeline (2017-05-30). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.cambridge.org/core/journals/proceedings-of-the-international-astronomical-union/article/what-can-the-programming-language-rust-do-for-astrophysics/B51B6DF72B7641F2352C05A502F3D881\">\"What can the programming language Rust do for astrophysics?\"</a>. <i><a href=\"/wiki/Proceedings_of_the_International_Astronomical_Union\" class=\"mw-redirect\" title=\"Proceedings of the International Astronomical Union\">Proceedings of the International Astronomical Union</a></i>. <b>12</b> (S325): <span class=\"nowrap\">341–</span>344. <a href=\"/wiki/ArXiv_(identifier)\" class=\"mw-redirect\" title=\"ArXiv (identifier)\">arXiv</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://arxiv.org/abs/1702.02951\">1702.02951</a></span>. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2017IAUS..325..341B\">2017IAUS..325..341B</a>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1017%2FS1743921316013168\">10.1017/S1743921316013168</a>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/1743-9213\">1743-9213</a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:7857871\">7857871</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220625140128/https://www.cambridge.org/core/journals/proceedings-of-the-international-astronomical-union/article/what-can-the-programming-language-rust-do-for-astrophysics/B51B6DF72B7641F2352C05A502F3D881\">Archived</a> from the original on 2022-06-25<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+International+Astronomical+Union&amp;rft.atitle=What+can+the+programming+language+Rust+do+for+astrophysics%3F&amp;rft.volume=12&amp;rft.issue=S325&amp;rft.pages=341-344&amp;rft.date=2017-05-30&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A7857871%23id-name%3DS2CID&amp;rft_id=info%3Abibcode%2F2017IAUS..325..341B&amp;rft_id=info%3Aarxiv%2F1702.02951&amp;rft.issn=1743-9213&amp;rft_id=info%3Adoi%2F10.1017%2FS1743921316013168&amp;rft.aulast=Blanco-Cuaresma&amp;rft.aufirst=Sergi&amp;rft.au=Bolmont%2C+Emeline&amp;rft_id=https%3A%2F%2Fwww.cambridge.org%2Fcore%2Fjournals%2Fproceedings-of-the-international-astronomical-union%2Farticle%2Fwhat-can-the-programming-language-rust-do-for-astrophysics%2FB51B6DF72B7641F2352C05A502F3D881&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-FOOTNOTEKlabnikNichols20194-205\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-FOOTNOTEKlabnikNichols20194_205-0\">^</a></b></span> <span class=\"reference-text\"><a href=\"#CITEREFKlabnikNichols2019\">Klabnik &amp; Nichols 2019</a>, p.&#160;4.</span>\n</li>\n<li id=\"cite_note-206\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-206\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.rust-lang.org/learn/get-started#ferris\">\"Getting Started\"</a>. <i>The Rust Programming Language</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201101145703/https://www.rust-lang.org/learn/get-started#ferris\">Archived</a> from the original on 2020-11-01<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2020-10-11</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language&amp;rft.atitle=Getting+Started&amp;rft_id=https%3A%2F%2Fwww.rust-lang.org%2Flearn%2Fget-started%23ferris&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-StateOfRustSurvey2024-207\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-StateOfRustSurvey2024_207-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFThe_Rust_Survey_Team2025\" class=\"citation web cs1\">The Rust Survey Team (2025-02-13). <a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2025/02/13/2024-State-Of-Rust-Survey-results.html\">\"2024 State of Rust Survey Results\"</a>. <i>The Rust Programming Language</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-09-07</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language&amp;rft.atitle=2024+State+of+Rust+Survey+Results&amp;rft.date=2025-02-13&amp;rft.au=The+Rust+Survey+Team&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2025%2F02%2F13%2F2024-State-Of-Rust-Survey-results.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-208\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-208\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://octoverse.github.com/2022/top-programming-languages\">\"The top programming languages\"</a>. <i>The State of the Octoverse</i>. 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+State+of+the+Octoverse&amp;rft.atitle=The+top+programming+languages&amp;rft.date=2022&amp;rft_id=https%3A%2F%2Foctoverse.github.com%2F2022%2Ftop-programming-languages&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-209\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-209\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.blog/news-insights/octoverse/octoverse-2024/\">\"Octoverse: AI leads Python to top language as the number of global developers surges\"</a>. <i>The GitHub Blog</i>. 2024-10-29<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2025-06-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+GitHub+Blog&amp;rft.atitle=Octoverse%3A+AI+leads+Python+to+top+language+as+the+number+of+global+developers+surges&amp;rft.date=2024-10-29&amp;rft_id=https%3A%2F%2Fgithub.blog%2Fnews-insights%2Foctoverse%2Foctoverse-2024%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-210\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-210\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTung2021\" class=\"citation web cs1\">Tung, Liam (2021-02-08). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/the-rust-programming-language-just-took-a-huge-step-forwards/\">\"The Rust programming language just took a huge step forwards\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714105527/https://www.zdnet.com/article/the-rust-programming-language-just-took-a-huge-step-forwards/\">Archived</a> from the original on 2022-07-14<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-07-14</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ZDNET&amp;rft.atitle=The+Rust+programming+language+just+took+a+huge+step+forwards&amp;rft.date=2021-02-08&amp;rft.aulast=Tung&amp;rft.aufirst=Liam&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fthe-rust-programming-language-just-took-a-huge-step-forwards%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-211\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-211\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKrill2021\" class=\"citation news cs1\">Krill, Paul (2021-02-09). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3606774/rust-language-moves-to-independent-foundation.html\">\"Rust language moves to independent foundation\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210410161528/https://www.infoworld.com/article/3606774/rust-language-moves-to-independent-foundation.html\">Archived</a> from the original on 2021-04-10<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-04-10</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=InfoWorld&amp;rft.atitle=Rust+language+moves+to+independent+foundation&amp;rft.date=2021-02-09&amp;rft.aulast=Krill&amp;rft.aufirst=Paul&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3606774%2Frust-language-moves-to-independent-foundation.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-212\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-212\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVaughan-Nichols2021\" class=\"citation news cs1\">Vaughan-Nichols, Steven J. (2021-04-09). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/awss-shane-miller-to-head-the-newly-created-rust-foundation/\">\"AWS's Shane Miller to head the newly created Rust Foundation\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210410031305/https://www.zdnet.com/article/awss-shane-miller-to-head-the-newly-created-rust-foundation/\">Archived</a> from the original on 2021-04-10<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-04-10</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=ZDNET&amp;rft.atitle=AWS%27s+Shane+Miller+to+head+the+newly+created+Rust+Foundation&amp;rft.date=2021-04-09&amp;rft.aulast=Vaughan-Nichols&amp;rft.aufirst=Steven+J.&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fawss-shane-miller-to-head-the-newly-created-rust-foundation%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-213\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-213\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVaughan-Nichols2021\" class=\"citation news cs1\">Vaughan-Nichols, Steven J. (2021-11-17). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/rust-foundation-appoints-rebecca-rumbul-as-executive-director/\">\"Rust Foundation appoints Rebecca Rumbul as executive director\"</a>. <i><a href=\"/wiki/ZDNET\" title=\"ZDNET\">ZDNET</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20211118062346/https://www.zdnet.com/article/rust-foundation-appoints-rebecca-rumbul-as-executive-director/\">Archived</a> from the original on 2021-11-18<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-11-18</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=ZDNET&amp;rft.atitle=Rust+Foundation+appoints+Rebecca+Rumbul+as+executive+director&amp;rft.date=2021-11-17&amp;rft.aulast=Vaughan-Nichols&amp;rft.aufirst=Steven+J.&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Frust-foundation-appoints-rebecca-rumbul-as-executive-director%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-214\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-214\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.rust-lang.org/governance\">\"Governance\"</a>. <i>The Rust Programming Language</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220510225505/https://www.rust-lang.org/governance\">Archived</a> from the original on 2022-05-10<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-07-18</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Rust+Programming+Language&amp;rft.atitle=Governance&amp;rft_id=https%3A%2F%2Fwww.rust-lang.org%2Fgovernance&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-215\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-215\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.rust-lang.org/2023/06/20/introducing-leadership-council.html\">\"Introducing the Rust Leadership Council\"</a>. <i>Rust Blog</i>. 2023-06-20<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2024-01-12</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Rust+Blog&amp;rft.atitle=Introducing+the+Rust+Leadership+Council&amp;rft.date=2023-06-20&amp;rft_id=https%3A%2F%2Fblog.rust-lang.org%2F2023%2F06%2F20%2Fintroducing-leadership-council.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ARust+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n</ol></div></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links\">External links</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Rust_(programming_language)&amp;action=edit&amp;section=53\" title=\"Edit section: External links\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1308029216\" /><style data-mw-deduplicate=\"TemplateStyles:r1307723979\">.mw-parser-output .sister-box .side-box-abovebelow{padding:0.75em 0;text-align:center}.mw-parser-output .sister-box .side-box-abovebelow>b{display:block}.mw-parser-output .sister-box .side-box-text>ul{border-top:1px solid #aaa;padding:0.75em 0;width:220px;margin:0 auto}.mw-parser-output .sister-box .side-box-text>ul>li{min-height:31px}.mw-parser-output .sister-logo{display:inline-block;width:31px;line-height:31px;vertical-align:middle;text-align:center}.mw-parser-output .sister-link{display:inline-block;margin-left:7px;width:182px;vertical-align:middle}@media print{body.ns-0 .mw-parser-output .sistersitebox{display:none!important}}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}</style><div role=\"navigation\" aria-labelledby=\"sister-projects\" class=\"side-box metadata side-box-right sister-box sistersitebox plainlinks\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" />\n<div class=\"side-box-abovebelow\">\n<b>Rust</b>  at Wikipedia's <a href=\"/wiki/Wikipedia:Wikimedia_sister_projects\" title=\"Wikipedia:Wikimedia sister projects\"><span id=\"sister-projects\">sister projects</span></a></div>\n<div class=\"side-box-flex\">\n<div class=\"side-box-text plainlist\"><ul><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Commons-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/20px-Commons-logo.svg.png\" decoding=\"async\" width=\"20\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/40px-Commons-logo.svg.png 1.5x\" data-file-width=\"1024\" data-file-height=\"1376\" /></a></span></span><span class=\"sister-link\"><a href=\"https://commons.wikimedia.org/wiki/Category:Rust_(programming_language)\" class=\"extiw\" title=\"c:Category:Rust (programming language)\">Media</a> from Commons</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikiversity_logo_2017.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/40px-Wikiversity_logo_2017.svg.png\" decoding=\"async\" width=\"27\" height=\"22\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/60px-Wikiversity_logo_2017.svg.png 1.5x\" data-file-width=\"626\" data-file-height=\"512\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikiversity.org/wiki/Rust\" class=\"extiw\" title=\"v:Rust\">Resources</a> from Wikiversity</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikidata-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/40px-Wikidata-logo.svg.png\" decoding=\"async\" width=\"27\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/60px-Wikidata-logo.svg.png 1.5x\" data-file-width=\"1050\" data-file-height=\"590\" /></a></span></span><span class=\"sister-link\"><a href=\"https://www.wikidata.org/wiki/Q575650\" class=\"extiw\" title=\"d:Q575650\">Data</a> from Wikidata</span></li></ul></div></div>\n</div>\n<ul><li><span class=\"official-website\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.rust-lang.org/\">Official website</a></span></span> <span class=\"mw-valign-text-top\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q575650#P856\" title=\"Edit this at Wikidata\"><img alt=\"Edit this at Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/rust-lang/rust\">Source code</a> on <a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://doc.rust-lang.org/stable/\">Documentation</a></li></ul>\n<div class=\"navbox-styles\"><style data-mw-deduplicate=\"TemplateStyles:r1129693374\">.mw-parser-output .hlist dl,.mw-parser-output .hlist ol,.mw-parser-output .hlist ul{margin:0;padding:0}.mw-parser-output .hlist dd,.mw-parser-output .hlist dt,.mw-parser-output .hlist li{margin:0;display:inline}.mw-parser-output .hlist.inline,.mw-parser-output .hlist.inline dl,.mw-parser-output .hlist.inline ol,.mw-parser-output .hlist.inline ul,.mw-parser-output .hlist dl dl,.mw-parser-output .hlist dl ol,.mw-parser-output .hlist dl ul,.mw-parser-output .hlist ol dl,.mw-parser-output .hlist ol ol,.mw-parser-output .hlist ol ul,.mw-parser-output .hlist ul dl,.mw-parser-output .hlist ul ol,.mw-parser-output .hlist ul ul{display:inline}.mw-parser-output .hlist .mw-empty-li{display:none}.mw-parser-output .hlist dt::after{content:\": \"}.mw-parser-output .hlist dd::after,.mw-parser-output .hlist li::after{content:\" · \";font-weight:bold}.mw-parser-output .hlist dd:last-child::after,.mw-parser-output .hlist dt:last-child::after,.mw-parser-output .hlist li:last-child::after{content:none}.mw-parser-output .hlist dd dd:first-child::before,.mw-parser-output .hlist dd dt:first-child::before,.mw-parser-output .hlist dd li:first-child::before,.mw-parser-output .hlist dt dd:first-child::before,.mw-parser-output .hlist dt dt:first-child::before,.mw-parser-output .hlist dt li:first-child::before,.mw-parser-output .hlist li dd:first-child::before,.mw-parser-output .hlist li dt:first-child::before,.mw-parser-output .hlist li li:first-child::before{content:\" (\";font-weight:normal}.mw-parser-output .hlist dd dd:last-child::after,.mw-parser-output .hlist dd dt:last-child::after,.mw-parser-output .hlist dd li:last-child::after,.mw-parser-output .hlist dt dd:last-child::after,.mw-parser-output .hlist dt dt:last-child::after,.mw-parser-output .hlist dt li:last-child::after,.mw-parser-output .hlist li dd:last-child::after,.mw-parser-output .hlist li dt:last-child::after,.mw-parser-output .hlist li li:last-child::after{content:\")\";font-weight:normal}.mw-parser-output .hlist ol{counter-reset:listitem}.mw-parser-output .hlist ol>li{counter-increment:listitem}.mw-parser-output .hlist ol>li::before{content:\" \"counter(listitem)\"\\a0 \"}.mw-parser-output .hlist dd ol>li:first-child::before,.mw-parser-output .hlist dt ol>li:first-child::before,.mw-parser-output .hlist li ol>li:first-child::before{content:\" (\"counter(listitem)\"\\a0 \"}</style><style data-mw-deduplicate=\"TemplateStyles:r1236075235\">.mw-parser-output .navbox{box-sizing:border-box;border:1px solid #a2a9b1;width:100%;clear:both;font-size:88%;text-align:center;padding:1px;margin:1em auto 0}.mw-parser-output .navbox .navbox{margin-top:0}.mw-parser-output .navbox+.navbox,.mw-parser-output .navbox+.navbox-styles+.navbox{margin-top:-1px}.mw-parser-output .navbox-inner,.mw-parser-output .navbox-subgroup{width:100%}.mw-parser-output .navbox-group,.mw-parser-output .navbox-title,.mw-parser-output .navbox-abovebelow{padding:0.25em 1em;line-height:1.5em;text-align:center}.mw-parser-output .navbox-group{white-space:nowrap;text-align:right}.mw-parser-output .navbox,.mw-parser-output .navbox-subgroup{background-color:#fdfdfd}.mw-parser-output .navbox-list{line-height:1.5em;border-color:#fdfdfd}.mw-parser-output .navbox-list-with-group{text-align:left;border-left-width:2px;border-left-style:solid}.mw-parser-output tr+tr>.navbox-abovebelow,.mw-parser-output tr+tr>.navbox-group,.mw-parser-output tr+tr>.navbox-image,.mw-parser-output tr+tr>.navbox-list{border-top:2px solid #fdfdfd}.mw-parser-output .navbox-title{background-color:#ccf}.mw-parser-output .navbox-abovebelow,.mw-parser-output .navbox-group,.mw-parser-output .navbox-subgroup .navbox-title{background-color:#ddf}.mw-parser-output .navbox-subgroup .navbox-group,.mw-parser-output .navbox-subgroup .navbox-abovebelow{background-color:#e6e6ff}.mw-parser-output .navbox-even{background-color:#f7f7f7}.mw-parser-output .navbox-odd{background-color:transparent}.mw-parser-output .navbox .hlist td dl,.mw-parser-output .navbox .hlist td ol,.mw-parser-output .navbox .hlist td ul,.mw-parser-output .navbox td.hlist dl,.mw-parser-output .navbox td.hlist ol,.mw-parser-output .navbox td.hlist ul{padding:0.125em 0}.mw-parser-output .navbox .navbar{display:block;font-size:100%}.mw-parser-output .navbox-title .navbar{float:left;text-align:left;margin-right:0.5em}body.skin--responsive .mw-parser-output .navbox-image img{max-width:none!important}@media print{body.ns-0 .mw-parser-output .navbox{display:none!important}}</style></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Programming_languages1944\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible expanded navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1239400231\">.mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}</style><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Programming_languages\" title=\"Template:Programming languages\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Programming_languages\" title=\"Template talk:Programming languages\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Programming_languages\" title=\"Special:EditPage/Template:Programming languages\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Programming_languages1944\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Programming_language\" title=\"Programming language\">Programming languages</a></div></th></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><a href=\"/wiki/Comparison_of_programming_languages\" title=\"Comparison of programming languages\">Comparison</a></li>\n<li><a href=\"/wiki/Timeline_of_programming_languages\" title=\"Timeline of programming languages\">Timeline</a></li>\n<li><a href=\"/wiki/History_of_programming_languages\" title=\"History of programming languages\">History</a></li></ul>\n</div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Ada_(programming_language)\" title=\"Ada (programming language)\">Ada</a></li>\n<li><a href=\"/wiki/ALGOL\" title=\"ALGOL\">ALGOL</a>\n<ul><li><a href=\"/wiki/Simula\" title=\"Simula\">Simula</a></li></ul></li>\n<li><a href=\"/wiki/APL_(programming_language)\" title=\"APL (programming language)\">APL</a></li>\n<li><a href=\"/wiki/Assembly_language\" title=\"Assembly language\">Assembly</a></li>\n<li><a href=\"/wiki/BASIC\" title=\"BASIC\">BASIC</a>\n<ul><li><a href=\"/wiki/Visual_Basic\" title=\"Visual Basic\">Visual Basic</a>\n<ul><li><a href=\"/wiki/Visual_Basic_(classic)\" title=\"Visual Basic (classic)\">classic</a></li>\n<li><a href=\"/wiki/Visual_Basic_(.NET)\" title=\"Visual Basic (.NET)\">.NET</a></li></ul></li></ul></li>\n<li><a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a></li>\n<li><a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a></li>\n<li><a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">C#</a></li>\n<li><a href=\"/wiki/COBOL\" title=\"COBOL\">COBOL</a></li>\n<li><a href=\"/wiki/Erlang_(programming_language)\" title=\"Erlang (programming language)\">Erlang</a>\n<ul><li><a href=\"/wiki/Elixir_(programming_language)\" title=\"Elixir (programming language)\">Elixir</a></li></ul></li>\n<li><a href=\"/wiki/Forth_(programming_language)\" title=\"Forth (programming language)\">Forth</a></li>\n<li><a href=\"/wiki/Fortran\" title=\"Fortran\">Fortran</a></li>\n<li><a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a></li>\n<li><a href=\"/wiki/Haskell\" title=\"Haskell\">Haskell</a></li>\n<li><a href=\"/wiki/Java_(programming_language)\" title=\"Java (programming language)\">Java</a></li>\n<li><a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a></li>\n<li><a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a></li>\n<li><a href=\"/wiki/Kotlin_(programming_language)\" title=\"Kotlin (programming language)\">Kotlin</a></li>\n<li><a href=\"/wiki/Lisp_(programming_language)\" title=\"Lisp (programming language)\">Lisp</a></li>\n<li><a href=\"/wiki/Lua\" title=\"Lua\">Lua</a></li>\n<li><a href=\"/wiki/MATLAB\" title=\"MATLAB\">MATLAB</a></li>\n<li><a href=\"/wiki/ML_(programming_language)\" title=\"ML (programming language)\">ML</a>\n<ul><li><a href=\"/wiki/Caml\" title=\"Caml\">Caml </a>\n<ul><li><a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a></li></ul></li></ul></li>\n<li><a href=\"/wiki/Pascal_(programming_language)\" title=\"Pascal (programming language)\">Pascal</a>\n<ul><li><a href=\"/wiki/Object_Pascal\" title=\"Object Pascal\">Object Pascal</a></li></ul></li>\n<li><a href=\"/wiki/Perl\" title=\"Perl\">Perl </a>\n<ul><li><a href=\"/wiki/Raku_(programming_language)\" title=\"Raku (programming language)\">Raku</a></li></ul></li>\n<li><a href=\"/wiki/PHP\" title=\"PHP\">PHP</a></li>\n<li><a href=\"/wiki/Prolog\" title=\"Prolog\">Prolog</a></li>\n<li><a href=\"/wiki/Python_(programming_language)\" title=\"Python (programming language)\">Python</a></li>\n<li><a href=\"/wiki/R_(programming_language)\" title=\"R (programming language)\">R</a></li>\n<li><a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a></li>\n<li><a class=\"mw-selflink selflink\">Rust</a></li>\n<li><a href=\"/wiki/SAS_language\" title=\"SAS language\">SAS</a></li>\n<li><a href=\"/wiki/SQL\" title=\"SQL\">SQL</a></li>\n<li><a href=\"/wiki/Scratch_(programming_language)\" title=\"Scratch (programming language)\">Scratch</a></li>\n<li><a href=\"/wiki/Shell_script\" title=\"Shell script\">Shell</a></li>\n<li><a href=\"/wiki/Smalltalk\" title=\"Smalltalk\">Smalltalk</a></li>\n<li><a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a></li>\n<li><i><a href=\"/wiki/List_of_programming_languages\" title=\"List of programming languages\">more...</a></i></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"List-Class article\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/d/db/Symbol_list_class.svg/20px-Symbol_list_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/d/db/Symbol_list_class.svg/40px-Symbol_list_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <b>Lists:</b> <a href=\"/wiki/List_of_programming_languages\" title=\"List of programming languages\">Alphabetical</a></li>\n<li><a href=\"/wiki/List_of_programming_languages_by_type\" title=\"List of programming languages by type\">Categorical</a></li>\n<li><a href=\"/wiki/Generational_list_of_programming_languages\" title=\"Generational list of programming languages\">Generational</a></li>\n<li><a href=\"/wiki/Non-English-based_programming_languages\" title=\"Non-English-based programming languages\">Non-English-based</a></li>\n<li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"Category\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/20px-Symbol_category_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/40px-Symbol_category_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <a href=\"/wiki/Category:Programming_languages\" title=\"Category:Programming languages\">Category</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Mozilla7930\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible mw-collapsed navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Mozilla\" title=\"Template:Mozilla\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Mozilla\" title=\"Template talk:Mozilla\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Mozilla\" title=\"Special:EditPage/Template:Mozilla\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Mozilla7930\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Mozilla\" title=\"Mozilla\">Mozilla</a></div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Projects4872\" style=\"font-size:114%;margin:0 4em\">Projects</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Mozilla<br />Labs</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Bugzilla\" title=\"Bugzilla\">Bugzilla</a></li>\n<li><a href=\"/wiki/ChatZilla\" title=\"ChatZilla\">ChatZilla</a></li>\n<li><i><a href=\"/wiki/Jetpack_(Firefox_project)\" title=\"Jetpack (Firefox project)\">Jetpack</a></i></li>\n<li><i><a href=\"/wiki/Lightning_(software)\" title=\"Lightning (software)\">Lightning</a></i></li>\n<li><i><a href=\"/wiki/Mozilla_Persona\" title=\"Mozilla Persona\">Persona</a></i></li>\n<li><i><a href=\"/wiki/Mozilla_Prism\" title=\"Mozilla Prism\">Prism</a></i></li>\n<li><i><a href=\"/wiki/Mozilla_Raindrop\" title=\"Mozilla Raindrop\">Raindrop</a></i></li>\n<li><i><a href=\"/wiki/Mozilla_Skywriter\" title=\"Mozilla Skywriter\">Skywriter</a></i></li>\n<li><i><a href=\"/wiki/Mozilla_Sunbird\" title=\"Mozilla Sunbird\">Sunbird</a></i></li>\n<li><a href=\"/wiki/PDF.js\" title=\"PDF.js\">PDF.js</a></li>\n<li><i><a href=\"/wiki/Ubiquity_(Firefox)\" title=\"Ubiquity (Firefox)\">Ubiquity</a></i></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Mozilla<br />Research</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Alliance_for_Open_Media\" title=\"Alliance for Open Media\">Open Media</a></li>\n<li><a class=\"mw-selflink selflink\">Rust</a></li>\n<li><a href=\"/wiki/Shumway_(software)\" title=\"Shumway (software)\">Shumway</a></li>\n<li><a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a></li>\n<li><a href=\"/wiki/WebXR\" title=\"WebXR\">WebXR</a></li>\n<li><i><a href=\"/wiki/Asm.js\" title=\"Asm.js\">asm.js</a></i></li>\n<li><i><a href=\"/wiki/Daala\" title=\"Daala\">Daala</a></i></li>\n<li><i><a href=\"/wiki/Firefox_OS\" title=\"Firefox OS\">Firefox OS</a></i></li>\n<li><i><a href=\"/wiki/OpenFlint\" title=\"OpenFlint\">OpenFlint</a></i></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Mozilla<br />Foundation</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozilla_Location_Service\" title=\"Mozilla Location Service\">Mozilla Location Service</a></li>\n<li><a href=\"/wiki/SeaMonkey\" title=\"SeaMonkey\">SeaMonkey</a></li>\n<li><a href=\"/wiki/Mozilla_Monitor\" title=\"Mozilla Monitor\">Mozilla Monitor</a></li>\n<li><a href=\"/wiki/Mozilla_Thunderbird\" title=\"Mozilla Thunderbird\">Thunderbird</a></li>\n<li><a href=\"/wiki/Mozilla_VPN\" title=\"Mozilla VPN\">Mozilla VPN</a></li>\n<li><a href=\"/wiki/List_of_Mozilla_products\" title=\"List of Mozilla products\">List of products</a></li></ul>\n</div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Firefox</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Firefox\" title=\"Firefox\">Firefox Browser</a>\n<ul><li><a href=\"/wiki/Firefox_early_version_history\" title=\"Firefox early version history\">Early version history</a></li>\n<li><i><a href=\"/wiki/Firefox_2\" title=\"Firefox 2\">2</a></i></li>\n<li><i><a href=\"/wiki/Firefox_3.0\" title=\"Firefox 3.0\">3</a></i></li>\n<li><i><a href=\"/wiki/Firefox_3.5\" title=\"Firefox 3.5\">3.5</a></i></li>\n<li><i><a href=\"/wiki/Firefox_3.6\" title=\"Firefox 3.6\">3.6</a></i></li>\n<li><i><a href=\"/wiki/Firefox_4\" title=\"Firefox 4\">4</a></i></li>\n<li><a href=\"/wiki/Firefox_version_history\" title=\"Firefox version history\">Version history</a></li>\n<li><a href=\"/wiki/Firefox_for_Android\" title=\"Firefox for Android\">for Android</a></li>\n<li><a href=\"/wiki/Firefox_Focus\" title=\"Firefox Focus\">Focus</a></li></ul></li>\n<li><a href=\"/wiki/Firefox_Sync\" title=\"Firefox Sync\">Sync</a></li>\n<li><a href=\"/wiki/Pocket_(service)\" title=\"Pocket (service)\">Pocket</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Origins</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><i><a href=\"/wiki/Mozilla_Application_Suite\" title=\"Mozilla Application Suite\">Mozilla Application Suite</a></i></li>\n<li><i><a href=\"/wiki/Netscape_Navigator\" title=\"Netscape Navigator\">Netscape Navigator</a></i></li>\n<li><i><a href=\"/wiki/Netscape_Communicator\" title=\"Netscape Communicator\">Netscape Communicator</a></i></li>\n<li><i><a href=\"/wiki/Netscape\" title=\"Netscape\">Netscape Communications</a></i></li>\n<li><i><a href=\"/wiki/Beonex_Communicator\" title=\"Beonex Communicator\">Beonex Communicator</a></i></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Frameworks</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Add-on_(Mozilla)\" title=\"Add-on (Mozilla)\">Add-on</a></li>\n<li><a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a></li>\n<li><a href=\"/wiki/Mozilla_application_framework\" title=\"Mozilla application framework\">Necko</a></li>\n<li><a href=\"/wiki/NPAPI\" title=\"NPAPI\">NPAPI</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Components</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozilla_Composer\" title=\"Mozilla Composer\">Composer</a></li>\n<li><a href=\"/wiki/Netscape_Portable_Runtime\" title=\"Netscape Portable Runtime\">NSPR</a></li>\n<li><a href=\"/wiki/Network_Security_Services\" title=\"Network Security Services\">NSS</a></li>\n<li><a href=\"/wiki/Rhino_(JavaScript_engine)\" title=\"Rhino (JavaScript engine)\">Rhino</a></li>\n<li><a href=\"/wiki/SpiderMonkey\" title=\"SpiderMonkey\">SpiderMonkey</a></li>\n<li><i><a href=\"/wiki/Tamarin_(software)\" title=\"Tamarin (software)\">Tamarin</a></i></li>\n<li>Features</li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Typefaces</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Fira_(typeface)\" title=\"Fira (typeface)\">Fira</a></li>\n<li><a href=\"/wiki/Zilla_Slab\" title=\"Zilla Slab\">Zilla Slab</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Discontinued</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><i><a href=\"/wiki/Mozilla_Calendar_Project\" title=\"Mozilla Calendar Project\">Calendar Project</a></i></li>\n<li><i><a href=\"/wiki/Camino_(web_browser)\" title=\"Camino (web browser)\">Camino</a></i></li>\n<li><i><a href=\"/wiki/Firefox_Lockwise\" title=\"Firefox Lockwise\">Firefox Lockwise</a></i></li>\n<li><i><a href=\"/wiki/Firefox_Send\" title=\"Firefox Send\">Firefox Send</a></i></li>\n<li><i><a href=\"/wiki/Minimo\" title=\"Minimo\">Minimo</a></i></li>\n<li><u><a href=\"/wiki/XUL\" title=\"XUL\">XUL</a></u>\n<ul><li><u><a href=\"/wiki/XBL\" title=\"XBL\">XBL</a></u></li>\n<li><u><a href=\"/wiki/XPCOM\" title=\"XPCOM\">XPCOM</a></u></li>\n<li><u><a href=\"/wiki/XPInstall\" title=\"XPInstall\">XPInstall</a></u></li>\n<li><u><a href=\"/wiki/XULRunner\" title=\"XULRunner\">XULRunner</a></u></li></ul></li></ul>\n</div></td></tr></tbody></table><div>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Forks</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Basilisk_(web_browser)\" title=\"Basilisk (web browser)\">Basilisk</a></li>\n<li><i><a href=\"/wiki/Classilla\" title=\"Classilla\">Classilla</a></i></li>\n<li><i><a href=\"/wiki/Flock_(web_browser)\" title=\"Flock (web browser)\">Flock</a></i></li>\n<li><a href=\"/wiki/Floorp\" title=\"Floorp\">Floorp</a></li>\n<li><a href=\"/wiki/Goanna_(software)\" title=\"Goanna (software)\">Goanna</a></li>\n<li><a href=\"/wiki/GNU_IceCat\" title=\"GNU IceCat\">IceCat</a></li>\n<li><a href=\"/wiki/LibreWolf\" title=\"LibreWolf\">LibreWolf</a></li>\n<li><i><a href=\"/wiki/Miro_(video_software)\" title=\"Miro (video software)\">Miro</a></i></li>\n<li><i><a href=\"/wiki/Netscape_Navigator_9\" title=\"Netscape Navigator 9\">Netscape 9</a></i></li>\n<li><a href=\"/wiki/Pale_Moon\" title=\"Pale Moon\">Pale Moon</a></li>\n<li><a href=\"/wiki/Firefox_Portable\" title=\"Firefox Portable\">Portable Edition</a></li>\n<li><i><a href=\"/wiki/Swiftfox\" title=\"Swiftfox\">Swiftfox</a></i></li>\n<li><i><a href=\"/wiki/Swiftweasel\" title=\"Swiftweasel\">Swiftweasel</a></i></li>\n<li><a href=\"/wiki/Waterfox\" title=\"Waterfox\">Waterfox</a></li>\n<li><i><a href=\"/wiki/XB_Browser\" title=\"XB Browser\">xB Browser</a></i></li>\n<li><a href=\"/wiki/Zen_Browser\" title=\"Zen Browser\">Zen Browser</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>Discontinued projects are in <i>italics</i>. Some projects abandoned by Mozilla that are still maintained by third parties are in <u>underline</u>.</div></td></tr></tbody></table><div></div></td></tr></tbody></table><div></div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Organization1160\" style=\"font-size:114%;margin:0 4em\">Organization</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th id=\"Foundation383\" scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Foundation</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozilla_Foundation\" title=\"Mozilla Foundation\">Mozilla Foundation</a></li>\n<li><a href=\"/wiki/Mozilla_Corporation\" title=\"Mozilla Corporation\">Mozilla Corporation</a></li>\n<li><a href=\"/wiki/Mozilla_Messaging\" title=\"Mozilla Messaging\">Mozilla Messaging</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Official affiliates</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozilla_China\" title=\"Mozilla China\">Mozilla China</a></li>\n<li><a href=\"/wiki/Mozilla_Europe\" title=\"Mozilla Europe\">Mozilla Europe</a> (defunct)</li>\n<li><a href=\"/wiki/Mozilla_Japan\" title=\"Mozilla Japan\">Mozilla Japan</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">People</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mitchell_Baker\" title=\"Mitchell Baker\">Mitchell Baker</a></li>\n<li><a href=\"/wiki/David_Baron_(computer_scientist)\" title=\"David Baron (computer scientist)\">David Baron</a></li>\n<li><a href=\"/wiki/Tantek_%C3%87elik\" title=\"Tantek Çelik\">Tantek Çelik</a></li>\n<li><a href=\"/wiki/Laura_Chambers\" title=\"Laura Chambers\">Laura Chambers</a></li>\n<li><a href=\"/wiki/Brendan_Eich\" title=\"Brendan Eich\">Brendan Eich</a></li>\n<li><a href=\"/wiki/John_Hammink\" title=\"John Hammink\">John Hammink</a></li>\n<li><a href=\"/wiki/Johnny_Stenb%C3%A4ck\" title=\"Johnny Stenbäck\">Johnny Stenbäck</a></li>\n<li><a href=\"/wiki/Doug_Turner_(Mozilla)\" title=\"Doug Turner (Mozilla)\">Doug Turner</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr></tbody></table><div></div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Community78\" style=\"font-size:114%;margin:0 4em\">Community</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozdev.org\" title=\"Mozdev.org\">mozdev.org</a></li>\n<li><a href=\"/wiki/MDN_Web_Docs\" title=\"MDN Web Docs\">MDN Web Docs</a></li>\n<li><a href=\"/wiki/MozillaZine\" title=\"MozillaZine\">MozillaZine</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Other_topics254\" style=\"font-size:114%;margin:0 4em\">Other topics</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Mozilla_Manifesto\" title=\"Mozilla Manifesto\">Mozilla Manifesto</a></li>\n<li><i><a href=\"/wiki/The_Book_of_Mozilla\" title=\"The Book of Mozilla\">The Book of Mozilla</a></i></li>\n<li><i><a href=\"/wiki/Code_Rush\" title=\"Code Rush\">Code Rush</a></i></li>\n<li><a href=\"/wiki/Mozilla_Public_License\" title=\"Mozilla Public License\">Mozilla Public License</a></li>\n<li><a href=\"/wiki/Mozilla_(mascot)\" title=\"Mozilla (mascot)\">Mascot</a></li>\n<li><a href=\"/wiki/Debian%E2%80%93Mozilla_trademark_dispute\" title=\"Debian–Mozilla trademark dispute\">Debian–Mozilla trademark dispute</a></li>\n<li><a href=\"/wiki/Common_Voice\" title=\"Common Voice\">Common Voice</a></li>\n<li><i><a href=\"/wiki/Mozilla_Corp._v._FCC\" title=\"Mozilla Corp. v. FCC\">Mozilla Corp. v. FCC</a></i> (2019)</li></ul>\n</div></td></tr></tbody></table><div></div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /><style data-mw-deduplicate=\"TemplateStyles:r1038841319\">.mw-parser-output .tooltip-dotted{border-bottom:1px dotted;cursor:help}</style></div><div role=\"navigation\" class=\"navbox authority-control\" aria-labelledby=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q575650#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1360\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q575650#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1360\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Help:Authority_control\" title=\"Help:Authority control\">Authority control databases</a> <span class=\"mw-valign-text-top noprint\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q575650#identifiers\" title=\"Edit this at Wikidata\"><img alt=\"Edit this at Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">International</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://d-nb.info/gnd/1078438080\">GND</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://id.worldcat.org/fast/2002371\">FAST</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">National</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://id.loc.gov/authorities/sh2018000672\">United States</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://catalogue.bnf.fr/ark:/12148/cb17808721b\">France</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://data.bnf.fr/ark:/12148/cb17808721b\">BnF data</a></span></li><li><span class=\"uid\"><span class=\"rt-commentedText tooltip tooltip-dotted\" title=\"Rust (programovací jazyk)\"><a rel=\"nofollow\" class=\"external text\" href=\"https://aleph.nkp.cz/F/?func=find-c&amp;local_base=aut&amp;ccl_term=ica=ph1269448&amp;CON_LNG=ENG\">Czech Republic</a></span></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://datos.bne.es/resource/XX6456854\">Spain</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.nli.org.il/en/authorities/987012402011505171\">Israel</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://lux.collections.yale.edu/view/concept/9bb5bffe-7522-4b18-9057-6d989ea6c162\">Yale LUX</a></span></li></ul></div></td></tr></tbody></table></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1130092004\">.mw-parser-output .portal-bar{font-size:88%;font-weight:bold;display:flex;justify-content:center;align-items:baseline}.mw-parser-output .portal-bar-bordered{padding:0 2em;background-color:#fdfdfd;border:1px solid #a2a9b1;clear:both;margin:1em auto 0}.mw-parser-output .portal-bar-related{font-size:100%;justify-content:flex-start}.mw-parser-output .portal-bar-unbordered{padding:0 1.7em;margin-left:0}.mw-parser-output .portal-bar-header{margin:0 1em 0 0.5em;flex:0 0 auto;min-height:24px}.mw-parser-output .portal-bar-content{display:flex;flex-flow:row wrap;flex:0 1 auto;padding:0.15em 0;column-gap:1em;align-items:baseline;margin:0;list-style:none}.mw-parser-output .portal-bar-content-related{margin:0;list-style:none}.mw-parser-output .portal-bar-item{display:inline-block;margin:0.15em 0.2em;min-height:24px;line-height:24px}@media screen and (max-width:768px){.mw-parser-output .portal-bar{font-size:88%;font-weight:bold;display:flex;flex-flow:column wrap;align-items:baseline}.mw-parser-output .portal-bar-header{text-align:center;flex:0;padding-left:0.5em;margin:0 auto}.mw-parser-output .portal-bar-related{font-size:100%;align-items:flex-start}.mw-parser-output .portal-bar-content{display:flex;flex-flow:row wrap;align-items:center;flex:0;column-gap:1em;border-top:1px solid #a2a9b1;margin:0 auto;list-style:none}.mw-parser-output .portal-bar-content-related{border-top:none;margin:0;list-style:none}}.mw-parser-output .navbox+link+.portal-bar,.mw-parser-output .navbox+style+.portal-bar,.mw-parser-output .navbox+link+.portal-bar-bordered,.mw-parser-output .navbox+style+.portal-bar-bordered,.mw-parser-output .sister-bar+link+.portal-bar,.mw-parser-output .sister-bar+style+.portal-bar,.mw-parser-output .portal-bar+.navbox-styles+.navbox,.mw-parser-output .portal-bar+.navbox-styles+.sister-bar{margin-top:-1px}</style><div class=\"portal-bar noprint metadata noviewer portal-bar-bordered\" role=\"navigation\" aria-label=\"Portals\"><span class=\"portal-bar-header\"><a href=\"/wiki/Wikipedia:Contents/Portals\" title=\"Wikipedia:Contents/Portals\">Portal</a>:</span><ul class=\"portal-bar-content\"><li class=\"portal-bar-item\"><span class=\"nowrap\"><span class=\"skin-invert-image noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Octicons-terminal.svg\" class=\"mw-file-description\"><img alt=\"icon\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/20px-Octicons-terminal.svg.png\" decoding=\"async\" width=\"17\" height=\"19\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/40px-Octicons-terminal.svg.png 1.5x\" data-file-width=\"896\" data-file-height=\"1024\" /></a></span> </span><a href=\"/wiki/Portal:Computer_programming\" title=\"Portal:Computer programming\">Computer programming</a></li></ul></div>\n<!--\nNewPP limit report\nParsed by mw‐api‐ext.codfw.main‐c88c6ffc5‐lxhqv\nCached time: 20251001042650\nCache expiry: 2592000\nReduced expiry: false\nComplications: [vary‐revision‐sha1, show‐toc]\nCPU time usage: 3.436 seconds\nReal time usage: 3.808 seconds\nPreprocessor visited node count: 23711/1000000\nRevision size: 115734/2097152 bytes\nPost‐expand include size: 429248/2097152 bytes\nTemplate argument size: 22344/2097152 bytes\nHighest expansion depth: 21/100\nExpensive parser function count: 167/500\nUnstrip recursion depth: 1/20\nUnstrip post‐expand size: 670031/5000000 bytes\nLua time usage: 2.162/10.000 seconds\nLua memory usage: 13603902/52428800 bytes\nNumber of Wikibase entities loaded: 1/500\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 3380.109      1 -total\n 38.55% 1302.903      2 Template:Reflist\n 21.92%  740.938    101 Template:Cite_web\n 16.44%  555.834      3 Template:Infobox\n 12.42%  419.864     75 Template:Sfn\n 11.95%  403.896      1 Template:Infobox_programming_language\n  8.38%  283.340      1 Template:Infobox_software/simple\n  7.05%  238.204      2 Template:Wikidata\n  6.12%  206.835     21 Template:Rp\n  5.44%  183.771     21 Template:R/superscript\n-->\n\n<!-- Saved in parser cache with key enwiki:pcache:29414838:|#|:idhash:canonical and timestamp 20251001042650 and revision id 1314392250. Rendering was triggered because: api-parse\n -->\n</div><noscript><img src=\"https://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1&amp;usesul3=1\" alt=\"\" width=\"1\" height=\"1\" style=\"border: none; position: absolute;\"></noscript>\n<div class=\"printfooter\" data-nosnippet=\"\">Retrieved from \"<a dir=\"ltr\" href=\"https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&amp;oldid=1314392250\">https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&amp;oldid=1314392250</a>\"</div></div>\n\t\t\t\t\t<div id=\"catlinks\" class=\"catlinks\" data-mw=\"interface\"><div id=\"mw-normal-catlinks\" class=\"mw-normal-catlinks\"><a href=\"/wiki/Help:Category\" title=\"Help:Category\">Categories</a>: <ul><li><a href=\"/wiki/Category:Rust_(programming_language)\" title=\"Category:Rust (programming language)\">Rust (programming language)</a></li><li><a href=\"/wiki/Category:Compiled_programming_languages\" title=\"Category:Compiled programming languages\">Compiled programming languages</a></li><li><a href=\"/wiki/Category:Concurrent_programming_languages\" title=\"Category:Concurrent programming languages\">Concurrent programming languages</a></li><li><a href=\"/wiki/Category:Free_and_open_source_compilers\" title=\"Category:Free and open source compilers\">Free and open source compilers</a></li><li><a href=\"/wiki/Category:Free_software_projects\" title=\"Category:Free software projects\">Free software projects</a></li><li><a href=\"/wiki/Category:Functional_languages\" title=\"Category:Functional languages\">Functional languages</a></li><li><a href=\"/wiki/Category:High-level_programming_languages\" title=\"Category:High-level programming languages\">High-level programming languages</a></li><li><a href=\"/wiki/Category:Mozilla\" title=\"Category:Mozilla\">Mozilla</a></li><li><a href=\"/wiki/Category:Multi-paradigm_programming_languages\" title=\"Category:Multi-paradigm programming languages\">Multi-paradigm programming languages</a></li><li><a href=\"/wiki/Category:Pattern_matching_programming_languages\" title=\"Category:Pattern matching programming languages\">Pattern matching programming languages</a></li><li><a href=\"/wiki/Category:Procedural_programming_languages\" title=\"Category:Procedural programming languages\">Procedural programming languages</a></li><li><a href=\"/wiki/Category:Programming_languages_created_in_2015\" title=\"Category:Programming languages created in 2015\">Programming languages created in 2015</a></li><li><a href=\"/wiki/Category:Software_using_the_Apache_license\" title=\"Category:Software using the Apache license\">Software using the Apache license</a></li><li><a href=\"/wiki/Category:Software_using_the_MIT_license\" title=\"Category:Software using the MIT license\">Software using the MIT license</a></li><li><a href=\"/wiki/Category:Statically_typed_programming_languages\" title=\"Category:Statically typed programming languages\">Statically typed programming languages</a></li><li><a href=\"/wiki/Category:Systems_programming_languages\" title=\"Category:Systems programming languages\">Systems programming languages</a></li></ul></div><div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks mw-hidden-cats-hidden\">Hidden categories: <ul><li><a href=\"/wiki/Category:Articles_with_short_description\" title=\"Category:Articles with short description\">Articles with short description</a></li><li><a href=\"/wiki/Category:Short_description_is_different_from_Wikidata\" title=\"Category:Short description is different from Wikidata\">Short description is different from Wikidata</a></li><li><a href=\"/wiki/Category:Good_articles\" title=\"Category:Good articles\">Good articles</a></li><li><a href=\"/wiki/Category:Use_American_English_from_July_2022\" title=\"Category:Use American English from July 2022\">Use American English from July 2022</a></li><li><a href=\"/wiki/Category:All_Wikipedia_articles_written_in_American_English\" title=\"Category:All Wikipedia articles written in American English\">All Wikipedia articles written in American English</a></li><li><a href=\"/wiki/Category:Use_mdy_dates_from_July_2022\" title=\"Category:Use mdy dates from July 2022\">Use mdy dates from July 2022</a></li><li><a href=\"/wiki/Category:Articles_with_example_C%2B%2B_code\" title=\"Category:Articles with example C++ code\">Articles with example C++ code</a></li><li><a href=\"/wiki/Category:Articles_with_excerpts\" title=\"Category:Articles with excerpts\">Articles with excerpts</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_2024\" title=\"Category:Articles containing potentially dated statements from 2024\">Articles containing potentially dated statements from 2024</a></li><li><a href=\"/wiki/Category:All_articles_containing_potentially_dated_statements\" title=\"Category:All articles containing potentially dated statements\">All articles containing potentially dated statements</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_July_2024\" title=\"Category:Articles containing potentially dated statements from July 2024\">Articles containing potentially dated statements from July 2024</a></li><li><a href=\"/wiki/Category:Pages_using_Sister_project_links_with_hidden_wikidata\" title=\"Category:Pages using Sister project links with hidden wikidata\">Pages using Sister project links with hidden wikidata</a></li><li><a href=\"/wiki/Category:Articles_with_example_Rust_code\" title=\"Category:Articles with example Rust code\">Articles with example Rust code</a></li></ul></div></div>\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t</div>\n\t\t<div class=\"mw-footer-container\">\n\n<footer id=\"footer\" class=\"mw-footer\" >\n\t<ul id=\"footer-info\">\n\t<li id=\"footer-info-lastmod\"> This page was last edited on 1 October 2025, at 04:26<span class=\"anonymous-show\">&#160;(UTC)</span>.</li>\n\t<li id=\"footer-info-copyright\">Text is available under the <a href=\"/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License\" title=\"Wikipedia:Text of the Creative Commons Attribution-ShareAlike 4.0 International License\">Creative Commons Attribution-ShareAlike 4.0 License</a>;\nadditional terms may apply. By using this site, you agree to the <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Terms of Use\">Terms of Use</a> and <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Privacy policy\">Privacy Policy</a>. Wikipedia® is a registered trademark of the <a rel=\"nofollow\" class=\"external text\" href=\"https://wikimediafoundation.org/\">Wikimedia Foundation, Inc.</a>, a non-profit organization.</li>\n</ul>\n\n\t<ul id=\"footer-places\">\n\t<li id=\"footer-places-privacy\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\">Privacy policy</a></li>\n\t<li id=\"footer-places-about\"><a href=\"/wiki/Wikipedia:About\">About Wikipedia</a></li>\n\t<li id=\"footer-places-disclaimers\"><a href=\"/wiki/Wikipedia:General_disclaimer\">Disclaimers</a></li>\n\t<li id=\"footer-places-contact\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\">Contact Wikipedia</a></li>\n\t<li id=\"footer-places-wm-codeofconduct\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Universal_Code_of_Conduct\">Code of Conduct</a></li>\n\t<li id=\"footer-places-developers\"><a href=\"https://developer.wikimedia.org\">Developers</a></li>\n\t<li id=\"footer-places-statslink\"><a href=\"https://stats.wikimedia.org/#/en.wikipedia.org\">Statistics</a></li>\n\t<li id=\"footer-places-cookiestatement\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Cookie_statement\">Cookie statement</a></li>\n\t<li id=\"footer-places-mobileview\"><a href=\"//en.m.wikipedia.org/w/index.php?title=Rust_(programming_language)&amp;mobileaction=toggle_view_mobile\" class=\"noprint stopMobileRedirectToggle\">Mobile view</a></li>\n</ul>\n\n\t<ul id=\"footer-icons\" class=\"noprint\">\n\t<li id=\"footer-copyrightico\"><a href=\"https://www.wikimedia.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/static/images/footer/wikimedia-button.svg\" width=\"84\" height=\"29\"><img src=\"/static/images/footer/wikimedia.svg\" width=\"25\" height=\"25\" alt=\"Wikimedia Foundation\" lang=\"en\" loading=\"lazy\"></picture></a></li>\n\t<li id=\"footer-poweredbyico\"><a href=\"https://www.mediawiki.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/w/resources/assets/poweredby_mediawiki.svg\" width=\"88\" height=\"31\"><img src=\"/w/resources/assets/mediawiki_compact.svg\" alt=\"Powered by MediaWiki\" lang=\"en\" width=\"25\" height=\"25\" loading=\"lazy\"></picture></a></li>\n</ul>\n\n</footer>\n\n\t\t</div>\n\t</div>\n</div>\n<div class=\"vector-header-container vector-sticky-header-container no-font-mode-scale\">\n\t<div id=\"vector-sticky-header\" class=\"vector-sticky-header\">\n\t\t<div class=\"vector-sticky-header-start\">\n\t\t\t<div class=\"vector-sticky-header-icon-start vector-button-flush-left vector-button-flush-right\" aria-hidden=\"true\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-sticky-header-search-toggle\" tabindex=\"-1\" data-event-name=\"ui.vector-sticky-search-form.icon\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div role=\"search\" class=\"vector-search-box-vue  vector-search-box-show-thumbnail vector-search-box\">\n\t\t\t<div class=\"vector-typeahead-search-container\">\n\t\t\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail\">\n\t\t\t\t\t<form action=\"/w/index.php\" id=\"vector-sticky-search-form\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t\t\t<div  class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\n\t\t\t\t\t\t\t\t\ttype=\"search\" name=\"search\" placeholder=\"Search Wikipedia\">\n\t\t\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-context-bar\">\n\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n\t\t\t\t\t<div id=\"vector-sticky-header-toc\" class=\"vector-dropdown mw-portlet mw-portlet-sticky-header-toc vector-sticky-header-toc vector-button-flush-left\"  >\n\t\t\t\t\t\t<input type=\"checkbox\" id=\"vector-sticky-header-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-sticky-header-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t\t\t\t\t\t<label id=\"vector-sticky-header-toc-label\" for=\"vector-sticky-header-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<div class=\"vector-dropdown-content\">\n\n\t\t\t\t\t\t<div id=\"vector-sticky-header-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t</nav>\n\t\t\t\t<div class=\"vector-sticky-header-context-bar-primary\" aria-hidden=\"true\" ><span class=\"mw-page-title-main\">Rust (programming language)</span></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-end\" aria-hidden=\"true\">\n\t\t\t<div class=\"vector-sticky-header-icons\">\n\t\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-talk-sticky-header\" tabindex=\"-1\" data-event-name=\"talk-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbles mw-ui-icon-wikimedia-speechBubbles\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-subject-sticky-header\" tabindex=\"-1\" data-event-name=\"subject-sticky-header\"><span class=\"vector-icon mw-ui-icon-article mw-ui-icon-wikimedia-article\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-history-sticky-header\" tabindex=\"-1\" data-event-name=\"history-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-history mw-ui-icon-wikimedia-wikimedia-history\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only mw-watchlink\" id=\"ca-watchstar-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-star mw-ui-icon-wikimedia-wikimedia-star\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only reading-lists-bookmark\" id=\"ca-bookmark-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-bookmark\"><span class=\"vector-icon mw-ui-icon-wikimedia-bookmarkOutline mw-ui-icon-wikimedia-wikimedia-bookmarkOutline\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"wikitext-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-wikiText mw-ui-icon-wikimedia-wikimedia-wikiText\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-ve-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-edit mw-ui-icon-wikimedia-wikimedia-edit\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-viewsource-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-protected-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-editLock mw-ui-icon-wikimedia-wikimedia-editLock\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-buttons\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet mw-interlanguage-selector\" id=\"p-lang-btn-sticky-header\" tabindex=\"-1\" data-event-name=\"ui.dropdown-p-lang-btn-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-language mw-ui-icon-wikimedia-wikimedia-language\"></span>\n\n<span>51 languages</span>\n\t\t\t</button>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive\" id=\"ca-addsection-sticky-header\" tabindex=\"-1\" data-event-name=\"addsection-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbleAdd-progressive mw-ui-icon-wikimedia-speechBubbleAdd-progressive\"></span>\n\n<span>Add topic</span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-icon-end\">\n\t\t\t\t<div class=\"vector-user-links\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"mw-portlet mw-portlet-dock-bottom emptyPortlet\" id=\"p-dock-bottom\">\n\t<ul>\n\n\t</ul>\n</div>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({\"wgHostname\":\"mw-web.codfw.main-7f6fd9975d-27fwf\",\"wgBackendResponseTime\":189,\"wgPageParseReport\":{\"limitreport\":{\"cputime\":\"3.436\",\"walltime\":\"3.808\",\"ppvisitednodes\":{\"value\":23711,\"limit\":1000000},\"revisionsize\":{\"value\":115734,\"limit\":2097152},\"postexpandincludesize\":{\"value\":429248,\"limit\":2097152},\"templateargumentsize\":{\"value\":22344,\"limit\":2097152},\"expansiondepth\":{\"value\":21,\"limit\":100},\"expensivefunctioncount\":{\"value\":167,\"limit\":500},\"unstrip-depth\":{\"value\":1,\"limit\":20},\"unstrip-size\":{\"value\":670031,\"limit\":5000000},\"entityaccesscount\":{\"value\":1,\"limit\":500},\"timingprofile\":[\"100.00% 3380.109      1 -total\",\" 38.55% 1302.903      2 Template:Reflist\",\" 21.92%  740.938    101 Template:Cite_web\",\" 16.44%  555.834      3 Template:Infobox\",\" 12.42%  419.864     75 Template:Sfn\",\" 11.95%  403.896      1 Template:Infobox_programming_language\",\"  8.38%  283.340      1 Template:Infobox_software/simple\",\"  7.05%  238.204      2 Template:Wikidata\",\"  6.12%  206.835     21 Template:Rp\",\"  5.44%  183.771     21 Template:R/superscript\"]},\"scribunto\":{\"limitreport-timeusage\":{\"value\":\"2.162\",\"limit\":\"10.000\"},\"limitreport-memusage\":{\"value\":13603902,\"limit\":52428800},\"limitreport-logs\":\"anchor_id_list = table#1 {\\n    [\\\"CITEREFAbrams2021\\\"] = 1,\\n    [\\\"CITEREFAmadeo2021\\\"] = 2,\\n    [\\\"CITEREFAnderson2012\\\"] = 1,\\n    [\\\"CITEREFAnderson2021\\\"] = 3,\\n    [\\\"CITEREFAnthony2012\\\"] = 1,\\n    [\\\"CITEREFAstrauskasMathejaPoliMüller2020\\\"] = 1,\\n    [\\\"CITEREFBalasubramanianBaranowskiBurtsevPanda2017\\\"] = 1,\\n    [\\\"CITEREFBaumgartner2025\\\"] = 1,\\n    [\\\"CITEREFBinstock2014\\\"] = 1,\\n    [\\\"CITEREFBlanco-CuaresmaBolmont2017\\\"] = 1,\\n    [\\\"CITEREFBlandyOrendorffTindall2021\\\"] = 1,\\n    [\\\"CITEREFBoosLiyanageIjazZhong2020\\\"] = 1,\\n    [\\\"CITEREFBrown2013\\\"] = 1,\\n    [\\\"CITEREFCimpanu2019\\\"] = 1,\\n    [\\\"CITEREFCimpanu2020\\\"] = 1,\\n    [\\\"CITEREFClaburn2022\\\"] = 2,\\n    [\\\"CITEREFClaburn2023\\\"] = 3,\\n    [\\\"CITEREFCooper2020\\\"] = 1,\\n    [\\\"CITEREFCorbet2022\\\"] = 2,\\n    [\\\"CITEREFCorbet2024\\\"] = 1,\\n    [\\\"CITEREFCouprie2015\\\"] = 1,\\n    [\\\"CITEREFDarkcrizt2021\\\"] = 1,\\n    [\\\"CITEREFDonovan2024\\\"] = 1,\\n    [\\\"CITEREFEvansCampbellSoffa2020\\\"] = 1,\\n    [\\\"CITEREFFarshinBarbetteRoozbehMaguire2021\\\"] = 1,\\n    [\\\"CITEREFGjengset2021\\\"] = 1,\\n    [\\\"CITEREFGross2024\\\"] = 1,\\n    [\\\"CITEREFHoare2010\\\"] = 1,\\n    [\\\"CITEREFHoare2016\\\"] = 1,\\n    [\\\"CITEREFHu2020\\\"] = 1,\\n    [\\\"CITEREFJack2024\\\"] = 1,\\n    [\\\"CITEREFJaloyan2017\\\"] = 1,\\n    [\\\"CITEREFJansens2023\\\"] = 1,\\n    [\\\"CITEREFJungJourdanKrebbersDreyer2017\\\"] = 1,\\n    [\\\"CITEREFKeizer2016\\\"] = 1,\\n    [\\\"CITEREFKharif2020\\\"] = 1,\\n    [\\\"CITEREFKlabnik2016\\\"] = 1,\\n    [\\\"CITEREFKlabnikNichols2019\\\"] = 1,\\n    [\\\"CITEREFKlabnikNichols2023\\\"] = 1,\\n    [\\\"CITEREFKrill2021\\\"] = 1,\\n    [\\\"CITEREFLardinois2015\\\"] = 1,\\n    [\\\"CITEREFLardinois2017\\\"] = 1,\\n    [\\\"CITEREFLattner\\\"] = 1,\\n    [\\\"CITEREFLattuadaHanceChoBrun2023\\\"] = 1,\\n    [\\\"CITEREFLevickBos\\\"] = 1,\\n    [\\\"CITEREFLiGuoYangWang2024\\\"] = 1,\\n    [\\\"CITEREFLyu2020\\\"] = 1,\\n    [\\\"CITEREFLyu2021\\\"] = 1,\\n    [\\\"CITEREFMcNamara2021\\\"] = 1,\\n    [\\\"CITEREFMilanoTurcottiMyers2022\\\"] = 1,\\n    [\\\"CITEREFNelson2022\\\"] = 1,\\n    [\\\"CITEREFNichols2018\\\"] = 1,\\n    [\\\"CITEREFNobleMackayWrigstad2023\\\"] = 1,\\n    [\\\"CITEREFPearce2021\\\"] = 1,\\n    [\\\"CITEREFPereiraCoutoRibeiroRua2017\\\"] = 1,\\n    [\\\"CITEREFPerkel2020\\\"] = 1,\\n    [\\\"CITEREFPopescuXuApostolakisAugust2021\\\"] = 2,\\n    [\\\"CITEREFProven2019\\\"] = 1,\\n    [\\\"CITEREFProven2022\\\"] = 1,\\n    [\\\"CITEREFProven2023\\\"] = 2,\\n    [\\\"CITEREFRosenblatt2013\\\"] = 1,\\n    [\\\"CITEREFSecurity_Research_Team2013\\\"] = 1,\\n    [\\\"CITEREFSei2018\\\"] = 1,\\n    [\\\"CITEREFShamrell-Harrington2022\\\"] = 1,\\n    [\\\"CITEREFShankland2016\\\"] = 1,\\n    [\\\"CITEREFSimone2019\\\"] = 2,\\n    [\\\"CITEREFStrom1983\\\"] = 1,\\n    [\\\"CITEREFStromYemini,_Shaula1986\\\"] = 1,\\n    [\\\"CITEREFThe_Rust_Survey_Team2025\\\"] = 1,\\n    [\\\"CITEREFThompson2023\\\"] = 1,\\n    [\\\"CITEREFTung\\\"] = 1,\\n    [\\\"CITEREFTung2020\\\"] = 2,\\n    [\\\"CITEREFTung2021\\\"] = 1,\\n    [\\\"CITEREFVanderveldenDe_SmetDeacSteenhaut2024\\\"] = 1,\\n    [\\\"CITEREFVaughan-Nichols2021\\\"] = 3,\\n    [\\\"CITEREFVigliarolo2021\\\"] = 1,\\n    [\\\"CITEREFWallach\\\"] = 1,\\n    [\\\"CITEREFWarminsky2024\\\"] = 1,\\n    [\\\"CITEREFYegulalp2016\\\"] = 2,\\n    [\\\"CITEREFYegulalp2021\\\"] = 1,\\n    [\\\"CITEREFZhang2023\\\"] = 1,\\n}\\ntemplate_list = table#1 {\\n    [\\\"!\\\"] = 2,\\n    [\\\"As of\\\"] = 2,\\n    [\\\"Authority control\\\"] = 1,\\n    [\\\"Cite AV media\\\"] = 1,\\n    [\\\"Cite arXiv\\\"] = 1,\\n    [\\\"Cite book\\\"] = 17,\\n    [\\\"Cite conference\\\"] = 1,\\n    [\\\"Cite journal\\\"] = 10,\\n    [\\\"Cite mailing list\\\"] = 1,\\n    [\\\"Cite news\\\"] = 10,\\n    [\\\"Cite web\\\"] = 100,\\n    [\\\"Code\\\"] = 5,\\n    [\\\"Cpp\\\"] = 2,\\n    [\\\"Cslist\\\"] = 4,\\n    [\\\"Excerpt\\\"] = 1,\\n    [\\\"GitHub\\\"] = 1,\\n    [\\\"Good article\\\"] = 1,\\n    [\\\"Harvnb\\\"] = 1,\\n    [\\\"Infobox organization\\\"] = 1,\\n    [\\\"Infobox programming language\\\"] = 1,\\n    [\\\"Main\\\"] = 1,\\n    [\\\"Mono\\\"] = 1,\\n    [\\\"Mozilla\\\"] = 1,\\n    [\\\"Official website\\\"] = 1,\\n    [\\\"Portal bar\\\"] = 1,\\n    [\\\"Programming languages\\\"] = 1,\\n    [\\\"Refbegin\\\"] = 1,\\n    [\\\"Refend\\\"] = 1,\\n    [\\\"Reflist\\\"] = 2,\\n    [\\\"Refn\\\"] = 9,\\n    [\\\"Rp\\\"] = 21,\\n    [\\\"Rust\\\"] = 119,\\n    [\\\"Section link\\\"] = 1,\\n    [\\\"See also\\\"] = 1,\\n    [\\\"Sfn\\\"] = 75,\\n    [\\\"Short description\\\"] = 1,\\n    [\\\"Sister project links\\\"] = 1,\\n    [\\\"Start date and age\\\"] = 3,\\n    [\\\"URL\\\"] = 1,\\n    [\\\"Unbulleted list\\\"] = 1,\\n    [\\\"Url\\\"] = 1,\\n    [\\\"Use American English\\\"] = 1,\\n    [\\\"Use mdy dates\\\"] = 1,\\n    [\\\"Wikibooks\\\"] = 1,\\n    [\\\"Wikidata\\\"] = 2,\\n}\\narticle_whitelist = table#1 {\\n}\\nciteref_patterns = table#1 {\\n}\\n\"},\"cachereport\":{\"origin\":\"mw-api-ext.codfw.main-c88c6ffc5-lxhqv\",\"timestamp\":\"20251001042650\",\"ttl\":2592000,\"transientcontent\":false}}});});</script>\n<script type=\"application/ld+json\">{\"@context\":\"https:\\/\\/schema.org\",\"@type\":\"Article\",\"name\":\"Rust (programming language)\",\"url\":\"https:\\/\\/en.wikipedia.org\\/wiki\\/Rust_(programming_language)\",\"sameAs\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q575650\",\"mainEntity\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q575650\",\"author\":{\"@type\":\"Organization\",\"name\":\"Contributors to Wikimedia projects\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Wikimedia Foundation, Inc.\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/www.wikimedia.org\\/static\\/images\\/wmf-hor-googpub.png\"}},\"datePublished\":\"2010-10-30T22:30:54Z\",\"dateModified\":\"2025-10-01T04:26:33Z\",\"image\":\"https:\\/\\/upload.wikimedia.org\\/wikipedia\\/commons\\/d\\/d5\\/Rust_programming_language_black_logo.svg\",\"headline\":\"memory-safe programming language without garbage collection\"}</script>\n</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/wikipedia/lists_timeline.html",
    "content": "<!DOCTYPE html>\n<html class=\"client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\" lang=\"en\" dir=\"ltr\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Timeline of computing - Wikipedia</title>\n<script>(function(){var className=\"client-js vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\";var cookie=document.cookie.match(/(?:^|; )enwikimwclientpreferences=([^;]+)/);if(cookie){cookie[1].split('%2C').forEach(function(pref){className=className.replace(new RegExp('(^| )'+pref.replace(/-clientpref-\\w+$|[^\\w-]+/g,'')+'-clientpref-\\\\w+( |$)'),'$1'+pref+'$2');});}document.documentElement.className=className;}());RLCONF={\"wgBreakFrames\":false,\"wgSeparatorTransformTable\":[\"\",\"\"],\"wgDigitTransformTable\":[\"\",\"\"],\"wgDefaultDateFormat\":\"dmy\",\"wgMonthNames\":[\"\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"wgRequestId\":\"a1bc4705-bfe6-4191-bfbc-6a1a32d7956f\",\"wgCanonicalNamespace\":\"\",\"wgCanonicalSpecialPageName\":false,\"wgNamespaceNumber\":0,\"wgPageName\":\"Timeline_of_computing\",\"wgTitle\":\"Timeline of computing\",\"wgCurRevisionId\":1278630980,\"wgRevisionId\":1278630980,\"wgArticleId\":6249,\"wgIsArticle\":true,\"wgIsRedirect\":false,\"wgAction\":\"view\",\"wgUserName\":null,\"wgUserGroups\":[\"*\"],\"wgCategories\":[\"Pages using the EasyTimeline extension\",\"Articles with short description\",\"Short description is different from Wikidata\",\"Computing timelines\",\"History of computing\",\"Digital Revolution\"],\"wgPageViewLanguage\":\"en\",\"wgPageContentLanguage\":\"en\",\"wgPageContentModel\":\"wikitext\",\"wgRelevantPageName\":\"Timeline_of_computing\",\"wgRelevantArticleId\":6249,\"wgIsProbablyEditable\":true,\"wgRelevantPageIsProbablyEditable\":true,\"wgRestrictionEdit\":[],\"wgRestrictionMove\":[],\"wgNoticeProject\":\"wikipedia\",\"wgFlaggedRevsParams\":{\"tags\":{\"status\":{\"levels\":1}}},\"wgMediaViewerOnClick\":true,\"wgMediaViewerEnabledByDefault\":true,\"wgPopupsFlags\":0,\"wgVisualEditor\":{\"pageLanguageCode\":\"en\",\"pageLanguageDir\":\"ltr\",\"pageVariantFallbacks\":\"en\"},\"wgMFDisplayWikibaseDescriptions\":{\"search\":true,\"watchlist\":true,\"tagline\":false,\"nearby\":true},\"wgWMESchemaEditAttemptStepOversample\":false,\"wgWMEPageLength\":2000,\"wgMetricsPlatformUserExperiments\":{\"active_experiments\":[],\"overrides\":[],\"enrolled\":[],\"assigned\":[],\"subject_ids\":[],\"sampling_units\":[]},\"wgEditSubmitButtonLabelPublish\":true,\"wgULSPosition\":\"interlanguage\",\"wgULSisCompactLinksEnabled\":false,\"wgVector2022LanguageInHeader\":true,\"wgULSisLanguageSelectorEmpty\":false,\"wgWikibaseItemId\":\"Q2586120\",\"wgCheckUserClientHintsHeadersJsApi\":[\"brands\",\"architecture\",\"bitness\",\"fullVersionList\",\"mobile\",\"model\",\"platform\",\"platformVersion\"],\"GEHomepageSuggestedEditsEnableTopics\":true,\"wgGESuggestedEditsTaskTypes\":{\"taskTypes\":[\"copyedit\",\"link-recommendation\"],\"unavailableTaskTypes\":[]},\"wgGETopicsMatchModeEnabled\":false,\"wgGELevelingUpEnabledForUser\":false};\nRLSTATE={\"ext.globalCssJs.user.styles\":\"ready\",\"site.styles\":\"ready\",\"user.styles\":\"ready\",\"ext.globalCssJs.user\":\"ready\",\"user\":\"ready\",\"user.options\":\"loading\",\"ext.timeline.styles\":\"ready\",\"ext.wikimediamessages.styles\":\"ready\",\"skins.vector.search.codex.styles\":\"ready\",\"skins.vector.styles\":\"ready\",\"skins.vector.icons\":\"ready\",\"jquery.makeCollapsible.styles\":\"ready\",\"ext.visualEditor.desktopArticleTarget.noscript\":\"ready\",\"ext.uls.interlanguage\":\"ready\",\"wikibase.client.init\":\"ready\"};RLPAGEMODULES=[\"ext.xLab\",\"site\",\"mediawiki.page.ready\",\"jquery.makeCollapsible\",\"mediawiki.toc\",\"skins.vector.js\",\"ext.centralNotice.geoIP\",\"ext.centralNotice.startUp\",\"ext.gadget.ReferenceTooltips\",\"ext.gadget.switcher\",\"ext.urlShortener.toolbar\",\"ext.centralauth.centralautologin\",\"mmv.bootstrap\",\"ext.popups\",\"ext.visualEditor.desktopArticleTarget.init\",\"ext.visualEditor.targetLoader\",\"ext.echo.centralauth\",\"ext.eventLogging\",\"ext.wikimediaEvents\",\"ext.navigationTiming\",\"ext.uls.interface\",\"ext.cx.eventlogging.campaigns\",\"ext.cx.uls.quick.actions\",\"wikibase.client.vector-2022\",\"ext.checkUser.clientHints\",\"ext.quicksurveys.init\",\"ext.growthExperiments.SuggestedEditSession\"];</script>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return[\"user.options@12s5i\",function($,jQuery,require,module){mw.user.tokens.set({\"patrolToken\":\"+\\\\\",\"watchToken\":\"+\\\\\",\"csrfToken\":\"+\\\\\"});\n}];});});</script>\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=ext.timeline.styles%7Cext.uls.interlanguage%7Cext.visualEditor.desktopArticleTarget.noscript%7Cext.wikimediamessages.styles%7Cjquery.makeCollapsible.styles%7Cskins.vector.icons%2Cstyles%7Cskins.vector.search.codex.styles%7Cwikibase.client.init&amp;only=styles&amp;skin=vector-2022\">\n<script async=\"\" src=\"/w/load.php?lang=en&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;skin=vector-2022\"></script>\n<meta name=\"ResourceLoaderDynamicStyles\" content=\"\">\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=vector-2022\">\n<meta name=\"generator\" content=\"MediaWiki 1.45.0-wmf.20\">\n<meta name=\"referrer\" content=\"origin\">\n<meta name=\"referrer\" content=\"origin-when-cross-origin\">\n<meta name=\"robots\" content=\"max-image-preview:standard\">\n<meta name=\"format-detection\" content=\"telephone=no\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg/1200px-Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"917\">\n<meta name=\"viewport\" content=\"width=1120\">\n<meta property=\"og:title\" content=\"Timeline of computing - Wikipedia\">\n<meta property=\"og:type\" content=\"website\">\n<link rel=\"preconnect\" href=\"//upload.wikimedia.org\">\n<link rel=\"alternate\" media=\"only screen and (max-width: 640px)\" href=\"//en.m.wikipedia.org/wiki/Timeline_of_computing\">\n<link rel=\"alternate\" type=\"application/x-wiki\" title=\"Edit this page\" href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit\">\n<link rel=\"apple-touch-icon\" href=\"/static/apple-touch/wikipedia.png\">\n<link rel=\"icon\" href=\"/static/favicon/wikipedia.ico\">\n<link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"/w/rest.php/v1/search\" title=\"Wikipedia (en)\">\n<link rel=\"EditURI\" type=\"application/rsd+xml\" href=\"//en.wikipedia.org/w/api.php?action=rsd\">\n<link rel=\"canonical\" href=\"https://en.wikipedia.org/wiki/Timeline_of_computing\">\n<link rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/4.0/deed.en\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Wikipedia Atom feed\" href=\"/w/index.php?title=Special:RecentChanges&amp;feed=atom\">\n<link rel=\"dns-prefetch\" href=\"//meta.wikimedia.org\" />\n<link rel=\"dns-prefetch\" href=\"auth.wikimedia.org\">\n</head>\n<body class=\"skin--responsive skin-vector skin-vector-search-vue mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject mw-editable page-Timeline_of_computing rootpage-Timeline_of_computing skin-vector-2022 action-view\"><a class=\"mw-jump-link\" href=\"#bodyContent\">Jump to content</a>\n<div class=\"vector-header-container\">\n\t<header class=\"vector-header mw-header no-font-mode-scale\">\n\t\t<div class=\"vector-header-start\">\n\t\t\t<nav class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\n<div id=\"vector-main-menu-dropdown\" class=\"vector-dropdown vector-main-menu-dropdown vector-button-flush-left vector-button-flush-right\"  title=\"Main menu\" >\n\t<input type=\"checkbox\" id=\"vector-main-menu-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-main-menu-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Main menu\"  >\n\t<label id=\"vector-main-menu-dropdown-label\" for=\"vector-main-menu-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu\"></span>\n\n<span class=\"vector-dropdown-label-text\">Main menu</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t<div id=\"vector-main-menu-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-main-menu\" class=\"vector-main-menu vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-main-menu-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"main-menu-pinned\"\n\tdata-pinnable-element-id=\"vector-main-menu\"\n\tdata-pinned-container-id=\"vector-main-menu-pinned-container\"\n\tdata-unpinned-container-id=\"vector-main-menu-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Main menu</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-main-menu.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-main-menu.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-navigation\" class=\"vector-menu mw-portlet mw-portlet-navigation\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tNavigation\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-mainpage-description\" class=\"mw-list-item\"><a href=\"/wiki/Main_Page\" title=\"Visit the main page [z]\" accesskey=\"z\"><span>Main page</span></a></li><li id=\"n-contents\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Contents\" title=\"Guides to browsing Wikipedia\"><span>Contents</span></a></li><li id=\"n-currentevents\" class=\"mw-list-item\"><a href=\"/wiki/Portal:Current_events\" title=\"Articles related to current events\"><span>Current events</span></a></li><li id=\"n-randompage\" class=\"mw-list-item\"><a href=\"/wiki/Special:Random\" title=\"Visit a randomly selected article [x]\" accesskey=\"x\"><span>Random article</span></a></li><li id=\"n-aboutsite\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:About\" title=\"Learn about Wikipedia and how it works\"><span>About Wikipedia</span></a></li><li id=\"n-contactpage\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\" title=\"How to contact Wikipedia\"><span>Contact us</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\n<div id=\"p-interaction\" class=\"vector-menu mw-portlet mw-portlet-interaction\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tContribute\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-help\" class=\"mw-list-item\"><a href=\"/wiki/Help:Contents\" title=\"Guidance on how to use and edit Wikipedia\"><span>Help</span></a></li><li id=\"n-introduction\" class=\"mw-list-item\"><a href=\"/wiki/Help:Introduction\" title=\"Learn how to edit Wikipedia\"><span>Learn to edit</span></a></li><li id=\"n-portal\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Community_portal\" title=\"The hub for editors\"><span>Community portal</span></a></li><li id=\"n-recentchanges\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChanges\" title=\"A list of recent changes to Wikipedia [r]\" accesskey=\"r\"><span>Recent changes</span></a></li><li id=\"n-upload\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:File_upload_wizard\" title=\"Add images or other media for use on Wikipedia\"><span>Upload file</span></a></li><li id=\"n-specialpages\" class=\"mw-list-item\"><a href=\"/wiki/Special:SpecialPages\"><span>Special pages</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t</nav>\n\n<a href=\"/wiki/Main_Page\" class=\"mw-logo\">\n\t<img class=\"mw-logo-icon\" src=\"/static/images/icons/wikipedia.png\" alt=\"\" aria-hidden=\"true\" height=\"50\" width=\"50\">\n\t<span class=\"mw-logo-container skin-invert\">\n\t\t<img class=\"mw-logo-wordmark\" alt=\"Wikipedia\" src=\"/static/images/mobile/copyright/wikipedia-wordmark-en.svg\" style=\"width: 7.5em; height: 1.125em;\">\n\t\t<img class=\"mw-logo-tagline\" alt=\"The Free Encyclopedia\" src=\"/static/images/mobile/copyright/wikipedia-tagline-en.svg\" width=\"117\" height=\"13\" style=\"width: 7.3125em; height: 0.8125em;\">\n\t</span>\n</a>\n\n\t\t</div>\n\t\t<div class=\"vector-header-end\">\n\n<div id=\"p-search\" role=\"search\" class=\"vector-search-box-vue  vector-search-box-collapses vector-search-box-show-thumbnail vector-search-box-auto-expand-width vector-search-box\">\n\t<a href=\"/wiki/Special:Search\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only search-toggle\" title=\"Search Wikipedia [f]\" accesskey=\"f\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t</a>\n\t<div class=\"vector-typeahead-search-container\">\n\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail cdx-typeahead-search--auto-expand-width\">\n\t\t\t<form action=\"/w/index.php\" id=\"searchform\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t<div id=\"simpleSearch\" class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\t\t\t\t\t\t\t type=\"search\" name=\"search\" placeholder=\"Search Wikipedia\" aria-label=\"Search Wikipedia\" autocapitalize=\"sentences\" spellcheck=\"false\" title=\"Search Wikipedia [f]\" accesskey=\"f\" id=\"searchInput\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t</div>\n\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n\t\t\t<nav class=\"vector-user-links vector-user-links-wide\" aria-label=\"Personal tools\">\n\t<div class=\"vector-user-links-main\">\n\n<div id=\"p-vector-user-menu-preferences\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-userpage\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\n<div id=\"vector-appearance-dropdown\" class=\"vector-dropdown \"  title=\"Change the appearance of the page&#039;s font size, width, and color\" >\n\t<input type=\"checkbox\" id=\"vector-appearance-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-appearance-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Appearance\"  >\n\t<label id=\"vector-appearance-dropdown-label\" for=\"vector-appearance-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-appearance mw-ui-icon-wikimedia-appearance\"></span>\n\n<span class=\"vector-dropdown-label-text\">Appearance</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t<div id=\"vector-appearance-unpinned-container\" class=\"vector-unpinned-container\">\n\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t</nav>\n\n<div id=\"p-vector-user-menu-notifications\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-overflow\" class=\"vector-menu mw-portlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\t\t\t<li id=\"pt-sitesupport-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\" class=\"\"><span>Donate</span></a>\n</li>\n<li id=\"pt-createaccount-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Timeline+of+computing\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\" class=\"\"><span>Create account</span></a>\n</li>\n<li id=\"pt-login-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Timeline+of+computing\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\" class=\"\"><span>Log in</span></a>\n</li>\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t</div>\n\n<div id=\"vector-user-links-dropdown\" class=\"vector-dropdown vector-user-menu vector-button-flush-right vector-user-menu-logged-out\"  title=\"Log in and more options\" >\n\t<input type=\"checkbox\" id=\"vector-user-links-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-user-links-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Personal tools\"  >\n\t<label id=\"vector-user-links-dropdown-label\" for=\"vector-user-links-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-ellipsis mw-ui-icon-wikimedia-ellipsis\"></span>\n\n<span class=\"vector-dropdown-label-text\">Personal tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-personal\" class=\"vector-menu mw-portlet mw-portlet-personal user-links-collapsible-item\"  title=\"User menu\" >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-sitesupport\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\"><span>Donate</span></a></li><li id=\"pt-createaccount\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Timeline+of+computing\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\"><span class=\"vector-icon mw-ui-icon-userAdd mw-ui-icon-wikimedia-userAdd\"></span> <span>Create account</span></a></li><li id=\"pt-login\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Timeline+of+computing\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\"><span class=\"vector-icon mw-ui-icon-logIn mw-ui-icon-wikimedia-logIn\"></span> <span>Log in</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-user-menu-anon-editor\" class=\"vector-menu mw-portlet mw-portlet-user-menu-anon-editor\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPages for logged out editors <a href=\"/wiki/Help:Introduction\" aria-label=\"Learn more about editing\"><span>learn more</span></a>\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-anoncontribs\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyContributions\" title=\"A list of edits made from this IP address [y]\" accesskey=\"y\"><span>Contributions</span></a></li><li id=\"pt-anontalk\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyTalk\" title=\"Discussion about edits from this IP address [n]\" accesskey=\"n\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n</nav>\n\n\t\t</div>\n\t</header>\n</div>\n<div class=\"mw-page-container\">\n\t<div class=\"mw-page-container-inner\">\n\t\t<div class=\"vector-sitenotice-container\">\n\t\t\t<div id=\"siteNotice\"><!-- CentralNotice --></div>\n\t\t</div>\n\t\t<div class=\"vector-column-start\">\n\t\t\t<div class=\"vector-main-menu-container\">\n\t\t<div id=\"mw-navigation\">\n\t\t\t<nav id=\"mw-panel\" class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\t\t\t\t<div id=\"vector-main-menu-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t</div>\n\t\t</nav>\n\t\t</div>\n\t</div>\n\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t<nav id=\"mw-panel-toc\" aria-label=\"Contents\" data-event-name=\"ui.sidebar-toc\" class=\"mw-table-of-contents-container vector-toc-landmark\">\n\t\t\t\t\t<div id=\"vector-toc-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t\t<div id=\"vector-toc\" class=\"vector-toc vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"toc-pinned\"\n\tdata-pinnable-element-id=\"vector-toc\"\n\n\n>\n\t<h2 class=\"vector-pinnable-header-label\">Contents</h2>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-toc.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-toc.unpin\">hide</button>\n</div>\n\n\n\t<ul class=\"vector-toc-contents\" id=\"mw-panel-toc-list\">\n\t\t<li id=\"toc-mw-content-text\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t\t<a href=\"#\" class=\"vector-toc-link\">\n\t\t\t\t<div class=\"vector-toc-text\">(Top)</div>\n\t\t\t</a>\n\t\t</li>\n\t\t<li id=\"toc-Graphical_timeline\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#Graphical_timeline\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">1</span>\n\t\t\t\t<span>Graphical timeline</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Graphical_timeline-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-See_also\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#See_also\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">2</span>\n\t\t\t\t<span>See also</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-See_also-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Resources\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#Resources\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">3</span>\n\t\t\t\t<span>Resources</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Resources-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-External_links\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">4</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n</ul>\n</div>\n\n\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mw-content-container\">\n\t\t\t<main id=\"content\" class=\"mw-body\">\n\t\t\t\t<header class=\"mw-body-header vector-page-titlebar no-font-mode-scale\">\n\t\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n<div id=\"vector-page-titlebar-toc\" class=\"vector-dropdown vector-page-titlebar-toc vector-button-flush-left\"  title=\"Table of Contents\" >\n\t<input type=\"checkbox\" id=\"vector-page-titlebar-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-titlebar-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t<label id=\"vector-page-titlebar-toc-label\" for=\"vector-page-titlebar-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t<div id=\"vector-page-titlebar-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t</nav>\n\t\t\t\t\t<h1 id=\"firstHeading\" class=\"firstHeading mw-first-heading\"><span class=\"mw-page-title-main\">Timeline of computing</span></h1>\n\n<div id=\"p-lang-btn\" class=\"vector-dropdown mw-portlet mw-portlet-lang\"  >\n\t<input type=\"checkbox\" id=\"p-lang-btn-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-p-lang-btn\" class=\"vector-dropdown-checkbox mw-interlanguage-selector\" aria-label=\"Go to an article in another language. Available in 7 languages\"   >\n\t<label id=\"p-lang-btn-label\" for=\"p-lang-btn-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive mw-portlet-lang-heading-7\" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-language-progressive mw-ui-icon-wikimedia-language-progressive\"></span>\n\n<span class=\"vector-dropdown-label-text\">7 languages</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\t\t<div class=\"vector-menu-content\">\n\n\t\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t\t<li class=\"interlanguage-link interwiki-ar mw-list-item\"><a href=\"https://ar.wikipedia.org/wiki/%D8%AE%D8%B7_%D8%B2%D9%85%D9%86%D9%8A_%D9%84%D9%84%D8%AD%D9%88%D8%B3%D8%A8%D8%A9\" title=\"خط زمني للحوسبة – Arabic\" lang=\"ar\" hreflang=\"ar\" data-title=\"خط زمني للحوسبة\" data-language-autonym=\"العربية\" data-language-local-name=\"Arabic\" class=\"interlanguage-link-target\"><span>العربية</span></a></li><li class=\"interlanguage-link interwiki-bs mw-list-item\"><a href=\"https://bs.wikipedia.org/wiki/Hronologija_historije_ra%C4%8Dunarstva\" title=\"Hronologija historije računarstva – Bosnian\" lang=\"bs\" hreflang=\"bs\" data-title=\"Hronologija historije računarstva\" data-language-autonym=\"Bosanski\" data-language-local-name=\"Bosnian\" class=\"interlanguage-link-target\"><span>Bosanski</span></a></li><li class=\"interlanguage-link interwiki-fa mw-list-item\"><a href=\"https://fa.wikipedia.org/wiki/%D8%B3%D8%A7%D9%84%E2%80%8C%D8%B4%D9%85%D8%A7%D8%B1_%D9%81%D9%86_%D9%85%D8%AD%D8%A7%D8%B3%D8%A8%D9%87\" title=\"سال‌شمار فن محاسبه – Persian\" lang=\"fa\" hreflang=\"fa\" data-title=\"سال‌شمار فن محاسبه\" data-language-autonym=\"فارسی\" data-language-local-name=\"Persian\" class=\"interlanguage-link-target\"><span>فارسی</span></a></li><li class=\"interlanguage-link interwiki-fr mw-list-item\"><a href=\"https://fr.wikipedia.org/wiki/Chronologie_de_l%27informatique\" title=\"Chronologie de l&#039;informatique – French\" lang=\"fr\" hreflang=\"fr\" data-title=\"Chronologie de l&#039;informatique\" data-language-autonym=\"Français\" data-language-local-name=\"French\" class=\"interlanguage-link-target\"><span>Français</span></a></li><li class=\"interlanguage-link interwiki-lv mw-list-item\"><a href=\"https://lv.wikipedia.org/wiki/Datoru_att%C4%ABst%C4%ABbas_hronolo%C4%A3ija\" title=\"Datoru attīstības hronoloģija – Latvian\" lang=\"lv\" hreflang=\"lv\" data-title=\"Datoru attīstības hronoloģija\" data-language-autonym=\"Latviešu\" data-language-local-name=\"Latvian\" class=\"interlanguage-link-target\"><span>Latviešu</span></a></li><li class=\"interlanguage-link interwiki-ru mw-list-item\"><a href=\"https://ru.wikipedia.org/wiki/%D0%A5%D1%80%D0%BE%D0%BD%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F_%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D1%8F_%D0%B2%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D0%BA%D0%B8\" title=\"Хронология развития вычислительной техники – Russian\" lang=\"ru\" hreflang=\"ru\" data-title=\"Хронология развития вычислительной техники\" data-language-autonym=\"Русский\" data-language-local-name=\"Russian\" class=\"interlanguage-link-target\"><span>Русский</span></a></li><li class=\"interlanguage-link interwiki-tr mw-list-item\"><a href=\"https://tr.wikipedia.org/wiki/Bilgisayar%C4%B1n_zaman_%C3%A7izelgesi\" title=\"Bilgisayarın zaman çizelgesi – Turkish\" lang=\"tr\" hreflang=\"tr\" data-title=\"Bilgisayarın zaman çizelgesi\" data-language-autonym=\"Türkçe\" data-language-local-name=\"Turkish\" class=\"interlanguage-link-target\"><span>Türkçe</span></a></li>\n\t\t\t</ul>\n\t\t\t<div class=\"after-portlet after-portlet-lang\"><span class=\"wb-langlinks-edit wb-langlinks-link\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q2586120#sitelinks-wikipedia\" title=\"Edit interlanguage links\" class=\"wbc-editpage\">Edit links</a></span></div>\n\t\t</div>\n\n\t</div>\n</div>\n</header>\n\t\t\t\t<div class=\"vector-page-toolbar vector-feature-custom-font-size-clientpref--excluded\">\n\t\t\t\t\t<div class=\"vector-page-toolbar-container\">\n\t\t\t\t\t\t<div id=\"left-navigation\">\n\t\t\t\t\t\t\t<nav aria-label=\"Namespaces\">\n\n<div id=\"p-associated-pages\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-associated-pages\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-nstab-main\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Timeline_of_computing\" title=\"View the content page [c]\" accesskey=\"c\"><span>Article</span></a></li><li id=\"ca-talk\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/wiki/Talk:Timeline_of_computing\" rel=\"discussion\" title=\"Discuss improvements to the content page [t]\" accesskey=\"t\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"vector-variants-dropdown\" class=\"vector-dropdown emptyPortlet\"  >\n\t<input type=\"checkbox\" id=\"vector-variants-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-variants-dropdown\" class=\"vector-dropdown-checkbox \" aria-label=\"Change language variant\"   >\n\t<label id=\"vector-variants-dropdown-label\" for=\"vector-variants-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">English</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-variants\" class=\"vector-menu mw-portlet mw-portlet-variants emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div id=\"right-navigation\" class=\"vector-collapsible\">\n\t\t\t\t\t\t\t<nav aria-label=\"Views\">\n\n<div id=\"p-views\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-views\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-view\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Timeline_of_computing\"><span>Read</span></a></li><li id=\"ca-edit\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-history\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=history\" title=\"Past revisions of this page [h]\" accesskey=\"h\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\n<div id=\"vector-page-tools-dropdown\" class=\"vector-dropdown vector-page-tools-dropdown\"  >\n\t<input type=\"checkbox\" id=\"vector-page-tools-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-tools-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Tools\"  >\n\t<label id=\"vector-page-tools-dropdown-label\" for=\"vector-page-tools-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">Tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t\t\t<div id=\"vector-page-tools-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-page-tools\" class=\"vector-page-tools vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-page-tools-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"page-tools-pinned\"\n\tdata-pinnable-element-id=\"vector-page-tools\"\n\tdata-pinned-container-id=\"vector-page-tools-pinned-container\"\n\tdata-unpinned-container-id=\"vector-page-tools-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Tools</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-page-tools.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-page-tools.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-cactions\" class=\"vector-menu mw-portlet mw-portlet-cactions emptyPortlet vector-has-collapsible-items\"  title=\"More options\" >\n\t<div class=\"vector-menu-heading\">\n\t\tActions\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-more-view\" class=\"selected vector-more-collapsible-item mw-list-item\"><a href=\"/wiki/Timeline_of_computing\"><span>Read</span></a></li><li id=\"ca-more-edit\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-more-history\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=history\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-tb\" class=\"vector-menu mw-portlet mw-portlet-tb\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tGeneral\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-whatlinkshere\" class=\"mw-list-item\"><a href=\"/wiki/Special:WhatLinksHere/Timeline_of_computing\" title=\"List of all English Wikipedia pages containing links to this page [j]\" accesskey=\"j\"><span>What links here</span></a></li><li id=\"t-recentchangeslinked\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChangesLinked/Timeline_of_computing\" rel=\"nofollow\" title=\"Recent changes in pages linked from this page [k]\" accesskey=\"k\"><span>Related changes</span></a></li><li id=\"t-upload\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:File_Upload_Wizard\" title=\"Upload files [u]\" accesskey=\"u\"><span>Upload file</span></a></li><li id=\"t-permalink\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;oldid=1278630980\" title=\"Permanent link to this revision of this page\"><span>Permanent link</span></a></li><li id=\"t-info\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=info\" title=\"More information about this page\"><span>Page information</span></a></li><li id=\"t-cite\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:CiteThisPage&amp;page=Timeline_of_computing&amp;id=1278630980&amp;wpFormIdentifier=titleform\" title=\"Information on how to cite this page\"><span>Cite this page</span></a></li><li id=\"t-urlshortener\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:UrlShortener&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FTimeline_of_computing\"><span>Get shortened URL</span></a></li><li id=\"t-urlshortener-qrcode\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:QrCode&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FTimeline_of_computing\"><span>Download QR code</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-coll-print_export\" class=\"vector-menu mw-portlet mw-portlet-coll-print_export\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPrint/export\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"coll-download-as-rl\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:DownloadAsPdf&amp;page=Timeline_of_computing&amp;action=show-download-screen\" title=\"Download this page as a PDF file\"><span>Download as PDF</span></a></li><li id=\"t-print\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Timeline_of_computing&amp;printable=yes\" title=\"Printable version of this page [p]\" accesskey=\"p\"><span>Printable version</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-wikibase-otherprojects\" class=\"vector-menu mw-portlet mw-portlet-wikibase-otherprojects\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tIn other projects\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-wikibase\" class=\"wb-otherproject-link wb-otherproject-wikibase-dataitem mw-list-item\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q2586120\" title=\"Structured data on this page hosted by Wikidata [g]\" accesskey=\"g\"><span>Wikidata item</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"vector-column-end no-font-mode-scale\">\n\t\t\t\t\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\t\t\t\t\t\t\t<div id=\"vector-page-tools-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\t\t\t\t\t\t\t<div id=\"vector-appearance-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t<div id=\"vector-appearance\" class=\"vector-appearance vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-appearance-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"appearance-pinned\"\n\tdata-pinnable-element-id=\"vector-appearance\"\n\tdata-pinned-container-id=\"vector-appearance-pinned-container\"\n\tdata-unpinned-container-id=\"vector-appearance-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Appearance</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-appearance.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-appearance.unpin\">hide</button>\n</div>\n\n\n</div>\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"bodyContent\" class=\"vector-body\" aria-labelledby=\"firstHeading\" data-mw-ve-target-container>\n\t\t\t\t\t<div class=\"vector-body-before-content\">\n\t\t\t\t\t\t\t<div class=\"mw-indicators\">\n\t\t</div>\n\n\t\t\t\t\t\t<div id=\"siteSub\" class=\"noprint\">From Wikipedia, the free encyclopedia</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"contentSub\"><div id=\"mw-content-subtitle\"></div></div>\n\n\n\t\t\t\t\t<div id=\"mw-content-text\" class=\"mw-body-content\"><div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><p class=\"mw-empty-elt\">\n</p>\n<style data-mw-deduplicate=\"TemplateStyles:r1129693374\">.mw-parser-output .hlist dl,.mw-parser-output .hlist ol,.mw-parser-output .hlist ul{margin:0;padding:0}.mw-parser-output .hlist dd,.mw-parser-output .hlist dt,.mw-parser-output .hlist li{margin:0;display:inline}.mw-parser-output .hlist.inline,.mw-parser-output .hlist.inline dl,.mw-parser-output .hlist.inline ol,.mw-parser-output .hlist.inline ul,.mw-parser-output .hlist dl dl,.mw-parser-output .hlist dl ol,.mw-parser-output .hlist dl ul,.mw-parser-output .hlist ol dl,.mw-parser-output .hlist ol ol,.mw-parser-output .hlist ol ul,.mw-parser-output .hlist ul dl,.mw-parser-output .hlist ul ol,.mw-parser-output .hlist ul ul{display:inline}.mw-parser-output .hlist .mw-empty-li{display:none}.mw-parser-output .hlist dt::after{content:\": \"}.mw-parser-output .hlist dd::after,.mw-parser-output .hlist li::after{content:\" · \";font-weight:bold}.mw-parser-output .hlist dd:last-child::after,.mw-parser-output .hlist dt:last-child::after,.mw-parser-output .hlist li:last-child::after{content:none}.mw-parser-output .hlist dd dd:first-child::before,.mw-parser-output .hlist dd dt:first-child::before,.mw-parser-output .hlist dd li:first-child::before,.mw-parser-output .hlist dt dd:first-child::before,.mw-parser-output .hlist dt dt:first-child::before,.mw-parser-output .hlist dt li:first-child::before,.mw-parser-output .hlist li dd:first-child::before,.mw-parser-output .hlist li dt:first-child::before,.mw-parser-output .hlist li li:first-child::before{content:\" (\";font-weight:normal}.mw-parser-output .hlist dd dd:last-child::after,.mw-parser-output .hlist dd dt:last-child::after,.mw-parser-output .hlist dd li:last-child::after,.mw-parser-output .hlist dt dd:last-child::after,.mw-parser-output .hlist dt dt:last-child::after,.mw-parser-output .hlist dt li:last-child::after,.mw-parser-output .hlist li dd:last-child::after,.mw-parser-output .hlist li dt:last-child::after,.mw-parser-output .hlist li li:last-child::after{content:\")\";font-weight:normal}.mw-parser-output .hlist ol{counter-reset:listitem}.mw-parser-output .hlist ol>li{counter-increment:listitem}.mw-parser-output .hlist ol>li::before{content:\" \"counter(listitem)\"\\a0 \"}.mw-parser-output .hlist dd ol>li:first-child::before,.mw-parser-output .hlist dt ol>li:first-child::before,.mw-parser-output .hlist li ol>li:first-child::before{content:\" (\"counter(listitem)\"\\a0 \"}</style><style data-mw-deduplicate=\"TemplateStyles:r1246091330\">.mw-parser-output .sidebar{width:22em;float:right;clear:right;margin:0.5em 0 1em 1em;background:var(--background-color-neutral-subtle,#f8f9fa);border:1px solid var(--border-color-base,#a2a9b1);padding:0.2em;text-align:center;line-height:1.4em;font-size:88%;border-collapse:collapse;display:table}body.skin-minerva .mw-parser-output .sidebar{display:table!important;float:right!important;margin:0.5em 0 1em 1em!important}.mw-parser-output .sidebar-subgroup{width:100%;margin:0;border-spacing:0}.mw-parser-output .sidebar-left{float:left;clear:left;margin:0.5em 1em 1em 0}.mw-parser-output .sidebar-none{float:none;clear:both;margin:0.5em 1em 1em 0}.mw-parser-output .sidebar-outer-title{padding:0 0.4em 0.2em;font-size:125%;line-height:1.2em;font-weight:bold}.mw-parser-output .sidebar-top-image{padding:0.4em}.mw-parser-output .sidebar-top-caption,.mw-parser-output .sidebar-pretitle-with-top-image,.mw-parser-output .sidebar-caption{padding:0.2em 0.4em 0;line-height:1.2em}.mw-parser-output .sidebar-pretitle{padding:0.4em 0.4em 0;line-height:1.2em}.mw-parser-output .sidebar-title,.mw-parser-output .sidebar-title-with-pretitle{padding:0.2em 0.8em;font-size:145%;line-height:1.2em}.mw-parser-output .sidebar-title-with-pretitle{padding:0.1em 0.4em}.mw-parser-output .sidebar-image{padding:0.2em 0.4em 0.4em}.mw-parser-output .sidebar-heading{padding:0.1em 0.4em}.mw-parser-output .sidebar-content{padding:0 0.5em 0.4em}.mw-parser-output .sidebar-content-with-subgroup{padding:0.1em 0.4em 0.2em}.mw-parser-output .sidebar-above,.mw-parser-output .sidebar-below{padding:0.3em 0.8em;font-weight:bold}.mw-parser-output .sidebar-collapse .sidebar-above,.mw-parser-output .sidebar-collapse .sidebar-below{border-top:1px solid #aaa;border-bottom:1px solid #aaa}.mw-parser-output .sidebar-navbar{text-align:right;font-size:115%;padding:0 0.4em 0.4em}.mw-parser-output .sidebar-list-title{padding:0 0.4em;text-align:left;font-weight:bold;line-height:1.6em;font-size:105%}.mw-parser-output .sidebar-list-title-c{padding:0 0.4em;text-align:center;margin:0 3.3em}@media(max-width:640px){body.mediawiki .mw-parser-output .sidebar{width:100%!important;clear:both;float:none!important;margin-left:0!important;margin-right:0!important}}body.skin--responsive .mw-parser-output .sidebar a>img{max-width:none!important}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-list-title,html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle{background:transparent!important}html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle a{color:var(--color-progressive)!important}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-list-title,html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle{background:transparent!important}html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle a{color:var(--color-progressive)!important}}@media print{body.ns-0 .mw-parser-output .sidebar{display:none!important}}</style><table class=\"sidebar nomobile nowraplinks hlist\"><tbody><tr><th class=\"sidebar-title\" style=\"background:#ccccff\"><a href=\"/wiki/History_of_computing\" title=\"History of computing\">History of computing</a></th></tr><tr><td class=\"sidebar-image\"><figure class=\"mw-halign-center\" typeof=\"mw:File\"><a href=\"/wiki/File:Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg/250px-Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg\" decoding=\"async\" width=\"250\" height=\"191\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg/500px-Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg 1.5x\" data-file-width=\"1340\" data-file-height=\"1024\" /></a><figcaption></figcaption></figure></td></tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\n<a href=\"/wiki/Computer_hardware\" title=\"Computer hardware\">Hardware</a></th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/History_of_computing_hardware_(1960s%E2%80%93present)\" title=\"History of computing hardware (1960s–present)\">Hardware 1960s to present</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\n<a href=\"/wiki/Software\" title=\"Software\">Software</a></th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/History_of_software\" title=\"History of software\">Software</a></li>\n<li><a href=\"/wiki/History_of_software_configuration_management\" title=\"History of software configuration management\">Software configuration management</a></li>\n<li><a href=\"/wiki/History_of_Unix\" title=\"History of Unix\">Unix</a></li>\n<li><a href=\"/wiki/History_of_free_and_open-source_software\" title=\"History of free and open-source software\">Free and open-source software</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\n<a href=\"/wiki/Computer_science\" title=\"Computer science\">Computer science</a></th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/History_of_artificial_intelligence\" title=\"History of artificial intelligence\">Artificial intelligence</a></li>\n<li><a href=\"/wiki/History_of_compiler_construction\" title=\"History of compiler construction\">Compiler construction</a></li>\n<li><a href=\"/wiki/History_of_computer_science\" title=\"History of computer science\">Early computer science</a></li>\n<li><a href=\"/wiki/History_of_operating_systems\" title=\"History of operating systems\">Operating systems</a></li>\n<li><a href=\"/wiki/History_of_programming_languages\" title=\"History of programming languages\">Programming languages</a></li>\n<li><a href=\"/wiki/List_of_pioneers_in_computer_science\" title=\"List of pioneers in computer science\">Prominent pioneers</a></li>\n<li><a href=\"/wiki/History_of_software_engineering\" title=\"History of software engineering\">Software engineering</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\nModern concepts</th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/History_of_general-purpose_CPUs\" title=\"History of general-purpose CPUs\">General-purpose CPUs</a></li>\n<li><a href=\"/wiki/History_of_the_graphical_user_interface\" title=\"History of the graphical user interface\">Graphical user interface</a></li>\n<li><a href=\"/wiki/History_of_the_Internet\" title=\"History of the Internet\">Internet</a></li>\n<li><a href=\"/wiki/History_of_laptops\" title=\"History of laptops\">Laptops</a></li>\n<li><a href=\"/wiki/History_of_personal_computers\" title=\"History of personal computers\">Personal computers</a></li>\n<li><a href=\"/wiki/History_of_video_games\" title=\"History of video games\">Video games</a></li>\n<li><a href=\"/wiki/History_of_the_World_Wide_Web\" title=\"History of the World Wide Web\">World Wide Web</a></li>\n<li><a href=\"/wiki/History_of_cloud_computing\" title=\"History of cloud computing\">Cloud</a></li>\n<li><a href=\"/wiki/Timeline_of_quantum_computing_and_communication\" title=\"Timeline of quantum computing and communication\">Quantum</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\nBy country</th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/History_of_computer_hardware_in_Bulgaria\" title=\"History of computer hardware in Bulgaria\">Bulgaria</a></li>\n<li><a href=\"/wiki/History_of_computer_hardware_in_Eastern_Bloc_countries\" title=\"History of computer hardware in Eastern Bloc countries\">Eastern Bloc</a></li>\n<li><a href=\"/wiki/History_of_computing_in_Poland\" title=\"History of computing in Poland\">Poland</a></li>\n<li><a href=\"/wiki/History_of_computing_in_Romania\" title=\"History of computing in Romania\">Romania</a></li>\n<li><a href=\"/wiki/History_of_computing_in_South_America\" title=\"History of computing in South America\">South America</a></li>\n<li><a href=\"/wiki/History_of_computing_in_the_Soviet_Union\" title=\"History of computing in the Soviet Union\">Soviet Union</a></li>\n<li><a href=\"/wiki/History_of_computer_hardware_in_Yugoslavia\" title=\"History of computer hardware in Yugoslavia\">Yugoslavia</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\n<a class=\"mw-selflink selflink\">Timeline of computing</a></th></tr><tr><td class=\"sidebar-content\" style=\"padding-top:0.2em;padding-bottom:0.4em;\">\n<ul><li><a href=\"/wiki/Timeline_of_computing_hardware_before_1950\" title=\"Timeline of computing hardware before 1950\">before 1950</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1950%E2%80%931979\" title=\"Timeline of computing 1950–1979\">1950–1979</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1980%E2%80%931989\" title=\"Timeline of computing 1980–1989\">1980–1989</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1990%E2%80%931999\" title=\"Timeline of computing 1990–1999\">1990–1999</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2000%E2%80%932009\" title=\"Timeline of computing 2000–2009\">2000–2009</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2010%E2%80%932019\" title=\"Timeline of computing 2010–2019\">2010–2019</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2020%E2%80%93present\" title=\"Timeline of computing 2020–present\">2020–present</a></li>\n<li><a href=\"/wiki/Category:Computing_timelines\" title=\"Category:Computing timelines\"><i>more timelines</i> ...</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\" style=\"background:#ddddff;\">\n<a href=\"/wiki/Glossary_of_computer_science\" title=\"Glossary of computer science\">Glossary of computer science</a></th></tr><tr><td class=\"sidebar-below\" style=\"border-top:1px solid #aaa;border-bottom:1px solid #aaa;\">\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"Category\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/20px-Symbol_category_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/40px-Symbol_category_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <a href=\"/wiki/Category:History_of_computing\" title=\"Category:History of computing\">Category</a></li></ul></td></tr><tr><td class=\"sidebar-navbar\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1239400231\">.mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}</style><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:History_of_computing\" title=\"Template:History of computing\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:History_of_computing\" title=\"Template talk:History of computing\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:History_of_computing\" title=\"Special:EditPage/Template:History of computing\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div></td></tr></tbody></table>\n<p><b>Timeline of computing</b> presents events in the history of computing organized by year and grouped into six topic areas: predictions and concepts, first use and inventions, hardware systems and processors, operating systems, programming languages, and new application areas.\n</p><p><b>Detailed computing timelines</b>: <a href=\"/wiki/Timeline_of_computing_2400_BC-1949\" class=\"mw-redirect\" title=\"Timeline of computing 2400 BC-1949\">before 1950</a>, <a href=\"/wiki/Timeline_of_computing_1950%E2%80%931979\" title=\"Timeline of computing 1950–1979\">1950–1979</a>, <a href=\"/wiki/Timeline_of_computing_1980%E2%80%931989\" title=\"Timeline of computing 1980–1989\">1980–1989</a>, <a href=\"/wiki/Timeline_of_computing_1990%E2%80%931999\" title=\"Timeline of computing 1990–1999\">1990–1999</a>, <a href=\"/wiki/Timeline_of_computing_2000%E2%80%932009\" title=\"Timeline of computing 2000–2009\">2000–2009</a>, <a href=\"/wiki/Timeline_of_computing_2010%E2%80%932019\" title=\"Timeline of computing 2010–2019\">2010–2019</a>, <a href=\"/wiki/Timeline_of_computing_2020%E2%80%93present\" title=\"Timeline of computing 2020–present\">2020–present</a>\n</p>\n<meta property=\"mw:PageProp/toc\" />\n<div style=\"clear:both;\" class=\"\"></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Graphical_timeline\">Graphical timeline</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit&amp;section=1\" title=\"Edit section: Graphical timeline\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<div class=\"timeline-wrapper\"><map name=\"timeline_dvng9uthgiznl8kgo84uz9w8dcs2l9r\"><area href=\"/wiki/Video_hosting_service\" coords=\"996,1862,1114,1886\" title=\"Video hosting service\" alt=\"Video hosting service\" /><area href=\"/wiki/IEEE_802.11\" coords=\"996,1846,1161,1870\" title=\"IEEE 802.11\" alt=\"IEEE 802.11\" /><area href=\"/wiki/blog\" coords=\"996,1829,1067,1853\" title=\"blog\" alt=\"blog\" /><area href=\"/wiki/wiki\" coords=\"961,1812,1033,1836\" title=\"wiki\" alt=\"wiki\" /><area href=\"/wiki/Web_search_engine\" coords=\"926,1795,1078,1819\" title=\"Web search engine\" alt=\"Web search engine\" /><area href=\"/wiki/World_Wide_Web\" coords=\"857,1846,988,1870\" title=\"World Wide Web\" alt=\"World Wide Web\" /><area href=\"/wiki/WYSIWYG\" coords=\"770,1812,855,1836\" title=\"WYSIWYG\" alt=\"WYSIWYG\" /><area href=\"/wiki/Port_Island_Line\" coords=\"717,1795,942,1819\" title=\"Port Island Line\" alt=\"Port Island Line\" /><area href=\"/wiki/Usenet\" coords=\"683,1896,761,1920\" title=\"Usenet\" alt=\"Usenet\" /><area href=\"/wiki/CATS_(trading_system)\" coords=\"648,1879,840,1903\" title=\"CATS (trading system)\" alt=\"CATS (trading system)\" /><area href=\"/wiki/VisiCalc\" coords=\"648,1862,760,1886\" title=\"VisiCalc\" alt=\"VisiCalc\" /><area href=\"/wiki/email\" coords=\"596,1846,667,1870\" title=\"email\" alt=\"email\" /><area href=\"/wiki/Pong\" coords=\"561,1829,713,1853\" title=\"Pong\" alt=\"Pong\" /><area href=\"/wiki/Non%2Dlinear_video_editing\" coords=\"544,1812,742,1836\" title=\"Non-linear video editing\" alt=\"Non-linear video editing\" /><area href=\"/wiki/NLS_(computer_system)\" coords=\"491,1930,596,1954\" title=\"NLS (computer system)\" alt=\"NLS (computer system)\" /><area href=\"/wiki/The_Mother_of_All_Demos\" coords=\"491,1913,670,1937\" title=\"The Mother of All Demos\" alt=\"The Mother of All Demos\" /><area href=\"/wiki/Proof_assistant\" coords=\"474,1896,612,1920\" title=\"Proof assistant\" alt=\"Proof assistant\" /><area href=\"/wiki/CDC_6600\" coords=\"439,1879,578,1903\" title=\"CDC 6600\" alt=\"CDC 6600\" /><area href=\"/wiki/Bulletin_board_system\" coords=\"439,1862,571,1886\" title=\"Bulletin board system\" alt=\"Bulletin board system\" /><area href=\"/wiki/Spacewar_(video_game)\" coords=\"387,1846,512,1870\" title=\"Spacewar (video game)\" alt=\"Spacewar (video game)\" /><area href=\"/wiki/computer_reservations_system\" coords=\"387,1829,519,1853\" title=\"computer reservations system\" alt=\"computer reservations system\" /><area href=\"/wiki/timesharing\" coords=\"370,1812,481,1836\" title=\"timesharing\" alt=\"timesharing\" /><area href=\"/wiki/compiler_compiler\" coords=\"352,1795,504,1819\" title=\"compiler compiler\" alt=\"compiler compiler\" /><area href=\"/wiki/CSIRAC\" coords=\"196,1795,341,1819\" title=\"CSIRAC\" alt=\"CSIRAC\" /><area href=\"/wiki/Microsoft_Power_Fx\" coords=\"1413,1527,1505,1551\" title=\"Microsoft Power Fx\" alt=\"Microsoft Power Fx\" /><area href=\"/wiki/C%2B%2B20\" coords=\"1396,1510,1467,1534\" title=\"C++20\" alt=\"C++20\" /><area href=\"/wiki/Bosque_(programming_language)\" coords=\"1378,1711,1457,1735\" title=\"Bosque (programming language)\" alt=\"Bosque (programming language)\" /><area href=\"/wiki/Fortran_2018\" coords=\"1361,1695,1479,1719\" title=\"Fortran 2018\" alt=\"Fortran 2018\" /><area href=\"/wiki/Q_Sharp\" coords=\"1343,1678,1395,1702\" title=\"Q Sharp\" alt=\"Q Sharp\" /><area href=\"/wiki/C%2B%2B17\" coords=\"1343,1661,1415,1685\" title=\"C++17\" alt=\"C++17\" /><area href=\"/wiki/Raku_(programming_language)\" coords=\"1309,1644,1374,1668\" title=\"Raku (programming language)\" alt=\"Raku (programming language)\" /><area href=\"/wiki/C%2B%2B14\" coords=\"1291,1627,1363,1651\" title=\"C++14\" alt=\"C++14\" /><area href=\"/wiki/Swift\" coords=\"1291,1611,1363,1635\" title=\"Swift\" alt=\"Swift\" /><area href=\"/wiki/TypeScript\" coords=\"1257,1594,1362,1618\" title=\"TypeScript\" alt=\"TypeScript\" /><area href=\"/wiki/Elm_(programming_language)\" coords=\"1257,1577,1315,1601\" title=\"Elm (programming language)\" alt=\"Elm (programming language)\" /><area href=\"/wiki/Elixir_(programming_language)\" coords=\"1257,1560,1335,1584\" title=\"Elixir (programming language)\" alt=\"Elixir (programming language)\" /><area href=\"/wiki/Kotlin\" coords=\"1239,1544,1317,1568\" title=\"Kotlin\" alt=\"Kotlin\" /><area href=\"/wiki/C%2B%2B11\" coords=\"1239,1527,1311,1551\" title=\"C++11\" alt=\"C++11\" /><area href=\"/wiki/Rust\" coords=\"1222,1510,1287,1534\" title=\"Rust\" alt=\"Rust\" /><area href=\"/wiki/CoffeeScript\" coords=\"1204,1644,1323,1668\" title=\"CoffeeScript\" alt=\"CoffeeScript\" /><area href=\"/wiki/Clojure\" coords=\"1170,1627,1255,1651\" title=\"Clojure\" alt=\"Clojure\" /><area href=\"/wiki/Go_(programming_language)\" coords=\"1170,1611,1221,1635\" title=\"Go (programming language)\" alt=\"Go (programming language)\" /><area href=\"/wiki/PowerShell\" coords=\"1152,1594,1257,1618\" title=\"PowerShell\" alt=\"PowerShell\" /><area href=\"/wiki/F_Sharp_(programming_language)\" coords=\"1135,1577,1186,1601\" title=\"F Sharp (programming language)\" alt=\"F Sharp (programming language)\" /><area href=\"/wiki/Scala_(programming_language)\" coords=\"1100,1560,1172,1584\" title=\"Scala (programming language)\" alt=\"Scala (programming language)\" /><area href=\"/wiki/ActionScript\" coords=\"1048,1544,1166,1568\" title=\"ActionScript\" alt=\"ActionScript\" /><area href=\"/wiki/C_Sharp_(programming_language)\" coords=\"1048,1527,1100,1551\" title=\"C Sharp (programming language)\" alt=\"C Sharp (programming language)\" /><area href=\"/wiki/D_(programming_language)\" coords=\"1048,1510,1093,1534\" title=\"D (programming language)\" alt=\"D (programming language)\" /><area href=\"/wiki/XSLT\" coords=\"1030,1678,1095,1702\" title=\"XSLT\" alt=\"XSLT\" /><area href=\"/wiki/PHP\" coords=\"996,1661,1054,1685\" title=\"PHP\" alt=\"PHP\" /><area href=\"/wiki/Objective_Caml\" coords=\"978,1644,1050,1668\" title=\"Objective Caml\" alt=\"Objective Caml\" /><area href=\"/wiki/Ruby_(programming_language)\" coords=\"961,1627,1026,1651\" title=\"Ruby (programming language)\" alt=\"Ruby (programming language)\" /><area href=\"/wiki/Java_(programming_language)\" coords=\"961,1611,1026,1635\" title=\"Java (programming language)\" alt=\"Java (programming language)\" /><area href=\"/wiki/JavaScript\" coords=\"961,1594,1066,1618\" title=\"JavaScript\" alt=\"JavaScript\" /><area href=\"/wiki/Delphi_(programming_language)\" coords=\"961,1577,1039,1601\" title=\"Delphi (programming language)\" alt=\"Delphi (programming language)\" /><area href=\"/wiki/AppleScript\" coords=\"926,1560,1038,1584\" title=\"AppleScript\" alt=\"AppleScript\" /><area href=\"/wiki/Visual_Basic\" coords=\"891,1544,1010,1568\" title=\"Visual Basic\" alt=\"Visual Basic\" /><area href=\"/wiki/Python_(programming_language)\" coords=\"891,1527,970,1551\" title=\"Python (programming language)\" alt=\"Python (programming language)\" /><area href=\"/wiki/Haskell\" coords=\"874,1510,959,1534\" title=\"Haskell\" alt=\"Haskell\" /><area href=\"/wiki/Mathematica\" coords=\"839,1745,951,1769\" title=\"Mathematica\" alt=\"Mathematica\" /><area href=\"/wiki/Tcl\" coords=\"822,1728,880,1752\" title=\"Tcl\" alt=\"Tcl\" /><area href=\"/wiki/Erlang\" coords=\"822,1711,900,1735\" title=\"Erlang\" alt=\"Erlang\" /><area href=\"/wiki/Perl\" coords=\"822,1695,887,1719\" title=\"Perl\" alt=\"Perl\" /><area href=\"/wiki/SQL_(programming_language)\" coords=\"822,1678,880,1702\" title=\"SQL (programming language)\" alt=\"SQL (programming language)\" /><area href=\"/wiki/HyperCard\" coords=\"822,1661,920,1685\" title=\"HyperCard\" alt=\"HyperCard\" /><area href=\"/wiki/Objective%2DC\" coords=\"804,1644,916,1668\" title=\"Objective-C\" alt=\"Objective-C\" /><area href=\"/wiki/Eiffel_(programming_language)\" coords=\"804,1627,883,1651\" title=\"Eiffel (programming language)\" alt=\"Eiffel (programming language)\" /><area href=\"/wiki/Caml\" coords=\"787,1611,852,1635\" title=\"Caml\" alt=\"Caml\" /><area href=\"/wiki/PostScript\" coords=\"787,1594,892,1618\" title=\"PostScript\" alt=\"PostScript\" /><area href=\"/wiki/PARADOX_programming_language\" coords=\"787,1577,872,1601\" title=\"PARADOX programming language\" alt=\"PARADOX programming language\" /><area href=\"/wiki/Common_Lisp\" coords=\"770,1560,881,1584\" title=\"Common Lisp\" alt=\"Common Lisp\" /><area href=\"/wiki/Turbo_Pascal\" coords=\"752,1544,871,1568\" title=\"Turbo Pascal\" alt=\"Turbo Pascal\" /><area href=\"/wiki/C%2B%2B\" coords=\"752,1527,811,1551\" title=\"C++\" alt=\"C++\" /><area href=\"/wiki/Ada_(programming_language)\" coords=\"752,1510,811,1534\" title=\"Ada (programming language)\" alt=\"Ada (programming language)\" /><area href=\"/wiki/DBASE\" coords=\"683,1745,774,1769\" title=\"DBASE\" alt=\"DBASE\" /><area href=\"/wiki/Rexx\" coords=\"683,1728,748,1752\" title=\"Rexx\" alt=\"Rexx\" /><area href=\"/wiki/AWK\" coords=\"683,1711,741,1735\" title=\"AWK\" alt=\"AWK\" /><area href=\"/wiki/SQL\" coords=\"665,1695,724,1719\" title=\"SQL\" alt=\"SQL\" /><area href=\"/wiki/Modula%2D2\" coords=\"665,1678,757,1702\" title=\"Modula-2\" alt=\"Modula-2\" /><area href=\"/wiki/VisiCalc\" coords=\"665,1661,757,1685\" title=\"VisiCalc\" alt=\"VisiCalc\" /><area href=\"/wiki/IDL_(programming_language)\" coords=\"648,1644,706,1668\" title=\"IDL (programming language)\" alt=\"IDL (programming language)\" /><area href=\"/wiki/FORTRAN_77\" coords=\"648,1627,753,1651\" title=\"FORTRAN 77\" alt=\"FORTRAN 77\" /><area href=\"/wiki/Scheme_(programming_language)\" coords=\"613,1611,691,1635\" title=\"Scheme (programming language)\" alt=\"Scheme (programming language)\" /><area href=\"/wiki/ML_(programming_language)\" coords=\"578,1594,630,1618\" title=\"ML (programming language)\" alt=\"ML (programming language)\" /><area href=\"/wiki/Prolog\" coords=\"561,1577,639,1601\" title=\"Prolog\" alt=\"Prolog\" /><area href=\"/wiki/C_(programming_language)\" coords=\"561,1560,606,1584\" title=\"C (programming language)\" alt=\"C (programming language)\" /><area href=\"/wiki/Smalltalk\" coords=\"561,1544,659,1568\" title=\"Smalltalk\" alt=\"Smalltalk\" /><area href=\"/wiki/Forth_(programming_language)\" coords=\"526,1527,598,1551\" title=\"Forth (programming language)\" alt=\"Forth (programming language)\" /><area href=\"/wiki/Pascal_(programming_language)\" coords=\"526,1510,605,1534\" title=\"Pascal (programming language)\" alt=\"Pascal (programming language)\" /><area href=\"/wiki/B_(programming_language)\" coords=\"509,1678,554,1702\" title=\"B (programming language)\" alt=\"B (programming language)\" /><area href=\"/wiki/PLI\" coords=\"509,1661,574,1685\" title=\"PLI\" alt=\"PLI\" /><area href=\"/wiki/Logo_(programming_language)\" coords=\"491,1644,556,1668\" title=\"Logo (programming language)\" alt=\"Logo (programming language)\" /><area href=\"/wiki/ALGOL_68\" coords=\"491,1627,583,1651\" title=\"ALGOL 68\" alt=\"ALGOL 68\" /><area href=\"/wiki/BCPL\" coords=\"474,1611,539,1635\" title=\"BCPL\" alt=\"BCPL\" /><area href=\"/wiki/FORTRAN_66\" coords=\"457,1594,562,1618\" title=\"FORTRAN 66\" alt=\"FORTRAN 66\" /><area href=\"/wiki/BASIC\" coords=\"422,1577,494,1601\" title=\"BASIC\" alt=\"BASIC\" /><area href=\"/wiki/SNOBOL\" coords=\"387,1560,465,1584\" title=\"SNOBOL\" alt=\"SNOBOL\" /><area href=\"/wiki/Simula\" coords=\"387,1544,465,1568\" title=\"Simula\" alt=\"Simula\" /><area href=\"/wiki/APL_(programming_language)\" coords=\"387,1527,445,1551\" title=\"APL (programming language)\" alt=\"APL (programming language)\" /><area href=\"/wiki/COBOL\" coords=\"352,1510,424,1534\" title=\"COBOL\" alt=\"COBOL\" /><area href=\"/wiki/MAD_(programming_language)\" coords=\"335,1594,393,1618\" title=\"MAD (programming language)\" alt=\"MAD (programming language)\" /><area href=\"/wiki/Lisp_(programming_language)\" coords=\"318,1577,383,1601\" title=\"Lisp (programming language)\" alt=\"Lisp (programming language)\" /><area href=\"/wiki/ALGOL_58\" coords=\"318,1560,409,1584\" title=\"ALGOL 58\" alt=\"ALGOL 58\" /><area href=\"/wiki/FORTRAN_I\" coords=\"300,1544,398,1568\" title=\"FORTRAN I\" alt=\"FORTRAN I\" /><area href=\"/wiki/Mark_I_Autocode\" coords=\"248,1527,326,1551\" title=\"Mark I Autocode\" alt=\"Mark I Autocode\" /><area href=\"/wiki/A%2D0_System\" coords=\"213,1510,272,1534\" title=\"A-0 System\" alt=\"A-0 System\" /><area href=\"/wiki/Plankalk%C3%BCl\" coords=\"91,1510,203,1534\" title=\"Plankalkül\" alt=\"Plankalkül\" /><area href=\"/wiki/Windows_Server_2022\" coords=\"1413,1107,1578,1131\" title=\"Windows Server 2022\" alt=\"Windows Server 2022\" /><area href=\"/wiki/Windows_11\" coords=\"1413,1090,1518,1114\" title=\"Windows 11\" alt=\"Windows 11\" /><area href=\"/wiki/Windows_Server_2019\" coords=\"1361,1225,1526,1249\" title=\"Windows Server 2019\" alt=\"Windows Server 2019\" /><area href=\"/wiki/ArcaOS\" coords=\"1343,1208,1422,1232\" title=\"ArcaOS\" alt=\"ArcaOS\" /><area href=\"/wiki/Windows_Server_2016\" coords=\"1326,1191,1491,1215\" title=\"Windows Server 2016\" alt=\"Windows Server 2016\" /><area href=\"/wiki/Windows_10\" coords=\"1309,1174,1414,1198\" title=\"Windows 10\" alt=\"Windows 10\" /><area href=\"/wiki/Windows_Server_2012_R2\" coords=\"1274,1158,1459,1182\" title=\"Windows Server 2012 R2\" alt=\"Windows Server 2012 R2\" /><area href=\"/wiki/Qubes_OS\" coords=\"1257,1141,1348,1165\" title=\"Qubes OS\" alt=\"Qubes OS\" /><area href=\"/wiki/Windows_Server_2012\" coords=\"1257,1124,1422,1148\" title=\"Windows Server 2012\" alt=\"Windows Server 2012\" /><area href=\"/wiki/Windows_8\" coords=\"1257,1107,1355,1131\" title=\"Windows 8\" alt=\"Windows 8\" /><area href=\"/wiki/Chrome_OS\" coords=\"1239,1090,1337,1114\" title=\"Chrome OS\" alt=\"Chrome OS\" /><area href=\"/wiki/Windows_Server_2008_R2\" coords=\"1204,1359,1389,1383\" title=\"Windows Server 2008 R2\" alt=\"Windows Server 2008 R2\" /><area href=\"/wiki/Windows_7\" coords=\"1204,1342,1303,1366\" title=\"Windows 7\" alt=\"Windows 7\" /><area href=\"/wiki/Android_(operating_system)\" coords=\"1187,1325,1272,1349\" title=\"Android (operating system)\" alt=\"Android (operating system)\" /><area href=\"/wiki/Windows_Server_2008\" coords=\"1170,1309,1335,1333\" title=\"Windows Server 2008\" alt=\"Windows Server 2008\" /><area href=\"/wiki/Windows_Vista\" coords=\"1170,1292,1295,1316\" title=\"Windows Vista\" alt=\"Windows Vista\" /><area href=\"/wiki/iOS\" coords=\"1170,1275,1228,1299\" title=\"iOS\" alt=\"iOS\" /><area href=\"/wiki/Windows_Server_2003_R2\" coords=\"1135,1258,1320,1282\" title=\"Windows Server 2003 R2\" alt=\"Windows Server 2003 R2\" /><area href=\"/wiki/Ubuntu_(operating_system)\" coords=\"1117,1241,1196,1265\" title=\"Ubuntu (operating system)\" alt=\"Ubuntu (operating system)\" /><area href=\"/wiki/ReactOS\" coords=\"1100,1225,1185,1249\" title=\"ReactOS\" alt=\"ReactOS\" /><area href=\"/wiki/Windows_Server_2003\" coords=\"1100,1208,1265,1232\" title=\"Windows Server 2003\" alt=\"Windows Server 2003\" /><area href=\"/wiki/Gentoo_Linux\" coords=\"1083,1191,1201,1215\" title=\"Gentoo Linux\" alt=\"Gentoo Linux\" /><area href=\"/wiki/z/OS\" coords=\"1065,1174,1130,1198\" title=\"z/OS\" alt=\"z/OS\" /><area href=\"/wiki/Windows_XP\" coords=\"1065,1158,1170,1182\" title=\"Windows XP\" alt=\"Windows XP\" /><area href=\"/wiki/Mac_OS_X\" coords=\"1065,1141,1157,1165\" title=\"Mac OS X\" alt=\"Mac OS X\" /><area href=\"/wiki/Windows_2000\" coords=\"1048,1124,1166,1148\" title=\"Windows 2000\" alt=\"Windows 2000\" /><area href=\"/wiki/Windows_Me\" coords=\"1048,1107,1153,1131\" title=\"Windows Me\" alt=\"Windows Me\" /><area href=\"/wiki/Mac_OS_history\" coords=\"1048,1090,1140,1114\" title=\"Mac OS history\" alt=\"Mac OS history\" /><area href=\"/wiki/Mac_OS_history\" coords=\"1030,1460,1122,1484\" title=\"Mac OS history\" alt=\"Mac OS history\" /><area href=\"/wiki/BlackBerry_OS\" coords=\"1030,1443,1155,1467\" title=\"BlackBerry OS\" alt=\"BlackBerry OS\" /><area href=\"/wiki/Windows_98\" coords=\"1013,1426,1118,1450\" title=\"Windows 98\" alt=\"Windows 98\" /><area href=\"/wiki/FreeDOS\" coords=\"1013,1409,1098,1433\" title=\"FreeDOS\" alt=\"FreeDOS\" /><area href=\"/wiki/Symbian\" coords=\"996,1392,1081,1416\" title=\"Symbian\" alt=\"Symbian\" /><area href=\"/wiki/Mac_OS_history\" coords=\"996,1376,1101,1400\" title=\"Mac OS history\" alt=\"Mac OS history\" /><area href=\"/wiki/SUSE_Linux\" coords=\"978,1359,1083,1383\" title=\"SUSE Linux\" alt=\"SUSE Linux\" /><area href=\"/wiki/MkLinux\" coords=\"978,1342,1063,1366\" title=\"MkLinux\" alt=\"MkLinux\" /><area href=\"/wiki/Windows_95\" coords=\"961,1325,1066,1349\" title=\"Windows 95\" alt=\"Windows 95\" /><area href=\"/wiki/OS/390\" coords=\"961,1309,1039,1333\" title=\"OS/390\" alt=\"OS/390\" /><area href=\"/wiki/OpenBSD\" coords=\"961,1292,1046,1316\" title=\"OpenBSD\" alt=\"OpenBSD\" /><area href=\"/wiki/Red_Hat_Linux\" coords=\"944,1275,1069,1299\" title=\"Red Hat Linux\" alt=\"Red Hat Linux\" /><area href=\"/wiki/Debian\" coords=\"926,1258,1071,1282\" title=\"Debian\" alt=\"Debian\" /><area href=\"/wiki/Slackware\" coords=\"926,1241,1064,1265\" title=\"Slackware\" alt=\"Slackware\" /><area href=\"/wiki/Windows_NT\" coords=\"926,1225,1058,1249\" title=\"Windows NT\" alt=\"Windows NT\" /><area href=\"/wiki/UnixWare\" coords=\"909,1208,1000,1232\" title=\"UnixWare\" alt=\"UnixWare\" /><area href=\"/wiki/SLS_Linux\" coords=\"909,1191,1007,1215\" title=\"SLS Linux\" alt=\"SLS Linux\" /><area href=\"/wiki/OS/2\" coords=\"909,1174,1000,1198\" title=\"OS/2\" alt=\"OS/2\" /><area href=\"/wiki/Windows_3.1x\" coords=\"909,1158,1020,1182\" title=\"Windows 3.1x\" alt=\"Windows 3.1x\" /><area href=\"/wiki/Solaris_Operating_Environment\" coords=\"909,1141,994,1165\" title=\"Solaris Operating Environment\" alt=\"Solaris Operating Environment\" /><area href=\"/wiki/Linux\" coords=\"891,1124,963,1148\" title=\"Linux\" alt=\"Linux\" /><area href=\"/wiki/OSF/1\" coords=\"874,1107,946,1131\" title=\"OSF/1\" alt=\"OSF/1\" /><area href=\"/wiki/BeOS\" coords=\"874,1090,939,1114\" title=\"BeOS\" alt=\"BeOS\" /><area href=\"/wiki/SCO_UNIX\" coords=\"857,1460,948,1484\" title=\"SCO UNIX\" alt=\"SCO UNIX\" /><area href=\"/wiki/NEXTSTEP\" coords=\"857,1443,948,1467\" title=\"NEXTSTEP\" alt=\"NEXTSTEP\" /><area href=\"/wiki/POSIX\" coords=\"839,1426,911,1450\" title=\"POSIX\" alt=\"POSIX\" /><area href=\"/wiki/OS/400\" coords=\"839,1409,918,1433\" title=\"OS/400\" alt=\"OS/400\" /><area href=\"/wiki/RISC_OS\" coords=\"822,1392,907,1416\" title=\"RISC OS\" alt=\"RISC OS\" /><area href=\"/wiki/Windows_2.0\" coords=\"822,1376,933,1400\" title=\"Windows 2.0\" alt=\"Windows 2.0\" /><area href=\"/wiki/OS/2\" coords=\"822,1359,887,1383\" title=\"OS/2\" alt=\"OS/2\" /><area href=\"/wiki/Minix\" coords=\"822,1342,893,1366\" title=\"Minix\" alt=\"Minix\" /><area href=\"/wiki/IRIX\" coords=\"822,1325,887,1349\" title=\"IRIX\" alt=\"IRIX\" /><area href=\"/wiki/HP%2DUX\" coords=\"804,1309,876,1333\" title=\"HP-UX\" alt=\"HP-UX\" /><area href=\"/wiki/Apple_IIgs\" coords=\"804,1292,876,1316\" title=\"Apple IIgs\" alt=\"Apple IIgs\" /><area href=\"/wiki/AIX_(operating_system)\" coords=\"804,1275,863,1299\" title=\"AIX (operating system)\" alt=\"AIX (operating system)\" /><area href=\"/wiki/Mach\" coords=\"804,1258,869,1282\" title=\"Mach\" alt=\"Mach\" /><area href=\"/wiki/Windows_1.0\" coords=\"787,1241,899,1265\" title=\"Windows 1.0\" alt=\"Windows 1.0\" /><area href=\"/wiki/Atari_TOS\" coords=\"787,1225,885,1249\" title=\"Atari TOS\" alt=\"Atari TOS\" /><area href=\"/wiki/AmigaOS\" coords=\"787,1208,872,1232\" title=\"AmigaOS\" alt=\"AmigaOS\" /><area href=\"/wiki/GNU\" coords=\"770,1191,881,1215\" title=\"GNU\" alt=\"GNU\" /><area href=\"/wiki/Mac_OS_history\" coords=\"770,1174,888,1198\" title=\"Mac OS history\" alt=\"Mac OS history\" /><area href=\"/wiki/UNIX_System_V\" coords=\"752,1158,877,1182\" title=\"UNIX System V\" alt=\"UNIX System V\" /><area href=\"/wiki/Apple_Lisa\" coords=\"752,1141,837,1165\" title=\"Apple Lisa\" alt=\"Apple Lisa\" /><area href=\"/wiki/SunOS\" coords=\"735,1124,807,1148\" title=\"SunOS\" alt=\"SunOS\" /><area href=\"/wiki/MS%2DDOS\" coords=\"717,1107,796,1131\" title=\"MS-DOS\" alt=\"MS-DOS\" /><area href=\"/wiki/OS%2D9\" coords=\"700,1090,765,1114\" title=\"OS-9\" alt=\"OS-9\" /><area href=\"/wiki/Virtual_Memory_System\" coords=\"665,1191,724,1215\" title=\"Virtual Memory System\" alt=\"Virtual Memory System\" /><area href=\"/wiki/Apple_DOS\" coords=\"665,1174,790,1198\" title=\"Apple DOS\" alt=\"Apple DOS\" /><area href=\"/wiki/CP/M_operating_system\" coords=\"631,1158,696,1182\" title=\"CP/M operating system\" alt=\"CP/M operating system\" /><area href=\"/wiki/MVS\" coords=\"596,1141,654,1165\" title=\"MVS\" alt=\"MVS\" /><area href=\"/wiki/VM/CMS\" coords=\"561,1124,639,1148\" title=\"VM/CMS\" alt=\"VM/CMS\" /><area href=\"/wiki/RSTS%2D11\" coords=\"526,1107,611,1131\" title=\"RSTS-11\" alt=\"RSTS-11\" /><area href=\"/wiki/RT%2D11\" coords=\"526,1090,598,1114\" title=\"RT-11\" alt=\"RT-11\" /><area href=\"/wiki/Unix\" coords=\"509,1325,574,1349\" title=\"Unix\" alt=\"Unix\" /><area href=\"/wiki/TOPS%2D20\" coords=\"509,1309,634,1333\" title=\"TOPS-20\" alt=\"TOPS-20\" /><area href=\"/wiki/Airline_Control_Program\" coords=\"509,1292,567,1316\" title=\"Airline Control Program\" alt=\"Airline Control Program\" /><area href=\"/wiki/WAITS\" coords=\"474,1275,546,1299\" title=\"WAITS\" alt=\"WAITS\" /><area href=\"/wiki/CP/CMS\" coords=\"474,1258,552,1282\" title=\"CP/CMS\" alt=\"CP/CMS\" /><area href=\"/wiki/Michigan_Terminal_System\" coords=\"474,1241,532,1265\" title=\"Michigan Terminal System\" alt=\"Michigan Terminal System\" /><area href=\"/wiki/Incompatible_Timesharing_System\" coords=\"474,1225,532,1249\" title=\"Incompatible Timesharing System\" alt=\"Incompatible Timesharing System\" /><area href=\"/wiki/Multics\" coords=\"439,1208,524,1232\" title=\"Multics\" alt=\"Multics\" /><area href=\"/wiki/OS/360\" coords=\"439,1191,518,1215\" title=\"OS/360\" alt=\"OS/360\" /><area href=\"/wiki/TOPS%2D10\" coords=\"422,1174,507,1198\" title=\"TOPS-10\" alt=\"TOPS-10\" /><area href=\"/wiki/Dartmouth_Time_Sharing_System\" coords=\"422,1158,487,1182\" title=\"Dartmouth Time Sharing System\" alt=\"Dartmouth Time Sharing System\" /><area href=\"/wiki/General_Comprehensive_Operating_System\" coords=\"387,1141,459,1165\" title=\"General Comprehensive Operating System\" alt=\"General Comprehensive Operating System\" /><area href=\"/wiki/Burroughs_MCP\" coords=\"370,1124,495,1148\" title=\"Burroughs MCP\" alt=\"Burroughs MCP\" /><area href=\"/wiki/CTSS\" coords=\"370,1107,435,1131\" title=\"CTSS\" alt=\"CTSS\" /><area href=\"/wiki/IBSYS\" coords=\"352,1090,424,1114\" title=\"IBSYS\" alt=\"IBSYS\" /><area href=\"/wiki/SHARE_Operating_System\" coords=\"335,1174,407,1198\" title=\"SHARE Operating System\" alt=\"SHARE Operating System\" /><area href=\"/wiki/University_of_Michigan_Executive_System\" coords=\"318,1158,383,1182\" title=\"University of Michigan Executive System\" alt=\"University of Michigan Executive System\" /><area href=\"/wiki/GM%2DNAA_I/O\" coords=\"283,1141,388,1165\" title=\"GM-NAA I/O\" alt=\"GM-NAA I/O\" /><area href=\"/wiki/IBM_Q_System_One\" coords=\"1378,788,1523,812\" title=\"IBM Q System One\" alt=\"IBM Q System One\" /><area href=\"/wiki/List_of_AMD_Ryzen_processors\" coords=\"1343,772,1448,796\" title=\"List of AMD Ryzen processors\" alt=\"List of AMD Ryzen processors\" /><area href=\"/wiki/iPad\" coords=\"1222,755,1287,779\" title=\"iPad\" alt=\"iPad\" /><area href=\"/wiki/Intel_Core\" coords=\"1222,738,1320,762\" title=\"Intel Core\" alt=\"Intel Core\" /><area href=\"/wiki/Intel_Core\" coords=\"1204,956,1303,980\" title=\"Intel Core\" alt=\"Intel Core\" /><area href=\"/wiki/Intel_Core\" coords=\"1187,939,1285,963\" title=\"Intel Core\" alt=\"Intel Core\" /><area href=\"/wiki/IPhone_(1st_generation)\" coords=\"1170,923,1288,947\" title=\"IPhone (1st generation)\" alt=\"IPhone (1st generation)\" /><area href=\"/wiki/Amazon_Kindle\" coords=\"1170,889,1295,913\" title=\"Amazon Kindle\" alt=\"Amazon Kindle\" /><area href=\"/wiki/Cell_(microprocessor)\" coords=\"1152,872,1224,896\" title=\"Cell (microprocessor)\" alt=\"Cell (microprocessor)\" /><area href=\"/wiki/Intel_Core_2\" coords=\"1152,855,1277,879\" title=\"Intel Core 2\" alt=\"Intel Core 2\" /><area href=\"/wiki/Pentium_Dual%2DCore\" coords=\"1152,839,1311,863\" title=\"Pentium Dual-Core\" alt=\"Pentium Dual-Core\" /><area href=\"/wiki/Pentium_D\" coords=\"1135,822,1240,846\" title=\"Pentium D\" alt=\"Pentium D\" /><area href=\"/wiki/Athlon_64\" coords=\"1100,805,1252,829\" title=\"Athlon 64\" alt=\"Athlon 64\" /><area href=\"/wiki/PowerPC_970\" coords=\"1100,788,1212,812\" title=\"PowerPC 970\" alt=\"PowerPC 970\" /><area href=\"/wiki/Itanium\" coords=\"1065,772,1157,796\" title=\"Itanium\" alt=\"Itanium\" /><area href=\"/wiki/POWER4\" coords=\"1065,755,1150,779\" title=\"POWER4\" alt=\"POWER4\" /><area href=\"/wiki/Pentium_4\" coords=\"1048,738,1153,762\" title=\"Pentium 4\" alt=\"Pentium 4\" /><area href=\"/wiki/Athlon\" coords=\"1030,923,1162,947\" title=\"Athlon\" alt=\"Athlon\" /><area href=\"/wiki/List_of_Intel_Pentium_III_processors\" coords=\"1030,906,1149,930\" title=\"List of Intel Pentium III processors\" alt=\"List of Intel Pentium III processors\" /><area href=\"/wiki/List_of_Intel_Celeron_processors\" coords=\"1013,889,1105,913\" title=\"List of Intel Celeron processors\" alt=\"List of Intel Celeron processors\" /><area href=\"/wiki/iMac\" coords=\"1013,872,1078,896\" title=\"iMac\" alt=\"iMac\" /><area href=\"/wiki/List_of_Intel_Pentium_II_processors\" coords=\"996,855,1107,879\" title=\"List of Intel Pentium II processors\" alt=\"List of Intel Pentium II processors\" /><area href=\"/wiki/AMD_K6\" coords=\"996,839,1081,863\" title=\"AMD K6\" alt=\"AMD K6\" /><area href=\"/wiki/List_of_Intel_Pentium_processors\" coords=\"996,822,1114,846\" title=\"List of Intel Pentium processors\" alt=\"List of Intel Pentium processors\" /><area href=\"/wiki/AMD_K5\" coords=\"978,805,1063,829\" title=\"AMD K5\" alt=\"AMD K5\" /><area href=\"/wiki/Power_Macintosh\" coords=\"944,788,1082,812\" title=\"Power Macintosh\" alt=\"Power Macintosh\" /><area href=\"/wiki/PowerPC_600\" coords=\"926,755,1038,779\" title=\"PowerPC 600\" alt=\"PowerPC 600\" /><area href=\"/wiki/Pentium_(original)\" coords=\"926,738,1064,762\" title=\"Pentium (original)\" alt=\"Pentium (original)\" /><area href=\"/wiki/i486\" coords=\"857,939,982,963\" title=\"i486\" alt=\"i486\" /><area href=\"/wiki/IBM_PS/2\" coords=\"822,923,913,947\" title=\"IBM PS/2\" alt=\"IBM PS/2\" /><area href=\"/wiki/Connection_Machine\" coords=\"822,906,980,930\" title=\"Connection Machine\" alt=\"Connection Machine\" /><area href=\"/wiki/Acorn_Archimedes\" coords=\"822,889,967,913\" title=\"Acorn Archimedes\" alt=\"Acorn Archimedes\" /><area href=\"/wiki/ARM_architecture\" coords=\"804,872,876,896\" title=\"ARM architecture\" alt=\"ARM architecture\" /><area href=\"/wiki/Amstrad_1512\" coords=\"804,855,923,879\" title=\"Amstrad 1512\" alt=\"Amstrad 1512\" /><area href=\"/wiki/Commodore_Amiga\" coords=\"787,839,925,863\" title=\"Commodore Amiga\" alt=\"Commodore Amiga\" /><area href=\"/wiki/Atari_ST\" coords=\"787,822,879,846\" title=\"Atari ST\" alt=\"Atari ST\" /><area href=\"/wiki/i386\" coords=\"787,805,912,829\" title=\"i386\" alt=\"i386\" /><area href=\"/wiki/MIPS_architecture\" coords=\"770,788,881,812\" title=\"MIPS architecture\" alt=\"MIPS architecture\" /><area href=\"/wiki/IBM_AT\" coords=\"770,772,848,796\" title=\"IBM AT\" alt=\"IBM AT\" /><area href=\"/wiki/Apple_Macintosh\" coords=\"770,738,868,762\" title=\"Apple Macintosh\" alt=\"Apple Macintosh\" /><area href=\"/wiki/IBM_PCjr\" coords=\"752,1023,844,1047\" title=\"IBM PCjr\" alt=\"IBM PCjr\" /><area href=\"/wiki/IBM_XT\" coords=\"752,1006,831,1030\" title=\"IBM XT\" alt=\"IBM XT\" /><area href=\"/wiki/Apple_Lisa\" coords=\"752,990,817,1014\" title=\"Apple Lisa\" alt=\"Apple Lisa\" /><area href=\"/wiki/Apple_IIe\" coords=\"752,973,851,997\" title=\"Apple IIe\" alt=\"Apple IIe\" /><!-- illegal element removed -->\n<p><!-- illegal element removed -->\n<area href=\"/wiki/Intel_80286\" coords=\"735,889,813,913\" title=\"Intel 80286\" alt=\"Intel 80286\" /><area href=\"/wiki/Commodore_64\" coords=\"735,872,793,896\" title=\"Commodore 64\" alt=\"Commodore 64\" /><area href=\"/wiki/BBC_Micro\" coords=\"735,855,833,879\" title=\"BBC Micro\" alt=\"BBC Micro\" /><area href=\"/wiki/IBM_PC\" coords=\"717,839,796,863\" title=\"IBM PC\" alt=\"IBM PC\" /><area href=\"/wiki/ZX81\" coords=\"717,822,782,846\" title=\"ZX81\" alt=\"ZX81\" /><!-- illegal element removed -->\n<!-- illegal element removed -->\n</p>\n<area href=\"/wiki/Apple_III\" coords=\"700,772,798,796\" title=\"Apple III\" alt=\"Apple III\" /><area href=\"/wiki/ZX80\" coords=\"700,755,765,779\" title=\"ZX80\" alt=\"ZX80\" /><area href=\"/wiki/Commodore_VIC%2D20\" coords=\"700,738,778,762\" title=\"Commodore VIC-20\" alt=\"Commodore VIC-20\" /><area href=\"/wiki/Motorola_68000\" coords=\"683,939,774,963\" title=\"Motorola 68000\" alt=\"Motorola 68000\" /><area href=\"/wiki/Intel_8086\" coords=\"665,923,737,947\" title=\"Intel 8086\" alt=\"Intel 8086\" /><area href=\"/wiki/TRS%2D80\" coords=\"648,906,726,930\" title=\"TRS-80\" alt=\"TRS-80\" /><area href=\"/wiki/Apple_II\" coords=\"648,889,740,913\" title=\"Apple II\" alt=\"Apple II\" /><area href=\"/wiki/VAX%2D11\" coords=\"648,872,753,896\" title=\"VAX-11\" alt=\"VAX-11\" /><area href=\"/wiki/Commodore_PET\" coords=\"648,855,706,879\" title=\"Commodore PET\" alt=\"Commodore PET\" /><area href=\"/wiki/Apple_I\" coords=\"631,839,716,863\" title=\"Apple I\" alt=\"Apple I\" /><area href=\"/wiki/Cray%2D1\" coords=\"631,822,709,846\" title=\"Cray-1\" alt=\"Cray-1\" /><area href=\"/wiki/MOS_Technologies_6502\" coords=\"631,805,702,829\" title=\"MOS Technologies 6502\" alt=\"MOS Technologies 6502\" /><area href=\"/wiki/Zilog_Z80\" coords=\"631,788,696,812\" title=\"Zilog Z80\" alt=\"Zilog Z80\" /><area href=\"/wiki/Altair_8800\" coords=\"613,738,725,762\" title=\"Altair 8800\" alt=\"Altair 8800\" /><area href=\"/wiki/Motorola_6800\" coords=\"596,990,687,1014\" title=\"Motorola 6800\" alt=\"Motorola 6800\" /><area href=\"/wiki/Intel_8080\" coords=\"596,973,667,997\" title=\"Intel 8080\" alt=\"Intel 8080\" /><area href=\"/wiki/Intel_8008\" coords=\"561,923,633,947\" title=\"Intel 8008\" alt=\"Intel 8008\" /><area href=\"/wiki/Intel_4004\" coords=\"544,872,615,896\" title=\"Intel 4004\" alt=\"Intel 4004\" /><area href=\"/wiki/PDP%2D11\" coords=\"526,822,631,846\" title=\"PDP-11\" alt=\"PDP-11\" /><area href=\"/wiki/Datapoint_2200\" coords=\"526,805,658,829\" title=\"Datapoint 2200\" alt=\"Datapoint 2200\" /><area href=\"/wiki/PDP%2D10\" coords=\"491,788,596,812\" title=\"PDP-10\" alt=\"PDP-10\" /><area href=\"/wiki/BESM\" coords=\"457,772,535,796\" title=\"BESM\" alt=\"BESM\" /><area href=\"/wiki/CDC_6600\" coords=\"439,738,531,762\" title=\"CDC 6600\" alt=\"CDC 6600\" /><area href=\"/wiki/PDP%2D8\" coords=\"422,1023,520,1047\" title=\"PDP-8\" alt=\"PDP-8\" /><area href=\"/wiki/IBM_360\" coords=\"422,1006,507,1030\" title=\"IBM 360\" alt=\"IBM 360\" /><area href=\"/wiki/PDP%2D6\" coords=\"404,990,503,1014\" title=\"PDP-6\" alt=\"PDP-6\" /><area href=\"/wiki/ReserVec\" coords=\"387,973,479,997\" title=\"ReserVec\" alt=\"ReserVec\" /><area href=\"/wiki/ATLAS_computer\" coords=\"387,956,459,980\" title=\"ATLAS computer\" alt=\"ATLAS computer\" /><area href=\"/wiki/IBM_7030_Stretch\" coords=\"370,939,515,963\" title=\"IBM 7030 Stretch\" alt=\"IBM 7030 Stretch\" /><area href=\"/wiki/CDC_1604\" coords=\"352,923,444,947\" title=\"CDC 1604\" alt=\"CDC 1604\" /><area href=\"/wiki/PDP%2D1\" coords=\"352,906,451,930\" title=\"PDP-1\" alt=\"PDP-1\" /><area href=\"/wiki/IBM_1401\" coords=\"335,889,427,913\" title=\"IBM 1401\" alt=\"IBM 1401\" /><area href=\"/wiki/AN/FSQ%2D7\" coords=\"318,855,409,879\" title=\"AN/FSQ-7\" alt=\"AN/FSQ-7\" /><area href=\"/wiki/UNIVAC_II\" coords=\"318,839,416,863\" title=\"UNIVAC II\" alt=\"UNIVAC II\" /><area href=\"/wiki/IBM_608\" coords=\"300,822,385,846\" title=\"IBM 608\" alt=\"IBM 608\" /><area href=\"/wiki/IBM_305_RAMAC\" coords=\"283,805,408,829\" title=\"IBM 305 RAMAC\" alt=\"IBM 305 RAMAC\" /><area href=\"/wiki/Harwell_CADET\" coords=\"265,788,390,812\" title=\"Harwell CADET\" alt=\"Harwell CADET\" /><area href=\"/wiki/IBM_704\" coords=\"248,772,333,796\" title=\"IBM 704\" alt=\"IBM 704\" /><area href=\"/wiki/IBM_650\" coords=\"248,755,333,779\" title=\"IBM 650\" alt=\"IBM 650\" /><area href=\"/wiki/Strela_computer\" coords=\"231,738,309,762\" title=\"Strela computer\" alt=\"Strela computer\" /><area href=\"/wiki/IBM_701\" coords=\"213,1040,298,1064\" title=\"IBM 701\" alt=\"IBM 701\" /><area href=\"/wiki/UNIVAC_1101\" coords=\"213,1023,325,1047\" title=\"UNIVAC 1101\" alt=\"UNIVAC 1101\" /><area href=\"/wiki/Whirlwind_(computer)\" coords=\"196,1006,294,1030\" title=\"Whirlwind (computer)\" alt=\"Whirlwind (computer)\" /><area href=\"/wiki/UNIVAC_I\" coords=\"196,990,287,1014\" title=\"UNIVAC I\" alt=\"UNIVAC I\" /><area href=\"/wiki/LEO_(computer)\" coords=\"196,956,267,980\" title=\"LEO (computer)\" alt=\"LEO (computer)\" /><area href=\"/wiki/Ferranti_Mark_1\" coords=\"196,923,334,947\" title=\"Ferranti Mark 1\" alt=\"Ferranti Mark 1\" /><area href=\"/wiki/Manchester_Mark_1\" coords=\"161,906,313,930\" title=\"Manchester Mark 1\" alt=\"Manchester Mark 1\" /><area href=\"/wiki/Small%2DScale_Experimental_Machine\" coords=\"144,889,282,913\" title=\"Small-Scale Experimental Machine\" alt=\"Small-Scale Experimental Machine\" /><area href=\"/wiki/IBM_SSEC\" coords=\"144,872,235,896\" title=\"IBM SSEC\" alt=\"IBM SSEC\" /><area href=\"/wiki/ENIAC\" coords=\"109,855,181,879\" title=\"ENIAC\" alt=\"ENIAC\" /><area href=\"/wiki/Colossus_computer\" coords=\"57,839,148,863\" title=\"Colossus computer\" alt=\"Colossus computer\" /><area href=\"/wiki/Harvard_Mark_I\" coords=\"57,805,188,829\" title=\"Harvard Mark I\" alt=\"Harvard Mark I\" /><area href=\"/wiki/Z3_(computer)\" coords=\"22,788,74,812\" title=\"Z3 (computer)\" alt=\"Z3 (computer)\" /><area href=\"/wiki/Atanasoff%E2%80%93Berry_Computer\" coords=\"22,772,234,796\" title=\"Atanasoff–Berry Computer\" alt=\"Atanasoff–Berry Computer\" /><area href=\"/wiki/Docker_(software)\" coords=\"1274,402,1352,426\" title=\"Docker (software)\" alt=\"Docker (software)\" /><area href=\"/wiki/Cryptocurrency\" coords=\"1204,386,1336,410\" title=\"Cryptocurrency\" alt=\"Cryptocurrency\" /><area href=\"/wiki/Tor_(anonymity_network)\" coords=\"1117,449,1182,473\" title=\"Tor (anonymity network)\" alt=\"Tor (anonymity network)\" /><area href=\"/wiki/Onion_routing\" coords=\"1117,436,1242,460\" title=\"Onion routing\" alt=\"Onion routing\" /><area href=\"/wiki/PCI_Express\" coords=\"1117,419,1229,443\" title=\"PCI Express\" alt=\"PCI Express\" /><area href=\"/wiki/Serial_ATA\" coords=\"1100,402,1205,426\" title=\"Serial ATA\" alt=\"Serial ATA\" /><area href=\"/wiki/Blade_server\" coords=\"1065,386,1190,410\" title=\"Blade server\" alt=\"Blade server\" /><area href=\"/wiki/Asymmetric_Digital_Subscriber_Line\" coords=\"1013,688,1078,712\" title=\"Asymmetric Digital Subscriber Line\" alt=\"Asymmetric Digital Subscriber Line\" /><area href=\"/wiki/Accelerated_Graphics_Port\" coords=\"996,671,1054,695\" title=\"Accelerated Graphics Port\" alt=\"Accelerated Graphics Port\" /><area href=\"/wiki/DVD%2DROM\" coords=\"961,654,1046,678\" title=\"DVD-ROM\" alt=\"DVD-ROM\" /><area href=\"/wiki/USB\" coords=\"961,637,1019,661\" title=\"USB\" alt=\"USB\" /><area href=\"/wiki/IEEE_1394_interface\" coords=\"961,621,1053,645\" title=\"IEEE 1394 interface\" alt=\"IEEE 1394 interface\" /><area href=\"/wiki/Beowulf_(computing)\" coords=\"944,604,1029,628\" title=\"Beowulf (computing)\" alt=\"Beowulf (computing)\" /><area href=\"/wiki/DNA_computing\" coords=\"944,587,1069,611\" title=\"DNA computing\" alt=\"DNA computing\" /><area href=\"/wiki/Smartphone\" coords=\"944,570,1049,594\" title=\"Smartphone\" alt=\"Smartphone\" /><area href=\"/wiki/Conventional_PCI\" coords=\"926,553,984,577\" title=\"Conventional PCI\" alt=\"Conventional PCI\" /><area href=\"/wiki/CD%2Di\" coords=\"909,537,974,561\" title=\"CD-i\" alt=\"CD-i\" /><area href=\"/wiki/PCMCIA\" coords=\"891,520,970,544\" title=\"PCMCIA\" alt=\"PCMCIA\" /><area href=\"/wiki/VESA\" coords=\"874,500,939,524\" title=\"VESA\" alt=\"VESA\" /><area href=\"/wiki/SVGA\" coords=\"874,486,939,510\" title=\"SVGA\" alt=\"SVGA\" /><area href=\"/wiki/Extended_Industry_Standard_Architecture\" coords=\"839,469,904,493\" title=\"Extended Industry Standard Architecture\" alt=\"Extended Industry Standard Architecture\" /><area href=\"/wiki/sound_card\" coords=\"822,436,927,460\" title=\"sound card\" alt=\"sound card\" /><area href=\"/wiki/VGA\" coords=\"822,419,880,443\" title=\"VGA\" alt=\"VGA\" /><area href=\"/wiki/Connection_Machine\" coords=\"822,399,987,423\" title=\"Connection Machine\" alt=\"Connection Machine\" /><area href=\"/wiki/Personal_digital_assistant\" coords=\"804,688,1016,712\" title=\"Personal digital assistant\" alt=\"Personal digital assistant\" /><area href=\"/wiki/SCSI\" coords=\"804,671,869,695\" title=\"SCSI\" alt=\"SCSI\" /><area href=\"/wiki/Parallel_ATA\" coords=\"804,654,863,678\" title=\"Parallel ATA\" alt=\"Parallel ATA\" /><area href=\"/wiki/CD_ROM\" coords=\"787,637,865,661\" title=\"CD ROM\" alt=\"CD ROM\" /><area href=\"/wiki/Enhanced_Graphics_Adapter\" coords=\"787,621,845,645\" title=\"Enhanced Graphics Adapter\" alt=\"Enhanced Graphics Adapter\" /><area href=\"/wiki/Expanded_Memory\" coords=\"787,604,925,628\" title=\"Expanded Memory\" alt=\"Expanded Memory\" /><area href=\"/wiki/Advanced_Technology_Attachment\" coords=\"770,587,828,611\" title=\"Advanced Technology Attachment\" alt=\"Advanced Technology Attachment\" /><area href=\"/wiki/Domain_Name_System\" coords=\"770,553,828,577\" title=\"Domain Name System\" alt=\"Domain Name System\" /><area href=\"/wiki/coprocessor\" coords=\"752,517,864,541\" title=\"coprocessor\" alt=\"coprocessor\" /><area href=\"/wiki/Multi%2Dtouch\" coords=\"735,486,920,510\" title=\"Multi-touch\" alt=\"Multi-touch\" /><area href=\"/wiki/RISC\" coords=\"735,453,800,477\" title=\"RISC\" alt=\"RISC\" /><area href=\"/wiki/MIDI\" coords=\"735,436,800,460\" title=\"MIDI\" alt=\"MIDI\" /><area href=\"/wiki/Hayes_Smartmodem\" coords=\"717,419,822,443\" title=\"Hayes Smartmodem\" alt=\"Hayes Smartmodem\" /><area href=\"/wiki/Color_Graphics_Adapter\" coords=\"717,402,776,426\" title=\"Color Graphics Adapter\" alt=\"Color Graphics Adapter\" /><area href=\"/wiki/Industry_Standard_Architecture\" coords=\"717,386,776,410\" title=\"Industry Standard Architecture\" alt=\"Industry Standard Architecture\" /><area href=\"/wiki/VMEbus\" coords=\"683,604,761,628\" title=\"VMEbus\" alt=\"VMEbus\" /><area href=\"/wiki/compact_disk\" coords=\"683,587,801,611\" title=\"compact disk\" alt=\"compact disk\" /><area href=\"/wiki/supercomputer\" coords=\"631,570,756,594\" title=\"supercomputer\" alt=\"supercomputer\" /><area href=\"/wiki/laser_printer\" coords=\"631,553,756,577\" title=\"laser printer\" alt=\"laser printer\" /><area href=\"/wiki/single%2Dboard_computer\" coords=\"613,537,791,561\" title=\"single-board computer\" alt=\"single-board computer\" /><area href=\"/wiki/TCP/IP\" coords=\"596,520,674,544\" title=\"TCP/IP\" alt=\"TCP/IP\" /><area href=\"/wiki/touchscreen\" coords=\"578,503,690,527\" title=\"touchscreen\" alt=\"touchscreen\" /><area href=\"/wiki/ethernet\" coords=\"578,486,670,510\" title=\"ethernet\" alt=\"ethernet\" /><area href=\"/wiki/Magnavox_Odyssey\" coords=\"561,466,713,490\" title=\"Magnavox Odyssey\" alt=\"Magnavox Odyssey\" /><area href=\"/wiki/game_console\" coords=\"561,453,679,477\" title=\"game console\" alt=\"game console\" /><area href=\"/wiki/microprocessor\" coords=\"544,419,675,443\" title=\"microprocessor\" alt=\"microprocessor\" /><area href=\"/wiki/floppy_disk\" coords=\"544,402,655,426\" title=\"floppy disk\" alt=\"floppy disk\" /><area href=\"/wiki/dynamic_RAM\" coords=\"526,386,638,410\" title=\"dynamic RAM\" alt=\"dynamic RAM\" /><area href=\"/wiki/NPL_network\" coords=\"509,537,620,561\" title=\"NPL network\" alt=\"NPL network\" /><area href=\"/wiki/ARPANET\" coords=\"509,520,594,544\" title=\"ARPANET\" alt=\"ARPANET\" /><area href=\"/wiki/RS%2D232\" coords=\"509,503,587,527\" title=\"RS-232\" alt=\"RS-232\" /><area href=\"/wiki/fuzzy_logic\" coords=\"439,486,551,510\" title=\"fuzzy logic\" alt=\"fuzzy logic\" /><area href=\"/wiki/packet_switching\" coords=\"422,469,574,493\" title=\"packet switching\" alt=\"packet switching\" /><area href=\"/wiki/computer_mouse\" coords=\"404,453,476,477\" title=\"computer mouse\" alt=\"computer mouse\" /><area href=\"/wiki/paging\" coords=\"387,436,465,460\" title=\"paging\" alt=\"paging\" /><area href=\"/wiki/virtual_memory\" coords=\"387,419,519,443\" title=\"virtual memory\" alt=\"virtual memory\" /><area href=\"/wiki/interrupts\" coords=\"387,402,492,426\" title=\"interrupts\" alt=\"interrupts\" /><area href=\"/wiki/spooling\" coords=\"387,386,479,410\" title=\"spooling\" alt=\"spooling\" /><area href=\"/wiki/garbage_collection_(computer_science)\" coords=\"335,587,493,611\" title=\"garbage collection (computer science)\" alt=\"garbage collection (computer science)\" /><area href=\"/wiki/time%2Dsharing\" coords=\"335,570,460,594\" title=\"time-sharing\" alt=\"time-sharing\" /><area href=\"/wiki/integrated_circuit\" coords=\"318,553,476,577\" title=\"integrated circuit\" alt=\"integrated circuit\" /><area href=\"/wiki/dot_matrix_printer\" coords=\"300,537,458,561\" title=\"dot matrix printer\" alt=\"dot matrix printer\" /><area href=\"/wiki/hard_disk\" coords=\"283,520,381,544\" title=\"hard disk\" alt=\"hard disk\" /><area href=\"/wiki/magnetic_core_memory\" coords=\"231,503,402,527\" title=\"magnetic core memory\" alt=\"magnetic core memory\" /><area href=\"/wiki/index_registers\" coords=\"161,486,259,510\" title=\"index registers\" alt=\"index registers\" /><area href=\"/wiki/magnetic_drum\" coords=\"144,469,269,493\" title=\"magnetic drum\" alt=\"magnetic drum\" /><area href=\"/wiki/RAM\" coords=\"144,453,315,477\" title=\"RAM\" alt=\"RAM\" /><area href=\"/wiki/transistor\" coords=\"126,436,238,460\" title=\"transistor\" alt=\"transistor\" /><area href=\"/wiki/Williams_tube\" coords=\"109,419,281,443\" title=\"Williams tube\" alt=\"Williams tube\" /><area href=\"/wiki/trackball\" coords=\"109,402,214,426\" title=\"trackball\" alt=\"trackball\" /><area href=\"/wiki/teletype\" coords=\"5,386,96,410\" title=\"teletype\" alt=\"teletype\" /><area href=\"/wiki/Hutter_Prize\" coords=\"1152,251,1277,275\" title=\"Hutter Prize\" alt=\"Hutter Prize\" /><area href=\"/wiki/POPLmark_challenge\" coords=\"1135,235,1300,259\" title=\"POPLmark challenge\" alt=\"POPLmark challenge\" /><area href=\"/wiki/DARPA_Grand_Challenge\" coords=\"1117,335,1302,359\" title=\"DARPA Grand Challenge\" alt=\"DARPA Grand Challenge\" /><area href=\"/wiki/Google_Code_Jam\" coords=\"1100,318,1245,342\" title=\"Google Code Jam\" alt=\"Google Code Jam\" /><area href=\"/wiki/TopCoder\" coords=\"1065,302,1164,326\" title=\"TopCoder\" alt=\"TopCoder\" /><area href=\"/wiki/ICFP_Programming_Contest\" coords=\"1013,285,1218,309\" title=\"ICFP Programming Contest\" alt=\"ICFP Programming Contest\" /><area href=\"/wiki/CADE_ATP_System_Competition\" coords=\"978,268,1203,292\" title=\"CADE ATP System Competition\" alt=\"CADE ATP System Competition\" /><area href=\"/wiki/International_Olympiad_in_Informatics\" coords=\"857,235,1148,259\" title=\"International Olympiad in Informatics\" alt=\"International Olympiad in Informatics\" /><area href=\"/wiki/Quantum_Computer\" coords=\"717,268,862,292\" title=\"Quantum Computer\" alt=\"Quantum Computer\" /><area href=\"/wiki/P_versus_NP_problem\" coords=\"544,251,709,275\" title=\"P versus NP problem\" alt=\"P versus NP problem\" /><area href=\"/wiki/North_American_Computer_Chess_Championship\" coords=\"526,235,851,259\" title=\"North American Computer Chess Championship\" alt=\"North American Computer Chess Championship\" /><area href=\"/wiki/Turing_Test\" coords=\"178,235,290,259\" title=\"Turing Test\" alt=\"Turing Test\" /><area href=\"/wiki/Relational_database\" coords=\"526,151,691,175\" title=\"Relational database\" alt=\"Relational database\" /><area href=\"/wiki/David_Levy_(chess_player)\" coords=\"491,131,616,155\" title=\"David Levy (chess player)\" alt=\"David Levy (chess player)\" /><area href=\"/wiki/Moore%27s_law\" coords=\"439,100,551,124\" title=\"Moore&#39;s law\" alt=\"Moore&#39;s law\" /></map><img usemap=\"#timeline_dvng9uthgiznl8kgo84uz9w8dcs2l9r\" src=\"//upload.wikimedia.org/wikipedia/en/timeline/dvng9uthgiznl8kgo84uz9w8dcs2l9r.png\" /></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"See_also\">See also</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit&amp;section=2\" title=\"Edit section: See also\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/History_of_compiler_construction\" title=\"History of compiler construction\">History of compiler construction</a></li>\n<li><a href=\"/wiki/History_of_computing_hardware\" title=\"History of computing hardware\">History of computing hardware</a> – up to third generation (1960s)</li>\n<li><a href=\"/wiki/History_of_computing_hardware_(1960s%E2%80%93present)\" title=\"History of computing hardware (1960s–present)\">History of computing hardware (1960s–present)</a> – third generation and later</li>\n<li><a href=\"/wiki/History_of_the_graphical_user_interface\" title=\"History of the graphical user interface\">History of the graphical user interface</a></li>\n<li><a href=\"/wiki/History_of_the_Internet\" title=\"History of the Internet\">History of the Internet</a></li>\n<li><a href=\"/wiki/History_of_the_World_Wide_Web\" title=\"History of the World Wide Web\">History of the World Wide Web</a></li>\n<li><a href=\"/wiki/List_of_pioneers_in_computer_science\" title=\"List of pioneers in computer science\">List of pioneers in computer science</a></li>\n<li><a href=\"/wiki/Timeline_of_electrical_and_electronic_engineering\" title=\"Timeline of electrical and electronic engineering\">Timeline of electrical and electronic engineering</a></li>\n<li><a href=\"/wiki/Microprocessor_chronology\" title=\"Microprocessor chronology\">Microprocessor chronology</a></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Resources\">Resources</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit&amp;section=3\" title=\"Edit section: Resources\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li>Stephen White, <a rel=\"nofollow\" class=\"external text\" href=\"http://trillian.randomstuff.org.uk/~stephen/history/\"><i>A Brief History of Computing</i></a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20030215072422/http://comp-hist.sourceforge.net/\"><i>The Computer History in time and space, Graphing Project</i></a>, an attempt to build a graphical image of computer history, in particular <a href=\"/wiki/Operating_system\" title=\"Operating system\">operating systems</a>.</li>\n<li><style data-mw-deduplicate=\"TemplateStyles:r1312463507\">@media screen{html.skin-theme-clientpref-night .mw-parser-output .sister-inline-image img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sister-inline-image img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}</style><span class=\"noviewer sister-inline-image\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo-en-noslogan.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/20px-Wikibooks-logo-en-noslogan.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/40px-Wikibooks-logo-en-noslogan.svg.png 1.5x\" data-file-width=\"400\" data-file-height=\"400\" /></a></span> <a href=\"https://en.wikibooks.org/wiki/The_Computer_Revolution/Timeline\" class=\"extiw\" title=\"wikibooks:The Computer Revolution/Timeline\">The Computer Revolution/Timeline</a> at Wikibooks</li>\n<li><style data-mw-deduplicate=\"TemplateStyles:r1238218222\">.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}.mw-parser-output .citation q{quotes:\"\\\"\"\"\\\"\"\"'\"\"'\"}.mw-parser-output .citation:target{background-color:rgba(0,127,255,0.133)}.mw-parser-output .id-lock-free.id-lock-free a{background:url(\"//upload.wikimedia.org/wikipedia/commons/6/65/Lock-green.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-limited.id-lock-limited a,.mw-parser-output .id-lock-registration.id-lock-registration a{background:url(\"//upload.wikimedia.org/wikipedia/commons/d/d6/Lock-gray-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-subscription.id-lock-subscription a{background:url(\"//upload.wikimedia.org/wikipedia/commons/a/aa/Lock-red-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .cs1-ws-icon a{background:url(\"//upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg\")right 0.1em center/12px no-repeat}body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-free a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-limited a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-registration a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-subscription a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .cs1-ws-icon a{background-size:contain;padding:0 1em 0 0}.mw-parser-output .cs1-code{color:inherit;background:inherit;border:none;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;color:var(--color-error,#d33)}.mw-parser-output .cs1-visible-error{color:var(--color-error,#d33)}.mw-parser-output .cs1-maint{display:none;color:#085;margin-left:0.3em}.mw-parser-output .cs1-kern-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right{padding-right:0.2em}.mw-parser-output .citation .mw-selflink{font-weight:inherit}@media screen{.mw-parser-output .cs1-format{font-size:95%}html.skin-theme-clientpref-night .mw-parser-output .cs1-maint{color:#18911f}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .cs1-maint{color:#18911f}}</style><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://ethw.org/File:Timeline.pdf\">\"File:Timeline.pdf - Engineering and Technology History Wiki\"</a> <span class=\"cs1-format\">(PDF)</span>. <i>ethw.org</i>. 2012. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20171031151254/http://ethw.org/File:Timeline.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 2017-10-31<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2018-03-03</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ethw.org&amp;rft.atitle=File%3ATimeline.pdf+-+Engineering+and+Technology+History+Wiki&amp;rft.date=2012&amp;rft_id=http%3A%2F%2Fethw.org%2FFile%3ATimeline.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3ATimeline+of+computing\" class=\"Z3988\"></span></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links\">External links</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Timeline_of_computing&amp;action=edit&amp;section=4\" title=\"Edit section: External links\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210412131620/https://www.akita.co.uk/computing-history/\">Visual History of Computing</a> 1944-2013 (archived)</li></ul>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1236075235\">.mw-parser-output .navbox{box-sizing:border-box;border:1px solid #a2a9b1;width:100%;clear:both;font-size:88%;text-align:center;padding:1px;margin:1em auto 0}.mw-parser-output .navbox .navbox{margin-top:0}.mw-parser-output .navbox+.navbox,.mw-parser-output .navbox+.navbox-styles+.navbox{margin-top:-1px}.mw-parser-output .navbox-inner,.mw-parser-output .navbox-subgroup{width:100%}.mw-parser-output .navbox-group,.mw-parser-output .navbox-title,.mw-parser-output .navbox-abovebelow{padding:0.25em 1em;line-height:1.5em;text-align:center}.mw-parser-output .navbox-group{white-space:nowrap;text-align:right}.mw-parser-output .navbox,.mw-parser-output .navbox-subgroup{background-color:#fdfdfd}.mw-parser-output .navbox-list{line-height:1.5em;border-color:#fdfdfd}.mw-parser-output .navbox-list-with-group{text-align:left;border-left-width:2px;border-left-style:solid}.mw-parser-output tr+tr>.navbox-abovebelow,.mw-parser-output tr+tr>.navbox-group,.mw-parser-output tr+tr>.navbox-image,.mw-parser-output tr+tr>.navbox-list{border-top:2px solid #fdfdfd}.mw-parser-output .navbox-title{background-color:#ccf}.mw-parser-output .navbox-abovebelow,.mw-parser-output .navbox-group,.mw-parser-output .navbox-subgroup .navbox-title{background-color:#ddf}.mw-parser-output .navbox-subgroup .navbox-group,.mw-parser-output .navbox-subgroup .navbox-abovebelow{background-color:#e6e6ff}.mw-parser-output .navbox-even{background-color:#f7f7f7}.mw-parser-output .navbox-odd{background-color:transparent}.mw-parser-output .navbox .hlist td dl,.mw-parser-output .navbox .hlist td ol,.mw-parser-output .navbox .hlist td ul,.mw-parser-output .navbox td.hlist dl,.mw-parser-output .navbox td.hlist ol,.mw-parser-output .navbox td.hlist ul{padding:0.125em 0}.mw-parser-output .navbox .navbar{display:block;font-size:100%}.mw-parser-output .navbox-title .navbar{float:left;text-align:left;margin-right:0.5em}body.skin--responsive .mw-parser-output .navbox-image img{max-width:none!important}@media print{body.ns-0 .mw-parser-output .navbox{display:none!important}}</style></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Timelines_of_computing2704\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Timelines_of_computing\" title=\"Template:Timelines of computing\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Timelines_of_computing\" title=\"Template talk:Timelines of computing\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Timelines_of_computing\" title=\"Special:EditPage/Template:Timelines of computing\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Timelines_of_computing2704\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Category:Computing_timelines\" title=\"Category:Computing timelines\">Timelines of computing</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a class=\"mw-selflink selflink\">Computing</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Timeline_of_computing_hardware_before_1950\" title=\"Timeline of computing hardware before 1950\">Before 1950</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1950%E2%80%931979\" title=\"Timeline of computing 1950–1979\">1950–1979</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1980%E2%80%931989\" title=\"Timeline of computing 1980–1989\">1980s</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_1990%E2%80%931999\" title=\"Timeline of computing 1990–1999\">1990s</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2000%E2%80%932009\" title=\"Timeline of computing 2000–2009\">2000s</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2010%E2%80%932019\" title=\"Timeline of computing 2010–2019\">2010s</a></li>\n<li><a href=\"/wiki/Timeline_of_computing_2020%E2%80%93present\" title=\"Timeline of computing 2020–present\">2020s</a></li>\n<li><a href=\"/wiki/Timeline_of_scientific_computing\" title=\"Timeline of scientific computing\">Scientific</a></li>\n<li><a href=\"/wiki/Timeline_of_women_in_computing\" title=\"Timeline of women in computing\">Women in computing</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Computer_science\" title=\"Computer science\">Computer<br />science</a></th><td class=\"navbox-list-with-group navbox-list navbox-even hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Timeline_of_algorithms\" title=\"Timeline of algorithms\">Algorithms</a></li>\n<li><a href=\"/wiki/Timeline_of_artificial_intelligence\" title=\"Timeline of artificial intelligence\">Artificial intelligence</a></li>\n<li><a href=\"/wiki/Timeline_of_binary_prefixes\" title=\"Timeline of binary prefixes\">Binary prefixes</a></li>\n<li><a href=\"/wiki/Timeline_of_cryptography\" title=\"Timeline of cryptography\">Cryptography</a></li>\n<li><a href=\"/wiki/Timeline_of_machine_learning\" title=\"Timeline of machine learning\">Machine learning</a></li>\n<li><a href=\"/wiki/Timeline_of_quantum_computing_and_communication\" title=\"Timeline of quantum computing and communication\">Quantum computing and communication</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Software\" title=\"Software\">Software</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Timeline_of_free_and_open-source_software\" title=\"Timeline of free and open-source software\">Free and open-source software</a></li>\n<li><a href=\"/wiki/Timeline_of_hypertext_technology\" title=\"Timeline of hypertext technology\">Hypertext technology</a></li>\n<li><a href=\"/wiki/Timeline_of_operating_systems\" title=\"Timeline of operating systems\">Operating systems</a>\n<ul><li><a href=\"/wiki/Timeline_of_DOS_operating_systems\" title=\"Timeline of DOS operating systems\">DOS family</a></li>\n<li><a href=\"/wiki/Timeline_of_Microsoft_Windows\" class=\"mw-redirect\" title=\"Timeline of Microsoft Windows\">Windows</a></li>\n<li><a href=\"/wiki/Linux_kernel_version_history\" title=\"Linux kernel version history\">Linux</a></li></ul></li>\n<li><a href=\"/wiki/Timeline_of_programming_languages\" title=\"Timeline of programming languages\">Programming languages</a></li>\n<li><a href=\"/wiki/Timeline_of_virtualization_development\" class=\"mw-redirect\" title=\"Timeline of virtualization development\">Virtualization development</a></li>\n<li><a href=\"/wiki/Timeline_of_computer_viruses_and_worms\" title=\"Timeline of computer viruses and worms\">Malware</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Internet\" title=\"Internet\">Internet</a></th><td class=\"navbox-list-with-group navbox-list navbox-even hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Timeline_of_Internet_conflicts\" title=\"Timeline of Internet conflicts\">Internet conflicts</a></li>\n<li><a href=\"/wiki/Timeline_of_web_browsers\" class=\"mw-redirect\" title=\"Timeline of web browsers\">Web browsers</a></li>\n<li><a href=\"/wiki/Timeline_of_web_search_engines\" title=\"Timeline of web search engines\">Web search engines</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Notable<br />people</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Kathleen_Antonelli\" title=\"Kathleen Antonelli\">Kathleen Antonelli</a></li>\n<li><a href=\"/wiki/John_Vincent_Atanasoff\" title=\"John Vincent Atanasoff\">John Vincent Atanasoff</a></li>\n<li><a href=\"/wiki/Charles_Babbage\" title=\"Charles Babbage\">Charles Babbage</a></li>\n<li><a href=\"/wiki/John_Backus\" title=\"John Backus\">John Backus</a></li>\n<li><a href=\"/wiki/Jean_Bartik\" title=\"Jean Bartik\">Jean Bartik</a></li>\n<li><a href=\"/wiki/George_Boole\" title=\"George Boole\">George Boole</a></li>\n<li><a href=\"/wiki/Vint_Cerf\" title=\"Vint Cerf\">Vint Cerf</a></li>\n<li><a href=\"/wiki/John_Cocke_(computer_scientist)\" title=\"John Cocke (computer scientist)\">John Cocke</a></li>\n<li><a href=\"/wiki/Stephen_Cook\" title=\"Stephen Cook\">Stephen Cook</a></li>\n<li><a href=\"/wiki/Edsger_W._Dijkstra\" title=\"Edsger W. Dijkstra\">Edsger W. Dijkstra</a></li>\n<li><a href=\"/wiki/J._Presper_Eckert\" title=\"J. Presper Eckert\">J. Presper Eckert</a></li>\n<li><a href=\"/wiki/Douglas_Engelbart\" title=\"Douglas Engelbart\">Douglas Engelbart</a></li>\n<li><a href=\"/wiki/Adele_Goldstine\" title=\"Adele Goldstine\">Adele Goldstine</a></li>\n<li><a href=\"/wiki/Lois_Haibt\" title=\"Lois Haibt\">Lois Haibt</a></li>\n<li><a href=\"/wiki/Betty_Holberton\" title=\"Betty Holberton\">Betty Holberton</a></li>\n<li><a href=\"/wiki/Margaret_Hamilton_(software_engineer)\" title=\"Margaret Hamilton (software engineer)\">Margaret Hamilton</a></li>\n<li><a href=\"/wiki/Grace_Hopper\" title=\"Grace Hopper\">Grace Hopper</a></li>\n<li><a href=\"/wiki/David_A._Huffman\" title=\"David A. Huffman\">David A. Huffman</a></li>\n<li><a href=\"/wiki/Robert_Kahn_(computer_scientist)\" title=\"Robert Kahn (computer scientist)\">Bob Kahn</a></li>\n<li><a href=\"/wiki/Alan_Kay\" title=\"Alan Kay\">Alan Kay</a></li>\n<li><a href=\"/wiki/Brian_Kernighan\" title=\"Brian Kernighan\">Brian Kernighan</a></li>\n<li><a href=\"/wiki/Andrew_Koenig_(programmer)\" title=\"Andrew Koenig (programmer)\">Andrew Koenig</a></li>\n<li><a href=\"/wiki/Semyon_Korsakov\" title=\"Semyon Korsakov\">Semyon Korsakov</a></li>\n<li><a href=\"/wiki/Donald_Knuth\" title=\"Donald Knuth\">Donald Knuth</a></li>\n<li><a href=\"/wiki/Joseph_Kruskal\" title=\"Joseph Kruskal\">Joseph Kruskal</a></li>\n<li><a href=\"/wiki/Nancy_Leveson\" title=\"Nancy Leveson\">Nancy Leveson</a></li>\n<li><a href=\"/wiki/Ada_Lovelace\" title=\"Ada Lovelace\">Ada Lovelace</a></li>\n<li><a href=\"/wiki/Douglas_McIlroy\" title=\"Douglas McIlroy\">Douglas McIlroy</a></li>\n<li><a href=\"/wiki/Marlyn_Meltzer\" title=\"Marlyn Meltzer\">Marlyn Meltzer</a></li>\n<li><a href=\"/wiki/John_von_Neumann\" title=\"John von Neumann\">John von Neumann</a></li>\n<li><a href=\"/wiki/Kl%C3%A1ra_D%C3%A1n_von_Neumann\" title=\"Klára Dán von Neumann\">Klára Dán von Neumann</a></li>\n<li><a href=\"/wiki/Dennis_Ritchie\" title=\"Dennis Ritchie\">Dennis Ritchie</a></li>\n<li><a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a></li>\n<li><a href=\"/wiki/Claude_Shannon\" title=\"Claude Shannon\">Claude Shannon</a></li>\n<li><a href=\"/wiki/Frances_Spence\" title=\"Frances Spence\">Frances Spence</a></li>\n<li><a href=\"/wiki/Bjarne_Stroustrup\" title=\"Bjarne Stroustrup\">Bjarne Stroustrup</a></li>\n<li><a href=\"/wiki/Ruth_Teitelbaum\" title=\"Ruth Teitelbaum\">Ruth Teitelbaum</a></li>\n<li><a href=\"/wiki/Ken_Thompson\" title=\"Ken Thompson\">Ken Thompson</a></li>\n<li><a href=\"/wiki/Linus_Torvalds\" title=\"Linus Torvalds\">Linus Torvalds</a></li>\n<li><a href=\"/wiki/Alan_Turing\" title=\"Alan Turing\">Alan Turing</a></li>\n<li><a href=\"/wiki/Paul_Vixie\" title=\"Paul Vixie\">Paul Vixie</a></li>\n<li><a href=\"/wiki/Larry_Wall\" title=\"Larry Wall\">Larry Wall</a></li>\n<li><a href=\"/wiki/Stephen_Wolfram\" title=\"Stephen Wolfram\">Stephen Wolfram</a></li>\n<li><a href=\"/wiki/Niklaus_Wirth\" title=\"Niklaus Wirth\">Niklaus Wirth</a></li>\n<li><a href=\"/wiki/Steve_Wozniak\" title=\"Steve Wozniak\">Steve Wozniak</a></li>\n<li><a href=\"/wiki/Konrad_Zuse\" title=\"Konrad Zuse\">Konrad Zuse</a></li></ul>\n</div></td></tr></tbody></table></div>\n<!--\nNewPP limit report\nParsed by mw‐web.codfw.main‐9794dfc48‐6hhfr\nCached time: 20251001152337\nCache expiry: 2592000\nReduced expiry: false\nComplications: [vary‐revision‐sha1, show‐toc]\nCPU time usage: 0.332 seconds\nReal time usage: 2.229 seconds\nPreprocessor visited node count: 484/1000000\nRevision size: 2325/2097152 bytes\nPost‐expand include size: 30854/2097152 bytes\nTemplate argument size: 378/2097152 bytes\nHighest expansion depth: 11/100\nExpensive parser function count: 1/500\nUnstrip recursion depth: 0/20\nUnstrip post‐expand size: 61360/5000000 bytes\nLua time usage: 0.201/10.000 seconds\nLua memory usage: 3962648/52428800 bytes\nNumber of Wikibase entities loaded: 0/500\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 2162.685      1 -total\n 85.70% 1853.327      1 Template:Timeline_History_of_Computing\n  4.62%   99.855      1 Template:History_of_computing\n  4.53%   97.995      1 Template:Sidebar\n  4.14%   89.564      1 Template:Cite_web\n  3.25%   70.217      1 Template:Short_description\n  1.63%   35.285      2 Template:Pagetype\n  1.59%   34.469      1 Template:Timelines_of_computing\n  1.50%   32.493      1 Template:Navbox\n  1.03%   22.293      2 Template:Main_other\n-->\n\n<!-- Saved in parser cache with key enwiki:pcache:6249:|#|:idhash:canonical and timestamp 20251001152337 and revision id 1278630980. Rendering was triggered because: page_view\n -->\n</div><noscript><img src=\"https://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1&amp;usesul3=1\" alt=\"\" width=\"1\" height=\"1\" style=\"border: none; position: absolute;\"></noscript>\n<div class=\"printfooter\" data-nosnippet=\"\">Retrieved from \"<a dir=\"ltr\" href=\"https://en.wikipedia.org/w/index.php?title=Timeline_of_computing&amp;oldid=1278630980\">https://en.wikipedia.org/w/index.php?title=Timeline_of_computing&amp;oldid=1278630980</a>\"</div></div>\n\t\t\t\t\t<div id=\"catlinks\" class=\"catlinks\" data-mw=\"interface\"><div id=\"mw-normal-catlinks\" class=\"mw-normal-catlinks\"><a href=\"/wiki/Help:Category\" title=\"Help:Category\">Categories</a>: <ul><li><a href=\"/wiki/Category:Computing_timelines\" title=\"Category:Computing timelines\">Computing timelines</a></li><li><a href=\"/wiki/Category:History_of_computing\" title=\"Category:History of computing\">History of computing</a></li><li><a href=\"/wiki/Category:Digital_Revolution\" title=\"Category:Digital Revolution\">Digital Revolution</a></li></ul></div><div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks mw-hidden-cats-hidden\">Hidden categories: <ul><li><a href=\"/wiki/Category:Pages_using_the_EasyTimeline_extension\" title=\"Category:Pages using the EasyTimeline extension\">Pages using the EasyTimeline extension</a></li><li><a href=\"/wiki/Category:Articles_with_short_description\" title=\"Category:Articles with short description\">Articles with short description</a></li><li><a href=\"/wiki/Category:Short_description_is_different_from_Wikidata\" title=\"Category:Short description is different from Wikidata\">Short description is different from Wikidata</a></li></ul></div></div>\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t</div>\n\t\t<div class=\"mw-footer-container\">\n\n<footer id=\"footer\" class=\"mw-footer\" >\n\t<ul id=\"footer-info\">\n\t<li id=\"footer-info-lastmod\"> This page was last edited on 3 March 2025, at 16:53<span class=\"anonymous-show\">&#160;(UTC)</span>.</li>\n\t<li id=\"footer-info-copyright\">Text is available under the <a href=\"/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License\" title=\"Wikipedia:Text of the Creative Commons Attribution-ShareAlike 4.0 International License\">Creative Commons Attribution-ShareAlike 4.0 License</a>;\nadditional terms may apply. By using this site, you agree to the <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Terms of Use\">Terms of Use</a> and <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Privacy policy\">Privacy Policy</a>. Wikipedia® is a registered trademark of the <a rel=\"nofollow\" class=\"external text\" href=\"https://wikimediafoundation.org/\">Wikimedia Foundation, Inc.</a>, a non-profit organization.</li>\n</ul>\n\n\t<ul id=\"footer-places\">\n\t<li id=\"footer-places-privacy\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\">Privacy policy</a></li>\n\t<li id=\"footer-places-about\"><a href=\"/wiki/Wikipedia:About\">About Wikipedia</a></li>\n\t<li id=\"footer-places-disclaimers\"><a href=\"/wiki/Wikipedia:General_disclaimer\">Disclaimers</a></li>\n\t<li id=\"footer-places-contact\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\">Contact Wikipedia</a></li>\n\t<li id=\"footer-places-wm-codeofconduct\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Universal_Code_of_Conduct\">Code of Conduct</a></li>\n\t<li id=\"footer-places-developers\"><a href=\"https://developer.wikimedia.org\">Developers</a></li>\n\t<li id=\"footer-places-statslink\"><a href=\"https://stats.wikimedia.org/#/en.wikipedia.org\">Statistics</a></li>\n\t<li id=\"footer-places-cookiestatement\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Cookie_statement\">Cookie statement</a></li>\n\t<li id=\"footer-places-mobileview\"><a href=\"//en.m.wikipedia.org/w/index.php?title=Timeline_of_computing&amp;mobileaction=toggle_view_mobile\" class=\"noprint stopMobileRedirectToggle\">Mobile view</a></li>\n</ul>\n\n\t<ul id=\"footer-icons\" class=\"noprint\">\n\t<li id=\"footer-copyrightico\"><a href=\"https://www.wikimedia.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/static/images/footer/wikimedia-button.svg\" width=\"84\" height=\"29\"><img src=\"/static/images/footer/wikimedia.svg\" width=\"25\" height=\"25\" alt=\"Wikimedia Foundation\" lang=\"en\" loading=\"lazy\"></picture></a></li>\n\t<li id=\"footer-poweredbyico\"><a href=\"https://www.mediawiki.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/w/resources/assets/poweredby_mediawiki.svg\" width=\"88\" height=\"31\"><img src=\"/w/resources/assets/mediawiki_compact.svg\" alt=\"Powered by MediaWiki\" lang=\"en\" width=\"25\" height=\"25\" loading=\"lazy\"></picture></a></li>\n</ul>\n\n</footer>\n\n\t\t</div>\n\t</div>\n</div>\n<div class=\"vector-header-container vector-sticky-header-container no-font-mode-scale\">\n\t<div id=\"vector-sticky-header\" class=\"vector-sticky-header\">\n\t\t<div class=\"vector-sticky-header-start\">\n\t\t\t<div class=\"vector-sticky-header-icon-start vector-button-flush-left vector-button-flush-right\" aria-hidden=\"true\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-sticky-header-search-toggle\" tabindex=\"-1\" data-event-name=\"ui.vector-sticky-search-form.icon\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div role=\"search\" class=\"vector-search-box-vue  vector-search-box-show-thumbnail vector-search-box\">\n\t\t\t<div class=\"vector-typeahead-search-container\">\n\t\t\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail\">\n\t\t\t\t\t<form action=\"/w/index.php\" id=\"vector-sticky-search-form\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t\t\t<div  class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\n\t\t\t\t\t\t\t\t\ttype=\"search\" name=\"search\" placeholder=\"Search Wikipedia\">\n\t\t\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-context-bar\">\n\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n\t\t\t\t\t<div id=\"vector-sticky-header-toc\" class=\"vector-dropdown mw-portlet mw-portlet-sticky-header-toc vector-sticky-header-toc vector-button-flush-left\"  >\n\t\t\t\t\t\t<input type=\"checkbox\" id=\"vector-sticky-header-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-sticky-header-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t\t\t\t\t\t<label id=\"vector-sticky-header-toc-label\" for=\"vector-sticky-header-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<div class=\"vector-dropdown-content\">\n\n\t\t\t\t\t\t<div id=\"vector-sticky-header-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t</nav>\n\t\t\t\t<div class=\"vector-sticky-header-context-bar-primary\" aria-hidden=\"true\" ><span class=\"mw-page-title-main\">Timeline of computing</span></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-end\" aria-hidden=\"true\">\n\t\t\t<div class=\"vector-sticky-header-icons\">\n\t\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-talk-sticky-header\" tabindex=\"-1\" data-event-name=\"talk-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbles mw-ui-icon-wikimedia-speechBubbles\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-subject-sticky-header\" tabindex=\"-1\" data-event-name=\"subject-sticky-header\"><span class=\"vector-icon mw-ui-icon-article mw-ui-icon-wikimedia-article\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-history-sticky-header\" tabindex=\"-1\" data-event-name=\"history-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-history mw-ui-icon-wikimedia-wikimedia-history\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only mw-watchlink\" id=\"ca-watchstar-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-star mw-ui-icon-wikimedia-wikimedia-star\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only reading-lists-bookmark\" id=\"ca-bookmark-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-bookmark\"><span class=\"vector-icon mw-ui-icon-wikimedia-bookmarkOutline mw-ui-icon-wikimedia-wikimedia-bookmarkOutline\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"wikitext-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-wikiText mw-ui-icon-wikimedia-wikimedia-wikiText\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-ve-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-edit mw-ui-icon-wikimedia-wikimedia-edit\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-viewsource-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-protected-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-editLock mw-ui-icon-wikimedia-wikimedia-editLock\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-buttons\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet mw-interlanguage-selector\" id=\"p-lang-btn-sticky-header\" tabindex=\"-1\" data-event-name=\"ui.dropdown-p-lang-btn-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-language mw-ui-icon-wikimedia-wikimedia-language\"></span>\n\n<span>7 languages</span>\n\t\t\t</button>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive\" id=\"ca-addsection-sticky-header\" tabindex=\"-1\" data-event-name=\"addsection-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbleAdd-progressive mw-ui-icon-wikimedia-speechBubbleAdd-progressive\"></span>\n\n<span>Add topic</span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-icon-end\">\n\t\t\t\t<div class=\"vector-user-links\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"mw-portlet mw-portlet-dock-bottom emptyPortlet\" id=\"p-dock-bottom\">\n\t<ul>\n\n\t</ul>\n</div>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({\"wgHostname\":\"mw-web.codfw.main-558cd6dccc-plpkf\",\"wgBackendResponseTime\":160,\"wgPageParseReport\":{\"limitreport\":{\"cputime\":\"0.332\",\"walltime\":\"2.229\",\"ppvisitednodes\":{\"value\":484,\"limit\":1000000},\"revisionsize\":{\"value\":2325,\"limit\":2097152},\"postexpandincludesize\":{\"value\":30854,\"limit\":2097152},\"templateargumentsize\":{\"value\":378,\"limit\":2097152},\"expansiondepth\":{\"value\":11,\"limit\":100},\"expensivefunctioncount\":{\"value\":1,\"limit\":500},\"unstrip-depth\":{\"value\":0,\"limit\":20},\"unstrip-size\":{\"value\":61360,\"limit\":5000000},\"entityaccesscount\":{\"value\":0,\"limit\":500},\"timingprofile\":[\"100.00% 2162.685      1 -total\",\" 85.70% 1853.327      1 Template:Timeline_History_of_Computing\",\"  4.62%   99.855      1 Template:History_of_computing\",\"  4.53%   97.995      1 Template:Sidebar\",\"  4.14%   89.564      1 Template:Cite_web\",\"  3.25%   70.217      1 Template:Short_description\",\"  1.63%   35.285      2 Template:Pagetype\",\"  1.59%   34.469      1 Template:Timelines_of_computing\",\"  1.50%   32.493      1 Template:Navbox\",\"  1.03%   22.293      2 Template:Main_other\"]},\"scribunto\":{\"limitreport-timeusage\":{\"value\":\"0.201\",\"limit\":\"10.000\"},\"limitreport-memusage\":{\"value\":3962648,\"limit\":52428800}},\"cachereport\":{\"origin\":\"mw-web.codfw.main-9794dfc48-6hhfr\",\"timestamp\":\"20251001152337\",\"ttl\":2592000,\"transientcontent\":false}}});});</script>\n<script type=\"application/ld+json\">{\"@context\":\"https:\\/\\/schema.org\",\"@type\":\"Article\",\"name\":\"Timeline of computing\",\"url\":\"https:\\/\\/en.wikipedia.org\\/wiki\\/Timeline_of_computing\",\"sameAs\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q2586120\",\"mainEntity\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q2586120\",\"author\":{\"@type\":\"Organization\",\"name\":\"Contributors to Wikimedia projects\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Wikimedia Foundation, Inc.\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/www.wikimedia.org\\/static\\/images\\/wmf-hor-googpub.png\"}},\"datePublished\":\"2001-11-10T22:34:58Z\",\"dateModified\":\"2025-03-03T16:53:55Z\",\"image\":\"https:\\/\\/upload.wikimedia.org\\/wikipedia\\/commons\\/d\\/d3\\/Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg\",\"headline\":\"timeline\"}</script>\n</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/wikipedia/medium_python.html",
    "content": "<!DOCTYPE html>\n<html class=\"client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\" lang=\"en\" dir=\"ltr\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Python (programming language) - Wikipedia</title>\n<script>(function(){var className=\"client-js vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\";var cookie=document.cookie.match(/(?:^|; )enwikimwclientpreferences=([^;]+)/);if(cookie){cookie[1].split('%2C').forEach(function(pref){className=className.replace(new RegExp('(^| )'+pref.replace(/-clientpref-\\w+$|[^\\w-]+/g,'')+'-clientpref-\\\\w+( |$)'),'$1'+pref+'$2');});}document.documentElement.className=className;}());RLCONF={\"wgBreakFrames\":false,\"wgSeparatorTransformTable\":[\"\",\"\"],\"wgDigitTransformTable\":[\"\",\"\"],\"wgDefaultDateFormat\":\"dmy\",\"wgMonthNames\":[\"\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"wgRequestId\":\"a07b3300-e2a2-40ea-880a-2f0e29397b6f\",\"wgCanonicalNamespace\":\"\",\"wgCanonicalSpecialPageName\":false,\"wgNamespaceNumber\":0,\"wgPageName\":\"Python_(programming_language)\",\"wgTitle\":\"Python (programming language)\",\"wgCurRevisionId\":1314465879,\"wgRevisionId\":1314465879,\"wgArticleId\":23862,\"wgIsArticle\":true,\"wgIsRedirect\":false,\"wgAction\":\"view\",\"wgUserName\":null,\"wgUserGroups\":[\"*\"],\"wgCategories\":[\"CS1 maint: article number as page number\",\"CS1: unfit URL\",\"Articles with short description\",\"Short description matches Wikidata\",\"Use dmy dates from November 2021\",\"Use American English from December 2024\",\"All Wikipedia articles written in American English\",\"All articles with failed verification\",\"Articles with failed verification from August 2025\",\"Articles containing potentially dated statements from August 2025\",\"All articles containing potentially dated statements\",\"Articles containing potentially dated statements from March 2025\",\"All articles with specifically marked weasel-worded phrases\",\"Articles with specifically marked weasel-worded phrases from August 2025\",\"All articles with unsourced statements\",\"Articles with unsourced statements from August 2025\",\"Articles containing potentially dated statements from December 2022\",\"Articles containing potentially dated statements from 2025\",\"Pages using Sister project links with wikidata namespace mismatch\",\"Pages using Sister project links with hidden wikidata\",\"Articles with example Python (programming language) code\",\"Python (programming language)\",\"Class-based programming languages\",\"Notebook interface\",\"Computer science in the Netherlands\",\"Concurrent programming languages\",\"Cross-platform free software\",\"Cross-platform software\",\"Dutch inventions\",\"Dynamically typed programming languages\",\"Educational programming languages\",\"High-level programming languages\",\"Information technology in the Netherlands\",\"Multi-paradigm programming languages\",\"Object-oriented programming languages\",\"Pattern matching programming languages\",\"Programming languages\",\"Programming languages created in 1991\",\"Scripting languages\",\"Text-oriented programming languages\",\"Monty Python references\"],\"wgPageViewLanguage\":\"en\",\"wgPageContentLanguage\":\"en\",\"wgPageContentModel\":\"wikitext\",\"wgRelevantPageName\":\"Python_(programming_language)\",\"wgRelevantArticleId\":23862,\"wgIsProbablyEditable\":true,\"wgRelevantPageIsProbablyEditable\":true,\"wgRestrictionEdit\":[],\"wgRestrictionMove\":[],\"wgNoticeProject\":\"wikipedia\",\"wgFlaggedRevsParams\":{\"tags\":{\"status\":{\"levels\":1}}},\"wgMediaViewerOnClick\":true,\"wgMediaViewerEnabledByDefault\":true,\"wgPopupsFlags\":0,\"wgVisualEditor\":{\"pageLanguageCode\":\"en\",\"pageLanguageDir\":\"ltr\",\"pageVariantFallbacks\":\"en\"},\"wgMFDisplayWikibaseDescriptions\":{\"search\":true,\"watchlist\":true,\"tagline\":false,\"nearby\":true},\"wgWMESchemaEditAttemptStepOversample\":false,\"wgWMEPageLength\":200000,\"wgMetricsPlatformUserExperiments\":{\"active_experiments\":[],\"overrides\":[],\"enrolled\":[],\"assigned\":[],\"subject_ids\":[],\"sampling_units\":[]},\"wgEditSubmitButtonLabelPublish\":true,\"wgULSPosition\":\"interlanguage\",\"wgULSisCompactLinksEnabled\":false,\"wgVector2022LanguageInHeader\":true,\"wgULSisLanguageSelectorEmpty\":false,\"wgWikibaseItemId\":\"Q28865\",\"wgCheckUserClientHintsHeadersJsApi\":[\"brands\",\"architecture\",\"bitness\",\"fullVersionList\",\"mobile\",\"model\",\"platform\",\"platformVersion\"],\"GEHomepageSuggestedEditsEnableTopics\":true,\"wgGESuggestedEditsTaskTypes\":{\"taskTypes\":[\"copyedit\",\"link-recommendation\"],\"unavailableTaskTypes\":[]},\"wgGETopicsMatchModeEnabled\":false,\"wgGELevelingUpEnabledForUser\":false};\nRLSTATE={\"ext.globalCssJs.user.styles\":\"ready\",\"site.styles\":\"ready\",\"user.styles\":\"ready\",\"ext.globalCssJs.user\":\"ready\",\"user\":\"ready\",\"user.options\":\"loading\",\"ext.cite.styles\":\"ready\",\"ext.pygments\":\"ready\",\"ext.wikimediamessages.styles\":\"ready\",\"skins.vector.search.codex.styles\":\"ready\",\"skins.vector.styles\":\"ready\",\"skins.vector.icons\":\"ready\",\"jquery.makeCollapsible.styles\":\"ready\",\"ext.visualEditor.desktopArticleTarget.noscript\":\"ready\",\"ext.uls.interlanguage\":\"ready\",\"wikibase.client.init\":\"ready\",\"ext.wikimediaBadges\":\"ready\"};RLPAGEMODULES=[\"ext.xLab\",\"ext.cite.ux-enhancements\",\"ext.pygments.view\",\"mediawiki.page.media\",\"site\",\"mediawiki.page.ready\",\"jquery.makeCollapsible\",\"mediawiki.toc\",\"skins.vector.js\",\"ext.centralNotice.geoIP\",\"ext.centralNotice.startUp\",\"ext.gadget.ReferenceTooltips\",\"ext.gadget.switcher\",\"ext.urlShortener.toolbar\",\"ext.centralauth.centralautologin\",\"mmv.bootstrap\",\"ext.popups\",\"ext.visualEditor.desktopArticleTarget.init\",\"ext.visualEditor.targetLoader\",\"ext.echo.centralauth\",\"ext.eventLogging\",\"ext.wikimediaEvents\",\"ext.navigationTiming\",\"ext.uls.interface\",\"ext.cx.eventlogging.campaigns\",\"ext.cx.uls.quick.actions\",\"wikibase.client.vector-2022\",\"ext.checkUser.clientHints\",\"ext.quicksurveys.init\",\"ext.growthExperiments.SuggestedEditSession\"];</script>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return[\"user.options@12s5i\",function($,jQuery,require,module){mw.user.tokens.set({\"patrolToken\":\"+\\\\\",\"watchToken\":\"+\\\\\",\"csrfToken\":\"+\\\\\"});\n}];});});</script>\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=ext.cite.styles%7Cext.pygments%2CwikimediaBadges%7Cext.uls.interlanguage%7Cext.visualEditor.desktopArticleTarget.noscript%7Cext.wikimediamessages.styles%7Cjquery.makeCollapsible.styles%7Cskins.vector.icons%2Cstyles%7Cskins.vector.search.codex.styles%7Cwikibase.client.init&amp;only=styles&amp;skin=vector-2022\">\n<script async=\"\" src=\"/w/load.php?lang=en&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;skin=vector-2022\"></script>\n<meta name=\"ResourceLoaderDynamicStyles\" content=\"\">\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=vector-2022\">\n<meta name=\"generator\" content=\"MediaWiki 1.45.0-wmf.20\">\n<meta name=\"referrer\" content=\"origin\">\n<meta name=\"referrer\" content=\"origin-when-cross-origin\">\n<meta name=\"robots\" content=\"max-image-preview:standard\">\n<meta name=\"format-detection\" content=\"telephone=no\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/1200px-Python-logo-notext.svg.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"1200\">\n<meta name=\"viewport\" content=\"width=1120\">\n<meta property=\"og:title\" content=\"Python (programming language) - Wikipedia\">\n<meta property=\"og:type\" content=\"website\">\n<link rel=\"preconnect\" href=\"//upload.wikimedia.org\">\n<link rel=\"alternate\" media=\"only screen and (max-width: 640px)\" href=\"//en.m.wikipedia.org/wiki/Python_(programming_language)\">\n<link rel=\"alternate\" type=\"application/x-wiki\" title=\"Edit this page\" href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit\">\n<link rel=\"apple-touch-icon\" href=\"/static/apple-touch/wikipedia.png\">\n<link rel=\"icon\" href=\"/static/favicon/wikipedia.ico\">\n<link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"/w/rest.php/v1/search\" title=\"Wikipedia (en)\">\n<link rel=\"EditURI\" type=\"application/rsd+xml\" href=\"//en.wikipedia.org/w/api.php?action=rsd\">\n<link rel=\"canonical\" href=\"https://en.wikipedia.org/wiki/Python_(programming_language)\">\n<link rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/4.0/deed.en\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Wikipedia Atom feed\" href=\"/w/index.php?title=Special:RecentChanges&amp;feed=atom\">\n<link rel=\"dns-prefetch\" href=\"//meta.wikimedia.org\" />\n<link rel=\"dns-prefetch\" href=\"auth.wikimedia.org\">\n</head>\n<body class=\"skin--responsive skin-vector skin-vector-search-vue mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject mw-editable page-Python_programming_language rootpage-Python_programming_language skin-vector-2022 action-view\"><a class=\"mw-jump-link\" href=\"#bodyContent\">Jump to content</a>\n<div class=\"vector-header-container\">\n\t<header class=\"vector-header mw-header no-font-mode-scale\">\n\t\t<div class=\"vector-header-start\">\n\t\t\t<nav class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\n<div id=\"vector-main-menu-dropdown\" class=\"vector-dropdown vector-main-menu-dropdown vector-button-flush-left vector-button-flush-right\"  title=\"Main menu\" >\n\t<input type=\"checkbox\" id=\"vector-main-menu-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-main-menu-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Main menu\"  >\n\t<label id=\"vector-main-menu-dropdown-label\" for=\"vector-main-menu-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu\"></span>\n\n<span class=\"vector-dropdown-label-text\">Main menu</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t<div id=\"vector-main-menu-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-main-menu\" class=\"vector-main-menu vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-main-menu-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"main-menu-pinned\"\n\tdata-pinnable-element-id=\"vector-main-menu\"\n\tdata-pinned-container-id=\"vector-main-menu-pinned-container\"\n\tdata-unpinned-container-id=\"vector-main-menu-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Main menu</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-main-menu.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-main-menu.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-navigation\" class=\"vector-menu mw-portlet mw-portlet-navigation\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tNavigation\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-mainpage-description\" class=\"mw-list-item\"><a href=\"/wiki/Main_Page\" title=\"Visit the main page [z]\" accesskey=\"z\"><span>Main page</span></a></li><li id=\"n-contents\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Contents\" title=\"Guides to browsing Wikipedia\"><span>Contents</span></a></li><li id=\"n-currentevents\" class=\"mw-list-item\"><a href=\"/wiki/Portal:Current_events\" title=\"Articles related to current events\"><span>Current events</span></a></li><li id=\"n-randompage\" class=\"mw-list-item\"><a href=\"/wiki/Special:Random\" title=\"Visit a randomly selected article [x]\" accesskey=\"x\"><span>Random article</span></a></li><li id=\"n-aboutsite\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:About\" title=\"Learn about Wikipedia and how it works\"><span>About Wikipedia</span></a></li><li id=\"n-contactpage\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\" title=\"How to contact Wikipedia\"><span>Contact us</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\n<div id=\"p-interaction\" class=\"vector-menu mw-portlet mw-portlet-interaction\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tContribute\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-help\" class=\"mw-list-item\"><a href=\"/wiki/Help:Contents\" title=\"Guidance on how to use and edit Wikipedia\"><span>Help</span></a></li><li id=\"n-introduction\" class=\"mw-list-item\"><a href=\"/wiki/Help:Introduction\" title=\"Learn how to edit Wikipedia\"><span>Learn to edit</span></a></li><li id=\"n-portal\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Community_portal\" title=\"The hub for editors\"><span>Community portal</span></a></li><li id=\"n-recentchanges\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChanges\" title=\"A list of recent changes to Wikipedia [r]\" accesskey=\"r\"><span>Recent changes</span></a></li><li id=\"n-upload\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:File_upload_wizard\" title=\"Add images or other media for use on Wikipedia\"><span>Upload file</span></a></li><li id=\"n-specialpages\" class=\"mw-list-item\"><a href=\"/wiki/Special:SpecialPages\"><span>Special pages</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t</nav>\n\n<a href=\"/wiki/Main_Page\" class=\"mw-logo\">\n\t<img class=\"mw-logo-icon\" src=\"/static/images/icons/wikipedia.png\" alt=\"\" aria-hidden=\"true\" height=\"50\" width=\"50\">\n\t<span class=\"mw-logo-container skin-invert\">\n\t\t<img class=\"mw-logo-wordmark\" alt=\"Wikipedia\" src=\"/static/images/mobile/copyright/wikipedia-wordmark-en.svg\" style=\"width: 7.5em; height: 1.125em;\">\n\t\t<img class=\"mw-logo-tagline\" alt=\"The Free Encyclopedia\" src=\"/static/images/mobile/copyright/wikipedia-tagline-en.svg\" width=\"117\" height=\"13\" style=\"width: 7.3125em; height: 0.8125em;\">\n\t</span>\n</a>\n\n\t\t</div>\n\t\t<div class=\"vector-header-end\">\n\n<div id=\"p-search\" role=\"search\" class=\"vector-search-box-vue  vector-search-box-collapses vector-search-box-show-thumbnail vector-search-box-auto-expand-width vector-search-box\">\n\t<a href=\"/wiki/Special:Search\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only search-toggle\" title=\"Search Wikipedia [f]\" accesskey=\"f\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t</a>\n\t<div class=\"vector-typeahead-search-container\">\n\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail cdx-typeahead-search--auto-expand-width\">\n\t\t\t<form action=\"/w/index.php\" id=\"searchform\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t<div id=\"simpleSearch\" class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\t\t\t\t\t\t\t type=\"search\" name=\"search\" placeholder=\"Search Wikipedia\" aria-label=\"Search Wikipedia\" autocapitalize=\"sentences\" spellcheck=\"false\" title=\"Search Wikipedia [f]\" accesskey=\"f\" id=\"searchInput\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t</div>\n\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n\t\t\t<nav class=\"vector-user-links vector-user-links-wide\" aria-label=\"Personal tools\">\n\t<div class=\"vector-user-links-main\">\n\n<div id=\"p-vector-user-menu-preferences\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-userpage\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\n<div id=\"vector-appearance-dropdown\" class=\"vector-dropdown \"  title=\"Change the appearance of the page&#039;s font size, width, and color\" >\n\t<input type=\"checkbox\" id=\"vector-appearance-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-appearance-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Appearance\"  >\n\t<label id=\"vector-appearance-dropdown-label\" for=\"vector-appearance-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-appearance mw-ui-icon-wikimedia-appearance\"></span>\n\n<span class=\"vector-dropdown-label-text\">Appearance</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t<div id=\"vector-appearance-unpinned-container\" class=\"vector-unpinned-container\">\n\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t</nav>\n\n<div id=\"p-vector-user-menu-notifications\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-overflow\" class=\"vector-menu mw-portlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\t\t\t<li id=\"pt-sitesupport-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\" class=\"\"><span>Donate</span></a>\n</li>\n<li id=\"pt-createaccount-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Python+%28programming+language%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\" class=\"\"><span>Create account</span></a>\n</li>\n<li id=\"pt-login-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Python+%28programming+language%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\" class=\"\"><span>Log in</span></a>\n</li>\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t</div>\n\n<div id=\"vector-user-links-dropdown\" class=\"vector-dropdown vector-user-menu vector-button-flush-right vector-user-menu-logged-out\"  title=\"Log in and more options\" >\n\t<input type=\"checkbox\" id=\"vector-user-links-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-user-links-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Personal tools\"  >\n\t<label id=\"vector-user-links-dropdown-label\" for=\"vector-user-links-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-ellipsis mw-ui-icon-wikimedia-ellipsis\"></span>\n\n<span class=\"vector-dropdown-label-text\">Personal tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-personal\" class=\"vector-menu mw-portlet mw-portlet-personal user-links-collapsible-item\"  title=\"User menu\" >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-sitesupport\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\"><span>Donate</span></a></li><li id=\"pt-createaccount\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=Python+%28programming+language%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\"><span class=\"vector-icon mw-ui-icon-userAdd mw-ui-icon-wikimedia-userAdd\"></span> <span>Create account</span></a></li><li id=\"pt-login\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:UserLogin&amp;returnto=Python+%28programming+language%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\"><span class=\"vector-icon mw-ui-icon-logIn mw-ui-icon-wikimedia-logIn\"></span> <span>Log in</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-user-menu-anon-editor\" class=\"vector-menu mw-portlet mw-portlet-user-menu-anon-editor\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPages for logged out editors <a href=\"/wiki/Help:Introduction\" aria-label=\"Learn more about editing\"><span>learn more</span></a>\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-anoncontribs\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyContributions\" title=\"A list of edits made from this IP address [y]\" accesskey=\"y\"><span>Contributions</span></a></li><li id=\"pt-anontalk\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyTalk\" title=\"Discussion about edits from this IP address [n]\" accesskey=\"n\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n</nav>\n\n\t\t</div>\n\t</header>\n</div>\n<div class=\"mw-page-container\">\n\t<div class=\"mw-page-container-inner\">\n\t\t<div class=\"vector-sitenotice-container\">\n\t\t\t<div id=\"siteNotice\"><!-- CentralNotice --></div>\n\t\t</div>\n\t\t<div class=\"vector-column-start\">\n\t\t\t<div class=\"vector-main-menu-container\">\n\t\t<div id=\"mw-navigation\">\n\t\t\t<nav id=\"mw-panel\" class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\t\t\t\t<div id=\"vector-main-menu-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t</div>\n\t\t</nav>\n\t\t</div>\n\t</div>\n\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t<nav id=\"mw-panel-toc\" aria-label=\"Contents\" data-event-name=\"ui.sidebar-toc\" class=\"mw-table-of-contents-container vector-toc-landmark\">\n\t\t\t\t\t<div id=\"vector-toc-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t\t<div id=\"vector-toc\" class=\"vector-toc vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"toc-pinned\"\n\tdata-pinnable-element-id=\"vector-toc\"\n\n\n>\n\t<h2 class=\"vector-pinnable-header-label\">Contents</h2>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-toc.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-toc.unpin\">hide</button>\n</div>\n\n\n\t<ul class=\"vector-toc-contents\" id=\"mw-panel-toc-list\">\n\t\t<li id=\"toc-mw-content-text\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t\t<a href=\"#\" class=\"vector-toc-link\">\n\t\t\t\t<div class=\"vector-toc-text\">(Top)</div>\n\t\t\t</a>\n\t\t</li>\n\t\t<li id=\"toc-History\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#History\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">1</span>\n\t\t\t\t<span>History</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-History-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Design_philosophy_and_features\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Design_philosophy_and_features\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">2</span>\n\t\t\t\t<span>Design philosophy and features</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Design_philosophy_and_features-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Syntax_and_semantics\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Syntax_and_semantics\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">3</span>\n\t\t\t\t<span>Syntax and semantics</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Syntax_and_semantics-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Syntax and semantics subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Syntax_and_semantics-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Indentation\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Indentation\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.1</span>\n\t\t\t\t\t<span>Indentation</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Indentation-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Statements_and_control_flow\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Statements_and_control_flow\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.2</span>\n\t\t\t\t\t<span>Statements and control flow</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Statements_and_control_flow-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Expressions\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Expressions\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.3</span>\n\t\t\t\t\t<span>Expressions</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Expressions-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Methods\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Methods\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.4</span>\n\t\t\t\t\t<span>Methods</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Methods-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Typing\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Typing\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.5</span>\n\t\t\t\t\t<span>Typing</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Typing-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Arithmetic_operations\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Arithmetic_operations\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.6</span>\n\t\t\t\t\t<span>Arithmetic operations</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Arithmetic_operations-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Function_syntax\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Function_syntax\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">3.7</span>\n\t\t\t\t\t<span>Function syntax</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Function_syntax-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Code_examples\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Code_examples\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">4</span>\n\t\t\t\t<span>Code examples</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Code_examples-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Libraries\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Libraries\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">5</span>\n\t\t\t\t<span>Libraries</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Libraries-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Development_environments\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Development_environments\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">6</span>\n\t\t\t\t<span>Development environments</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Development_environments-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Implementations\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Implementations\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">7</span>\n\t\t\t\t<span>Implementations</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Implementations-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Implementations subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Implementations-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Reference_implementation\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Reference_implementation\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">7.1</span>\n\t\t\t\t\t<span>Reference implementation</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Reference_implementation-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Other_implementations\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Other_implementations\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">7.2</span>\n\t\t\t\t\t<span>Other implementations</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Other_implementations-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Unsupported_implementations\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Unsupported_implementations\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">7.3</span>\n\t\t\t\t\t<span>Unsupported implementations</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Unsupported_implementations-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Cross-compilers_to_other_languages\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Cross-compilers_to_other_languages\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">7.4</span>\n\t\t\t\t\t<span>Cross-compilers to other languages</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Cross-compilers_to_other_languages-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Performance\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Performance\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">7.5</span>\n\t\t\t\t\t<span>Performance</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Performance-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Language_Development\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Language_Development\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">8</span>\n\t\t\t\t<span>Language Development</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Language_Development-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-API_documentation_generators\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#API_documentation_generators\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">9</span>\n\t\t\t\t<span>API documentation generators</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-API_documentation_generators-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Naming\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Naming\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">10</span>\n\t\t\t\t<span>Naming</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Naming-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Popularity\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Popularity\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">11</span>\n\t\t\t\t<span>Popularity</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Popularity-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Types_of_use\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Types_of_use\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">12</span>\n\t\t\t\t<span>Types of use</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Types_of_use-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Limitations\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Limitations\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">13</span>\n\t\t\t\t<span>Limitations</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Limitations-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Languages_influenced_by_Python\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Languages_influenced_by_Python\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">14</span>\n\t\t\t\t<span>Languages influenced by Python</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Languages_influenced_by_Python-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-See_also\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#See_also\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">15</span>\n\t\t\t\t<span>See also</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-See_also-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-External_links\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">16</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Notes\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Notes\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">17</span>\n\t\t\t\t<span>Notes</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Notes-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-References\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#References\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">18</span>\n\t\t\t\t<span>References</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-References-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle References subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-References-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Sources\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Sources\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">18.1</span>\n\t\t\t\t\t<span>Sources</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Sources-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Further_reading\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Further_reading\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">19</span>\n\t\t\t\t<span>Further reading</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Further_reading-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-External_links_2\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links_2\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">20</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links_2-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n</ul>\n</div>\n\n\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mw-content-container\">\n\t\t\t<main id=\"content\" class=\"mw-body\">\n\t\t\t\t<header class=\"mw-body-header vector-page-titlebar no-font-mode-scale\">\n\t\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n<div id=\"vector-page-titlebar-toc\" class=\"vector-dropdown vector-page-titlebar-toc vector-button-flush-left\"  title=\"Table of Contents\" >\n\t<input type=\"checkbox\" id=\"vector-page-titlebar-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-titlebar-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t<label id=\"vector-page-titlebar-toc-label\" for=\"vector-page-titlebar-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t<div id=\"vector-page-titlebar-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t</nav>\n\t\t\t\t\t<h1 id=\"firstHeading\" class=\"firstHeading mw-first-heading\"><span class=\"mw-page-title-main\">Python (programming language)</span></h1>\n\n<div id=\"p-lang-btn\" class=\"vector-dropdown mw-portlet mw-portlet-lang\"  >\n\t<input type=\"checkbox\" id=\"p-lang-btn-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-p-lang-btn\" class=\"vector-dropdown-checkbox mw-interlanguage-selector\" aria-label=\"Go to an article in another language. Available in 116 languages\"   >\n\t<label id=\"p-lang-btn-label\" for=\"p-lang-btn-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive mw-portlet-lang-heading-116\" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-language-progressive mw-ui-icon-wikimedia-language-progressive\"></span>\n\n<span class=\"vector-dropdown-label-text\">116 languages</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\t\t<div class=\"vector-menu-content\">\n\n\t\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t\t<li class=\"interlanguage-link interwiki-af mw-list-item\"><a href=\"https://af.wikipedia.org/wiki/Python_(programmeertaal)\" title=\"Python (programmeertaal) – Afrikaans\" lang=\"af\" hreflang=\"af\" data-title=\"Python (programmeertaal)\" data-language-autonym=\"Afrikaans\" data-language-local-name=\"Afrikaans\" class=\"interlanguage-link-target\"><span>Afrikaans</span></a></li><li class=\"interlanguage-link interwiki-als mw-list-item\"><a href=\"https://als.wikipedia.org/wiki/Python_(Programmiersprache)\" title=\"Python (Programmiersprache) – Alemannic\" lang=\"gsw\" hreflang=\"gsw\" data-title=\"Python (Programmiersprache)\" data-language-autonym=\"Alemannisch\" data-language-local-name=\"Alemannic\" class=\"interlanguage-link-target\"><span>Alemannisch</span></a></li><li class=\"interlanguage-link interwiki-ar badge-Q17437798 badge-goodarticle mw-list-item\" title=\"good article\"><a href=\"https://ar.wikipedia.org/wiki/%D8%A8%D8%A7%D9%8A%D8%AB%D9%88%D9%86_(%D9%84%D8%BA%D8%A9_%D8%A8%D8%B1%D9%85%D8%AC%D8%A9)\" title=\"بايثون (لغة برمجة) – Arabic\" lang=\"ar\" hreflang=\"ar\" data-title=\"بايثون (لغة برمجة)\" data-language-autonym=\"العربية\" data-language-local-name=\"Arabic\" class=\"interlanguage-link-target\"><span>العربية</span></a></li><li class=\"interlanguage-link interwiki-an mw-list-item\"><a href=\"https://an.wikipedia.org/wiki/Python\" title=\"Python – Aragonese\" lang=\"an\" hreflang=\"an\" data-title=\"Python\" data-language-autonym=\"Aragonés\" data-language-local-name=\"Aragonese\" class=\"interlanguage-link-target\"><span>Aragonés</span></a></li><li class=\"interlanguage-link interwiki-as mw-list-item\"><a href=\"https://as.wikipedia.org/wiki/%E0%A6%AA%E0%A6%BE%E0%A6%87%E0%A6%A5%E0%A6%A8\" title=\"পাইথন – Assamese\" lang=\"as\" hreflang=\"as\" data-title=\"পাইথন\" data-language-autonym=\"অসমীয়া\" data-language-local-name=\"Assamese\" class=\"interlanguage-link-target\"><span>অসমীয়া</span></a></li><li class=\"interlanguage-link interwiki-ast mw-list-item\"><a href=\"https://ast.wikipedia.org/wiki/Python\" title=\"Python – Asturian\" lang=\"ast\" hreflang=\"ast\" data-title=\"Python\" data-language-autonym=\"Asturianu\" data-language-local-name=\"Asturian\" class=\"interlanguage-link-target\"><span>Asturianu</span></a></li><li class=\"interlanguage-link interwiki-az mw-list-item\"><a href=\"https://az.wikipedia.org/wiki/Python_(proqramla%C5%9Fd%C4%B1rma_dili)\" title=\"Python (proqramlaşdırma dili) – Azerbaijani\" lang=\"az\" hreflang=\"az\" data-title=\"Python (proqramlaşdırma dili)\" data-language-autonym=\"Azərbaycanca\" data-language-local-name=\"Azerbaijani\" class=\"interlanguage-link-target\"><span>Azərbaycanca</span></a></li><li class=\"interlanguage-link interwiki-azb mw-list-item\"><a href=\"https://azb.wikipedia.org/wiki/%D9%BE%D8%A7%DB%8C%D8%AA%D9%88%D9%86\" title=\"پایتون – South Azerbaijani\" lang=\"azb\" hreflang=\"azb\" data-title=\"پایتون\" data-language-autonym=\"تۆرکجه\" data-language-local-name=\"South Azerbaijani\" class=\"interlanguage-link-target\"><span>تۆرکجه</span></a></li><li class=\"interlanguage-link interwiki-ban mw-list-item\"><a href=\"https://ban.wikipedia.org/wiki/Python\" title=\"Python – Balinese\" lang=\"ban\" hreflang=\"ban\" data-title=\"Python\" data-language-autonym=\"Basa Bali\" data-language-local-name=\"Balinese\" class=\"interlanguage-link-target\"><span>Basa Bali</span></a></li><li class=\"interlanguage-link interwiki-bn mw-list-item\"><a href=\"https://bn.wikipedia.org/wiki/%E0%A6%AA%E0%A6%BE%E0%A6%87%E0%A6%A5%E0%A6%A8_(%E0%A6%AA%E0%A7%8D%E0%A6%B0%E0%A7%8B%E0%A6%97%E0%A7%8D%E0%A6%B0%E0%A6%BE%E0%A6%AE%E0%A6%BF%E0%A6%82_%E0%A6%AD%E0%A6%BE%E0%A6%B7%E0%A6%BE)\" title=\"পাইথন (প্রোগ্রামিং ভাষা) – Bangla\" lang=\"bn\" hreflang=\"bn\" data-title=\"পাইথন (প্রোগ্রামিং ভাষা)\" data-language-autonym=\"বাংলা\" data-language-local-name=\"Bangla\" class=\"interlanguage-link-target\"><span>বাংলা</span></a></li><li class=\"interlanguage-link interwiki-zh-min-nan mw-list-item\"><a href=\"https://zh-min-nan.wikipedia.org/wiki/Python\" title=\"Python – Minnan\" lang=\"nan\" hreflang=\"nan\" data-title=\"Python\" data-language-autonym=\"閩南語 / Bân-lâm-gí\" data-language-local-name=\"Minnan\" class=\"interlanguage-link-target\"><span>閩南語 / Bân-lâm-gí</span></a></li><li class=\"interlanguage-link interwiki-be mw-list-item\"><a href=\"https://be.wikipedia.org/wiki/Python_(%D0%BC%D0%BE%D0%B2%D0%B0_%D0%BF%D1%80%D0%B0%D0%B3%D1%80%D0%B0%D0%BC%D0%B0%D0%B2%D0%B0%D0%BD%D0%BD%D1%8F)\" title=\"Python (мова праграмавання) – Belarusian\" lang=\"be\" hreflang=\"be\" data-title=\"Python (мова праграмавання)\" data-language-autonym=\"Беларуская\" data-language-local-name=\"Belarusian\" class=\"interlanguage-link-target\"><span>Беларуская</span></a></li><li class=\"interlanguage-link interwiki-be-x-old mw-list-item\"><a href=\"https://be-tarask.wikipedia.org/wiki/Python\" title=\"Python – Belarusian (Taraškievica orthography)\" lang=\"be-tarask\" hreflang=\"be-tarask\" data-title=\"Python\" data-language-autonym=\"Беларуская (тарашкевіца)\" data-language-local-name=\"Belarusian (Taraškievica orthography)\" class=\"interlanguage-link-target\"><span>Беларуская (тарашкевіца)</span></a></li><li class=\"interlanguage-link interwiki-bh mw-list-item\"><a href=\"https://bh.wikipedia.org/wiki/%E0%A4%AA%E0%A4%BE%E0%A4%87%E0%A4%A5%E0%A4%A8\" title=\"पाइथन – Bhojpuri\" lang=\"bh\" hreflang=\"bh\" data-title=\"पाइथन\" data-language-autonym=\"भोजपुरी\" data-language-local-name=\"Bhojpuri\" class=\"interlanguage-link-target\"><span>भोजपुरी</span></a></li><li class=\"interlanguage-link interwiki-bg mw-list-item\"><a href=\"https://bg.wikipedia.org/wiki/Python\" title=\"Python – Bulgarian\" lang=\"bg\" hreflang=\"bg\" data-title=\"Python\" data-language-autonym=\"Български\" data-language-local-name=\"Bulgarian\" class=\"interlanguage-link-target\"><span>Български</span></a></li><li class=\"interlanguage-link interwiki-bs mw-list-item\"><a href=\"https://bs.wikipedia.org/wiki/Python_(programski_jezik)\" title=\"Python (programski jezik) – Bosnian\" lang=\"bs\" hreflang=\"bs\" data-title=\"Python (programski jezik)\" data-language-autonym=\"Bosanski\" data-language-local-name=\"Bosnian\" class=\"interlanguage-link-target\"><span>Bosanski</span></a></li><li class=\"interlanguage-link interwiki-br mw-list-item\"><a href=\"https://br.wikipedia.org/wiki/Python_(lavar_programmi%C3%B1)\" title=\"Python (lavar programmiñ) – Breton\" lang=\"br\" hreflang=\"br\" data-title=\"Python (lavar programmiñ)\" data-language-autonym=\"Brezhoneg\" data-language-local-name=\"Breton\" class=\"interlanguage-link-target\"><span>Brezhoneg</span></a></li><li class=\"interlanguage-link interwiki-ca mw-list-item\"><a href=\"https://ca.wikipedia.org/wiki/Python\" title=\"Python – Catalan\" lang=\"ca\" hreflang=\"ca\" data-title=\"Python\" data-language-autonym=\"Català\" data-language-local-name=\"Catalan\" class=\"interlanguage-link-target\"><span>Català</span></a></li><li class=\"interlanguage-link interwiki-ceb mw-list-item\"><a href=\"https://ceb.wikipedia.org/wiki/Python_(programming_language)\" title=\"Python (programming language) – Cebuano\" lang=\"ceb\" hreflang=\"ceb\" data-title=\"Python (programming language)\" data-language-autonym=\"Cebuano\" data-language-local-name=\"Cebuano\" class=\"interlanguage-link-target\"><span>Cebuano</span></a></li><li class=\"interlanguage-link interwiki-cs mw-list-item\"><a href=\"https://cs.wikipedia.org/wiki/Python\" title=\"Python – Czech\" lang=\"cs\" hreflang=\"cs\" data-title=\"Python\" data-language-autonym=\"Čeština\" data-language-local-name=\"Czech\" class=\"interlanguage-link-target\"><span>Čeština</span></a></li><li class=\"interlanguage-link interwiki-cy mw-list-item\"><a href=\"https://cy.wikipedia.org/wiki/Python_(iaith_raglennu)\" title=\"Python (iaith raglennu) – Welsh\" lang=\"cy\" hreflang=\"cy\" data-title=\"Python (iaith raglennu)\" data-language-autonym=\"Cymraeg\" data-language-local-name=\"Welsh\" class=\"interlanguage-link-target\"><span>Cymraeg</span></a></li><li class=\"interlanguage-link interwiki-da mw-list-item\"><a href=\"https://da.wikipedia.org/wiki/Python_(programmeringssprog)\" title=\"Python (programmeringssprog) – Danish\" lang=\"da\" hreflang=\"da\" data-title=\"Python (programmeringssprog)\" data-language-autonym=\"Dansk\" data-language-local-name=\"Danish\" class=\"interlanguage-link-target\"><span>Dansk</span></a></li><li class=\"interlanguage-link interwiki-de badge-Q17437798 badge-goodarticle mw-list-item\" title=\"good article\"><a href=\"https://de.wikipedia.org/wiki/Python_(Programmiersprache)\" title=\"Python (Programmiersprache) – German\" lang=\"de\" hreflang=\"de\" data-title=\"Python (Programmiersprache)\" data-language-autonym=\"Deutsch\" data-language-local-name=\"German\" class=\"interlanguage-link-target\"><span>Deutsch</span></a></li><li class=\"interlanguage-link interwiki-et mw-list-item\"><a href=\"https://et.wikipedia.org/wiki/Python_(programmeerimiskeel)\" title=\"Python (programmeerimiskeel) – Estonian\" lang=\"et\" hreflang=\"et\" data-title=\"Python (programmeerimiskeel)\" data-language-autonym=\"Eesti\" data-language-local-name=\"Estonian\" class=\"interlanguage-link-target\"><span>Eesti</span></a></li><li class=\"interlanguage-link interwiki-el mw-list-item\"><a href=\"https://el.wikipedia.org/wiki/Python\" title=\"Python – Greek\" lang=\"el\" hreflang=\"el\" data-title=\"Python\" data-language-autonym=\"Ελληνικά\" data-language-local-name=\"Greek\" class=\"interlanguage-link-target\"><span>Ελληνικά</span></a></li><li class=\"interlanguage-link interwiki-es mw-list-item\"><a href=\"https://es.wikipedia.org/wiki/Python\" title=\"Python – Spanish\" lang=\"es\" hreflang=\"es\" data-title=\"Python\" data-language-autonym=\"Español\" data-language-local-name=\"Spanish\" class=\"interlanguage-link-target\"><span>Español</span></a></li><li class=\"interlanguage-link interwiki-eo mw-list-item\"><a href=\"https://eo.wikipedia.org/wiki/Python_(programlingvo)\" title=\"Python (programlingvo) – Esperanto\" lang=\"eo\" hreflang=\"eo\" data-title=\"Python (programlingvo)\" data-language-autonym=\"Esperanto\" data-language-local-name=\"Esperanto\" class=\"interlanguage-link-target\"><span>Esperanto</span></a></li><li class=\"interlanguage-link interwiki-eu mw-list-item\"><a href=\"https://eu.wikipedia.org/wiki/Python_(informatika)\" title=\"Python (informatika) – Basque\" lang=\"eu\" hreflang=\"eu\" data-title=\"Python (informatika)\" data-language-autonym=\"Euskara\" data-language-local-name=\"Basque\" class=\"interlanguage-link-target\"><span>Euskara</span></a></li><li class=\"interlanguage-link interwiki-fa mw-list-item\"><a href=\"https://fa.wikipedia.org/wiki/%D9%BE%D8%A7%DB%8C%D8%AA%D9%88%D9%86_(%D8%B2%D8%A8%D8%A7%D9%86_%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%E2%80%8C%D9%86%D9%88%DB%8C%D8%B3%DB%8C)\" title=\"پایتون (زبان برنامه‌نویسی) – Persian\" lang=\"fa\" hreflang=\"fa\" data-title=\"پایتون (زبان برنامه‌نویسی)\" data-language-autonym=\"فارسی\" data-language-local-name=\"Persian\" class=\"interlanguage-link-target\"><span>فارسی</span></a></li><li class=\"interlanguage-link interwiki-fr mw-list-item\"><a href=\"https://fr.wikipedia.org/wiki/Python_(langage)\" title=\"Python (langage) – French\" lang=\"fr\" hreflang=\"fr\" data-title=\"Python (langage)\" data-language-autonym=\"Français\" data-language-local-name=\"French\" class=\"interlanguage-link-target\"><span>Français</span></a></li><li class=\"interlanguage-link interwiki-gl mw-list-item\"><a href=\"https://gl.wikipedia.org/wiki/Python\" title=\"Python – Galician\" lang=\"gl\" hreflang=\"gl\" data-title=\"Python\" data-language-autonym=\"Galego\" data-language-local-name=\"Galician\" class=\"interlanguage-link-target\"><span>Galego</span></a></li><li class=\"interlanguage-link interwiki-glk mw-list-item\"><a href=\"https://glk.wikipedia.org/wiki/%D9%BE%D8%A7%D9%8A%D8%AA%D8%A4%D9%86_(%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%E2%80%8C%D9%86%D9%8A%D9%88%D9%8A%D8%B3%D9%8A_%D8%B2%D9%88%D8%A7%D9%86)\" title=\"پايتؤن (برنامه‌نيويسي زوان) – Gilaki\" lang=\"glk\" hreflang=\"glk\" data-title=\"پايتؤن (برنامه‌نيويسي زوان)\" data-language-autonym=\"گیلکی\" data-language-local-name=\"Gilaki\" class=\"interlanguage-link-target\"><span>گیلکی</span></a></li><li class=\"interlanguage-link interwiki-gu mw-list-item\"><a href=\"https://gu.wikipedia.org/wiki/%E0%AA%AA%E0%AA%BE%E0%AA%AF%E0%AA%A5%E0%AB%8B%E0%AA%A8(%E0%AA%AA%E0%AB%8D%E0%AA%B0%E0%AB%8B%E0%AA%97%E0%AB%8D%E0%AA%B0%E0%AA%BE%E0%AA%AE%E0%AA%BF%E0%AA%82%E0%AA%97_%E0%AA%AD%E0%AA%BE%E0%AA%B7%E0%AA%BE)\" title=\"પાયથોન(પ્રોગ્રામિંગ ભાષા) – Gujarati\" lang=\"gu\" hreflang=\"gu\" data-title=\"પાયથોન(પ્રોગ્રામિંગ ભાષા)\" data-language-autonym=\"ગુજરાતી\" data-language-local-name=\"Gujarati\" class=\"interlanguage-link-target\"><span>ગુજરાતી</span></a></li><li class=\"interlanguage-link interwiki-ko mw-list-item\"><a href=\"https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%B4%EC%8D%AC\" title=\"파이썬 – Korean\" lang=\"ko\" hreflang=\"ko\" data-title=\"파이썬\" data-language-autonym=\"한국어\" data-language-local-name=\"Korean\" class=\"interlanguage-link-target\"><span>한국어</span></a></li><li class=\"interlanguage-link interwiki-ha mw-list-item\"><a href=\"https://ha.wikipedia.org/wiki/Python_programming_language\" title=\"Python programming language – Hausa\" lang=\"ha\" hreflang=\"ha\" data-title=\"Python programming language\" data-language-autonym=\"Hausa\" data-language-local-name=\"Hausa\" class=\"interlanguage-link-target\"><span>Hausa</span></a></li><li class=\"interlanguage-link interwiki-hy mw-list-item\"><a href=\"https://hy.wikipedia.org/wiki/Python\" title=\"Python – Armenian\" lang=\"hy\" hreflang=\"hy\" data-title=\"Python\" data-language-autonym=\"Հայերեն\" data-language-local-name=\"Armenian\" class=\"interlanguage-link-target\"><span>Հայերեն</span></a></li><li class=\"interlanguage-link interwiki-hi mw-list-item\"><a href=\"https://hi.wikipedia.org/wiki/%E0%A4%AA%E0%A4%BE%E0%A4%87%E0%A4%A5%E0%A4%A8\" title=\"पाइथन – Hindi\" lang=\"hi\" hreflang=\"hi\" data-title=\"पाइथन\" data-language-autonym=\"हिन्दी\" data-language-local-name=\"Hindi\" class=\"interlanguage-link-target\"><span>हिन्दी</span></a></li><li class=\"interlanguage-link interwiki-hr mw-list-item\"><a href=\"https://hr.wikipedia.org/wiki/Python_(programski_jezik)\" title=\"Python (programski jezik) – Croatian\" lang=\"hr\" hreflang=\"hr\" data-title=\"Python (programski jezik)\" data-language-autonym=\"Hrvatski\" data-language-local-name=\"Croatian\" class=\"interlanguage-link-target\"><span>Hrvatski</span></a></li><li class=\"interlanguage-link interwiki-io mw-list-item\"><a href=\"https://io.wikipedia.org/wiki/Python\" title=\"Python – Ido\" lang=\"io\" hreflang=\"io\" data-title=\"Python\" data-language-autonym=\"Ido\" data-language-local-name=\"Ido\" class=\"interlanguage-link-target\"><span>Ido</span></a></li><li class=\"interlanguage-link interwiki-id mw-list-item\"><a href=\"https://id.wikipedia.org/wiki/Python_(bahasa_pemrograman)\" title=\"Python (bahasa pemrograman) – Indonesian\" lang=\"id\" hreflang=\"id\" data-title=\"Python (bahasa pemrograman)\" data-language-autonym=\"Bahasa Indonesia\" data-language-local-name=\"Indonesian\" class=\"interlanguage-link-target\"><span>Bahasa Indonesia</span></a></li><li class=\"interlanguage-link interwiki-ia mw-list-item\"><a href=\"https://ia.wikipedia.org/wiki/Python_(linguage_de_programmation)\" title=\"Python (linguage de programmation) – Interlingua\" lang=\"ia\" hreflang=\"ia\" data-title=\"Python (linguage de programmation)\" data-language-autonym=\"Interlingua\" data-language-local-name=\"Interlingua\" class=\"interlanguage-link-target\"><span>Interlingua</span></a></li><li class=\"interlanguage-link interwiki-is mw-list-item\"><a href=\"https://is.wikipedia.org/wiki/Python_(forritunarm%C3%A1l)\" title=\"Python (forritunarmál) – Icelandic\" lang=\"is\" hreflang=\"is\" data-title=\"Python (forritunarmál)\" data-language-autonym=\"Íslenska\" data-language-local-name=\"Icelandic\" class=\"interlanguage-link-target\"><span>Íslenska</span></a></li><li class=\"interlanguage-link interwiki-it mw-list-item\"><a href=\"https://it.wikipedia.org/wiki/Python\" title=\"Python – Italian\" lang=\"it\" hreflang=\"it\" data-title=\"Python\" data-language-autonym=\"Italiano\" data-language-local-name=\"Italian\" class=\"interlanguage-link-target\"><span>Italiano</span></a></li><li class=\"interlanguage-link interwiki-he mw-list-item\"><a href=\"https://he.wikipedia.org/wiki/%D7%A4%D7%99%D7%99%D7%AA%D7%95%D7%9F\" title=\"פייתון – Hebrew\" lang=\"he\" hreflang=\"he\" data-title=\"פייתון\" data-language-autonym=\"עברית\" data-language-local-name=\"Hebrew\" class=\"interlanguage-link-target\"><span>עברית</span></a></li><li class=\"interlanguage-link interwiki-ka mw-list-item\"><a href=\"https://ka.wikipedia.org/wiki/%E1%83%9E%E1%83%90%E1%83%98%E1%83%97%E1%83%9D%E1%83%9C%E1%83%98_(%E1%83%9E%E1%83%A0%E1%83%9D%E1%83%92%E1%83%A0%E1%83%90%E1%83%9B%E1%83%98%E1%83%A0%E1%83%94%E1%83%91%E1%83%98%E1%83%A1_%E1%83%94%E1%83%9C%E1%83%90)\" title=\"პაითონი (პროგრამირების ენა) – Georgian\" lang=\"ka\" hreflang=\"ka\" data-title=\"პაითონი (პროგრამირების ენა)\" data-language-autonym=\"ქართული\" data-language-local-name=\"Georgian\" class=\"interlanguage-link-target\"><span>ქართული</span></a></li><li class=\"interlanguage-link interwiki-kk mw-list-item\"><a href=\"https://kk.wikipedia.org/wiki/Python\" title=\"Python – Kazakh\" lang=\"kk\" hreflang=\"kk\" data-title=\"Python\" data-language-autonym=\"Қазақша\" data-language-local-name=\"Kazakh\" class=\"interlanguage-link-target\"><span>Қазақша</span></a></li><li class=\"interlanguage-link interwiki-sw mw-list-item\"><a href=\"https://sw.wikipedia.org/wiki/Python_(lugha_ya_programu)\" title=\"Python (lugha ya programu) – Swahili\" lang=\"sw\" hreflang=\"sw\" data-title=\"Python (lugha ya programu)\" data-language-autonym=\"Kiswahili\" data-language-local-name=\"Swahili\" class=\"interlanguage-link-target\"><span>Kiswahili</span></a></li><li class=\"interlanguage-link interwiki-ku mw-list-item\"><a href=\"https://ku.wikipedia.org/wiki/Python_(ziman%C3%AA_bernamesaziy%C3%AA)\" title=\"Python (zimanê bernamesaziyê) – Kurdish\" lang=\"ku\" hreflang=\"ku\" data-title=\"Python (zimanê bernamesaziyê)\" data-language-autonym=\"Kurdî\" data-language-local-name=\"Kurdish\" class=\"interlanguage-link-target\"><span>Kurdî</span></a></li><li class=\"interlanguage-link interwiki-ky mw-list-item\"><a href=\"https://ky.wikipedia.org/wiki/Python\" title=\"Python – Kyrgyz\" lang=\"ky\" hreflang=\"ky\" data-title=\"Python\" data-language-autonym=\"Кыргызча\" data-language-local-name=\"Kyrgyz\" class=\"interlanguage-link-target\"><span>Кыргызча</span></a></li><li class=\"interlanguage-link interwiki-lo mw-list-item\"><a href=\"https://lo.wikipedia.org/wiki/%E0%BB%84%E0%BA%9E%E0%BA%97%E0%BA%AD%E0%BA%99_(%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BB%82%E0%BA%9B%E0%BA%A3%E0%BB%81%E0%BA%81%E0%BA%A3%E0%BA%A1)\" title=\"ໄພທອນ (ພາສາໂປຣແກຣມ) – Lao\" lang=\"lo\" hreflang=\"lo\" data-title=\"ໄພທອນ (ພາສາໂປຣແກຣມ)\" data-language-autonym=\"ລາວ\" data-language-local-name=\"Lao\" class=\"interlanguage-link-target\"><span>ລາວ</span></a></li><li class=\"interlanguage-link interwiki-la mw-list-item\"><a href=\"https://la.wikipedia.org/wiki/Python_(lingua_programmationis)\" title=\"Python (lingua programmationis) – Latin\" lang=\"la\" hreflang=\"la\" data-title=\"Python (lingua programmationis)\" data-language-autonym=\"Latina\" data-language-local-name=\"Latin\" class=\"interlanguage-link-target\"><span>Latina</span></a></li><li class=\"interlanguage-link interwiki-lv mw-list-item\"><a href=\"https://lv.wikipedia.org/wiki/Python_(programm%C4%93%C5%A1anas_valoda)\" title=\"Python (programmēšanas valoda) – Latvian\" lang=\"lv\" hreflang=\"lv\" data-title=\"Python (programmēšanas valoda)\" data-language-autonym=\"Latviešu\" data-language-local-name=\"Latvian\" class=\"interlanguage-link-target\"><span>Latviešu</span></a></li><li class=\"interlanguage-link interwiki-lt mw-list-item\"><a href=\"https://lt.wikipedia.org/wiki/Python\" title=\"Python – Lithuanian\" lang=\"lt\" hreflang=\"lt\" data-title=\"Python\" data-language-autonym=\"Lietuvių\" data-language-local-name=\"Lithuanian\" class=\"interlanguage-link-target\"><span>Lietuvių</span></a></li><li class=\"interlanguage-link interwiki-jbo mw-list-item\"><a href=\"https://jbo.wikipedia.org/wiki/paiton\" title=\"paiton – Lojban\" lang=\"jbo\" hreflang=\"jbo\" data-title=\"paiton\" data-language-autonym=\"La .lojban.\" data-language-local-name=\"Lojban\" class=\"interlanguage-link-target\"><span>La .lojban.</span></a></li><li class=\"interlanguage-link interwiki-lmo mw-list-item\"><a href=\"https://lmo.wikipedia.org/wiki/Python\" title=\"Python – Lombard\" lang=\"lmo\" hreflang=\"lmo\" data-title=\"Python\" data-language-autonym=\"Lombard\" data-language-local-name=\"Lombard\" class=\"interlanguage-link-target\"><span>Lombard</span></a></li><li class=\"interlanguage-link interwiki-hu mw-list-item\"><a href=\"https://hu.wikipedia.org/wiki/Python_(programoz%C3%A1si_nyelv)\" title=\"Python (programozási nyelv) – Hungarian\" lang=\"hu\" hreflang=\"hu\" data-title=\"Python (programozási nyelv)\" data-language-autonym=\"Magyar\" data-language-local-name=\"Hungarian\" class=\"interlanguage-link-target\"><span>Magyar</span></a></li><li class=\"interlanguage-link interwiki-mk mw-list-item\"><a href=\"https://mk.wikipedia.org/wiki/%D0%9F%D0%B0%D1%98%D1%82%D0%BE%D0%BD_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D1%81%D0%BA%D0%B8_%D1%98%D0%B0%D0%B7%D0%B8%D0%BA)\" title=\"Пајтон (програмски јазик) – Macedonian\" lang=\"mk\" hreflang=\"mk\" data-title=\"Пајтон (програмски јазик)\" data-language-autonym=\"Македонски\" data-language-local-name=\"Macedonian\" class=\"interlanguage-link-target\"><span>Македонски</span></a></li><li class=\"interlanguage-link interwiki-ml mw-list-item\"><a href=\"https://ml.wikipedia.org/wiki/%E0%B4%AA%E0%B5%88%E0%B4%A4%E0%B5%8D%E0%B4%A4%E0%B5%BA_(%E0%B4%AA%E0%B5%8D%E0%B4%B0%E0%B5%8B%E0%B4%97%E0%B5%8D%E0%B4%B0%E0%B4%BE%E0%B4%AE%E0%B4%BF%E0%B4%99%E0%B5%8D%E0%B4%99%E0%B5%8D_%E0%B4%AD%E0%B4%BE%E0%B4%B7)\" title=\"പൈത്തൺ (പ്രോഗ്രാമിങ്ങ് ഭാഷ) – Malayalam\" lang=\"ml\" hreflang=\"ml\" data-title=\"പൈത്തൺ (പ്രോഗ്രാമിങ്ങ് ഭാഷ)\" data-language-autonym=\"മലയാളം\" data-language-local-name=\"Malayalam\" class=\"interlanguage-link-target\"><span>മലയാളം</span></a></li><li class=\"interlanguage-link interwiki-mr mw-list-item\"><a href=\"https://mr.wikipedia.org/wiki/%E0%A4%AA%E0%A4%BE%E0%A4%AF%E0%A4%A5%E0%A4%A8_(%E0%A4%86%E0%A4%9C%E0%A5%8D%E0%A4%9E%E0%A4%BE%E0%A4%B5%E0%A4%B2%E0%A5%80_%E0%A4%AD%E0%A4%BE%E0%A4%B7%E0%A4%BE)\" title=\"पायथन (आज्ञावली भाषा) – Marathi\" lang=\"mr\" hreflang=\"mr\" data-title=\"पायथन (आज्ञावली भाषा)\" data-language-autonym=\"मराठी\" data-language-local-name=\"Marathi\" class=\"interlanguage-link-target\"><span>मराठी</span></a></li><li class=\"interlanguage-link interwiki-xmf mw-list-item\"><a href=\"https://xmf.wikipedia.org/wiki/Python_(%E1%83%9E%E1%83%A0%E1%83%9D%E1%83%92%E1%83%A0%E1%83%90%E1%83%9B%E1%83%98%E1%83%A0%E1%83%90%E1%83%A4%E1%83%90%E1%83%A8_%E1%83%9C%E1%83%98%E1%83%9C%E1%83%90)\" title=\"Python (პროგრამირაფაშ ნინა) – Mingrelian\" lang=\"xmf\" hreflang=\"xmf\" data-title=\"Python (პროგრამირაფაშ ნინა)\" data-language-autonym=\"მარგალური\" data-language-local-name=\"Mingrelian\" class=\"interlanguage-link-target\"><span>მარგალური</span></a></li><li class=\"interlanguage-link interwiki-ms mw-list-item\"><a href=\"https://ms.wikipedia.org/wiki/Python\" title=\"Python – Malay\" lang=\"ms\" hreflang=\"ms\" data-title=\"Python\" data-language-autonym=\"Bahasa Melayu\" data-language-local-name=\"Malay\" class=\"interlanguage-link-target\"><span>Bahasa Melayu</span></a></li><li class=\"interlanguage-link interwiki-mn mw-list-item\"><a href=\"https://mn.wikipedia.org/wiki/Python\" title=\"Python – Mongolian\" lang=\"mn\" hreflang=\"mn\" data-title=\"Python\" data-language-autonym=\"Монгол\" data-language-local-name=\"Mongolian\" class=\"interlanguage-link-target\"><span>Монгол</span></a></li><li class=\"interlanguage-link interwiki-my mw-list-item\"><a href=\"https://my.wikipedia.org/wiki/Python_(programming_language)\" title=\"Python (programming language) – Burmese\" lang=\"my\" hreflang=\"my\" data-title=\"Python (programming language)\" data-language-autonym=\"မြန်မာဘာသာ\" data-language-local-name=\"Burmese\" class=\"interlanguage-link-target\"><span>မြန်မာဘာသာ</span></a></li><li class=\"interlanguage-link interwiki-fj mw-list-item\"><a href=\"https://fj.wikipedia.org/wiki/Python\" title=\"Python – Fijian\" lang=\"fj\" hreflang=\"fj\" data-title=\"Python\" data-language-autonym=\"Na Vosa Vakaviti\" data-language-local-name=\"Fijian\" class=\"interlanguage-link-target\"><span>Na Vosa Vakaviti</span></a></li><li class=\"interlanguage-link interwiki-nl mw-list-item\"><a href=\"https://nl.wikipedia.org/wiki/Python_(programmeertaal)\" title=\"Python (programmeertaal) – Dutch\" lang=\"nl\" hreflang=\"nl\" data-title=\"Python (programmeertaal)\" data-language-autonym=\"Nederlands\" data-language-local-name=\"Dutch\" class=\"interlanguage-link-target\"><span>Nederlands</span></a></li><li class=\"interlanguage-link interwiki-ne mw-list-item\"><a href=\"https://ne.wikipedia.org/wiki/%E0%A4%AA%E0%A4%BE%E0%A4%87%E0%A4%A5%E0%A4%A8_(%E0%A4%AA%E0%A5%8D%E0%A4%B0%E0%A5%8B%E0%A4%97%E0%A4%BE%E0%A4%AE%E0%A4%BF%E0%A4%99_%E0%A4%AD%E0%A4%BE%E0%A4%B7%E0%A4%BE)\" title=\"पाइथन (प्रोगामिङ भाषा) – Nepali\" lang=\"ne\" hreflang=\"ne\" data-title=\"पाइथन (प्रोगामिङ भाषा)\" data-language-autonym=\"नेपाली\" data-language-local-name=\"Nepali\" class=\"interlanguage-link-target\"><span>नेपाली</span></a></li><li class=\"interlanguage-link interwiki-ja mw-list-item\"><a href=\"https://ja.wikipedia.org/wiki/Python\" title=\"Python – Japanese\" lang=\"ja\" hreflang=\"ja\" data-title=\"Python\" data-language-autonym=\"日本語\" data-language-local-name=\"Japanese\" class=\"interlanguage-link-target\"><span>日本語</span></a></li><li class=\"interlanguage-link interwiki-nqo mw-list-item\"><a href=\"https://nqo.wikipedia.org/wiki/%DF%94%DF%8A%DF%8C%DF%95%DF%90%DF%B2%DF%AC\" title=\"ߔߊߌߕߐ߲߬ – N’Ko\" lang=\"nqo\" hreflang=\"nqo\" data-title=\"ߔߊߌߕߐ߲߬\" data-language-autonym=\"ߒߞߏ\" data-language-local-name=\"N’Ko\" class=\"interlanguage-link-target\"><span>ߒߞߏ</span></a></li><li class=\"interlanguage-link interwiki-no mw-list-item\"><a href=\"https://no.wikipedia.org/wiki/Python\" title=\"Python – Norwegian Bokmål\" lang=\"nb\" hreflang=\"nb\" data-title=\"Python\" data-language-autonym=\"Norsk bokmål\" data-language-local-name=\"Norwegian Bokmål\" class=\"interlanguage-link-target\"><span>Norsk bokmål</span></a></li><li class=\"interlanguage-link interwiki-nn mw-list-item\"><a href=\"https://nn.wikipedia.org/wiki/Python\" title=\"Python – Norwegian Nynorsk\" lang=\"nn\" hreflang=\"nn\" data-title=\"Python\" data-language-autonym=\"Norsk nynorsk\" data-language-local-name=\"Norwegian Nynorsk\" class=\"interlanguage-link-target\"><span>Norsk nynorsk</span></a></li><li class=\"interlanguage-link interwiki-or mw-list-item\"><a href=\"https://or.wikipedia.org/wiki/%E0%AC%AA%E0%AC%BE%E0%AC%87%E0%AC%A5%E0%AC%A8%E0%AD%8D_(%E0%AC%AA%E0%AD%8D%E0%AC%B0%E0%AD%8B%E0%AC%97%E0%AD%8D%E0%AC%B0%E0%AC%BE%E0%AC%AE%E0%AC%BF%E0%AC%82_%E0%AC%AD%E0%AC%BE%E0%AC%B7%E0%AC%BE)\" title=\"ପାଇଥନ୍ (ପ୍ରୋଗ୍ରାମିଂ ଭାଷା) – Odia\" lang=\"or\" hreflang=\"or\" data-title=\"ପାଇଥନ୍ (ପ୍ରୋଗ୍ରାମିଂ ଭାଷା)\" data-language-autonym=\"ଓଡ଼ିଆ\" data-language-local-name=\"Odia\" class=\"interlanguage-link-target\"><span>ଓଡ଼ିଆ</span></a></li><li class=\"interlanguage-link interwiki-uz mw-list-item\"><a href=\"https://uz.wikipedia.org/wiki/Python\" title=\"Python – Uzbek\" lang=\"uz\" hreflang=\"uz\" data-title=\"Python\" data-language-autonym=\"Oʻzbekcha / ўзбекча\" data-language-local-name=\"Uzbek\" class=\"interlanguage-link-target\"><span>Oʻzbekcha / ўзбекча</span></a></li><li class=\"interlanguage-link interwiki-pa mw-list-item\"><a href=\"https://pa.wikipedia.org/wiki/%E0%A8%AA%E0%A8%BE%E0%A8%88%E0%A8%A5%E0%A8%A8_(%E0%A8%AA%E0%A9%8D%E0%A8%B0%E0%A9%8B%E0%A8%97%E0%A8%B0%E0%A8%BE%E0%A8%AE%E0%A8%BF%E0%A9%B0%E0%A8%97_%E0%A8%AD%E0%A8%BE%E0%A8%B8%E0%A8%BC%E0%A8%BE)\" title=\"ਪਾਈਥਨ (ਪ੍ਰੋਗਰਾਮਿੰਗ ਭਾਸ਼ਾ) – Punjabi\" lang=\"pa\" hreflang=\"pa\" data-title=\"ਪਾਈਥਨ (ਪ੍ਰੋਗਰਾਮਿੰਗ ਭਾਸ਼ਾ)\" data-language-autonym=\"ਪੰਜਾਬੀ\" data-language-local-name=\"Punjabi\" class=\"interlanguage-link-target\"><span>ਪੰਜਾਬੀ</span></a></li><li class=\"interlanguage-link interwiki-pnb mw-list-item\"><a href=\"https://pnb.wikipedia.org/wiki/%D9%BE%D8%A7%D8%A6%DB%8C%D8%AA%DA%BE%D9%86_(%DA%A9%D9%85%D9%BE%DB%8C%D9%88%D9%B9%D8%B1_%D8%A8%D9%88%D9%84%DB%8C)\" title=\"پائیتھن (کمپیوٹر بولی) – Western Punjabi\" lang=\"pnb\" hreflang=\"pnb\" data-title=\"پائیتھن (کمپیوٹر بولی)\" data-language-autonym=\"پنجابی\" data-language-local-name=\"Western Punjabi\" class=\"interlanguage-link-target\"><span>پنجابی</span></a></li><li class=\"interlanguage-link interwiki-km mw-list-item\"><a href=\"https://km.wikipedia.org/wiki/%E1%9E%95%E1%9E%B6%E1%9E%99%E1%9E%90%E1%9E%BB%E1%9E%93\" title=\"ផាយថុន – Khmer\" lang=\"km\" hreflang=\"km\" data-title=\"ផាយថុន\" data-language-autonym=\"ភាសាខ្មែរ\" data-language-local-name=\"Khmer\" class=\"interlanguage-link-target\"><span>ភាសាខ្មែរ</span></a></li><li class=\"interlanguage-link interwiki-pms mw-list-item\"><a href=\"https://pms.wikipedia.org/wiki/Python_(lengagi_%C3%ABd_programassion)\" title=\"Python (lengagi ëd programassion) – Piedmontese\" lang=\"pms\" hreflang=\"pms\" data-title=\"Python (lengagi ëd programassion)\" data-language-autonym=\"Piemontèis\" data-language-local-name=\"Piedmontese\" class=\"interlanguage-link-target\"><span>Piemontèis</span></a></li><li class=\"interlanguage-link interwiki-nds mw-list-item\"><a href=\"https://nds.wikipedia.org/wiki/Python_(Programmeerspraak)\" title=\"Python (Programmeerspraak) – Low German\" lang=\"nds\" hreflang=\"nds\" data-title=\"Python (Programmeerspraak)\" data-language-autonym=\"Plattdüütsch\" data-language-local-name=\"Low German\" class=\"interlanguage-link-target\"><span>Plattdüütsch</span></a></li><li class=\"interlanguage-link interwiki-pl mw-list-item\"><a href=\"https://pl.wikipedia.org/wiki/Python\" title=\"Python – Polish\" lang=\"pl\" hreflang=\"pl\" data-title=\"Python\" data-language-autonym=\"Polski\" data-language-local-name=\"Polish\" class=\"interlanguage-link-target\"><span>Polski</span></a></li><li class=\"interlanguage-link interwiki-pt mw-list-item\"><a href=\"https://pt.wikipedia.org/wiki/Python\" title=\"Python – Portuguese\" lang=\"pt\" hreflang=\"pt\" data-title=\"Python\" data-language-autonym=\"Português\" data-language-local-name=\"Portuguese\" class=\"interlanguage-link-target\"><span>Português</span></a></li><li class=\"interlanguage-link interwiki-kaa mw-list-item\"><a href=\"https://kaa.wikipedia.org/wiki/Python_(programmalast%C4%B1r%C4%B1w_tili)\" title=\"Python (programmalastırıw tili) – Kara-Kalpak\" lang=\"kaa\" hreflang=\"kaa\" data-title=\"Python (programmalastırıw tili)\" data-language-autonym=\"Qaraqalpaqsha\" data-language-local-name=\"Kara-Kalpak\" class=\"interlanguage-link-target\"><span>Qaraqalpaqsha</span></a></li><li class=\"interlanguage-link interwiki-ro mw-list-item\"><a href=\"https://ro.wikipedia.org/wiki/Python\" title=\"Python – Romanian\" lang=\"ro\" hreflang=\"ro\" data-title=\"Python\" data-language-autonym=\"Română\" data-language-local-name=\"Romanian\" class=\"interlanguage-link-target\"><span>Română</span></a></li><li class=\"interlanguage-link interwiki-qu mw-list-item\"><a href=\"https://qu.wikipedia.org/wiki/Python\" title=\"Python – Quechua\" lang=\"qu\" hreflang=\"qu\" data-title=\"Python\" data-language-autonym=\"Runa Simi\" data-language-local-name=\"Quechua\" class=\"interlanguage-link-target\"><span>Runa Simi</span></a></li><li class=\"interlanguage-link interwiki-ru mw-list-item\"><a href=\"https://ru.wikipedia.org/wiki/Python\" title=\"Python – Russian\" lang=\"ru\" hreflang=\"ru\" data-title=\"Python\" data-language-autonym=\"Русский\" data-language-local-name=\"Russian\" class=\"interlanguage-link-target\"><span>Русский</span></a></li><li class=\"interlanguage-link interwiki-sah mw-list-item\"><a href=\"https://sah.wikipedia.org/wiki/Python\" title=\"Python – Yakut\" lang=\"sah\" hreflang=\"sah\" data-title=\"Python\" data-language-autonym=\"Саха тыла\" data-language-local-name=\"Yakut\" class=\"interlanguage-link-target\"><span>Саха тыла</span></a></li><li class=\"interlanguage-link interwiki-sat mw-list-item\"><a href=\"https://sat.wikipedia.org/wiki/%E1%B1%AF%E1%B1%9F%E1%B1%AD%E1%B1%9B%E1%B1%B7%E1%B1%9A%E1%B1%B1_(%E1%B1%AF%E1%B1%A8%E1%B1%B3%E1%B1%9C%E1%B1%BD%E1%B1%A8%E1%B1%9F%E1%B1%A2%E1%B1%A4%E1%B1%9D_%E1%B1%AF%E1%B1%9F%E1%B1%B9%E1%B1%A8%E1%B1%A5%E1%B1%A4)\" title=\"ᱯᱟᱭᱛᱷᱚᱱ (ᱯᱨᱳᱜᱽᱨᱟᱢᱤᱝ ᱯᱟᱹᱨᱥᱤ) – Santali\" lang=\"sat\" hreflang=\"sat\" data-title=\"ᱯᱟᱭᱛᱷᱚᱱ (ᱯᱨᱳᱜᱽᱨᱟᱢᱤᱝ ᱯᱟᱹᱨᱥᱤ)\" data-language-autonym=\"ᱥᱟᱱᱛᱟᱲᱤ\" data-language-local-name=\"Santali\" class=\"interlanguage-link-target\"><span>ᱥᱟᱱᱛᱟᱲᱤ</span></a></li><li class=\"interlanguage-link interwiki-sco mw-list-item\"><a href=\"https://sco.wikipedia.org/wiki/Python_(programmin_leid)\" title=\"Python (programmin leid) – Scots\" lang=\"sco\" hreflang=\"sco\" data-title=\"Python (programmin leid)\" data-language-autonym=\"Scots\" data-language-local-name=\"Scots\" class=\"interlanguage-link-target\"><span>Scots</span></a></li><li class=\"interlanguage-link interwiki-sq mw-list-item\"><a href=\"https://sq.wikipedia.org/wiki/Python\" title=\"Python – Albanian\" lang=\"sq\" hreflang=\"sq\" data-title=\"Python\" data-language-autonym=\"Shqip\" data-language-local-name=\"Albanian\" class=\"interlanguage-link-target\"><span>Shqip</span></a></li><li class=\"interlanguage-link interwiki-si mw-list-item\"><a href=\"https://si.wikipedia.org/wiki/%E0%B6%B4%E0%B6%BA%E0%B7%92%E0%B6%AD%E0%B6%B1%E0%B7%8A\" title=\"පයිතන් – Sinhala\" lang=\"si\" hreflang=\"si\" data-title=\"පයිතන්\" data-language-autonym=\"සිංහල\" data-language-local-name=\"Sinhala\" class=\"interlanguage-link-target\"><span>සිංහල</span></a></li><li class=\"interlanguage-link interwiki-simple mw-list-item\"><a href=\"https://simple.wikipedia.org/wiki/Python_(programming_language)\" title=\"Python (programming language) – Simple English\" lang=\"en-simple\" hreflang=\"en-simple\" data-title=\"Python (programming language)\" data-language-autonym=\"Simple English\" data-language-local-name=\"Simple English\" class=\"interlanguage-link-target\"><span>Simple English</span></a></li><li class=\"interlanguage-link interwiki-sk mw-list-item\"><a href=\"https://sk.wikipedia.org/wiki/Python_(programovac%C3%AD_jazyk)\" title=\"Python (programovací jazyk) – Slovak\" lang=\"sk\" hreflang=\"sk\" data-title=\"Python (programovací jazyk)\" data-language-autonym=\"Slovenčina\" data-language-local-name=\"Slovak\" class=\"interlanguage-link-target\"><span>Slovenčina</span></a></li><li class=\"interlanguage-link interwiki-sl mw-list-item\"><a href=\"https://sl.wikipedia.org/wiki/Python_(programski_jezik)\" title=\"Python (programski jezik) – Slovenian\" lang=\"sl\" hreflang=\"sl\" data-title=\"Python (programski jezik)\" data-language-autonym=\"Slovenščina\" data-language-local-name=\"Slovenian\" class=\"interlanguage-link-target\"><span>Slovenščina</span></a></li><li class=\"interlanguage-link interwiki-ckb mw-list-item\"><a href=\"https://ckb.wikipedia.org/wiki/%D9%BE%D8%A7%DB%8C%D8%AA%DB%86%D9%86_(%D8%B2%D9%85%D8%A7%D9%86%DB%8C_%D8%A8%DB%95%D8%B1%D9%86%D8%A7%D9%85%DB%95%D8%B3%D8%A7%D8%B2%DB%8C)\" title=\"پایتۆن (زمانی بەرنامەسازی) – Central Kurdish\" lang=\"ckb\" hreflang=\"ckb\" data-title=\"پایتۆن (زمانی بەرنامەسازی)\" data-language-autonym=\"کوردی\" data-language-local-name=\"Central Kurdish\" class=\"interlanguage-link-target\"><span>کوردی</span></a></li><li class=\"interlanguage-link interwiki-sr mw-list-item\"><a href=\"https://sr.wikipedia.org/wiki/Python_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D1%81%D0%BA%D0%B8_%D1%98%D0%B5%D0%B7%D0%B8%D0%BA)\" title=\"Python (програмски језик) – Serbian\" lang=\"sr\" hreflang=\"sr\" data-title=\"Python (програмски језик)\" data-language-autonym=\"Српски / srpski\" data-language-local-name=\"Serbian\" class=\"interlanguage-link-target\"><span>Српски / srpski</span></a></li><li class=\"interlanguage-link interwiki-sh mw-list-item\"><a href=\"https://sh.wikipedia.org/wiki/Python_(programski_jezik)\" title=\"Python (programski jezik) – Serbo-Croatian\" lang=\"sh\" hreflang=\"sh\" data-title=\"Python (programski jezik)\" data-language-autonym=\"Srpskohrvatski / српскохрватски\" data-language-local-name=\"Serbo-Croatian\" class=\"interlanguage-link-target\"><span>Srpskohrvatski / српскохрватски</span></a></li><li class=\"interlanguage-link interwiki-fi mw-list-item\"><a href=\"https://fi.wikipedia.org/wiki/Python_(ohjelmointikieli)\" title=\"Python (ohjelmointikieli) – Finnish\" lang=\"fi\" hreflang=\"fi\" data-title=\"Python (ohjelmointikieli)\" data-language-autonym=\"Suomi\" data-language-local-name=\"Finnish\" class=\"interlanguage-link-target\"><span>Suomi</span></a></li><li class=\"interlanguage-link interwiki-sv mw-list-item\"><a href=\"https://sv.wikipedia.org/wiki/Python_(programspr%C3%A5k)\" title=\"Python (programspråk) – Swedish\" lang=\"sv\" hreflang=\"sv\" data-title=\"Python (programspråk)\" data-language-autonym=\"Svenska\" data-language-local-name=\"Swedish\" class=\"interlanguage-link-target\"><span>Svenska</span></a></li><li class=\"interlanguage-link interwiki-tl mw-list-item\"><a href=\"https://tl.wikipedia.org/wiki/Python_(wikang_pamprograma)\" title=\"Python (wikang pamprograma) – Tagalog\" lang=\"tl\" hreflang=\"tl\" data-title=\"Python (wikang pamprograma)\" data-language-autonym=\"Tagalog\" data-language-local-name=\"Tagalog\" class=\"interlanguage-link-target\"><span>Tagalog</span></a></li><li class=\"interlanguage-link interwiki-ta mw-list-item\"><a href=\"https://ta.wikipedia.org/wiki/%E0%AE%AA%E0%AF%88%E0%AE%A4%E0%AF%8D%E0%AE%A4%E0%AE%BE%E0%AE%A9%E0%AF%8D\" title=\"பைத்தான் – Tamil\" lang=\"ta\" hreflang=\"ta\" data-title=\"பைத்தான்\" data-language-autonym=\"தமிழ்\" data-language-local-name=\"Tamil\" class=\"interlanguage-link-target\"><span>தமிழ்</span></a></li><li class=\"interlanguage-link interwiki-tt mw-list-item\"><a href=\"https://tt.wikipedia.org/wiki/Python\" title=\"Python – Tatar\" lang=\"tt\" hreflang=\"tt\" data-title=\"Python\" data-language-autonym=\"Татарча / tatarça\" data-language-local-name=\"Tatar\" class=\"interlanguage-link-target\"><span>Татарча / tatarça</span></a></li><li class=\"interlanguage-link interwiki-shn mw-list-item\"><a href=\"https://shn.wikipedia.org/wiki/Python_(programming_language)\" title=\"Python (programming language) – Shan\" lang=\"shn\" hreflang=\"shn\" data-title=\"Python (programming language)\" data-language-autonym=\"တႆး\" data-language-local-name=\"Shan\" class=\"interlanguage-link-target\"><span>တႆး</span></a></li><li class=\"interlanguage-link interwiki-te mw-list-item\"><a href=\"https://te.wikipedia.org/wiki/%E0%B0%AA%E0%B1%88%E0%B0%A5%E0%B0%BE%E0%B0%A8%E0%B1%8D_(%E0%B0%95%E0%B0%82%E0%B0%AA%E0%B1%8D%E0%B0%AF%E0%B1%82%E0%B0%9F%E0%B0%B0%E0%B1%8D_%E0%B0%AD%E0%B0%BE%E0%B0%B7)\" title=\"పైథాన్ (కంప్యూటర్ భాష) – Telugu\" lang=\"te\" hreflang=\"te\" data-title=\"పైథాన్ (కంప్యూటర్ భాష)\" data-language-autonym=\"తెలుగు\" data-language-local-name=\"Telugu\" class=\"interlanguage-link-target\"><span>తెలుగు</span></a></li><li class=\"interlanguage-link interwiki-th mw-list-item\"><a href=\"https://th.wikipedia.org/wiki/%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%9E%E0%B8%97%E0%B8%AD%E0%B8%99\" title=\"ภาษาไพทอน – Thai\" lang=\"th\" hreflang=\"th\" data-title=\"ภาษาไพทอน\" data-language-autonym=\"ไทย\" data-language-local-name=\"Thai\" class=\"interlanguage-link-target\"><span>ไทย</span></a></li><li class=\"interlanguage-link interwiki-tg mw-list-item\"><a href=\"https://tg.wikipedia.org/wiki/Python\" title=\"Python – Tajik\" lang=\"tg\" hreflang=\"tg\" data-title=\"Python\" data-language-autonym=\"Тоҷикӣ\" data-language-local-name=\"Tajik\" class=\"interlanguage-link-target\"><span>Тоҷикӣ</span></a></li><li class=\"interlanguage-link interwiki-tr mw-list-item\"><a href=\"https://tr.wikipedia.org/wiki/Python\" title=\"Python – Turkish\" lang=\"tr\" hreflang=\"tr\" data-title=\"Python\" data-language-autonym=\"Türkçe\" data-language-local-name=\"Turkish\" class=\"interlanguage-link-target\"><span>Türkçe</span></a></li><li class=\"interlanguage-link interwiki-bug mw-list-item\"><a href=\"https://bug.wikipedia.org/wiki/Python\" title=\"Python – Buginese\" lang=\"bug\" hreflang=\"bug\" data-title=\"Python\" data-language-autonym=\"Basa Ugi\" data-language-local-name=\"Buginese\" class=\"interlanguage-link-target\"><span>Basa Ugi</span></a></li><li class=\"interlanguage-link interwiki-uk mw-list-item\"><a href=\"https://uk.wikipedia.org/wiki/Python\" title=\"Python – Ukrainian\" lang=\"uk\" hreflang=\"uk\" data-title=\"Python\" data-language-autonym=\"Українська\" data-language-local-name=\"Ukrainian\" class=\"interlanguage-link-target\"><span>Українська</span></a></li><li class=\"interlanguage-link interwiki-ur mw-list-item\"><a href=\"https://ur.wikipedia.org/wiki/%D9%BE%D8%A7%D8%A6%DB%8C%D8%AA%DA%BE%D9%86_(%D9%BE%D8%B1%D9%88%DA%AF%D8%B1%D8%A7%D9%85%D9%86%DA%AF_%D8%B2%D8%A8%D8%A7%D9%86)\" title=\"پائیتھن (پروگرامنگ زبان) – Urdu\" lang=\"ur\" hreflang=\"ur\" data-title=\"پائیتھن (پروگرامنگ زبان)\" data-language-autonym=\"اردو\" data-language-local-name=\"Urdu\" class=\"interlanguage-link-target\"><span>اردو</span></a></li><li class=\"interlanguage-link interwiki-ug mw-list-item\"><a href=\"https://ug.wikipedia.org/wiki/%D9%BE%D8%A7%D9%8A%D8%B3%D9%88%D9%86\" title=\"پايسون – Uyghur\" lang=\"ug\" hreflang=\"ug\" data-title=\"پايسون\" data-language-autonym=\"ئۇيغۇرچە / Uyghurche\" data-language-local-name=\"Uyghur\" class=\"interlanguage-link-target\"><span>ئۇيغۇرچە / Uyghurche</span></a></li><li class=\"interlanguage-link interwiki-vi mw-list-item\"><a href=\"https://vi.wikipedia.org/wiki/Python_(ng%C3%B4n_ng%E1%BB%AF_l%E1%BA%ADp_tr%C3%ACnh)\" title=\"Python (ngôn ngữ lập trình) – Vietnamese\" lang=\"vi\" hreflang=\"vi\" data-title=\"Python (ngôn ngữ lập trình)\" data-language-autonym=\"Tiếng Việt\" data-language-local-name=\"Vietnamese\" class=\"interlanguage-link-target\"><span>Tiếng Việt</span></a></li><li class=\"interlanguage-link interwiki-wa mw-list-item\"><a href=\"https://wa.wikipedia.org/wiki/Python_(lingaedje_%C3%A9ndjolike)\" title=\"Python (lingaedje éndjolike) – Walloon\" lang=\"wa\" hreflang=\"wa\" data-title=\"Python (lingaedje éndjolike)\" data-language-autonym=\"Walon\" data-language-local-name=\"Walloon\" class=\"interlanguage-link-target\"><span>Walon</span></a></li><li class=\"interlanguage-link interwiki-zh-classical mw-list-item\"><a href=\"https://zh-classical.wikipedia.org/wiki/%E8%9F%92%E8%AA%9E\" title=\"蟒語 – Literary Chinese\" lang=\"lzh\" hreflang=\"lzh\" data-title=\"蟒語\" data-language-autonym=\"文言\" data-language-local-name=\"Literary Chinese\" class=\"interlanguage-link-target\"><span>文言</span></a></li><li class=\"interlanguage-link interwiki-war mw-list-item\"><a href=\"https://war.wikipedia.org/wiki/Python_(programming_language)\" title=\"Python (programming language) – Waray\" lang=\"war\" hreflang=\"war\" data-title=\"Python (programming language)\" data-language-autonym=\"Winaray\" data-language-local-name=\"Waray\" class=\"interlanguage-link-target\"><span>Winaray</span></a></li><li class=\"interlanguage-link interwiki-wuu mw-list-item\"><a href=\"https://wuu.wikipedia.org/wiki/Python\" title=\"Python – Wu\" lang=\"wuu\" hreflang=\"wuu\" data-title=\"Python\" data-language-autonym=\"吴语\" data-language-local-name=\"Wu\" class=\"interlanguage-link-target\"><span>吴语</span></a></li><li class=\"interlanguage-link interwiki-zh-yue mw-list-item\"><a href=\"https://zh-yue.wikipedia.org/wiki/Python\" title=\"Python – Cantonese\" lang=\"yue\" hreflang=\"yue\" data-title=\"Python\" data-language-autonym=\"粵語\" data-language-local-name=\"Cantonese\" class=\"interlanguage-link-target\"><span>粵語</span></a></li><li class=\"interlanguage-link interwiki-zh mw-list-item\"><a href=\"https://zh.wikipedia.org/wiki/Python\" title=\"Python – Chinese\" lang=\"zh\" hreflang=\"zh\" data-title=\"Python\" data-language-autonym=\"中文\" data-language-local-name=\"Chinese\" class=\"interlanguage-link-target\"><span>中文</span></a></li><li class=\"interlanguage-link interwiki-dtp mw-list-item\"><a href=\"https://dtp.wikipedia.org/wiki/Python_(boros_tokud)\" title=\"Python (boros tokud) – Central Dusun\" lang=\"dtp\" hreflang=\"dtp\" data-title=\"Python (boros tokud)\" data-language-autonym=\"Kadazandusun\" data-language-local-name=\"Central Dusun\" class=\"interlanguage-link-target\"><span>Kadazandusun</span></a></li>\n\t\t\t</ul>\n\t\t\t<div class=\"after-portlet after-portlet-lang\"><span class=\"wb-langlinks-edit wb-langlinks-link\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q28865#sitelinks-wikipedia\" title=\"Edit interlanguage links\" class=\"wbc-editpage\">Edit links</a></span></div>\n\t\t</div>\n\n\t</div>\n</div>\n</header>\n\t\t\t\t<div class=\"vector-page-toolbar vector-feature-custom-font-size-clientpref--excluded\">\n\t\t\t\t\t<div class=\"vector-page-toolbar-container\">\n\t\t\t\t\t\t<div id=\"left-navigation\">\n\t\t\t\t\t\t\t<nav aria-label=\"Namespaces\">\n\n<div id=\"p-associated-pages\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-associated-pages\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-nstab-main\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Python_(programming_language)\" title=\"View the content page [c]\" accesskey=\"c\"><span>Article</span></a></li><li id=\"ca-talk\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/wiki/Talk:Python_(programming_language)\" rel=\"discussion\" title=\"Discuss improvements to the content page [t]\" accesskey=\"t\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"vector-variants-dropdown\" class=\"vector-dropdown emptyPortlet\"  >\n\t<input type=\"checkbox\" id=\"vector-variants-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-variants-dropdown\" class=\"vector-dropdown-checkbox \" aria-label=\"Change language variant\"   >\n\t<label id=\"vector-variants-dropdown-label\" for=\"vector-variants-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">English</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-variants\" class=\"vector-menu mw-portlet mw-portlet-variants emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div id=\"right-navigation\" class=\"vector-collapsible\">\n\t\t\t\t\t\t\t<nav aria-label=\"Views\">\n\n<div id=\"p-views\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-views\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-view\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/Python_(programming_language)\"><span>Read</span></a></li><li id=\"ca-edit\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-history\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=history\" title=\"Past revisions of this page [h]\" accesskey=\"h\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\n<div id=\"vector-page-tools-dropdown\" class=\"vector-dropdown vector-page-tools-dropdown\"  >\n\t<input type=\"checkbox\" id=\"vector-page-tools-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-tools-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Tools\"  >\n\t<label id=\"vector-page-tools-dropdown-label\" for=\"vector-page-tools-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">Tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t\t\t<div id=\"vector-page-tools-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-page-tools\" class=\"vector-page-tools vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-page-tools-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"page-tools-pinned\"\n\tdata-pinnable-element-id=\"vector-page-tools\"\n\tdata-pinned-container-id=\"vector-page-tools-pinned-container\"\n\tdata-unpinned-container-id=\"vector-page-tools-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Tools</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-page-tools.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-page-tools.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-cactions\" class=\"vector-menu mw-portlet mw-portlet-cactions emptyPortlet vector-has-collapsible-items\"  title=\"More options\" >\n\t<div class=\"vector-menu-heading\">\n\t\tActions\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-more-view\" class=\"selected vector-more-collapsible-item mw-list-item\"><a href=\"/wiki/Python_(programming_language)\"><span>Read</span></a></li><li id=\"ca-more-edit\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-more-history\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=history\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-tb\" class=\"vector-menu mw-portlet mw-portlet-tb\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tGeneral\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-whatlinkshere\" class=\"mw-list-item\"><a href=\"/wiki/Special:WhatLinksHere/Python_(programming_language)\" title=\"List of all English Wikipedia pages containing links to this page [j]\" accesskey=\"j\"><span>What links here</span></a></li><li id=\"t-recentchangeslinked\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChangesLinked/Python_(programming_language)\" rel=\"nofollow\" title=\"Recent changes in pages linked from this page [k]\" accesskey=\"k\"><span>Related changes</span></a></li><li id=\"t-upload\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:File_Upload_Wizard\" title=\"Upload files [u]\" accesskey=\"u\"><span>Upload file</span></a></li><li id=\"t-permalink\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;oldid=1314465879\" title=\"Permanent link to this revision of this page\"><span>Permanent link</span></a></li><li id=\"t-info\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=info\" title=\"More information about this page\"><span>Page information</span></a></li><li id=\"t-cite\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:CiteThisPage&amp;page=Python_%28programming_language%29&amp;id=1314465879&amp;wpFormIdentifier=titleform\" title=\"Information on how to cite this page\"><span>Cite this page</span></a></li><li id=\"t-urlshortener\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:UrlShortener&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPython_%28programming_language%29\"><span>Get shortened URL</span></a></li><li id=\"t-urlshortener-qrcode\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:QrCode&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPython_%28programming_language%29\"><span>Download QR code</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-coll-print_export\" class=\"vector-menu mw-portlet mw-portlet-coll-print_export\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPrint/export\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"coll-download-as-rl\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:DownloadAsPdf&amp;page=Python_%28programming_language%29&amp;action=show-download-screen\" title=\"Download this page as a PDF file\"><span>Download as PDF</span></a></li><li id=\"t-print\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Python_(programming_language)&amp;printable=yes\" title=\"Printable version of this page [p]\" accesskey=\"p\"><span>Printable version</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-wikibase-otherprojects\" class=\"vector-menu mw-portlet mw-portlet-wikibase-otherprojects\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tIn other projects\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li class=\"wb-otherproject-link wb-otherproject-commons mw-list-item\"><a href=\"https://commons.wikimedia.org/wiki/Python_(programming_language)\" hreflang=\"en\"><span>Wikimedia Commons</span></a></li><li class=\"wb-otherproject-link wb-otherproject-mediawiki mw-list-item\"><a href=\"https://www.mediawiki.org/wiki/Python\" hreflang=\"en\"><span>MediaWiki</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikibooks mw-list-item\"><a href=\"https://en.wikibooks.org/wiki/Python_Programming\" hreflang=\"en\"><span>Wikibooks</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikifunctions mw-list-item\"><a href=\"https://www.wikifunctions.org/wiki/Z610\" hreflang=\"en\"><span>Wikifunctions</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikiquote mw-list-item\"><a href=\"https://en.wikiquote.org/wiki/Python\" hreflang=\"en\"><span>Wikiquote</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikiversity mw-list-item\"><a href=\"https://en.wikiversity.org/wiki/Python\" hreflang=\"en\"><span>Wikiversity</span></a></li><li id=\"t-wikibase\" class=\"wb-otherproject-link wb-otherproject-wikibase-dataitem mw-list-item\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q28865\" title=\"Structured data on this page hosted by Wikidata [g]\" accesskey=\"g\"><span>Wikidata item</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"vector-column-end no-font-mode-scale\">\n\t\t\t\t\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\t\t\t\t\t\t\t<div id=\"vector-page-tools-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\t\t\t\t\t\t\t<div id=\"vector-appearance-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t<div id=\"vector-appearance\" class=\"vector-appearance vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-appearance-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"appearance-pinned\"\n\tdata-pinnable-element-id=\"vector-appearance\"\n\tdata-pinned-container-id=\"vector-appearance-pinned-container\"\n\tdata-unpinned-container-id=\"vector-appearance-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Appearance</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-appearance.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-appearance.unpin\">hide</button>\n</div>\n\n\n</div>\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"bodyContent\" class=\"vector-body\" aria-labelledby=\"firstHeading\" data-mw-ve-target-container>\n\t\t\t\t\t<div class=\"vector-body-before-content\">\n\t\t\t\t\t\t\t<div class=\"mw-indicators\">\n\t\t</div>\n\n\t\t\t\t\t\t<div id=\"siteSub\" class=\"noprint\">From Wikipedia, the free encyclopedia</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"contentSub\"><div id=\"mw-content-subtitle\"></div></div>\n\n\n\t\t\t\t\t<div id=\"mw-content-text\" class=\"mw-body-content\"><div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><div class=\"shortdescription nomobile noexcerpt noprint searchaux\" style=\"display:none\">General-purpose programming language</div>\n<p class=\"mw-empty-elt\">\n\n</p>\n<style data-mw-deduplicate=\"TemplateStyles:r1129693374\">.mw-parser-output .hlist dl,.mw-parser-output .hlist ol,.mw-parser-output .hlist ul{margin:0;padding:0}.mw-parser-output .hlist dd,.mw-parser-output .hlist dt,.mw-parser-output .hlist li{margin:0;display:inline}.mw-parser-output .hlist.inline,.mw-parser-output .hlist.inline dl,.mw-parser-output .hlist.inline ol,.mw-parser-output .hlist.inline ul,.mw-parser-output .hlist dl dl,.mw-parser-output .hlist dl ol,.mw-parser-output .hlist dl ul,.mw-parser-output .hlist ol dl,.mw-parser-output .hlist ol ol,.mw-parser-output .hlist ol ul,.mw-parser-output .hlist ul dl,.mw-parser-output .hlist ul ol,.mw-parser-output .hlist ul ul{display:inline}.mw-parser-output .hlist .mw-empty-li{display:none}.mw-parser-output .hlist dt::after{content:\": \"}.mw-parser-output .hlist dd::after,.mw-parser-output .hlist li::after{content:\" · \";font-weight:bold}.mw-parser-output .hlist dd:last-child::after,.mw-parser-output .hlist dt:last-child::after,.mw-parser-output .hlist li:last-child::after{content:none}.mw-parser-output .hlist dd dd:first-child::before,.mw-parser-output .hlist dd dt:first-child::before,.mw-parser-output .hlist dd li:first-child::before,.mw-parser-output .hlist dt dd:first-child::before,.mw-parser-output .hlist dt dt:first-child::before,.mw-parser-output .hlist dt li:first-child::before,.mw-parser-output .hlist li dd:first-child::before,.mw-parser-output .hlist li dt:first-child::before,.mw-parser-output .hlist li li:first-child::before{content:\" (\";font-weight:normal}.mw-parser-output .hlist dd dd:last-child::after,.mw-parser-output .hlist dd dt:last-child::after,.mw-parser-output .hlist dd li:last-child::after,.mw-parser-output .hlist dt dd:last-child::after,.mw-parser-output .hlist dt dt:last-child::after,.mw-parser-output .hlist dt li:last-child::after,.mw-parser-output .hlist li dd:last-child::after,.mw-parser-output .hlist li dt:last-child::after,.mw-parser-output .hlist li li:last-child::after{content:\")\";font-weight:normal}.mw-parser-output .hlist ol{counter-reset:listitem}.mw-parser-output .hlist ol>li{counter-increment:listitem}.mw-parser-output .hlist ol>li::before{content:\" \"counter(listitem)\"\\a0 \"}.mw-parser-output .hlist dd ol>li:first-child::before,.mw-parser-output .hlist dt ol>li:first-child::before,.mw-parser-output .hlist li ol>li:first-child::before{content:\" (\"counter(listitem)\"\\a0 \"}</style><style data-mw-deduplicate=\"TemplateStyles:r1295905060\">.mw-parser-output .infobox-subbox{padding:0;border:none;margin:-3px;width:auto;min-width:100%;font-size:100%;clear:none;float:none;background-color:transparent}.mw-parser-output .infobox-3cols-child{margin:auto}.mw-parser-output .infobox .navbar{font-size:100%}@media screen{html.skin-theme-clientpref-night .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media(min-width:640px){body.skin--responsive .mw-parser-output .infobox-table{display:table!important}body.skin--responsive .mw-parser-output .infobox-table>caption{display:table-caption!important}body.skin--responsive .mw-parser-output .infobox-table>tbody{display:table-row-group}body.skin--responsive .mw-parser-output .infobox-table th,body.skin--responsive .mw-parser-output .infobox-table td{padding-left:inherit;padding-right:inherit}}</style><table class=\"infobox vevent\"><tbody><tr><th colspan=\"2\" class=\"infobox-above\" style=\"background-color:#e0e0e0;\">Python</th></tr><tr><td colspan=\"2\" class=\"infobox-image\"><span typeof=\"mw:File\"><a href=\"/wiki/File:Python-logo-notext.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/150px-Python-logo-notext.svg.png\" decoding=\"async\" width=\"150\" height=\"150\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/225px-Python-logo-notext.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/300px-Python-logo-notext.svg.png 2x\" data-file-width=\"110\" data-file-height=\"110\" /></a></span></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Programming_paradigm\" title=\"Programming paradigm\">Paradigm</a></th><td class=\"infobox-data\"><a href=\"/wiki/Multi-paradigm\" class=\"mw-redirect\" title=\"Multi-paradigm\">Multi-paradigm</a>: <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented</a>,<sup id=\"cite_ref-1\" class=\"reference\"><a href=\"#cite_note-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Procedural_programming\" title=\"Procedural programming\">procedural</a> (<a href=\"/wiki/Imperative_programming\" title=\"Imperative programming\">imperative</a>), <a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional</a>, <a href=\"/wiki/Structured_programming\" title=\"Structured programming\">structured</a>, <a href=\"/wiki/Reflective_programming\" title=\"Reflective programming\">reflective</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Software_design\" title=\"Software design\">Designed&#160;by</a></th><td class=\"infobox-data\"><a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Software_developer\" class=\"mw-redirect\" title=\"Software developer\">Developer</a></th><td class=\"infobox-data organiser\"><a href=\"/wiki/Python_Software_Foundation\" title=\"Python Software Foundation\">Python Software Foundation</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\">First&#160;appeared</th><td class=\"infobox-data\">20&#160;February 1991<span class=\"noprint\">&#59;&#32;34 years ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">1991-02-20</span>)</span><sup id=\"cite_ref-alt-sources-history_2-0\" class=\"reference\"><a href=\"#cite_note-alt-sources-history-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1295905060\" /></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"white-space: nowrap;\"><a href=\"/wiki/Software_release_life_cycle\" title=\"Software release life cycle\">Stable release</a></th><td class=\"infobox-data\"><div style=\"margin:0px;\">3.13.7<sup id=\"cite_ref-wikidata-94d5b2798914e50f7103b872a798e54cb7cdf1fe-v20_3-0\" class=\"reference\"><a href=\"#cite_note-wikidata-94d5b2798914e50f7103b872a798e54cb7cdf1fe-v20-3\"><span class=\"cite-bracket\">&#91;</span>3<span class=\"cite-bracket\">&#93;</span></a></sup>&#160;<span class=\"mw-valign-text-top\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q28865?uselang=en#P348\" title=\"Edit this on Wikidata\"><img alt=\"Edit this on Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span>\n   / 14 August 2025<span class=\"noprint\">&#59;&#32;48 days ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">14 August 2025</span>)</span></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"white-space: nowrap;\"><a href=\"/wiki/Software_release_life_cycle#Beta\" title=\"Software release life cycle\">Preview release</a></th><td class=\"infobox-data\"><div style=\"margin:0px;\">3.14.0rc3\n   / 18&#160;August 2025<span class=\"noprint\">&#59;&#32;44 days ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">2025-08-18</span>)</span></div></td></tr><tr style=\"display:none\"><td colspan=\"2\">\n</td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Type_system\" title=\"Type system\">Typing discipline</a></th><td class=\"infobox-data\"><a href=\"/wiki/Duck_typing\" title=\"Duck typing\">duck</a>, <a href=\"/wiki/Dynamic_typing\" class=\"mw-redirect\" title=\"Dynamic typing\">dynamic</a>, <a href=\"/wiki/Strong_and_weak_typing\" title=\"Strong and weak typing\">strong</a>;<sup id=\"cite_ref-4\" class=\"reference\"><a href=\"#cite_note-4\"><span class=\"cite-bracket\">&#91;</span>4<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Optional_typing\" class=\"mw-redirect\" title=\"Optional typing\">optional type annotations</a><sup id=\"cite_ref-6\" class=\"reference\"><a href=\"#cite_note-6\"><span class=\"cite-bracket\">&#91;</span>a<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Operating_system\" title=\"Operating system\">OS</a></th><td class=\"infobox-data\"><a href=\"/wiki/Cross-platform\" class=\"mw-redirect\" title=\"Cross-platform\">Cross-platform</a> including e.g. for mobile/<a href=\"/wiki/Android_(operating_system)\" title=\"Android (operating system)\">Android</a><sup id=\"cite_ref-12\" class=\"reference\"><a href=\"#cite_note-12\"><span class=\"cite-bracket\">&#91;</span>b<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Software_license\" title=\"Software license\">License</a></th><td class=\"infobox-data\"><a href=\"/wiki/Python_Software_Foundation_License\" title=\"Python Software Foundation License\">Python Software Foundation License</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\"><a href=\"/wiki/Filename_extension\" title=\"Filename extension\">Filename extensions</a></th><td class=\"infobox-data\">.py, .pyw, .pyz,<sup id=\"cite_ref-13\" class=\"reference\"><a href=\"#cite_note-13\"><span class=\"cite-bracket\">&#91;</span>11<span class=\"cite-bracket\">&#93;</span></a></sup><br />\n.pyi, .pyc, .pyd</td></tr><tr><th scope=\"row\" class=\"infobox-label\">Website</th><td class=\"infobox-data\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/\">python.org</a></span></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\">Major <a href=\"/wiki/Programming_language_implementation\" title=\"Programming language implementation\">implementations</a></th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><a href=\"/wiki/CPython\" title=\"CPython\">CPython</a>, <a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a>, <a href=\"/wiki/MicroPython\" title=\"MicroPython\">MicroPython</a>, <a href=\"/wiki/CircuitPython\" title=\"CircuitPython\">CircuitPython</a>, <a href=\"/wiki/IronPython\" title=\"IronPython\">IronPython</a>, <a href=\"/wiki/Jython\" title=\"Jython\">Jython</a>, <a href=\"/wiki/Stackless_Python\" title=\"Stackless Python\">Stackless Python</a></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\"><a href=\"/wiki/Programming_language#Dialects,_flavors_and_implementations\" title=\"Programming language\">Dialects</a></th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><a href=\"/wiki/Cython\" title=\"Cython\">Cython</a>, <a href=\"/wiki/RPython\" class=\"mw-redirect\" title=\"RPython\">RPython</a>, <a href=\"/wiki/Starlark\" title=\"Starlark\">Starlark</a><sup id=\"cite_ref-14\" class=\"reference\"><a href=\"#cite_note-14\"><span class=\"cite-bracket\">&#91;</span>12<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\">Influenced by</th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><a href=\"/wiki/ABC_(programming_language)\" title=\"ABC (programming language)\">ABC</a>,<sup id=\"cite_ref-faq-created_15-0\" class=\"reference\"><a href=\"#cite_note-faq-created-15\"><span class=\"cite-bracket\">&#91;</span>13<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Ada_(programming_language)\" title=\"Ada (programming language)\">Ada</a>,<sup id=\"cite_ref-16\" class=\"reference\"><a href=\"#cite_note-16\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"noprint Inline-Template\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Verifiability\" title=\"Wikipedia:Verifiability\"><span title=\"The source does not talk about Python. The &quot;raise&quot; keyword it describes is very similar to the Python version, but this is original research (see WP:OR). (August 2025)\">failed verification</span></a></i>&#93;</sup> <a href=\"/wiki/ALGOL_68\" title=\"ALGOL 68\">ALGOL 68</a>,<sup id=\"cite_ref-98-interview_17-0\" class=\"reference\"><a href=\"#cite_note-98-interview-17\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup> <br /><a href=\"/wiki/APL_(programming_language)\" title=\"APL (programming language)\">APL</a>,<sup id=\"cite_ref-python.org_18-0\" class=\"reference\"><a href=\"#cite_note-python.org-18\"><span class=\"cite-bracket\">&#91;</span>16<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a>,<sup id=\"cite_ref-AutoNT-1_19-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-1-19\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a>,<sup id=\"cite_ref-classmix_20-0\" class=\"reference\"><a href=\"#cite_note-classmix-20\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/CLU_(programming_language)\" title=\"CLU (programming language)\">CLU</a>,<sup id=\"cite_ref-effbot-call-by-object_21-0\" class=\"reference\"><a href=\"#cite_note-effbot-call-by-object-21\"><span class=\"cite-bracket\">&#91;</span>19<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Dylan_(programming_language)\" title=\"Dylan (programming language)\">Dylan</a>,<sup id=\"cite_ref-AutoNT-2_22-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-2-22\"><span class=\"cite-bracket\">&#91;</span>20<span class=\"cite-bracket\">&#93;</span></a></sup> <br /><a href=\"/wiki/Haskell\" title=\"Haskell\">Haskell</a>,<sup id=\"cite_ref-AutoNT-3_23-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-3-23\"><span class=\"cite-bracket\">&#91;</span>21<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-python.org_18-1\" class=\"reference\"><a href=\"#cite_note-python.org-18\"><span class=\"cite-bracket\">&#91;</span>16<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Icon_(programming_language)\" title=\"Icon (programming language)\">Icon</a>,<sup id=\"cite_ref-AutoNT-4_24-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-4-24\"><span class=\"cite-bracket\">&#91;</span>22<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Lisp_(programming_language)\" title=\"Lisp (programming language)\">Lisp</a>,<sup id=\"cite_ref-AutoNT-6_25-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-6-25\"><span class=\"cite-bracket\">&#91;</span>23<span class=\"cite-bracket\">&#93;</span></a></sup> <span class=\"nowrap\"><br /><a href=\"/wiki/Modula-3\" title=\"Modula-3\">Modula-3</a></span>,<sup id=\"cite_ref-98-interview_17-1\" class=\"reference\"><a href=\"#cite_note-98-interview-17\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-classmix_20-1\" class=\"reference\"><a href=\"#cite_note-classmix-20\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Perl\" title=\"Perl\">Perl</a>,<sup id=\"cite_ref-26\" class=\"reference\"><a href=\"#cite_note-26\"><span class=\"cite-bracket\">&#91;</span>24<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Standard_ML\" title=\"Standard ML\">Standard ML</a><sup id=\"cite_ref-python.org_18-2\" class=\"reference\"><a href=\"#cite_note-python.org-18\"><span class=\"cite-bracket\">&#91;</span>16<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><th colspan=\"2\" class=\"infobox-header\" style=\"background-color: #EEEEEE;\">Influenced</th></tr><tr><td colspan=\"2\" class=\"infobox-full-data\"><a href=\"/wiki/Apache_Groovy\" title=\"Apache Groovy\">Apache Groovy</a>, <a href=\"/wiki/Boo_(programming_language)\" title=\"Boo (programming language)\">Boo</a>, <a href=\"/wiki/Cobra_(programming_language)\" title=\"Cobra (programming language)\">Cobra</a>, <a href=\"/wiki/CoffeeScript\" title=\"CoffeeScript\">CoffeeScript</a>,<sup id=\"cite_ref-27\" class=\"reference\"><a href=\"#cite_note-27\"><span class=\"cite-bracket\">&#91;</span>25<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/D_(programming_language)\" title=\"D (programming language)\">D</a>, <a href=\"/wiki/F_Sharp_(programming_language)\" title=\"F Sharp (programming language)\">F#</a>, <a href=\"/wiki/GDScript\" class=\"mw-redirect\" title=\"GDScript\">GDScript</a>, <a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a>, <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>,<sup id=\"cite_ref-28\" class=\"reference\"><a href=\"#cite_note-28\"><span class=\"cite-bracket\">&#91;</span>26<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-29\" class=\"reference\"><a href=\"#cite_note-29\"><span class=\"cite-bracket\">&#91;</span>27<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a>,<sup id=\"cite_ref-Julia_30-0\" class=\"reference\"><a href=\"#cite_note-Julia-30\"><span class=\"cite-bracket\">&#91;</span>28<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Mojo_(programming_language)\" title=\"Mojo (programming language)\">Mojo</a>,<sup id=\"cite_ref-Mojo_31-0\" class=\"reference\"><a href=\"#cite_note-Mojo-31\"><span class=\"cite-bracket\">&#91;</span>29<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Nim_(programming_language)\" title=\"Nim (programming language)\">Nim</a>, <a href=\"/wiki/Ring_(programming_language)\" title=\"Ring (programming language)\">Ring</a>,<sup id=\"cite_ref-The_Ring_programming_language_and_other_languages_32-0\" class=\"reference\"><a href=\"#cite_note-The_Ring_programming_language_and_other_languages-32\"><span class=\"cite-bracket\">&#91;</span>30<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a>,<sup id=\"cite_ref-bini_33-0\" class=\"reference\"><a href=\"#cite_note-bini-33\"><span class=\"cite-bracket\">&#91;</span>31<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a>,<sup id=\"cite_ref-lattner2014_34-0\" class=\"reference\"><a href=\"#cite_note-lattner2014-34\"><span class=\"cite-bracket\">&#91;</span>32<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/V_(programming_language)\" title=\"V (programming language)\">V</a><sup id=\"cite_ref-vpeople_35-0\" class=\"reference\"><a href=\"#cite_note-vpeople-35\"><span class=\"cite-bracket\">&#91;</span>33<span class=\"cite-bracket\">&#93;</span></a></sup></td></tr><tr><td colspan=\"2\" class=\"infobox-below hlist\" style=\"border-top: 1px solid #aaa; padding-top: 3px;\">\n<ul><li><style data-mw-deduplicate=\"TemplateStyles:r1312463507\">@media screen{html.skin-theme-clientpref-night .mw-parser-output .sister-inline-image img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sister-inline-image img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}</style><span class=\"noviewer sister-inline-image\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo-en-noslogan.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/20px-Wikibooks-logo-en-noslogan.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/40px-Wikibooks-logo-en-noslogan.svg.png 1.5x\" data-file-width=\"400\" data-file-height=\"400\" /></a></span> <a href=\"https://en.wikibooks.org/wiki/Python_Programming\" class=\"extiw\" title=\"wikibooks:Python Programming\">Python Programming</a> at Wikibooks</li></ul>\n</td></tr></tbody></table>\n<p><b>Python</b> is a <a href=\"/wiki/High-level_programming_language\" title=\"High-level programming language\">high-level</a>, <a href=\"/wiki/General-purpose_programming_language\" title=\"General-purpose programming language\">general-purpose programming language</a>. Its design philosophy emphasizes <a href=\"/wiki/Code_readability\" class=\"mw-redirect\" title=\"Code readability\">code readability</a> with the use of <a href=\"/wiki/Significant_indentation\" class=\"mw-redirect\" title=\"Significant indentation\">significant indentation</a>.<sup id=\"cite_ref-AutoNT-7_36-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-7-36\"><span class=\"cite-bracket\">&#91;</span>34<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python is <a href=\"/wiki/Type_system#DYNAMIC\" title=\"Type system\">dynamically type-checked</a> and <a href=\"/wiki/Garbage_collection_(computer_science)\" title=\"Garbage collection (computer science)\">garbage-collected</a>. It supports multiple <a href=\"/wiki/Programming_paradigm\" title=\"Programming paradigm\">programming paradigms</a>, including <a href=\"/wiki/Structured_programming\" title=\"Structured programming\">structured</a> (particularly <a href=\"/wiki/Procedural_programming\" title=\"Procedural programming\">procedural</a>), <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented</a> and <a href=\"/wiki/Functional_programming\" title=\"Functional programming\">functional programming</a>.\n</p><p><a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a> began working on Python in the late 1980s as a successor to the <a href=\"/wiki/ABC_(programming_language)\" title=\"ABC (programming language)\">ABC</a> programming language. Python&#160;3.0, released in 2008, was a major revision and not completely <a href=\"/wiki/Backward-compatible\" class=\"mw-redirect\" title=\"Backward-compatible\">backward-compatible</a> with earlier versions. Recent versions, such as Python 3.13, 3.12 and older (and 3.14), have added capabilities and keywords for typing (and more; e.g. increasing speed); helping with (optional) <a href=\"/wiki/Static_typing\" class=\"mw-redirect\" title=\"Static typing\">static typing</a>.<sup id=\"cite_ref-37\" class=\"reference\"><a href=\"#cite_note-37\"><span class=\"cite-bracket\">&#91;</span>35<span class=\"cite-bracket\">&#93;</span></a></sup> Currently only versions in the 3.x series are supported.\n</p><p>Python consistently ranks as one of the most popular programming languages, and it has gained widespread use in the <a href=\"/wiki/Machine_learning\" title=\"Machine learning\">machine learning</a> community.<sup id=\"cite_ref-38\" class=\"reference\"><a href=\"#cite_note-38\"><span class=\"cite-bracket\">&#91;</span>36<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-39\" class=\"reference\"><a href=\"#cite_note-39\"><span class=\"cite-bracket\">&#91;</span>37<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-tiobecurrent_40-0\" class=\"reference\"><a href=\"#cite_note-tiobecurrent-40\"><span class=\"cite-bracket\">&#91;</span>38<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-41\" class=\"reference\"><a href=\"#cite_note-41\"><span class=\"cite-bracket\">&#91;</span>39<span class=\"cite-bracket\">&#93;</span></a></sup> It is widely taught as an introductory programming language.<sup id=\"cite_ref-42\" class=\"reference\"><a href=\"#cite_note-42\"><span class=\"cite-bracket\">&#91;</span>40<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<meta property=\"mw:PageProp/toc\" />\n<div class=\"mw-heading mw-heading2\"><h2 id=\"History\">History</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=1\" title=\"Edit section: History\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1236090951\">.mw-parser-output .hatnote{font-style:italic}.mw-parser-output div.hatnote{padding-left:1.6em;margin-bottom:0.5em}.mw-parser-output .hatnote i{font-style:normal}.mw-parser-output .hatnote+link+.hatnote{margin-top:-0.5em}@media print{body.ns-0 .mw-parser-output .hatnote{display:none!important}}</style><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/History_of_Python\" title=\"History of Python\">History of Python</a></div>\n<figure typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Guido_van_Rossum_in_PyConUS24.jpg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Guido_van_Rossum_in_PyConUS24.jpg/250px-Guido_van_Rossum_in_PyConUS24.jpg\" decoding=\"async\" width=\"208\" height=\"311\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Guido_van_Rossum_in_PyConUS24.jpg/330px-Guido_van_Rossum_in_PyConUS24.jpg 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/2/21/Guido_van_Rossum_in_PyConUS24.jpg/500px-Guido_van_Rossum_in_PyConUS24.jpg 2x\" data-file-width=\"3992\" data-file-height=\"5976\" /></a><figcaption>The designer of Python, <a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a>, at PyCon US 2024</figcaption></figure>\n<p>Python was conceived in the late 1980s<sup id=\"cite_ref-venners-interview-pt-1_43-0\" class=\"reference\"><a href=\"#cite_note-venners-interview-pt-1-43\"><span class=\"cite-bracket\">&#91;</span>41<span class=\"cite-bracket\">&#93;</span></a></sup> by <a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a> at <a href=\"/wiki/Centrum_Wiskunde_%26_Informatica\" title=\"Centrum Wiskunde &amp; Informatica\">Centrum Wiskunde &amp; Informatica</a> (CWI) in the <a href=\"/wiki/Netherlands\" title=\"Netherlands\">Netherlands</a>.<sup id=\"cite_ref-:2_44-0\" class=\"reference\"><a href=\"#cite_note-:2-44\"><span class=\"cite-bracket\">&#91;</span>42<span class=\"cite-bracket\">&#93;</span></a></sup> It was designed as a successor to the <a href=\"/wiki/ABC_(programming_language)\" title=\"ABC (programming language)\">ABC</a> programming language, which was inspired by <a href=\"/wiki/SETL\" title=\"SETL\">SETL</a>,<sup id=\"cite_ref-AutoNT-12_45-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-12-45\"><span class=\"cite-bracket\">&#91;</span>43<span class=\"cite-bracket\">&#93;</span></a></sup> capable of <a href=\"/wiki/Exception_handling\" title=\"Exception handling\">exception handling</a> and interfacing with the <a href=\"/wiki/Amoeba_(operating_system)\" title=\"Amoeba (operating system)\">Amoeba</a> operating system.<sup id=\"cite_ref-faq-created_15-1\" class=\"reference\"><a href=\"#cite_note-faq-created-15\"><span class=\"cite-bracket\">&#91;</span>13<span class=\"cite-bracket\">&#93;</span></a></sup> Python implementation began in December,&#160;1989.<sup id=\"cite_ref-timeline-of-python_46-0\" class=\"reference\"><a href=\"#cite_note-timeline-of-python-46\"><span class=\"cite-bracket\">&#91;</span>44<span class=\"cite-bracket\">&#93;</span></a></sup> Van Rossum first released it in 1991 as Python&#160;0.9.0.<sup id=\"cite_ref-:2_44-1\" class=\"reference\"><a href=\"#cite_note-:2-44\"><span class=\"cite-bracket\">&#91;</span>42<span class=\"cite-bracket\">&#93;</span></a></sup> Van Rossum assumed sole responsibility for the project, as the lead developer, until 12 July 2018, when he announced his \"permanent vacation\" from responsibilities as Python's \"<a href=\"/wiki/Benevolent_dictator_for_life\" title=\"Benevolent dictator for life\">benevolent dictator for life</a>\" (BDFL); this title was bestowed on him by the Python community to reflect his long-term commitment as the project's chief decision-maker.<sup id=\"cite_ref-lj-bdfl-resignation_47-0\" class=\"reference\"><a href=\"#cite_note-lj-bdfl-resignation-47\"><span class=\"cite-bracket\">&#91;</span>45<span class=\"cite-bracket\">&#93;</span></a></sup> (He has since come out of retirement and is self-titled \"BDFL-emeritus\".) In January&#160;2019, active Python core developers elected a five-member Steering Council to lead the project.<sup id=\"cite_ref-48\" class=\"reference\"><a href=\"#cite_note-48\"><span class=\"cite-bracket\">&#91;</span>46<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-49\" class=\"reference\"><a href=\"#cite_note-49\"><span class=\"cite-bracket\">&#91;</span>47<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The name <i>Python</i> derives from the British comedy series <a href=\"/wiki/Monty_Python%27s_Flying_Circus\" title=\"Monty Python&#39;s Flying Circus\">Monty Python's Flying Circus</a>.<sup id=\"cite_ref-:0_50-0\" class=\"reference\"><a href=\"#cite_note-:0-50\"><span class=\"cite-bracket\">&#91;</span>48<span class=\"cite-bracket\">&#93;</span></a></sup> (See <a href=\"#Naming\">§&#160;Naming</a>.)\n</p><p>Python 2.0 was released on 16 October 2000, with many major new features such as <a href=\"/wiki/List_comprehension\" title=\"List comprehension\">list comprehensions</a>, <a href=\"/wiki/Cycle_detection\" title=\"Cycle detection\">cycle-detecting</a> garbage collection, <a href=\"/wiki/Reference_counting\" title=\"Reference counting\">reference counting</a>, and <a href=\"/wiki/Unicode\" title=\"Unicode\">Unicode</a> support.<sup id=\"cite_ref-newin-2.0_51-0\" class=\"reference\"><a href=\"#cite_note-newin-2.0-51\"><span class=\"cite-bracket\">&#91;</span>49<span class=\"cite-bracket\">&#93;</span></a></sup> Python 2.7's <a href=\"/wiki/End-of-life_product\" title=\"End-of-life product\">end-of-life</a> was initially set for 2015, and then postponed to 2020 out of concern that a large body of existing code could not easily be forward-ported to Python&#160;3.<sup id=\"cite_ref-52\" class=\"reference\"><a href=\"#cite_note-52\"><span class=\"cite-bracket\">&#91;</span>50<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-53\" class=\"reference\"><a href=\"#cite_note-53\"><span class=\"cite-bracket\">&#91;</span>51<span class=\"cite-bracket\">&#93;</span></a></sup> It no longer receives security patches or updates.<sup id=\"cite_ref-54\" class=\"reference\"><a href=\"#cite_note-54\"><span class=\"cite-bracket\">&#91;</span>52<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-55\" class=\"reference\"><a href=\"#cite_note-55\"><span class=\"cite-bracket\">&#91;</span>53<span class=\"cite-bracket\">&#93;</span></a></sup> While Python 2.7 and older versions are officially unsupported, a different unofficial Python implementation, <a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a>, continues to support Python 2, i.e., \"2.7.18+\" (plus 3.10), with the plus signifying (at least some) \"<a href=\"/wiki/Backporting\" title=\"Backporting\">backported</a> security updates\".<sup id=\"cite_ref-56\" class=\"reference\"><a href=\"#cite_note-56\"><span class=\"cite-bracket\">&#91;</span>54<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python&#160;3.0 was released on 3 December 2008, and was a major revision and not completely <a href=\"/wiki/Backward-compatible\" class=\"mw-redirect\" title=\"Backward-compatible\">backward-compatible</a> with earlier versions, with some new semantics and changed syntax. Python&#160;2.7.18, released in 2020, was the last release of Python&#160;2.<sup id=\"cite_ref-57\" class=\"reference\"><a href=\"#cite_note-57\"><span class=\"cite-bracket\">&#91;</span>55<span class=\"cite-bracket\">&#93;</span></a></sup> Several releases in the Python 3.x series have added new syntax to the language, and made a few (considered very minor) backwards-incompatible changes.\n</p><p>As of 14&#160;August&#160;2025<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup>, Python 3.13 is the latest stable release and Python 3.10 is the oldest supported release.<sup id=\"cite_ref-58\" class=\"reference\"><a href=\"#cite_note-58\"><span class=\"cite-bracket\">&#91;</span>56<span class=\"cite-bracket\">&#93;</span></a></sup> Releases receive two years of full support followed by three years of security support.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Design_philosophy_and_features\">Design philosophy and features</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=2\" title=\"Edit section: Design philosophy and features\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python is a <a href=\"/wiki/Multi-paradigm_programming_language\" class=\"mw-redirect\" title=\"Multi-paradigm programming language\">multi-paradigm programming language</a>. Object-oriented programming and structured programming are fully supported, and many of their features support functional programming and <a href=\"/wiki/Aspect-oriented_programming\" title=\"Aspect-oriented programming\">aspect-oriented programming</a> – including <a href=\"/wiki/Metaprogramming\" title=\"Metaprogramming\">metaprogramming</a><sup id=\"cite_ref-AutoNT-13_59-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-13-59\"><span class=\"cite-bracket\">&#91;</span>57<span class=\"cite-bracket\">&#93;</span></a></sup> and <a href=\"/wiki/Metaobject\" title=\"Metaobject\">metaobjects</a>.<sup id=\"cite_ref-AutoNT-14_60-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-14-60\"><span class=\"cite-bracket\">&#91;</span>58<span class=\"cite-bracket\">&#93;</span></a></sup> Many other paradigms are supported via extensions, including <a href=\"/wiki/Design_by_contract\" title=\"Design by contract\">design by contract</a><sup id=\"cite_ref-AutoNT-15_61-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-15-61\"><span class=\"cite-bracket\">&#91;</span>59<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-16_62-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-16-62\"><span class=\"cite-bracket\">&#91;</span>60<span class=\"cite-bracket\">&#93;</span></a></sup> and <a href=\"/wiki/Logic_programming\" title=\"Logic programming\">logic programming</a>.<sup id=\"cite_ref-AutoNT-17_63-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-17-63\"><span class=\"cite-bracket\">&#91;</span>61<span class=\"cite-bracket\">&#93;</span></a></sup> Python is often referred to as a <i><a href=\"/wiki/Glue_language\" class=\"mw-redirect\" title=\"Glue language\">'glue language'</a></i><sup id=\"cite_ref-64\" class=\"reference\"><a href=\"#cite_note-64\"><span class=\"cite-bracket\">&#91;</span>62<span class=\"cite-bracket\">&#93;</span></a></sup> because it is purposely designed to be able to integrate components written in other languages.\n</p><p>Python uses dynamic typing and a combination of <a href=\"/wiki/Reference_counting\" title=\"Reference counting\">reference counting</a> and a cycle-detecting garbage collector for <a href=\"/wiki/Memory_management\" title=\"Memory management\">memory management</a>.<sup id=\"cite_ref-Reference_counting_65-0\" class=\"reference\"><a href=\"#cite_note-Reference_counting-65\"><span class=\"cite-bracket\">&#91;</span>63<span class=\"cite-bracket\">&#93;</span></a></sup> It uses dynamic <a href=\"/wiki/Name_resolution_(programming_languages)\" title=\"Name resolution (programming languages)\">name resolution</a> (<a href=\"/wiki/Late_binding\" title=\"Late binding\">late binding</a>), which binds method and variable names during program execution.\n</p><p>Python's design offers some support for functional programming in the \"<a href=\"/wiki/Lisp_(programming_language)\" title=\"Lisp (programming language)\">Lisp</a> tradition\". It has <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">filter</code>, <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">map</code>, and <code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">reduce</code> functions; <a href=\"/wiki/List_comprehension\" title=\"List comprehension\">list comprehensions</a>, <a href=\"/wiki/Associative_array\" title=\"Associative array\">dictionaries</a>, <a href=\"/wiki/Set_(mathematics)\" title=\"Set (mathematics)\">sets</a>, and <a href=\"/wiki/Generator_(computer_programming)\" title=\"Generator (computer programming)\">generator</a> expressions.<sup id=\"cite_ref-AutoNT-59_66-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-59-66\"><span class=\"cite-bracket\">&#91;</span>64<span class=\"cite-bracket\">&#93;</span></a></sup> The standard library has two modules (<code>itertools</code> and <code>functools</code>) that implement functional tools borrowed from <a href=\"/wiki/Haskell\" title=\"Haskell\">Haskell</a> and <a href=\"/wiki/Standard_ML\" title=\"Standard ML\">Standard ML</a>.<sup id=\"cite_ref-AutoNT-18_67-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-18-67\"><span class=\"cite-bracket\">&#91;</span>65<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python's core philosophy is summarized in the <a href=\"/wiki/Zen_of_Python\" title=\"Zen of Python\">Zen of Python</a> (PEP 20) written by <a href=\"/wiki/Tim_Peters_(software_engineer)\" title=\"Tim Peters (software engineer)\">Tim Peters</a>, which includes aphorisms such as these:<sup id=\"cite_ref-PEP20_68-0\" class=\"reference\"><a href=\"#cite_note-PEP20-68\"><span class=\"cite-bracket\">&#91;</span>66<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<ul><li>Explicit is better than implicit.</li>\n<li>Simple is better than complex.</li>\n<li>Readability counts.</li>\n<li>Special cases aren't special enough to break the rules.</li>\n<li>Although practicality beats purity.</li>\n<li>Errors should never pass silently.</li>\n<li>Unless explicitly silenced.</li>\n<li>There should be one-- and preferably only one --obvious way to do it.</li></ul>\n<p>However, Python have received criticism for violating these principles and adding unnecessary language bloat.<sup id=\"cite_ref-Python-Changes-2014_69-0\" class=\"reference\"><a href=\"#cite_note-Python-Changes-2014-69\"><span class=\"cite-bracket\">&#91;</span>67<span class=\"cite-bracket\">&#93;</span></a></sup> Responses to these criticisms note that the Zen of Python is a guideline rather than a rule.<sup id=\"cite_ref-Confusion-regarding-a-rule-in-the-Zen-of-Python_70-0\" class=\"reference\"><a href=\"#cite_note-Confusion-regarding-a-rule-in-the-Zen-of-Python-70\"><span class=\"cite-bracket\">&#91;</span>68<span class=\"cite-bracket\">&#93;</span></a></sup> The addition of some new features had been controversial: Guido van Rossum resigned as <i>Benevolent Dictator for Life</i> after conflict about adding the assignment expression operator in <span class=\"nowrap\">Python 3.8&#8201;.</span><sup id=\"cite_ref-The-Most-Controversial-Python-Walrus-Operator_71-0\" class=\"reference\"><a href=\"#cite_note-The-Most-Controversial-Python-Walrus-Operator-71\"><span class=\"cite-bracket\">&#91;</span>69<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-The-Controversy-Behind-The-Walrus-Operator-in-Python_72-0\" class=\"reference\"><a href=\"#cite_note-The-Controversy-Behind-The-Walrus-Operator-in-Python-72\"><span class=\"cite-bracket\">&#91;</span>70<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Nevertheless, rather than building all functionality into its core, Python was designed to be highly <a href=\"/wiki/Extensible\" class=\"mw-redirect\" title=\"Extensible\">extensible</a> via modules. This compact modularity has made it particularly popular as a means of adding programmable interfaces to existing applications. Van Rossum's vision of a small core language with a large standard library and easily extensible interpreter stemmed from his frustrations with ABC, which represented the opposite approach.<sup id=\"cite_ref-venners-interview-pt-1_43-1\" class=\"reference\"><a href=\"#cite_note-venners-interview-pt-1-43\"><span class=\"cite-bracket\">&#91;</span>41<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python claims to strive for a simpler, less-cluttered syntax and grammar, while giving developers a choice in their coding methodology. In contrast to <a href=\"/wiki/Perl\" title=\"Perl\">Perl</a>'s motto \"<a href=\"/wiki/There_is_more_than_one_way_to_do_it\" class=\"mw-redirect\" title=\"There is more than one way to do it\">there is more than one way to do it</a>\", Python advocates an approach where \"there should be one – and preferably only one – obvious way to do it\".<sup id=\"cite_ref-PEP20_68-1\" class=\"reference\"><a href=\"#cite_note-PEP20-68\"><span class=\"cite-bracket\">&#91;</span>66<span class=\"cite-bracket\">&#93;</span></a></sup> In practice, however, Python provides many ways to achieve a given goal. There are, for example, at least three ways to format a string literal, with no certainty as to which one a programmer should use.<sup id=\"cite_ref-Python-String-Formatting-Best-Practices_73-0\" class=\"reference\"><a href=\"#cite_note-Python-String-Formatting-Best-Practices-73\"><span class=\"cite-bracket\">&#91;</span>71<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Alex_Martelli\" title=\"Alex Martelli\">Alex Martelli</a> is a <a href=\"/wiki/Fellow\" title=\"Fellow\">Fellow</a> at the <a href=\"/wiki/Python_Software_Foundation\" title=\"Python Software Foundation\">Python Software Foundation</a> and Python book author; he wrote that \"To describe something as 'clever' is <i>not</i> considered a compliment in the Python culture.\"<sup id=\"cite_ref-AutoNT-19_74-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-19-74\"><span class=\"cite-bracket\">&#91;</span>72<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python's developers usually try to avoid <a href=\"/wiki/Premature_optimization\" class=\"mw-redirect\" title=\"Premature optimization\">premature optimization</a>; they also reject patches to non-critical parts of the <a href=\"/wiki/CPython\" title=\"CPython\">CPython</a> reference implementation that would offer marginal increases in speed at the cost of clarity.<sup id=\"cite_ref-AutoNT-20_75-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-20-75\"><span class=\"cite-bracket\">&#91;</span>73<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"noprint Inline-Template\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Verifiability\" title=\"Wikipedia:Verifiability\"><span title=\"The referenced source didn&#39;t mention either detail. (August 2025)\">failed verification</span></a></i>&#93;</sup> Execution speed can be improved by moving speed-critical functions to extension modules written in languages such as <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a>, or by using a <a href=\"/wiki/Just-in-time_compiler\" class=\"mw-redirect\" title=\"Just-in-time compiler\">just-in-time compiler</a> like <a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a>. It is also possible to <a href=\"#Cross-compilers_to_other_languages\">cross-compile to other languages</a>; but this approach either fails to achieve the expected speed-up, since Python is a very <a href=\"/wiki/Dynamic_language\" class=\"mw-redirect\" title=\"Dynamic language\">dynamic language</a>, or only a restricted subset of Python is compiled (with potential minor semantic changes).<sup id=\"cite_ref-PyJL_76-0\" class=\"reference\"><a href=\"#cite_note-PyJL-76\"><span class=\"cite-bracket\">&#91;</span>74<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python's developers aim for the language to be fun to use. This goal is reflected in the name – a tribute to the British comedy group <a href=\"/wiki/Monty_Python\" title=\"Monty Python\">Monty Python</a><sup id=\"cite_ref-whyname_77-0\" class=\"reference\"><a href=\"#cite_note-whyname-77\"><span class=\"cite-bracket\">&#91;</span>75<span class=\"cite-bracket\">&#93;</span></a></sup> – and in playful approaches to some tutorials and reference materials. For instance, some code examples use the terms \"spam\" and \"eggs\" (in reference to <a href=\"/wiki/Spam_(Monty_Python)\" class=\"mw-redirect\" title=\"Spam (Monty Python)\">a Monty Python sketch</a>), rather than the typical terms <a href=\"/wiki/Foobar\" title=\"Foobar\">\"foo\" and \"bar\"</a>.<sup id=\"cite_ref-78\" class=\"reference\"><a href=\"#cite_note-78\"><span class=\"cite-bracket\">&#91;</span>76<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-pprint-doc_79-0\" class=\"reference\"><a href=\"#cite_note-pprint-doc-79\"><span class=\"cite-bracket\">&#91;</span>77<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>A common <a href=\"/wiki/Neologism\" title=\"Neologism\">neologism</a> in the Python community is <i>pythonic</i>, which has a wide range of meanings related to program style: Pythonic code may use Python <a href=\"/wiki/Programming_idiom\" title=\"Programming idiom\">idioms</a> well; be natural or show fluency in the language; or conform with Python's minimalist philosophy and emphasis on readability.<sup id=\"cite_ref-80\" class=\"reference\"><a href=\"#cite_note-80\"><span class=\"cite-bracket\">&#91;</span>78<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Syntax_and_semantics\">Syntax and semantics</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=3\" title=\"Edit section: Syntax and semantics\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/Python_syntax_and_semantics\" title=\"Python syntax and semantics\">Python syntax and semantics</a></div>\n<figure typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Hello_World_in_Python.png\" class=\"mw-file-description\"><img alt=\"Block of Python code showing sample source code\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Hello_World_in_Python.png/250px-Hello_World_in_Python.png\" decoding=\"async\" width=\"231\" height=\"151\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Hello_World_in_Python.png/500px-Hello_World_in_Python.png 1.5x\" data-file-width=\"528\" data-file-height=\"346\" /></a><figcaption>An example of Python code and indentation</figcaption></figure>\n<figure typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Af-Helloworld_(C_Sharp).svg\" class=\"mw-file-description\"><img alt=\"C code featuring curly braces and semicolon\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Af-Helloworld_%28C_Sharp%29.svg/250px-Af-Helloworld_%28C_Sharp%29.svg.png\" decoding=\"async\" width=\"233\" height=\"144\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Af-Helloworld_%28C_Sharp%29.svg/350px-Af-Helloworld_%28C_Sharp%29.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Af-Helloworld_%28C_Sharp%29.svg/466px-Af-Helloworld_%28C_Sharp%29.svg.png 2x\" data-file-width=\"285\" data-file-height=\"176\" /></a><figcaption>Example of <a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">C#</a> code with curly braces and semicolons</figcaption></figure>\n<p>Python is meant to be an easily readable language. Its formatting is visually uncluttered and often uses English keywords where other languages use punctuation. Unlike many other languages, it does not use <a href=\"/wiki/Curly_bracket_programming_language\" class=\"mw-redirect\" title=\"Curly bracket programming language\">curly brackets</a> to delimit blocks, and semicolons after statements are allowed but rarely used. It has fewer syntactic exceptions and special cases than <a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a> or <a href=\"/wiki/Pascal_(programming_language)\" title=\"Pascal (programming language)\">Pascal</a>.<sup id=\"cite_ref-AutoNT-52_81-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-52-81\"><span class=\"cite-bracket\">&#91;</span>79<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Indentation\">Indentation</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=4\" title=\"Edit section: Indentation\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Further information: <a href=\"/wiki/Python_syntax_and_semantics#Indentation\" title=\"Python syntax and semantics\">Python syntax and semantics §&#160;Indentation</a></div>\n<p>Python uses <a href=\"/wiki/Whitespace_character\" title=\"Whitespace character\">whitespace</a> indentation, rather than curly brackets or keywords, to delimit <a href=\"/wiki/Block_(programming)\" title=\"Block (programming)\">blocks</a>. An increase in indentation comes after certain statements; a decrease in indentation signifies the end of the current block.<sup id=\"cite_ref-AutoNT-53_82-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-53-82\"><span class=\"cite-bracket\">&#91;</span>80<span class=\"cite-bracket\">&#93;</span></a></sup> Thus, the program's visual structure accurately represents its semantic structure.<sup id=\"cite_ref-guttag_83-0\" class=\"reference\"><a href=\"#cite_note-guttag-83\"><span class=\"cite-bracket\">&#91;</span>81<span class=\"cite-bracket\">&#93;</span></a></sup> This feature is sometimes termed the <a href=\"/wiki/Off-side_rule\" title=\"Off-side rule\">off-side rule</a>. Some other languages use indentation this way; but in most, indentation has no semantic meaning. The recommended indent size is four spaces.<sup id=\"cite_ref-84\" class=\"reference\"><a href=\"#cite_note-84\"><span class=\"cite-bracket\">&#91;</span>82<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Statements_and_control_flow\">Statements and control flow</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=5\" title=\"Edit section: Statements and control flow\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's <a href=\"/wiki/Statement_(computer_science)\" title=\"Statement (computer science)\">statements</a> include the following:\n</p>\n<ul><li>The <a href=\"/wiki/Assignment_(computer_science)\" class=\"mw-redirect\" title=\"Assignment (computer science)\">assignment</a> statement, using a single equals sign <code>=</code></li>\n<li>The <code><a href=\"/wiki/If-then-else\" class=\"mw-redirect\" title=\"If-then-else\">if</a></code> statement, which conditionally executes a block of code, along with <code><a href=\"/wiki/Conditional_(computer_programming)#If–then(–else)\" title=\"Conditional (computer programming)\">else</a></code> and <code>elif</code> (a contraction of <code><a href=\"/wiki/Conditional_(computer_programming)#Else_if\" title=\"Conditional (computer programming)\">else if</a></code>)</li>\n<li>The <code><a href=\"/wiki/Foreach#Python\" class=\"mw-redirect\" title=\"Foreach\">for</a></code> statement, which iterates over an <i>iterable</i> object, capturing each element to a local variable for use by the attached block</li>\n<li>The <code><a href=\"/wiki/While_loop#Python\" title=\"While loop\">while</a></code> statement, which executes a block of code as long as boolean condition is true</li>\n<li>The <code><a href=\"/wiki/Exception_handling_syntax#Python\" title=\"Exception handling syntax\">try</a></code> statement, which allows exceptions raised in its attached code block to be caught and handled by <code>except</code> clauses (or new syntax <code>except*</code> in Python 3.11 for exception groups<sup id=\"cite_ref-85\" class=\"reference\"><a href=\"#cite_note-85\"><span class=\"cite-bracket\">&#91;</span>83<span class=\"cite-bracket\">&#93;</span></a></sup>); the <code>try</code> statement also ensures that clean-up code in a <code>finally</code> block is always run regardless of how the block exits</li>\n<li>The <code>raise</code> statement, used to raise a specified exception or re-raise a caught exception</li>\n<li>The <code>class</code> statement, which executes a block of code and attaches its local namespace to a <a href=\"/wiki/Class_(computer_science)\" class=\"mw-redirect\" title=\"Class (computer science)\">class</a>, for use in object-oriented programming</li>\n<li>The <code>def</code> statement, which defines a <a href=\"/wiki/Function_(computing)\" class=\"mw-redirect\" title=\"Function (computing)\">function</a> or <a href=\"/wiki/Method_(computing)\" class=\"mw-redirect\" title=\"Method (computing)\">method</a></li>\n<li>The <code><a href=\"/wiki/Dispose_pattern#Language_constructs\" title=\"Dispose pattern\">with</a></code> statement, which encloses a code block within a context manager, allowing <a href=\"/wiki/Resource_acquisition_is_initialization\" title=\"Resource acquisition is initialization\">resource-acquisition-is-initialization</a> (RAII)-like behavior and replacing a common try/finally idiom<sup id=\"cite_ref-86\" class=\"reference\"><a href=\"#cite_note-86\"><span class=\"cite-bracket\">&#91;</span>84<span class=\"cite-bracket\">&#93;</span></a></sup> Examples of a context include acquiring a <a href=\"/wiki/Lock_(computer_science)\" title=\"Lock (computer science)\">lock</a> before some code is run, and then releasing the lock; or opening and then closing a <a href=\"/wiki/Computer_file\" title=\"Computer file\">file</a></li>\n<li>The <code><a href=\"/wiki/Break_statement\" class=\"mw-redirect\" title=\"Break statement\">break</a></code> statement, which exits a loop</li>\n<li>The <code>continue</code> statement, which skips the rest of the current iteration and continues with the next</li>\n<li>The <code>del</code> statement, which removes a variable—deleting the reference from the name to the value, and producing an error if the variable is referred to before it is redefined <sup id=\"cite_ref-87\" class=\"reference\"><a href=\"#cite_note-87\"><span class=\"cite-bracket\">&#91;</span>c<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>The <code>pass</code> statement, serving as a <a href=\"/wiki/NOP_(code)\" title=\"NOP (code)\">NOP</a> (i.e., no operation), which is syntactically needed to create an empty code block</li>\n<li>The <code><a href=\"/wiki/Assertion_(programming)\" class=\"mw-redirect\" title=\"Assertion (programming)\">assert</a></code> statement, used in debugging to check for conditions that should apply</li>\n<li>The <code>yield</code> statement, which returns a value from a <a href=\"/wiki/Generator_(computer_programming)#Python\" title=\"Generator (computer programming)\">generator</a> function (and also an operator); used to implement <a href=\"/wiki/Coroutine\" title=\"Coroutine\">coroutines</a></li>\n<li>The <code>return</code> statement, used to return a value from a function</li>\n<li>The <code><a href=\"/wiki/Include_directive\" title=\"Include directive\">import</a></code> and <code>from</code> statements, used to import modules whose functions or variables can be used in the current program</li>\n<li>The <code>match</code> and <code>case</code> statements, analogous to a <a href=\"/wiki/Switch_statement\" title=\"Switch statement\">switch statement</a> construct, which compares an expression against one or more cases as a control-flow measure</li></ul>\n<p>The assignment statement (<code>=</code>) binds a name as a <a href=\"/wiki/Pointer_(computer_programming)\" title=\"Pointer (computer programming)\">reference</a> to a separate, dynamically allocated <a href=\"/wiki/Object_(computer_science)\" title=\"Object (computer science)\">object</a>. Variables may subsequently be rebound at any time to any object. In Python, a variable name is a generic reference holder without a fixed <a href=\"/wiki/Type_system\" title=\"Type system\">data type</a>; however, it always refers to <i>some</i> object with a type. This is called <a href=\"/wiki/Type_system#Dynamic_type_checking_and_runtime_type_information\" title=\"Type system\">dynamic typing</a>—in contrast to <a href=\"/wiki/Statically-typed\" class=\"mw-redirect\" title=\"Statically-typed\">statically-typed</a> languages, where each variable may contain only a value of a certain type.\n</p><p>Python does not support <a href=\"/wiki/Tail_call\" title=\"Tail call\">tail call</a> optimization or <a href=\"/wiki/First-class_continuations\" class=\"mw-redirect\" title=\"First-class continuations\">first-class continuations</a>; according to Van Rossum, the language never will.<sup id=\"cite_ref-AutoNT-55_88-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-55-88\"><span class=\"cite-bracket\">&#91;</span>85<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-56_89-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-56-89\"><span class=\"cite-bracket\">&#91;</span>86<span class=\"cite-bracket\">&#93;</span></a></sup> However, better support for <a href=\"/wiki/Coroutine\" title=\"Coroutine\">coroutine</a>-like functionality is provided by extending Python's generators.<sup id=\"cite_ref-AutoNT-57_90-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-57-90\"><span class=\"cite-bracket\">&#91;</span>87<span class=\"cite-bracket\">&#93;</span></a></sup> Before 2.5, generators were <a href=\"/wiki/Lazy_evaluation\" title=\"Lazy evaluation\">lazy</a> <a href=\"/wiki/Iterator\" title=\"Iterator\">iterators</a>; data was passed unidirectionally out of the generator. From Python&#160;2.5 on, it is possible to pass data back into a generator function; and from version 3.3, data can be passed through multiple stack levels.<sup id=\"cite_ref-AutoNT-58_91-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-58-91\"><span class=\"cite-bracket\">&#91;</span>88<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Expressions\">Expressions</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=6\" title=\"Edit section: Expressions\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's <a href=\"/wiki/Expression_(computer_science)\" title=\"Expression (computer science)\">expressions</a> include the following:\n</p>\n<ul><li>The <code>+</code>, <code>-</code>, and <code>*</code> operators for mathematical addition, subtraction, and multiplication are similar to other languages, but the behavior of division differs. There are two types of division in Python: <a href=\"/wiki/Floor_division\" class=\"mw-redirect\" title=\"Floor division\">floor division</a> (or integer division) <code>//</code>, and floating-point division <code>/</code>.<sup id=\"cite_ref-92\" class=\"reference\"><a href=\"#cite_note-92\"><span class=\"cite-bracket\">&#91;</span>89<span class=\"cite-bracket\">&#93;</span></a></sup> Python uses the <code>**</code> operator for exponentiation.</li>\n<li>Python uses the <code>+</code> operator for string concatenation. The language uses the <code>*</code> operator for duplicating a string a specified number of times.</li>\n<li>The <code>@</code> infix operator is intended to be used by libraries such as <a href=\"/wiki/NumPy\" title=\"NumPy\">NumPy</a> for <a href=\"/wiki/Matrix_multiplication\" title=\"Matrix multiplication\">matrix multiplication</a>.<sup id=\"cite_ref-PEP465_93-0\" class=\"reference\"><a href=\"#cite_note-PEP465-93\"><span class=\"cite-bracket\">&#91;</span>90<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-Python3.5Changelog_94-0\" class=\"reference\"><a href=\"#cite_note-Python3.5Changelog-94\"><span class=\"cite-bracket\">&#91;</span>91<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>The syntax <code>:=</code>, called the \"<style data-mw-deduplicate=\"TemplateStyles:r1238216509\">.mw-parser-output .vanchor>:target~.vanchor-text{background-color:#b1d2ff}@media screen{html.skin-theme-clientpref-night .mw-parser-output .vanchor>:target~.vanchor-text{background-color:#0f4dc9}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .vanchor>:target~.vanchor-text{background-color:#0f4dc9}}</style><span class=\"vanchor\"><span id=\"walrus_operator\"></span><span class=\"vanchor-text\">walrus operator</span></span>\", was introduced in Python 3.8. This operator assigns values to variables as part of a larger expression.<sup id=\"cite_ref-Python3.8Changelog_95-0\" class=\"reference\"><a href=\"#cite_note-Python3.8Changelog-95\"><span class=\"cite-bracket\">&#91;</span>92<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>In Python, <code>==</code> compares two objects by value. Python's <code>is</code> operator may be used to compare object identities (i.e., comparison by reference), and comparisons may be chained—for example, <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">a</span> <span class=\"o\">&lt;=</span> <span class=\"n\">b</span> <span class=\"o\">&lt;=</span> <span class=\"n\">c</span></code>.</li>\n<li>Python uses <code>and</code>, <code>or</code>, and <code>not</code> as Boolean operators.</li>\n<li>Python has a type of expression called a <i><a href=\"/wiki/List_comprehension#Python\" title=\"List comprehension\">list comprehension</a></i>, and a more general expression called a <i>generator expression</i>.<sup id=\"cite_ref-AutoNT-59_66-1\" class=\"reference\"><a href=\"#cite_note-AutoNT-59-66\"><span class=\"cite-bracket\">&#91;</span>64<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Anonymous_function\" title=\"Anonymous function\">Anonymous functions</a> are implemented using <a href=\"/wiki/Lambda_(programming)\" class=\"mw-redirect\" title=\"Lambda (programming)\">lambda expressions</a>; however, there may be only one expression in each body.</li>\n<li>Conditional expressions are written as <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">x</span> <span class=\"k\">if</span> <span class=\"n\">c</span> <span class=\"k\">else</span> <span class=\"n\">y</span></code>.<sup id=\"cite_ref-AutoNT-60_96-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-60-96\"><span class=\"cite-bracket\">&#91;</span>93<span class=\"cite-bracket\">&#93;</span></a></sup> (This is different in operand order from the <code><a href=\"/wiki/%3F:\" class=\"mw-redirect\" title=\"?:\">c&#160;? x&#160;: y</a></code> operator common to many other languages.)</li>\n<li>Python makes a distinction between <a href=\"/wiki/List_(computer_science)\" class=\"mw-redirect\" title=\"List (computer science)\">lists</a> and <a href=\"/wiki/Tuple\" title=\"Tuple\">tuples</a>. Lists are written as <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">[</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">]</span></code>, are mutable, and cannot be used as the keys of dictionaries (since dictionary keys must be <a href=\"/wiki/Immutable\" class=\"mw-redirect\" title=\"Immutable\">immutable</a> in Python). Tuples, written as <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span></code>, are immutable and thus can be used as the keys of dictionaries, provided that all of the tuple's elements are immutable. The <code>+</code> operator can be used to concatenate two tuples, which does not directly modify their contents, but produces a new tuple containing the elements of both. For example, given the variable <code>t</code> initially equal to <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">)</span></code>, executing <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">t</span> <span class=\"o\">=</span> <span class=\"n\">t</span> <span class=\"o\">+</span> <span class=\"p\">(</span><span class=\"mi\">4</span><span class=\"p\">,</span> <span class=\"mi\">5</span><span class=\"p\">)</span></code> first evaluates <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">t</span> <span class=\"o\">+</span> <span class=\"p\">(</span><span class=\"mi\">4</span><span class=\"p\">,</span> <span class=\"mi\">5</span><span class=\"p\">)</span></code>, which yields <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">,</span> <span class=\"mi\">4</span><span class=\"p\">,</span> <span class=\"mi\">5</span><span class=\"p\">)</span></code>; this result is then assigned back to <code>t</code>—thereby effectively \"modifying the contents\" of <code>t</code> while conforming to the immutable nature of tuple objects. Parentheses are optional for tuples in unambiguous contexts.<sup id=\"cite_ref-97\" class=\"reference\"><a href=\"#cite_note-97\"><span class=\"cite-bracket\">&#91;</span>94<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Python features <i>sequence unpacking</i> where multiple expressions, each evaluating to something assignable (e.g., a variable or a writable property) are associated just as in forming tuple literal; as a whole, the results are then put on the left-hand side of the equal sign in an assignment statement. This statement expects an <i>iterable</i> object on the right-hand side of the equal sign to produce the same number of values as the writable expressions on the left-hand side; while iterating, the statement assigns each of the values produced on the right to the corresponding expression on the left.<sup id=\"cite_ref-98\" class=\"reference\"><a href=\"#cite_note-98\"><span class=\"cite-bracket\">&#91;</span>95<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Python has a \"string format\" operator <code>%</code> that functions analogously to <code><a href=\"/wiki/Printf\" title=\"Printf\">printf</a></code> format strings in the C language—e.g. <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s2\">&quot;spam=</span><span class=\"si\">%s</span><span class=\"s2\"> eggs=</span><span class=\"si\">%d</span><span class=\"s2\">&quot;</span> <span class=\"o\">%</span> <span class=\"p\">(</span><span class=\"s2\">&quot;blah&quot;</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">)</span></code> evaluates to <code>\"spam=blah eggs=2\"</code>. In Python&#160;2.6+ and 3+, this operator was supplemented by the <code>format()</code> method of the <code>str</code> class, e.g., <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s2\">&quot;spam=</span><span class=\"si\">{0}</span><span class=\"s2\"> eggs=</span><span class=\"si\">{1}</span><span class=\"s2\">&quot;</span><span class=\"o\">.</span><span class=\"n\">format</span><span class=\"p\">(</span><span class=\"s2\">&quot;blah&quot;</span><span class=\"p\">,</span> <span class=\"mi\">2</span><span class=\"p\">)</span></code>. Python&#160;3.6 added \"f-strings\": <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">spam</span> <span class=\"o\">=</span> <span class=\"s2\">&quot;blah&quot;</span><span class=\"p\">;</span> <span class=\"n\">eggs</span> <span class=\"o\">=</span> <span class=\"mi\">2</span><span class=\"p\">;</span> <span class=\"sa\">f</span><span class=\"s1\">&#39;spam=</span><span class=\"si\">{</span><span class=\"n\">spam</span><span class=\"si\">}</span><span class=\"s1\"> eggs=</span><span class=\"si\">{</span><span class=\"n\">eggs</span><span class=\"si\">}</span><span class=\"s1\">&#39;</span></code>.<sup id=\"cite_ref-pep-0498_99-0\" class=\"reference\"><a href=\"#cite_note-pep-0498-99\"><span class=\"cite-bracket\">&#91;</span>96<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Strings in Python can be <a href=\"/wiki/Concatenated\" class=\"mw-redirect\" title=\"Concatenated\">concatenated</a> by \"adding\" them (using the same operator as for adding integers and floats); e.g., <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s2\">&quot;spam&quot;</span> <span class=\"o\">+</span> <span class=\"s2\">&quot;eggs&quot;</span></code> returns <code>\"spameggs\"</code>. If strings contain numbers, they are concatenated as strings rather than as integers, e.g. <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s2\">&quot;2&quot;</span> <span class=\"o\">+</span> <span class=\"s2\">&quot;2&quot;</span></code> returns <code>\"22\"</code>.</li>\n<li>Python supports <a href=\"/wiki/String_literal\" title=\"String literal\">string literals</a> in several ways:\n<ul><li>Delimited by single or double quotation marks; single and double quotation marks have equivalent functionality (unlike in <a href=\"/wiki/Unix_shell\" title=\"Unix shell\">Unix shells</a>, <a href=\"/wiki/Perl\" title=\"Perl\">Perl</a>, and Perl-influenced languages). Both marks use the backslash (<code>\\</code>) as an <a href=\"/wiki/Escape_character\" title=\"Escape character\">escape character</a>. <a href=\"/wiki/String_interpolation\" title=\"String interpolation\">String interpolation</a> became available in Python&#160;3.6 as \"formatted string literals\".<sup id=\"cite_ref-pep-0498_99-1\" class=\"reference\"><a href=\"#cite_note-pep-0498-99\"><span class=\"cite-bracket\">&#91;</span>96<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Triple-quoted, i.e., starting and ending with three single or double quotation marks; this may span multiple lines and function like <a href=\"/wiki/Here_document\" title=\"Here document\">here documents</a> in shells, Perl, and <a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a>.</li>\n<li><a href=\"/wiki/Raw_string\" class=\"mw-redirect\" title=\"Raw string\">Raw string</a> varieties, denoted by prefixing the string literal with <code>r</code>. Escape sequences are not interpreted; hence raw strings are useful where literal backslashes are common, such as in <a href=\"/wiki/Regular_expression\" title=\"Regular expression\">regular expressions</a> and <a href=\"/wiki/Windows\" class=\"mw-redirect\" title=\"Windows\">Windows</a>-style paths. (Compare \"<code>@</code>-quoting\" in <a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">C#</a>.)</li></ul></li>\n<li>Python has <a href=\"/wiki/Array_index\" class=\"mw-redirect\" title=\"Array index\">array index</a> and <a href=\"/wiki/Array_slicing\" title=\"Array slicing\">array slicing</a> expressions in lists, which are written as <code>a[key]</code>, <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">a</span><span class=\"p\">[</span><span class=\"n\">start</span><span class=\"p\">:</span><span class=\"n\">stop</span><span class=\"p\">]</span></code> or <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">a</span><span class=\"p\">[</span><span class=\"n\">start</span><span class=\"p\">:</span><span class=\"n\">stop</span><span class=\"p\">:</span><span class=\"n\">step</span><span class=\"p\">]</span></code>. Indexes are <a href=\"/wiki/Zero-based_numbering\" title=\"Zero-based numbering\">zero-based</a>, and negative indexes are relative to the end. Slices take elements from the <i>start</i> index up to, but not including, the <i>stop</i> index. The (optional) third slice <a href=\"/wiki/Parameter_(computer_programming)\" title=\"Parameter (computer programming)\">parameter</a>, called <i>step</i> or <i>stride</i>, allows elements to be skipped or reversed. Slice indexes may be omitted—for example, <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">a</span><span class=\"p\">[:]</span></code> returns a copy of the entire list. Each element of a slice is a <a href=\"/wiki/Shallow_copy\" class=\"mw-redirect\" title=\"Shallow copy\">shallow copy</a>.</li></ul>\n<p>In Python, a distinction between expressions and statements is rigidly enforced, in contrast to languages such as <a href=\"/wiki/Common_Lisp\" title=\"Common Lisp\">Common Lisp</a>, <a href=\"/wiki/Scheme_(programming_language)\" title=\"Scheme (programming language)\">Scheme</a>, or <a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a>. This distinction leads to duplicating some functionality, for example:\n</p>\n<ul><li><a href=\"/wiki/List_comprehensions\" class=\"mw-redirect\" title=\"List comprehensions\">List comprehensions</a> vs. <code>for</code>-loops</li>\n<li><a href=\"/wiki/Conditional_(programming)\" class=\"mw-redirect\" title=\"Conditional (programming)\">Conditional</a> expressions vs. <code>if</code> blocks</li>\n<li>The <code>eval()</code> vs. <code>exec()</code> built-in functions (in Python&#160;2, <code>exec</code> is a statement); the former function is for expressions, while the latter is for statements</li></ul>\n<p>A statement cannot be part of an expression; because of this restriction, expressions such as list and <code>dict</code> comprehensions (and lambda expressions) cannot contain statements. As a particular case, an assignment statement such as <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">a</span> <span class=\"o\">=</span> <span class=\"mi\">1</span></code> cannot be part of the conditional expression of a conditional statement.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Methods\">Methods</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=7\" title=\"Edit section: Methods\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Method_(computer_programming)\" title=\"Method (computer programming)\">Methods</a> of objects are functions attached to the object's class; the syntax for normal methods and functions, <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">instance</span><span class=\"o\">.</span><span class=\"n\">method</span><span class=\"p\">(</span><span class=\"n\">argument</span><span class=\"p\">)</span></code>, is <a href=\"/wiki/Syntactic_sugar\" title=\"Syntactic sugar\">syntactic sugar</a> for <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">Class</span><span class=\"o\">.</span><span class=\"n\">method</span><span class=\"p\">(</span><span class=\"n\">instance</span><span class=\"p\">,</span> <span class=\"n\">argument</span><span class=\"p\">)</span></code>. Python methods have an explicit <code><a href=\"/wiki/This_(computer_programming)\" title=\"This (computer programming)\">self</a></code> parameter to access <a href=\"/wiki/Instance_data\" class=\"mw-redirect\" title=\"Instance data\">instance data</a>, in contrast to the implicit self (or <code>this</code>) parameter in some object-oriented programming languages (e.g., <a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a>, <a href=\"/wiki/Java_(programming_language)\" title=\"Java (programming language)\">Java</a>, <a href=\"/wiki/Objective-C\" title=\"Objective-C\">Objective-C</a>, <a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a>).<sup id=\"cite_ref-AutoNT-61_100-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-61-100\"><span class=\"cite-bracket\">&#91;</span>97<span class=\"cite-bracket\">&#93;</span></a></sup> Python also provides methods, often called <i>dunder methods</i> (because their names begin and end with double underscores); these methods allow user-defined classes to modify how they are handled by native operations including length, comparison, <a href=\"/wiki/Arithmetic_operations\" class=\"mw-redirect\" title=\"Arithmetic operations\">arithmetic</a>, and type conversion.<sup id=\"cite_ref-101\" class=\"reference\"><a href=\"#cite_note-101\"><span class=\"cite-bracket\">&#91;</span>98<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Typing\">Typing</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=8\" title=\"Edit section: Typing\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Python_3.13_Standrd_Type_Hierarchy-en.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Python_3.13_Standrd_Type_Hierarchy-en.svg/250px-Python_3.13_Standrd_Type_Hierarchy-en.svg.png\" decoding=\"async\" width=\"250\" height=\"250\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Python_3.13_Standrd_Type_Hierarchy-en.svg/500px-Python_3.13_Standrd_Type_Hierarchy-en.svg.png 1.5x\" data-file-width=\"566\" data-file-height=\"566\" /></a><figcaption>The standard type hierarchy in Python&#160;3</figcaption></figure>\n<p>Python uses <a href=\"/wiki/Duck_typing\" title=\"Duck typing\">duck typing</a>, and it has typed objects but untyped variable names. Type constraints are not checked at definition time; rather, operations on an object may fail at usage time, indicating that the object is not of an appropriate type. Despite being <a href=\"/wiki/Dynamically_typed\" class=\"mw-redirect\" title=\"Dynamically typed\">dynamically typed</a>, Python is <a href=\"/wiki/Strongly_typed\" class=\"mw-redirect\" title=\"Strongly typed\">strongly typed</a>, forbidding operations that are poorly defined (e.g., adding a number and a string) rather than quietly attempting to interpret them.\n</p><p>Python allows programmers to define their own types using <a href=\"/wiki/Class_(computer_science)\" class=\"mw-redirect\" title=\"Class (computer science)\">classes</a>, most often for <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented programming</a>. New <a href=\"/wiki/Object_(computer_science)\" title=\"Object (computer science)\">instances</a> of classes are constructed by calling the class, for example, <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">SpamClass</span><span class=\"p\">()</span></code> or <code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">EggsClass</span><span class=\"p\">()</span></code>); the classes are instances of the <a href=\"/wiki/Metaclass\" title=\"Metaclass\">metaclass</a> <code>type</code> (which is an instance of itself), thereby allowing metaprogramming and <a href=\"/wiki/Reflective_programming\" title=\"Reflective programming\">reflection</a>.\n</p><p>Before version 3.0, Python had two kinds of classes, both using the same syntax: <i>old-style</i> and <i>new-style</i>.<sup id=\"cite_ref-classy_102-0\" class=\"reference\"><a href=\"#cite_note-classy-102\"><span class=\"cite-bracket\">&#91;</span>99<span class=\"cite-bracket\">&#93;</span></a></sup> Current Python versions support the semantics of only the new style.\n</p><p>Python supports <a href=\"/wiki/Optional_typing\" class=\"mw-redirect\" title=\"Optional typing\">optional type annotations</a>.<sup id=\"cite_ref-type_hint-PEP_5-1\" class=\"reference\"><a href=\"#cite_note-type_hint-PEP-5\"><span class=\"cite-bracket\">&#91;</span>5<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-103\" class=\"reference\"><a href=\"#cite_note-103\"><span class=\"cite-bracket\">&#91;</span>100<span class=\"cite-bracket\">&#93;</span></a></sup> These annotations are not enforced by the language, but may be used by external tools such as <b>mypy</b> to catch errors. Python includes a module <code>typing</code> including several type names for type annotations.<sup id=\"cite_ref-104\" class=\"reference\"><a href=\"#cite_note-104\"><span class=\"cite-bracket\">&#91;</span>101<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-105\" class=\"reference\"><a href=\"#cite_note-105\"><span class=\"cite-bracket\">&#91;</span>102<span class=\"cite-bracket\">&#93;</span></a></sup> Mypy also supports a Python compiler called mypyc, which leverages type annotations for optimization.<sup id=\"cite_ref-106\" class=\"reference\"><a href=\"#cite_note-106\"><span class=\"cite-bracket\">&#91;</span>103<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<table class=\"wikitable\">\n<caption>Summary of Python 3's built-in types\n</caption>\n<tbody><tr>\n<th>Type\n</th>\n<th><a href=\"/wiki/Immutable_object\" title=\"Immutable object\">Mutability</a>\n</th>\n<th>Description\n</th>\n<th>Syntax examples\n</th></tr>\n<tr>\n<td><code>bool</code>\n</td>\n<td>immutable\n</td>\n<td><a href=\"/wiki/Boolean_value\" class=\"mw-redirect\" title=\"Boolean value\">Boolean value</a>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"kc\">True</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"kc\">False</span></code>\n</td></tr>\n<tr>\n<td><code>bytearray</code>\n</td>\n<td>mutable\n</td>\n<td>Sequence of <a href=\"/wiki/Byte\" title=\"Byte\">bytes</a>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">bytearray</span><span class=\"p\">(</span><span class=\"sa\">b</span><span class=\"s1\">&#39;Some ASCII&#39;</span><span class=\"p\">)</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">bytearray</span><span class=\"p\">(</span><span class=\"sa\">b</span><span class=\"s2\">&quot;Some ASCII&quot;</span><span class=\"p\">)</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">bytearray</span><span class=\"p\">([</span><span class=\"mi\">119</span><span class=\"p\">,</span> <span class=\"mi\">105</span><span class=\"p\">,</span> <span class=\"mi\">107</span><span class=\"p\">,</span> <span class=\"mi\">105</span><span class=\"p\">])</span></code>\n</td></tr>\n<tr>\n<td><code>bytes</code>\n</td>\n<td>immutable\n</td>\n<td>Sequence of bytes\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"sa\">b</span><span class=\"s1\">&#39;Some ASCII&#39;</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"sa\">b</span><span class=\"s2\">&quot;Some ASCII&quot;</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">bytes</span><span class=\"p\">([</span><span class=\"mi\">119</span><span class=\"p\">,</span> <span class=\"mi\">105</span><span class=\"p\">,</span> <span class=\"mi\">107</span><span class=\"p\">,</span> <span class=\"mi\">105</span><span class=\"p\">])</span></code>\n</td></tr>\n<tr>\n<td><code>complex</code>\n</td>\n<td>immutable\n</td>\n<td><a href=\"/wiki/Complex_number\" title=\"Complex number\">Complex number</a> with real and imaginary parts\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"mi\">3</span><span class=\"o\">+</span><span class=\"mf\">2.7</span><span class=\"n\">j</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"mi\">3</span> <span class=\"o\">+</span> <span class=\"mf\">2.7</span><span class=\"n\">j</span></code>\n</td></tr>\n<tr>\n<td><code>dict</code>\n</td>\n<td>mutable\n</td>\n<td><a href=\"/wiki/Associative_array\" title=\"Associative array\">Associative array</a> (or dictionary) of key and value pairs; can contain mixed types (keys and values); keys must be a hashable type\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">{</span><span class=\"s1\">&#39;key1&#39;</span><span class=\"p\">:</span> <span class=\"mf\">1.0</span><span class=\"p\">,</span> <span class=\"mi\">3</span><span class=\"p\">:</span> <span class=\"kc\">False</span><span class=\"p\">}</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">{}</span></code>\n</td></tr>\n<tr>\n<td><code>types.EllipsisType</code>\n</td>\n<td>immutable\n</td>\n<td>An <a href=\"/wiki/Ellipsis_(programming_operator)\" class=\"mw-redirect\" title=\"Ellipsis (programming operator)\">ellipsis</a> placeholder to be used as an index in <a href=\"/wiki/NumPy\" title=\"NumPy\">NumPy</a> arrays\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"o\">...</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"bp\">Ellipsis</span></code>\n</td></tr>\n<tr>\n<td><code>float</code>\n</td>\n<td>immutable\n</td>\n<td><a href=\"/wiki/Double-precision\" class=\"mw-redirect\" title=\"Double-precision\">Double-precision</a> <a href=\"/wiki/Floating-point_number\" class=\"mw-redirect\" title=\"Floating-point number\">floating-point number</a>. The precision is machine-dependent, but in practice it is generally implemented as a 64-bit <a href=\"/wiki/IEEE_754\" title=\"IEEE 754\">IEEE&#160;754</a> number with 53&#160;bits of precision.<sup id=\"cite_ref-107\" class=\"reference\"><a href=\"#cite_note-107\"><span class=\"cite-bracket\">&#91;</span>104<span class=\"cite-bracket\">&#93;</span></a></sup>\n</td>\n<td>\n<p><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"mf\">1.33333</span></code>\n</p>\n</td></tr>\n<tr>\n<td><code>frozenset</code>\n</td>\n<td>immutable\n</td>\n<td>Unordered <a href=\"/wiki/Set_(computer_science)\" class=\"mw-redirect\" title=\"Set (computer science)\">set</a>, contains no duplicates; can contain mixed types, if hashable\n</td>\n<td><span class=\"nowrap\"><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">frozenset</span><span class=\"p\">([</span><span class=\"mf\">4.0</span><span class=\"p\">,</span> <span class=\"s1\">&#39;string&#39;</span><span class=\"p\">,</span> <span class=\"kc\">True</span><span class=\"p\">])</span></code></span>\n</td></tr>\n<tr>\n<td><code>int</code>\n</td>\n<td>immutable\n</td>\n<td><a href=\"/wiki/Integer_(computer_science)\" title=\"Integer (computer science)\">Integer</a> of unlimited magnitude<sup id=\"cite_ref-pep0237_108-0\" class=\"reference\"><a href=\"#cite_note-pep0237-108\"><span class=\"cite-bracket\">&#91;</span>105<span class=\"cite-bracket\">&#93;</span></a></sup>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"mi\">42</span></code>\n</td></tr>\n<tr>\n<td><code>list</code>\n</td>\n<td>mutable\n</td>\n<td><a href=\"/wiki/List_(computer_science)\" class=\"mw-redirect\" title=\"List (computer science)\">List</a>, can contain mixed types\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">[</span><span class=\"mf\">4.0</span><span class=\"p\">,</span> <span class=\"s1\">&#39;string&#39;</span><span class=\"p\">,</span> <span class=\"kc\">True</span><span class=\"p\">]</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">[]</span></code>\n</td></tr>\n<tr>\n<td><code>types.NoneType</code>\n</td>\n<td>immutable\n</td>\n<td>An object representing the absence of a value, often called <a href=\"/wiki/Null_pointer\" title=\"Null pointer\">null</a> in other languages\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"kc\">None</span></code>\n</td></tr>\n<tr>\n<td><code>types.NotImplementedType</code>\n</td>\n<td>immutable\n</td>\n<td>A placeholder that can be returned from <a href=\"/wiki/Operator_overloading\" title=\"Operator overloading\">overloaded operators</a> to indicate unsupported operand types.\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"bp\">NotImplemented</span></code>\n</td></tr>\n<tr>\n<td><code>range</code>\n</td>\n<td>immutable\n</td>\n<td>An <i>immutable sequence</i> of numbers, commonly used for iterating a specific number of times in <code>for</code> loops<sup id=\"cite_ref-109\" class=\"reference\"><a href=\"#cite_note-109\"><span class=\"cite-bracket\">&#91;</span>106<span class=\"cite-bracket\">&#93;</span></a></sup>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"err\">−</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">10</span><span class=\"p\">)</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"mi\">10</span><span class=\"p\">,</span> <span class=\"err\">−</span><span class=\"mi\">5</span><span class=\"p\">,</span> <span class=\"err\">−</span><span class=\"mi\">2</span><span class=\"p\">)</span></code>\n</td></tr>\n<tr>\n<td><code>set</code>\n</td>\n<td>mutable\n</td>\n<td>Unordered <a href=\"/wiki/Set_(computer_science)\" class=\"mw-redirect\" title=\"Set (computer science)\">set</a>, contains no duplicates; can contain mixed types, if hashable\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">{</span><span class=\"mf\">4.0</span><span class=\"p\">,</span> <span class=\"s1\">&#39;string&#39;</span><span class=\"p\">,</span> <span class=\"kc\">True</span><span class=\"p\">}</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"nb\">set</span><span class=\"p\">()</span></code>\n</td></tr>\n<tr>\n<td><code>str</code>\n</td>\n<td>immutable\n</td>\n<td>A <a href=\"/wiki/Character_string\" class=\"mw-redirect\" title=\"Character string\">character string</a>: sequence of Unicode codepoints\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s1\">&#39;Wikipedia&#39;</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"s2\">&quot;Wikipedia&quot;</span></code><div class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"sd\">&quot;&quot;&quot;Spanning</span>\n<span class=\"sd\">multiple</span>\n<span class=\"sd\">lines&quot;&quot;&quot;</span>\n</pre></div>\n</td></tr>\n<tr>\n<td><code>tuple</code>\n</td>\n<td>immutable\n</td>\n<td><a href=\"/wiki/Tuple\" title=\"Tuple\">Tuple</a>, can contain mixed types\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"mf\">4.0</span><span class=\"p\">,</span> <span class=\"s1\">&#39;string&#39;</span><span class=\"p\">,</span> <span class=\"kc\">True</span><span class=\"p\">)</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"s1\">&#39;single element&#39;</span><span class=\"p\">,)</span></code><br /><code class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">()</span></code>\n</td></tr></tbody></table>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Arithmetic_operations\">Arithmetic operations</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=9\" title=\"Edit section: Arithmetic operations\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python includes conventional symbols for arithmetic operators (<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>), the floor-division operator <code>//</code>, and the <a href=\"/wiki/Modulo_operation\" class=\"mw-redirect\" title=\"Modulo operation\">modulo operator</a> <code>%</code>. (With the modulo operator, a remainder can be negative, e.g., <code>4&#160;% -3 == -2</code>.) Python also offers the <code>**</code> symbol for <a href=\"/wiki/Exponentiation\" title=\"Exponentiation\">exponentiation</a>, e.g. <code>5**3 == 125</code> and <code>9**0.5 == 3.0</code>; it also offers the matrix‑multiplication operator <code>@</code> .<sup id=\"cite_ref-110\" class=\"reference\"><a href=\"#cite_note-110\"><span class=\"cite-bracket\">&#91;</span>107<span class=\"cite-bracket\">&#93;</span></a></sup> These operators work as in traditional mathematics; with the same <a href=\"/wiki/Order_of_operations\" title=\"Order of operations\">precedence rules</a>, the <a href=\"/wiki/Infix_notation\" title=\"Infix notation\">infix</a> operators <code>+</code> and <code>-</code> can also be <a href=\"/wiki/Unary_operation\" title=\"Unary operation\">unary</a>, to represent positive and negative numbers respectively.\n</p><p>Division between integers produces floating-point results. The behavior of division has changed significantly over time:<sup id=\"cite_ref-pep0238_111-0\" class=\"reference\"><a href=\"#cite_note-pep0238-111\"><span class=\"cite-bracket\">&#91;</span>108<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<ul><li>The current version of Python (i.e., since 3.0) changed the <code>/</code> operator to always represent floating-point division, e.g., <code class=\"nowrap mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"mi\">5</span><span class=\"o\">/</span><span class=\"mi\">2</span> <span class=\"o\">==</span> <span class=\"mf\">2.5</span></code>.</li>\n<li>The floor division <code>//</code> operator was introduced, meaning that <code>7//3 == 2</code>, <code>-7//3 == -3</code>, <code>7.5//3 == 2.0</code>, and <code>-7.5//3 == -3.0</code>. For Python 2.7, adding the <code class=\"nowrap mw-highlight mw-highlight-lang-python2 mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"kn\">from</span><span class=\"w\"> </span><span class=\"nn\">__future__</span> <span class=\"kn\">import</span> <span class=\"n\">division</span></code> statement allows a module in Python 2.7 to use Python 3.x rules for division (see above).</li></ul>\n<p>In Python terms, the <code>/</code> operator represents <i>true division</i> (or simply <i>division</i>), while the <code>//</code> operator represents <i>floor division.</i> Before version 3.0, the <code>/</code> operator represents <i>classic division</i>.<sup id=\"cite_ref-pep0238_111-1\" class=\"reference\"><a href=\"#cite_note-pep0238-111\"><span class=\"cite-bracket\">&#91;</span>108<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p><a href=\"/wiki/Rounding\" title=\"Rounding\">Rounding</a> towards negative infinity, though a different method than in most languages, adds consistency to Python. For instance, this rounding implies that the equation <code class=\"nowrap mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">(</span><span class=\"n\">a</span> <span class=\"o\">+</span> <span class=\"n\">b</span><span class=\"p\">)</span><span class=\"o\">//</span><span class=\"n\">b</span> <span class=\"o\">==</span> <span class=\"n\">a</span><span class=\"o\">//</span><span class=\"n\">b</span> <span class=\"o\">+</span> <span class=\"mi\">1</span></code> is always true. The rounding also implies that the equation <code class=\"nowrap mw-highlight mw-highlight-lang-python mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"n\">b</span><span class=\"o\">*</span><span class=\"p\">(</span><span class=\"n\">a</span><span class=\"o\">//</span><span class=\"n\">b</span><span class=\"p\">)</span> <span class=\"o\">+</span> <span class=\"n\">a</span><span class=\"o\">%</span><span class=\"n\">b</span> <span class=\"o\">==</span> <span class=\"n\">a</span></code> is valid for both positive and negative values of <code>a</code>. As expected, the result of <code>a%b</code> lies in the <a href=\"/wiki/Half-open_interval\" class=\"mw-redirect\" title=\"Half-open interval\">half-open interval</a> [0, <i>b</i>), where <code>b</code> is a positive integer; however, maintaining the validity of the equation requires that the result must lie in the interval (<i>b</i>, 0] when <code>b</code> is negative.<sup id=\"cite_ref-AutoNT-62_112-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-62-112\"><span class=\"cite-bracket\">&#91;</span>109<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python provides a <code>round</code> function for rounding a float to the nearest integer. For <a href=\"/wiki/Rounding#Tie-breaking\" title=\"Rounding\">tie-breaking</a>, Python&#160;3 uses the <i>round to even</i> method: <code>round(1.5)</code> and <code>round(2.5)</code> both produce <code>2</code>.<sup id=\"cite_ref-AutoNT-64_113-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-64-113\"><span class=\"cite-bracket\">&#91;</span>110<span class=\"cite-bracket\">&#93;</span></a></sup> Python versions before 3 used the <a href=\"/wiki/Rounding#Rounding_away_from_zero\" title=\"Rounding\">round-away-from-zero</a> method: <code>round(0.5)</code> is <code>1.0</code>, and <code>round(-0.5)</code> is <code>−1.0</code>.<sup id=\"cite_ref-AutoNT-63_114-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-63-114\"><span class=\"cite-bracket\">&#91;</span>111<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python allows Boolean expressions that contain multiple equality relations to be consistent with general usage in mathematics. For example, the expression <code>a &lt; b &lt; c</code> tests whether <code>a</code> is less than <code>b</code> and <code>b</code> is less than <code>c</code>.<sup id=\"cite_ref-AutoNT-65_115-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-65-115\"><span class=\"cite-bracket\">&#91;</span>112<span class=\"cite-bracket\">&#93;</span></a></sup> C-derived languages interpret this expression differently: in C, the expression would first evaluate <code>a &lt; b</code>, resulting in 0 or 1, and that result would then be compared with <code>c</code>.<sup id=\"cite_ref-CPL_116-0\" class=\"reference\"><a href=\"#cite_note-CPL-116\"><span class=\"cite-bracket\">&#91;</span>113<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python uses <a href=\"/wiki/Arbitrary-precision_arithmetic\" title=\"Arbitrary-precision arithmetic\">arbitrary-precision arithmetic</a> for all integer operations. The <code>Decimal</code> type/class in the <code>decimal</code> module provides <a href=\"/wiki/Decimal_floating_point\" title=\"Decimal floating point\">decimal floating-point numbers</a> to a pre-defined arbitrary precision with several rounding modes.<sup id=\"cite_ref-AutoNT-88_117-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-88-117\"><span class=\"cite-bracket\">&#91;</span>114<span class=\"cite-bracket\">&#93;</span></a></sup> The <code>Fraction</code> class in the <code>fractions</code> module provides arbitrary precision for <a href=\"/wiki/Rational_number\" title=\"Rational number\">rational numbers</a>.<sup id=\"cite_ref-118\" class=\"reference\"><a href=\"#cite_note-118\"><span class=\"cite-bracket\">&#91;</span>115<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Due to Python's extensive mathematics library and the third-party library <a href=\"/wiki/NumPy\" title=\"NumPy\">NumPy</a>, the language is frequently used for scientific scripting in tasks such as numerical data processing and manipulation.<sup id=\"cite_ref-119\" class=\"reference\"><a href=\"#cite_note-119\"><span class=\"cite-bracket\">&#91;</span>116<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-120\" class=\"reference\"><a href=\"#cite_note-120\"><span class=\"cite-bracket\">&#91;</span>117<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Function_syntax\">Function syntax</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=10\" title=\"Edit section: Function syntax\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Function_(computer_programming)\" title=\"Function (computer programming)\">Functions</a> are created in Python by using the <code>def</code> keyword. A function is defined similarly to how it is called, by first providing the function name and then the required parameters. Here is an example of a function that prints its inputs:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-python3 mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">def</span><span class=\"w\"> </span><span class=\"nf\">printer</span><span class=\"p\">(</span><span class=\"n\">input1</span><span class=\"p\">:</span> <span class=\"nb\">str</span><span class=\"p\">,</span> <span class=\"n\">input2</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">=</span> <span class=\"s2\">&quot;already there&quot;</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"kc\">None</span><span class=\"p\">:</span>\n    <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"n\">input1</span><span class=\"p\">)</span>\n    <span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"n\">input2</span><span class=\"p\">)</span>\n\n<span class=\"n\">printer</span><span class=\"p\">(</span><span class=\"s2\">&quot;hello&quot;</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Example output:</span>\n<span class=\"c1\"># hello</span>\n<span class=\"c1\"># already there</span>\n</pre></div><p>To assign a default value to a function parameter in case no actual value is provided at run time, variable-definition syntax can be used inside the function header.\n</p><div class=\"mw-heading mw-heading2\"><h2 id=\"Code_examples\">Code examples</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=11\" title=\"Edit section: Code examples\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/%22Hello,_World!%22_program\" title=\"&quot;Hello, World!&quot; program\">\"Hello, World!\" program</a>:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-python mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"s1\">&#39;Hello, World!&#39;</span><span class=\"p\">)</span>\n</pre></div>\n<p>Program to calculate the <a href=\"/wiki/Factorial\" title=\"Factorial\">factorial</a> of a positive integer:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-python mw-content-ltr mw-highlight-lines\" dir=\"ltr\"><pre><span></span><span class=\"linenos\" data-line=\"1\"></span><span class=\"n\">text</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">=</span> <span class=\"nb\">input</span><span class=\"p\">(</span><span class=\"s1\">&#39;Type a number, and its factorial will be printed: &#39;</span><span class=\"p\">)</span>\n<span class=\"linenos\" data-line=\"2\"></span><span class=\"n\">n</span><span class=\"p\">:</span> <span class=\"nb\">int</span> <span class=\"o\">=</span> <span class=\"nb\">int</span><span class=\"p\">(</span><span class=\"n\">text</span><span class=\"p\">)</span>\n<span class=\"linenos\" data-line=\"3\"></span>\n<span class=\"linenos\" data-line=\"4\"></span><span class=\"k\">if</span> <span class=\"n\">n</span> <span class=\"o\">&lt;</span> <span class=\"mi\">0</span><span class=\"p\">:</span>\n<span class=\"linenos\" data-line=\"5\"></span>    <span class=\"k\">raise</span> <span class=\"ne\">ValueError</span><span class=\"p\">(</span><span class=\"s1\">&#39;You must enter a non-negative integer&#39;</span><span class=\"p\">)</span>\n<span class=\"linenos\" data-line=\"6\"></span>\n<span class=\"linenos\" data-line=\"7\"></span><span class=\"n\">factorial</span><span class=\"p\">:</span> <span class=\"nb\">int</span> <span class=\"o\">=</span> <span class=\"mi\">1</span>\n<span class=\"linenos\" data-line=\"8\"></span><span class=\"k\">for</span> <span class=\"n\">i</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"mi\">2</span><span class=\"p\">,</span> <span class=\"n\">n</span> <span class=\"o\">+</span> <span class=\"mi\">1</span><span class=\"p\">):</span>\n<span class=\"linenos\" data-line=\"9\"></span>    <span class=\"n\">factorial</span> <span class=\"o\">*=</span> <span class=\"n\">i</span>\n<span class=\"linenos\" data-line=\"10\"></span>\n<span class=\"linenos\" data-line=\"11\"></span><span class=\"nb\">print</span><span class=\"p\">(</span><span class=\"n\">factorial</span><span class=\"p\">)</span>\n</pre></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Libraries\">Libraries</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=12\" title=\"Edit section: Libraries\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's large standard library<sup id=\"cite_ref-AutoNT-86_121-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-86-121\"><span class=\"cite-bracket\">&#91;</span>118<span class=\"cite-bracket\">&#93;</span></a></sup> is commonly cited as one of its greatest strengths. For Internet-facing applications, many standard formats and protocols such as <a href=\"/wiki/MIME\" title=\"MIME\">MIME</a> and <a href=\"/wiki/HTTP\" title=\"HTTP\">HTTP</a> are supported. The language includes modules for creating <a href=\"/wiki/Graphical_user_interface\" title=\"Graphical user interface\">graphical user interfaces</a>, connecting to <a href=\"/wiki/Relational_database\" title=\"Relational database\">relational databases</a>, <a href=\"/wiki/Pseudorandom_number_generator\" title=\"Pseudorandom number generator\">generating pseudorandom numbers</a>, arithmetic with arbitrary-precision decimals,<sup id=\"cite_ref-AutoNT-88_117-1\" class=\"reference\"><a href=\"#cite_note-AutoNT-88-117\"><span class=\"cite-bracket\">&#91;</span>114<span class=\"cite-bracket\">&#93;</span></a></sup> manipulating <a href=\"/wiki/Regular_expression\" title=\"Regular expression\">regular expressions</a>, and <a href=\"/wiki/Unit_testing\" title=\"Unit testing\">unit testing</a>.\n</p><p>Some parts of the standard library are covered by specifications—for example, the <a href=\"/wiki/Web_Server_Gateway_Interface\" title=\"Web Server Gateway Interface\">Web Server Gateway Interface</a> (WSGI) implementation <code>wsgiref</code> follows PEP 333<sup id=\"cite_ref-AutoNT-89_122-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-89-122\"><span class=\"cite-bracket\">&#91;</span>119<span class=\"cite-bracket\">&#93;</span></a></sup>—but most parts are specified by their code, internal documentation, and <a href=\"/wiki/Test_suite\" title=\"Test suite\">test suites</a>. However, because most of the standard library is cross-platform Python code, only a few modules must be altered or rewritten for variant implementations.\n</p><p>As of 13&#160;March&#160;2025,<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup> the <a href=\"/wiki/Python_Package_Index\" title=\"Python Package Index\">Python Package Index</a> (PyPI), the official repository for third-party Python software, contains over 614,339<sup id=\"cite_ref-PyPI_123-0\" class=\"reference\"><a href=\"#cite_note-PyPI-123\"><span class=\"cite-bracket\">&#91;</span>120<span class=\"cite-bracket\">&#93;</span></a></sup> packages. These have a wide range of functionality, including the following:\n</p>\n<style data-mw-deduplicate=\"TemplateStyles:r1184024115\">.mw-parser-output .div-col{margin-top:0.3em;column-width:30em}.mw-parser-output .div-col-small{font-size:90%}.mw-parser-output .div-col-rules{column-rule:1px solid #aaa}.mw-parser-output .div-col dl,.mw-parser-output .div-col ol,.mw-parser-output .div-col ul{margin-top:0}.mw-parser-output .div-col li,.mw-parser-output .div-col dd{page-break-inside:avoid;break-inside:avoid-column}</style><div class=\"div-col\" style=\"column-width: 30em;\">\n<ul><li><a href=\"/wiki/Automation\" title=\"Automation\">Automation</a></li>\n<li><a href=\"/wiki/Data_analytics\" class=\"mw-redirect\" title=\"Data analytics\">Data analytics</a></li>\n<li><a href=\"/wiki/Database\" title=\"Database\">Databases</a></li>\n<li><a href=\"/wiki/Documentation\" title=\"Documentation\">Documentation</a></li>\n<li><a href=\"/wiki/Graphical_user_interface\" title=\"Graphical user interface\">Graphical user interfaces</a></li>\n<li><a href=\"/wiki/Image_processing\" class=\"mw-redirect\" title=\"Image processing\">Image processing</a></li>\n<li><a href=\"/wiki/Machine_learning\" title=\"Machine learning\">Machine learning</a></li>\n<li><a href=\"/wiki/Mobile_app\" title=\"Mobile app\">Mobile apps</a></li>\n<li><a href=\"/wiki/Multimedia\" title=\"Multimedia\">Multimedia</a></li>\n<li><a href=\"/wiki/Computer_networking\" class=\"mw-redirect\" title=\"Computer networking\">Computer networking</a></li>\n<li><a href=\"/wiki/Scientific_computing\" class=\"mw-redirect\" title=\"Scientific computing\">Scientific computing</a></li>\n<li><a href=\"/wiki/System_administration\" class=\"mw-redirect\" title=\"System administration\">System administration</a></li>\n<li><a href=\"/wiki/Test_framework\" class=\"mw-redirect\" title=\"Test framework\">Test frameworks</a></li>\n<li><a href=\"/wiki/Text_processing\" title=\"Text processing\">Text processing</a></li>\n<li><a href=\"/wiki/Web_framework\" title=\"Web framework\">Web frameworks</a></li>\n<li><a href=\"/wiki/Web_scraping\" title=\"Web scraping\">Web scraping</a></li></ul></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Development_environments\">Development environments</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=13\" title=\"Edit section: Development environments\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/Comparison_of_integrated_development_environments#Python\" title=\"Comparison of integrated development environments\">Comparison of integrated development environments §&#160;Python</a></div>\n<p>Most<sup class=\"noprint Inline-Template\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Avoid_weasel_words\" class=\"mw-redirect\" title=\"Wikipedia:Avoid weasel words\"><span title=\"The material near this tag possibly uses too vague attribution or weasel words. (August 2025)\">which?</span></a></i>&#93;</sup> Python implementations (including CPython) include a <a href=\"/wiki/Read%E2%80%93eval%E2%80%93print_loop\" title=\"Read–eval–print loop\">read–eval–print loop</a> (REPL); this permits the environment to function as a <a href=\"/wiki/Command_line_interpreter\" class=\"mw-redirect\" title=\"Command line interpreter\">command line interpreter</a>, with which users enter statements sequentially and receive results immediately.<sup id=\"cite_ref-124\" class=\"reference\"><a href=\"#cite_note-124\"><span class=\"cite-bracket\">&#91;</span>121<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python is also bundled with an <a href=\"/wiki/Integrated_development_environment\" title=\"Integrated development environment\">integrated development environment (IDE)</a> called <a href=\"/wiki/IDLE\" title=\"IDLE\">IDLE</a>,<sup id=\"cite_ref-idle_125-0\" class=\"reference\"><a href=\"#cite_note-idle-125\"><span class=\"cite-bracket\">&#91;</span>122<span class=\"cite-bracket\">&#93;</span></a></sup> which is oriented toward beginners.<sup class=\"noprint Inline-Template Template-Fact\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Citation_needed\" title=\"Wikipedia:Citation needed\"><span title=\"This claim needs references to reliable sources. (August 2025)\">citation needed</span></a></i>&#93;</sup>\n</p><p>Other shells, including <a href=\"/wiki/IDLE\" title=\"IDLE\">IDLE</a> and <a href=\"/wiki/IPython\" title=\"IPython\">IPython</a>, add additional capabilities such as improved auto-completion, session-state retention, and <a href=\"/wiki/Syntax_highlighting\" title=\"Syntax highlighting\">syntax highlighting</a>.<sup id=\"cite_ref-idle_125-1\" class=\"reference\"><a href=\"#cite_note-idle-125\"><span class=\"cite-bracket\">&#91;</span>122<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-126\" class=\"reference\"><a href=\"#cite_note-126\"><span class=\"cite-bracket\">&#91;</span>123<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Standard desktop IDEs include <a href=\"/wiki/PyCharm\" title=\"PyCharm\">PyCharm</a>, <a href=\"/wiki/Spyder_(software)\" title=\"Spyder (software)\">Spyder</a>, and <a href=\"/wiki/Visual_Studio_Code\" title=\"Visual Studio Code\">Visual Studio Code</a>;<sup class=\"noprint Inline-Template Template-Fact\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Citation_needed\" title=\"Wikipedia:Citation needed\"><span title=\"This claim needs references to reliable sources. (August 2025)\">citation needed</span></a></i>&#93;</sup> there are also <a href=\"/wiki/Web_browser\" title=\"Web browser\">web browser</a>-based IDEs, such as the following environments:\n</p>\n<ul><li><a href=\"/wiki/Project_Jupyter\" title=\"Project Jupyter\">Jupyter Notebooks</a>, an open-source interactive computing platform;<sup id=\"cite_ref-127\" class=\"reference\"><a href=\"#cite_note-127\"><span class=\"cite-bracket\">&#91;</span>124<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/PythonAnywhere\" title=\"PythonAnywhere\">PythonAnywhere</a>, a browser-based IDE and hosting environment; and</li>\n<li>Canopy, a commercial IDE from <a href=\"/wiki/Enthought\" title=\"Enthought\">Enthought</a> that emphasizes <a href=\"/wiki/Scientific_computing\" class=\"mw-redirect\" title=\"Scientific computing\">scientific computing</a>.<sup id=\"cite_ref-128\" class=\"reference\"><a href=\"#cite_note-128\"><span class=\"cite-bracket\">&#91;</span>125<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-129\" class=\"reference\"><a href=\"#cite_note-129\"><span class=\"cite-bracket\">&#91;</span>126<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Implementations\">Implementations</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=14\" title=\"Edit section: Implementations\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/List_of_Python_software#Python_implementations\" title=\"List of Python software\">List of Python software §&#160;Python implementations</a></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Reference_implementation\">Reference implementation</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=15\" title=\"Edit section: Reference implementation\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/CPython\" title=\"CPython\">CPython</a> is the <a href=\"/wiki/Reference_implementation\" title=\"Reference implementation\">reference implementation</a> of Python. This implementation is written in C, meeting the <a href=\"/wiki/C11_(C_standard_revision)\" title=\"C11 (C standard revision)\">C11</a> standard<sup id=\"cite_ref-130\" class=\"reference\"><a href=\"#cite_note-130\"><span class=\"cite-bracket\">&#91;</span>127<span class=\"cite-bracket\">&#93;</span></a></sup> since version 3.11. Older versions use the <a href=\"/wiki/C89_(C_version)\" class=\"mw-redirect\" title=\"C89 (C version)\">C89</a> standard with several select <a href=\"/wiki/C99\" title=\"C99\">C99</a> features, but third-party extensions are not limited to older C versions—e.g., they can be implemented using C11 or C++.<sup id=\"cite_ref-131\" class=\"reference\"><a href=\"#cite_note-131\"><span class=\"cite-bracket\">&#91;</span>128<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-66_132-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-66-132\"><span class=\"cite-bracket\">&#91;</span>129<span class=\"cite-bracket\">&#93;</span></a></sup> CPython <a href=\"/wiki/Compiler\" title=\"Compiler\">compiles</a> Python programs into an intermediate <a href=\"/wiki/Bytecode\" title=\"Bytecode\">bytecode</a>,<sup id=\"cite_ref-AutoNT-67_133-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-67-133\"><span class=\"cite-bracket\">&#91;</span>130<span class=\"cite-bracket\">&#93;</span></a></sup> which is then executed by a <a href=\"/wiki/Virtual_machine\" title=\"Virtual machine\">virtual machine</a>.<sup id=\"cite_ref-AutoNT-68_134-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-68-134\"><span class=\"cite-bracket\">&#91;</span>131<span class=\"cite-bracket\">&#93;</span></a></sup> CPython is distributed with a large standard library written in a combination of C and native Python.\n</p><p>CPython is available for many platforms, including Windows and most modern <a href=\"/wiki/Unix-like\" title=\"Unix-like\">Unix-like</a> systems, including macOS (and <a href=\"/wiki/Apple_M1\" title=\"Apple M1\">Apple M1</a> Macs, since Python&#160;3.9.1, using an experimental installer). Starting with Python&#160;3.9, the Python installer intentionally fails to install on <a href=\"/wiki/Windows_7\" title=\"Windows 7\">Windows 7</a> and 8;<sup id=\"cite_ref-135\" class=\"reference\"><a href=\"#cite_note-135\"><span class=\"cite-bracket\">&#91;</span>132<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-136\" class=\"reference\"><a href=\"#cite_note-136\"><span class=\"cite-bracket\">&#91;</span>133<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Windows_XP\" title=\"Windows XP\">Windows XP</a> was supported until Python&#160;3.5, with unofficial support for <a href=\"/wiki/OpenVMS\" title=\"OpenVMS\">VMS</a>.<sup id=\"cite_ref-137\" class=\"reference\"><a href=\"#cite_note-137\"><span class=\"cite-bracket\">&#91;</span>134<span class=\"cite-bracket\">&#93;</span></a></sup> Platform portability was one of Python's earliest priorities.<sup id=\"cite_ref-AutoNT-69_138-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-69-138\"><span class=\"cite-bracket\">&#91;</span>135<span class=\"cite-bracket\">&#93;</span></a></sup> During development of Python&#160;1 and 2, even <a href=\"/wiki/OS/2\" title=\"OS/2\">OS/2</a> and <a href=\"/wiki/Solaris_(operating_system)\" class=\"mw-redirect\" title=\"Solaris (operating system)\">Solaris</a> were supported;<sup id=\"cite_ref-139\" class=\"reference\"><a href=\"#cite_note-139\"><span class=\"cite-bracket\">&#91;</span>136<span class=\"cite-bracket\">&#93;</span></a></sup> since that time, support has been dropped for many platforms.\n</p><p>All current Python versions (since 3.7) support only operating systems that feature multithreading, by now supporting not nearly as many operating systems (dropping many outdated) than in the past.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Other_implementations\">Other implementations</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=16\" title=\"Edit section: Other implementations\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>All alternative implementations have at least slightly different semantics. For example, an alternative may include unordered dictionaries, in contrast to other current Python versions. As another example in the larger Python ecosystem, PyPy does not support the full C Python API. Alternative implementations include the following:\n</p>\n<ul><li><a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a> is a fast, compliant interpreter of Python&#160;2.7 and  3.10.<sup id=\"cite_ref-AutoNT-70_140-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-70-140\"><span class=\"cite-bracket\">&#91;</span>137<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-141\" class=\"reference\"><a href=\"#cite_note-141\"><span class=\"cite-bracket\">&#91;</span>138<span class=\"cite-bracket\">&#93;</span></a></sup> PyPy's <a href=\"/wiki/Just-in-time_compiler\" class=\"mw-redirect\" title=\"Just-in-time compiler\">just-in-time compiler</a> often improves speed significantly relative to CPython, but PyPy does not support some libraries written in C.<sup id=\"cite_ref-AutoNT-71_142-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-71-142\"><span class=\"cite-bracket\">&#91;</span>139<span class=\"cite-bracket\">&#93;</span></a></sup> PyPy offers support for the <a href=\"/wiki/RISC-V\" title=\"RISC-V\">RISC-V</a> instruction-set architecture.</li>\n<li>Codon is an implementation with an <a href=\"/wiki/Ahead-of-time_compilation\" title=\"Ahead-of-time compilation\">ahead-of-time (AOT) compiler</a>, which compiles a statically-typed Python-like language whose \"syntax and semantics are nearly identical to Python's, there are some notable differences\"<sup id=\"cite_ref-143\" class=\"reference\"><a href=\"#cite_note-143\"><span class=\"cite-bracket\">&#91;</span>140<span class=\"cite-bracket\">&#93;</span></a></sup> For example, Codon uses 64-bit machine integers for speed, not arbitrarily as with Python; Codon developers claim that speedups over CPython are usually on the order of ten to a hundred times. Codon compiles to machine code (via <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a>) and supports native multithreading.<sup id=\"cite_ref-144\" class=\"reference\"><a href=\"#cite_note-144\"><span class=\"cite-bracket\">&#91;</span>141<span class=\"cite-bracket\">&#93;</span></a></sup>  Codon can also compile to Python extension modules that can be imported and used from Python.</li>\n<li><a href=\"/wiki/MicroPython\" title=\"MicroPython\">MicroPython</a> and <a href=\"/wiki/CircuitPython\" title=\"CircuitPython\">CircuitPython</a> are Python&#160;3 variants that are optimized for <a href=\"/wiki/Microcontroller\" title=\"Microcontroller\">microcontrollers</a>, including the <a href=\"/wiki/Lego_Mindstorms_EV3\" title=\"Lego Mindstorms EV3\">Lego Mindstorms EV3</a>.<sup id=\"cite_ref-145\" class=\"reference\"><a href=\"#cite_note-145\"><span class=\"cite-bracket\">&#91;</span>142<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Pyston is a variant of the Python runtime that uses just-in-time compilation to speed up execution of Python programs.<sup id=\"cite_ref-146\" class=\"reference\"><a href=\"#cite_note-146\"><span class=\"cite-bracket\">&#91;</span>143<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Cinder is a performance-oriented fork of CPython 3.8 that features a number of optimizations, including bytecode inline caching, eager evaluation of coroutines, a method-at-a-time <a href=\"/wiki/Just-in-time_compilation\" title=\"Just-in-time compilation\">JIT</a>, and an experimental bytecode compiler.<sup id=\"cite_ref-147\" class=\"reference\"><a href=\"#cite_note-147\"><span class=\"cite-bracket\">&#91;</span>144<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>The Snek<sup id=\"cite_ref-148\" class=\"reference\"><a href=\"#cite_note-148\"><span class=\"cite-bracket\">&#91;</span>145<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-149\" class=\"reference\"><a href=\"#cite_note-149\"><span class=\"cite-bracket\">&#91;</span>146<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-150\" class=\"reference\"><a href=\"#cite_note-150\"><span class=\"cite-bracket\">&#91;</span>147<span class=\"cite-bracket\">&#93;</span></a></sup> embedded computing language \"is Python-inspired, but it is not Python. It is possible to write Snek programs that run under a full Python system, but most Python programs will not run under Snek.\"<sup id=\"cite_ref-151\" class=\"reference\"><a href=\"#cite_note-151\"><span class=\"cite-bracket\">&#91;</span>148<span class=\"cite-bracket\">&#93;</span></a></sup> Snek is compatible with 8-bit <a href=\"/wiki/AVR_microcontrollers\" title=\"AVR microcontrollers\">AVR microcontrollers</a> such as <a href=\"/wiki/ATmega\" class=\"mw-redirect\" title=\"ATmega\">ATmega 328P</a>-based Arduino, as well as larger microcontrollers that are compatible with <a href=\"/wiki/MicroPython\" title=\"MicroPython\">MicroPython</a>. Snek is an imperative language that (unlike Python) omits <a href=\"/wiki/Object-oriented_programming\" title=\"Object-oriented programming\">object-oriented programming</a>. Snek supports only one numeric data type, which features 32-bit <a href=\"/wiki/Single-precision\" class=\"mw-redirect\" title=\"Single-precision\">single precision</a> (resembling <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a> numbers, though smaller).</li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Unsupported_implementations\">Unsupported implementations</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=17\" title=\"Edit section: Unsupported implementations\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p><a href=\"/wiki/Stackless_Python\" title=\"Stackless Python\">Stackless Python</a> is a significant fork of CPython that implements <a href=\"/wiki/Microthread\" title=\"Microthread\">microthreads</a>. This implementation uses the <a href=\"/wiki/Call_stack\" title=\"Call stack\">call stack</a> differently, thus allowing massively concurrent programs. PyPy also offers a stackless version.<sup id=\"cite_ref-AutoNT-73_152-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-73-152\"><span class=\"cite-bracket\">&#91;</span>149<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Just-in-time Python compilers have been developed, but are now unsupported:\n</p>\n<ul><li>Google began a project named <a href=\"/wiki/Unladen_Swallow\" title=\"Unladen Swallow\">Unladen Swallow</a> in 2009: this project aimed to speed up the Python interpreter five-fold by using <a href=\"/wiki/LLVM\" title=\"LLVM\">LLVM</a>, and improve <a href=\"/wiki/Multithreading_(computer_architecture)\" title=\"Multithreading (computer architecture)\">multithreading</a> capability for scaling to thousands of cores,<sup id=\"cite_ref-AutoNT-74_153-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-74-153\"><span class=\"cite-bracket\">&#91;</span>150<span class=\"cite-bracket\">&#93;</span></a></sup> while typical implementations are limited by the <a href=\"/wiki/Global_interpreter_lock\" title=\"Global interpreter lock\">global interpreter lock</a>.</li>\n<li><a href=\"/wiki/Psyco\" title=\"Psyco\">Psyco</a> is a discontinued <a href=\"/wiki/Just-in-time_compilation\" title=\"Just-in-time compilation\">just-in-time</a> <a href=\"/wiki/Run-time_algorithm_specialization\" title=\"Run-time algorithm specialization\">specializing</a> compiler, which integrates with CPython and transforms bytecode to machine code at runtime. The emitted code is specialized for certain <a href=\"/wiki/Data_type\" title=\"Data type\">data types</a> and is faster than standard Python code. Psyco does not support Python&#160;2.7 or later.</li>\n<li><a href=\"/wiki/PyS60\" class=\"mw-redirect\" title=\"PyS60\">PyS60</a> was a Python&#160;2 interpreter for <a href=\"/wiki/Series_60\" class=\"mw-redirect\" title=\"Series 60\">Series 60</a> mobile phones, which was released by <a href=\"/wiki/Nokia\" title=\"Nokia\">Nokia</a> in 2005. The interpreter implemented many modules from Python's standard library, as well as additional modules for integration with the <a href=\"/wiki/Symbian\" title=\"Symbian\">Symbian</a> operating system. The Nokia <a href=\"/wiki/N900\" class=\"mw-redirect\" title=\"N900\">N900</a> also supports Python through the <a href=\"/wiki/GTK\" title=\"GTK\">GTK</a> widget library, allowing programs to be written and run on the target device.<sup id=\"cite_ref-154\" class=\"reference\"><a href=\"#cite_note-154\"><span class=\"cite-bracket\">&#91;</span>151<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Cross-compilers_to_other_languages\">Cross-compilers to other languages</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=18\" title=\"Edit section: Cross-compilers to other languages\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>There are several compilers/<a href=\"/wiki/Transpiler\" class=\"mw-redirect\" title=\"Transpiler\">transpilers</a> to high-level object languages; the source language is unrestricted Python, a subset of Python, or a language similar to Python:\n</p>\n<ul><li>Brython<sup id=\"cite_ref-155\" class=\"reference\"><a href=\"#cite_note-155\"><span class=\"cite-bracket\">&#91;</span>152<span class=\"cite-bracket\">&#93;</span></a></sup> and Transcrypt<sup id=\"cite_ref-156\" class=\"reference\"><a href=\"#cite_note-156\"><span class=\"cite-bracket\">&#91;</span>153<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-157\" class=\"reference\"><a href=\"#cite_note-157\"><span class=\"cite-bracket\">&#91;</span>154<span class=\"cite-bracket\">&#93;</span></a></sup> compile Python to <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>.</li>\n<li><a href=\"/wiki/Cython\" title=\"Cython\">Cython</a> compiles a superset of Python to C. The resulting code can be used with Python via direct C-level API calls into the Python interpreter.</li>\n<li>PyJL compiles/transpiles a subset of Python to \"human-readable, maintainable, and high-performance Julia source code\".<sup id=\"cite_ref-PyJL_76-1\" class=\"reference\"><a href=\"#cite_note-PyJL-76\"><span class=\"cite-bracket\">&#91;</span>74<span class=\"cite-bracket\">&#93;</span></a></sup> Despite the developers' performance claims, this is not possible for <i>arbitrary</i> Python code; that is, compiling to a faster language or machine code is known to be impossible in the general case. The semantics of Python might potentially be changed, but in many cases speedup is possible with few or no changes in the Python code. The faster Julia source code can then be used from Python or compiled to machine code.</li>\n<li><a href=\"/wiki/Nuitka\" title=\"Nuitka\">Nuitka</a> compiles Python into C.<sup id=\"cite_ref-158\" class=\"reference\"><a href=\"#cite_note-158\"><span class=\"cite-bracket\">&#91;</span>155<span class=\"cite-bracket\">&#93;</span></a></sup> This compiler works with Python 3.4 to 3.12 (and 2.6 and 2.7) for Python's main supported platforms (and Windows 7 or even Windows XP) and for Android. The compiler developers claim full support for Python 3.10, partial support for Python 3.11 and 3.12,  and experimental support for Python 3.13. Nuitka supports macOS including Apple Silicon-based versions.  The compiler is free of cost, though it has commercial add-ons (e.g., for hiding source code).</li>\n<li><a href=\"/wiki/Numba\" title=\"Numba\">Numba</a> is a JIT compiler that is used from Python; the compiler translates a subset of Python and NumPy code into fast machine code. This tool is enabled by adding a decorator to the relevant Python code.</li>\n<li>Pythran compiles a subset of Python&#160;3 to C++ (<a href=\"/wiki/C%2B%2B11\" title=\"C++11\">C++11</a>).<sup id=\"cite_ref-Guelton_Brunet_Amini_Merlini_2015_p=014001_159-0\" class=\"reference\"><a href=\"#cite_note-Guelton_Brunet_Amini_Merlini_2015_p=014001-159\"><span class=\"cite-bracket\">&#91;</span>156<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/RPython\" class=\"mw-redirect\" title=\"RPython\">RPython</a> can be compiled to C, and it is used to build the PyPy interpreter for Python.</li>\n<li>The Python → 11l → C++ transpiler<sup id=\"cite_ref-160\" class=\"reference\"><a href=\"#cite_note-160\"><span class=\"cite-bracket\">&#91;</span>157<span class=\"cite-bracket\">&#93;</span></a></sup> compiles a subset of Python&#160;3 to C++ (<a href=\"/wiki/C%2B%2B17\" title=\"C++17\">C++17</a>).</li></ul>\n<p>There are also specialized compilers:\n</p>\n<ul><li><a href=\"/wiki/MyHDL\" title=\"MyHDL\">MyHDL</a> is a Python-based <a href=\"/wiki/Hardware_description_language\" title=\"Hardware description language\">hardware description language</a> (HDL) that converts MyHDL code to <a href=\"/wiki/Verilog\" title=\"Verilog\">Verilog</a> or <a href=\"/wiki/VHDL\" title=\"VHDL\">VHDL</a> code.</li></ul>\n<p>Some older projects existed, as well as compilers not designed for use with Python 3.x and related syntax:\n</p>\n<ul><li>Google's Grumpy <a href=\"/wiki/Transpile\" class=\"mw-redirect\" title=\"Transpile\">transpiles</a> Python&#160;2 to <a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a>.<sup id=\"cite_ref-161\" class=\"reference\"><a href=\"#cite_note-161\"><span class=\"cite-bracket\">&#91;</span>158<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-162\" class=\"reference\"><a href=\"#cite_note-162\"><span class=\"cite-bracket\">&#91;</span>159<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-163\" class=\"reference\"><a href=\"#cite_note-163\"><span class=\"cite-bracket\">&#91;</span>160<span class=\"cite-bracket\">&#93;</span></a></sup> The latest release was in 2017.</li>\n<li><a href=\"/wiki/IronPython\" title=\"IronPython\">IronPython</a> allows running Python&#160;2.7 programs with the .NET <a href=\"/wiki/Common_Language_Runtime\" title=\"Common Language Runtime\">Common Language Runtime</a>.<sup id=\"cite_ref-164\" class=\"reference\"><a href=\"#cite_note-164\"><span class=\"cite-bracket\">&#91;</span>161<span class=\"cite-bracket\">&#93;</span></a></sup> An <a href=\"/wiki/Software_release_life_cycle#Alpha\" title=\"Software release life cycle\">alpha</a> version (released in 2021), is available for \"Python&#160;3.4, although features and behaviors from later versions may be included.\"<sup id=\"cite_ref-165\" class=\"reference\"><a href=\"#cite_note-165\"><span class=\"cite-bracket\">&#91;</span>162<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Jython\" title=\"Jython\">Jython</a> compiles Python&#160;2.7 to Java bytecode, allowing the use of Java libraries from a Python program.<sup id=\"cite_ref-166\" class=\"reference\"><a href=\"#cite_note-166\"><span class=\"cite-bracket\">&#91;</span>163<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Pyrex_(programming_language)\" title=\"Pyrex (programming language)\">Pyrex</a> (last released in 2010) and <a href=\"/wiki/Shed_Skin\" title=\"Shed Skin\">Shed Skin</a> (last released in 2013) compile to C and C++ respectively.</li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Performance\">Performance</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=19\" title=\"Edit section: Performance\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>A performance comparison among various Python implementations, using a non-numerical (combinatorial) workload, was presented at EuroSciPy '13.<sup id=\"cite_ref-167\" class=\"reference\"><a href=\"#cite_note-167\"><span class=\"cite-bracket\">&#91;</span>164<span class=\"cite-bracket\">&#93;</span></a></sup> In addition, Python's performance relative to other programming languages is benchmarked by <a href=\"/wiki/The_Computer_Language_Benchmarks_Game\" title=\"The Computer Language Benchmarks Game\">The Computer Language Benchmarks Game</a>.<sup id=\"cite_ref-168\" class=\"reference\"><a href=\"#cite_note-168\"><span class=\"cite-bracket\">&#91;</span>165<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>There are several approaches to optimizing Python performance, given the inherent slowness of an <a href=\"/wiki/Interpreted_language\" class=\"mw-redirect\" title=\"Interpreted language\">interpreted language</a>. These approaches include the following strategies or tools:\n</p>\n<ul><li><a href=\"/wiki/Just-in-time_compilation\" title=\"Just-in-time compilation\">Just-in-time compilation</a>: Dynamically compiling Python code just before it is executed. This technique is used in libraries such as <a href=\"/wiki/Numba\" title=\"Numba\">Numba</a> and <a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a>.</li>\n<li><a href=\"/wiki/Compiler\" title=\"Compiler\">Static compilation</a>: Python code is compiled into machine code sometime before execution. An example of this approach is Cython, which compiles Python into C.</li>\n<li>Concurrency and parallelism: Multiple tasks can be run simultaneously. Python contains modules such as `multiprocessing` to support this form of parallelism. Moreover, this approach helps to overcome limitations of the <a href=\"/wiki/Global_interpreter_lock\" title=\"Global interpreter lock\">Global Interpreter Lock</a> (GIL) in CPU tasks.</li>\n<li>Efficient data structures: Performance can also be improved by using data types such as <code>Set</code> for membership tests, or <code>deque</code> from <code>collections</code> for <a href=\"/wiki/Queueing_theory\" title=\"Queueing theory\">queue</a> operations.</li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Language_Development\">Language Development</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=20\" title=\"Edit section: Language Development\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's development is conducted largely through the <i>Python Enhancement Proposal</i> (PEP) process; this process is the primary mechanism for proposing major new features, collecting community input on issues, and documenting Python design decisions.<sup id=\"cite_ref-PepCite000_169-0\" class=\"reference\"><a href=\"#cite_note-PepCite000-169\"><span class=\"cite-bracket\">&#91;</span>166<span class=\"cite-bracket\">&#93;</span></a></sup> Python coding style is covered in PEP&#160;8.<sup id=\"cite_ref-170\" class=\"reference\"><a href=\"#cite_note-170\"><span class=\"cite-bracket\">&#91;</span>167<span class=\"cite-bracket\">&#93;</span></a></sup> Outstanding PEPs are reviewed and commented on by the Python community and the steering council.<sup id=\"cite_ref-PepCite000_169-1\" class=\"reference\"><a href=\"#cite_note-PepCite000-169\"><span class=\"cite-bracket\">&#91;</span>166<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Enhancement of the language corresponds with development of the CPython reference implementation. The mailing list python-dev is the primary forum for the language's development. Specific issues were originally discussed in the <a href=\"/wiki/Roundup_(issue_tracker)\" title=\"Roundup (issue tracker)\">Roundup</a> <a href=\"/wiki/Bug_tracker\" class=\"mw-redirect\" title=\"Bug tracker\">bug tracker</a> hosted by the foundation.<sup id=\"cite_ref-AutoNT-21_171-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-21-171\"><span class=\"cite-bracket\">&#91;</span>168<span class=\"cite-bracket\">&#93;</span></a></sup> In 2022, all issues and discussions were migrated to <a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a>.<sup id=\"cite_ref-172\" class=\"reference\"><a href=\"#cite_note-172\"><span class=\"cite-bracket\">&#91;</span>169<span class=\"cite-bracket\">&#93;</span></a></sup> Development originally took place on a <a href=\"/wiki/Self-hosting_(web_services)\" title=\"Self-hosting (web services)\">self-hosted</a> source-code repository running <a href=\"/wiki/Mercurial\" title=\"Mercurial\">Mercurial</a>, until Python moved to <a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a> in January 2017.<sup id=\"cite_ref-py_dev_guide_173-0\" class=\"reference\"><a href=\"#cite_note-py_dev_guide-173\"><span class=\"cite-bracket\">&#91;</span>170<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>CPython's public releases have three types, distinguished by which part of the version number is incremented:\n</p>\n<ul><li><i>Backward-incompatible versions</i>, where code is expected to break and must be manually <a href=\"/wiki/Ported\" class=\"mw-redirect\" title=\"Ported\">ported</a>. The first part of the version number is incremented. These releases happen infrequently—version 3.0 was released 8 years after 2.0. According to Guido van Rossum, a version 4.0 will probably never exist.<sup id=\"cite_ref-174\" class=\"reference\"><a href=\"#cite_note-174\"><span class=\"cite-bracket\">&#91;</span>171<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><i>Major or \"feature\" releases</i> are largely compatible with the previous version but introduce new features. The second part of the version number is incremented. Starting with Python&#160;3.9, these releases are expected to occur annually.<sup id=\"cite_ref-175\" class=\"reference\"><a href=\"#cite_note-175\"><span class=\"cite-bracket\">&#91;</span>172<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-176\" class=\"reference\"><a href=\"#cite_note-176\"><span class=\"cite-bracket\">&#91;</span>173<span class=\"cite-bracket\">&#93;</span></a></sup> Each major version is supported by bug fixes for several years after its release.<sup id=\"cite_ref-release-schedule_177-0\" class=\"reference\"><a href=\"#cite_note-release-schedule-177\"><span class=\"cite-bracket\">&#91;</span>174<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><i>Bug fix releases</i>,<sup id=\"cite_ref-AutoNT-22_178-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-22-178\"><span class=\"cite-bracket\">&#91;</span>175<span class=\"cite-bracket\">&#93;</span></a></sup> which introduce no new features, occur approximately every three months; these releases are made when a sufficient number of bugs have been fixed <a href=\"/wiki/Upstream_(software_development)\" title=\"Upstream (software development)\">upstream</a> since the last release. Security vulnerabilities are also patched in these releases. The third and final part of the version number is incremented.<sup id=\"cite_ref-AutoNT-22_178-1\" class=\"reference\"><a href=\"#cite_note-AutoNT-22-178\"><span class=\"cite-bracket\">&#91;</span>175<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n<p>Many <a href=\"/wiki/Beta_release\" class=\"mw-redirect\" title=\"Beta release\">alpha, beta, and release-candidates</a> are also released as previews and for testing before final releases. Although there is a rough schedule for releases, they are often delayed if the code is not ready yet. Python's development team monitors the state of the code by running a large <a href=\"/wiki/Unit_test\" class=\"mw-redirect\" title=\"Unit test\">unit test</a> suite during development.<sup id=\"cite_ref-AutoNT-23_179-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-23-179\"><span class=\"cite-bracket\">&#91;</span>176<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The major <a href=\"/wiki/Academic_conference\" title=\"Academic conference\">academic conference</a> on Python is <a href=\"/wiki/PyCon\" class=\"mw-redirect\" title=\"PyCon\">PyCon</a>. There are also special Python mentoring programs, such as <a href=\"/wiki/PyLadies\" title=\"PyLadies\">PyLadies</a>.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"API_documentation_generators\">API documentation generators</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=21\" title=\"Edit section: API documentation generators\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Tools that can generate documentation for Python <a href=\"/wiki/API\" title=\"API\">API</a> include <a href=\"/wiki/Pydoc\" title=\"Pydoc\">pydoc</a> (available as part of the standard library); <a href=\"/wiki/Sphinx_(documentation_generator)\" title=\"Sphinx (documentation generator)\">Sphinx</a>; and <a href=\"/wiki/Pdoc\" title=\"Pdoc\">Pdoc</a> and its forks, <a href=\"/wiki/Doxygen\" title=\"Doxygen\">Doxygen</a> and <a href=\"/wiki/Graphviz\" title=\"Graphviz\">Graphviz</a>.<sup id=\"cite_ref-180\" class=\"reference\"><a href=\"#cite_note-180\"><span class=\"cite-bracket\">&#91;</span>177<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Naming\">Naming</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=22\" title=\"Edit section: Naming\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's name is inspired by the British comedy group <a href=\"/wiki/Monty_Python\" title=\"Monty Python\">Monty Python</a>, whom Python creator Guido van Rossum enjoyed while developing the language. Monty Python references appear frequently in Python code and culture;<sup id=\"cite_ref-tutorial-chapter1_181-0\" class=\"reference\"><a href=\"#cite_note-tutorial-chapter1-181\"><span class=\"cite-bracket\">&#91;</span>178<span class=\"cite-bracket\">&#93;</span></a></sup> for example, the <a href=\"/wiki/Metasyntactic_variable\" title=\"Metasyntactic variable\">metasyntactic variables</a> often used in Python literature are <a href=\"/wiki/Spam_(Monty_Python)\" class=\"mw-redirect\" title=\"Spam (Monty Python)\"><i>spam</i> and <i>eggs</i></a>, rather than the traditional <a href=\"/wiki/Foobar\" title=\"Foobar\"><i>foo</i> and <i>bar</i></a>.<sup id=\"cite_ref-tutorial-chapter1_181-1\" class=\"reference\"><a href=\"#cite_note-tutorial-chapter1-181\"><span class=\"cite-bracket\">&#91;</span>178<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-26_182-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-26-182\"><span class=\"cite-bracket\">&#91;</span>179<span class=\"cite-bracket\">&#93;</span></a></sup> The official Python documentation also contains various references to Monty Python routines.<sup id=\"cite_ref-183\" class=\"reference\"><a href=\"#cite_note-183\"><span class=\"cite-bracket\">&#91;</span>180<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-184\" class=\"reference\"><a href=\"#cite_note-184\"><span class=\"cite-bracket\">&#91;</span>181<span class=\"cite-bracket\">&#93;</span></a></sup> Python users are sometimes referred to as \"Pythonistas\".<sup id=\"cite_ref-introducing_python_185-0\" class=\"reference\"><a href=\"#cite_note-introducing_python-185\"><span class=\"cite-bracket\">&#91;</span>182<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The <a href=\"/wiki/Affix\" title=\"Affix\">affix</a> <i>Py</i> is often used when naming Python applications or libraries. Some examples include the following:\n</p>\n<ul><li><a href=\"/wiki/Pygame\" title=\"Pygame\">Pygame</a>, a <a href=\"/wiki/Language_binding\" title=\"Language binding\">binding</a> of <a href=\"/wiki/Simple_DirectMedia_Layer\" title=\"Simple DirectMedia Layer\">Simple DirectMedia Layer</a> to Python (commonly used to create games);</li>\n<li><a href=\"/wiki/PyQt\" title=\"PyQt\">PyQt</a> and <a href=\"/wiki/PyGTK\" title=\"PyGTK\">PyGTK</a>, which bind <a href=\"/wiki/Qt_(software)\" title=\"Qt (software)\">Qt</a> and GTK to Python respectively;</li>\n<li><a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a>, a Python implementation originally written in Python;</li>\n<li><a href=\"/wiki/NumPy\" title=\"NumPy\">NumPy</a>, a Python library for numerical processing.</li>\n<li><a href=\"/wiki/Jupyter\" class=\"mw-redirect\" title=\"Jupyter\">Jupyter</a>, a <a href=\"/wiki/Notebook_interface\" title=\"Notebook interface\">notebook interface</a> and associated project for interactive computing</li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Popularity\">Popularity</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=23\" title=\"Edit section: Popularity\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Since 2003, Python has consistently ranked in the top ten of the most popular programming languages in the <a href=\"/wiki/TIOBE_Programming_Community_Index\" class=\"mw-redirect\" title=\"TIOBE Programming Community Index\">TIOBE Programming Community Index</a>; as of December&#160;2022<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup>, Python was the most popular language.<sup id=\"cite_ref-tiobecurrent_40-1\" class=\"reference\"><a href=\"#cite_note-tiobecurrent-40\"><span class=\"cite-bracket\">&#91;</span>38<span class=\"cite-bracket\">&#93;</span></a></sup> Python was selected as Programming Language of the Year (for \"the highest rise in ratings in a year\") in 2007, 2010, 2018, 2020, 2021, and 2024 —the only language to have done so six times as of 2025<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;action=edit\">&#91;update&#93;</a></sup><sup id=\"cite_ref-186\" class=\"reference\"><a href=\"#cite_note-186\"><span class=\"cite-bracket\">&#91;</span>183<span class=\"cite-bracket\">&#93;</span></a></sup>). In the TIOBE Index, monthly rankings are based on the volume of searches for programming languages on Google, Amazon, Wikipedia, Bing, and 20 other platforms. According to the accompanying graph, Python has shown a marked upward trend since the early 2000s, eventually passing more established languages such as C, C++, and Java. This trend can be attributed to Python's readable syntax, comprehensive standard library, and application in data science and machine learning fields.<sup id=\"cite_ref-187\" class=\"reference\"><a href=\"#cite_note-187\"><span class=\"cite-bracket\">&#91;</span>184<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Tiobeindex.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Tiobeindex.png/250px-Tiobeindex.png\" decoding=\"async\" width=\"250\" height=\"82\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Tiobeindex.png/500px-Tiobeindex.png 1.5x\" data-file-width=\"1368\" data-file-height=\"450\" /></a><figcaption>TIOBE Index Chart showing Python's popularity compared to other programming languages</figcaption></figure>\n<p>Large organizations that use Python include <a href=\"/wiki/Wikipedia\" title=\"Wikipedia\">Wikipedia</a>, <a href=\"/wiki/Google\" title=\"Google\">Google</a>,<sup id=\"cite_ref-quotes-about-python_188-0\" class=\"reference\"><a href=\"#cite_note-quotes-about-python-188\"><span class=\"cite-bracket\">&#91;</span>185<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Yahoo!\" class=\"mw-redirect\" title=\"Yahoo!\">Yahoo!</a>,<sup id=\"cite_ref-AutoNT-29_189-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-29-189\"><span class=\"cite-bracket\">&#91;</span>186<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/CERN\" title=\"CERN\">CERN</a>,<sup id=\"cite_ref-AutoNT-30_190-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-30-190\"><span class=\"cite-bracket\">&#91;</span>187<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/NASA\" title=\"NASA\">NASA</a>,<sup id=\"cite_ref-AutoNT-31_191-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-31-191\"><span class=\"cite-bracket\">&#91;</span>188<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Facebook\" title=\"Facebook\">Facebook</a>,<sup id=\"cite_ref-192\" class=\"reference\"><a href=\"#cite_note-192\"><span class=\"cite-bracket\">&#91;</span>189<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Amazon_(company)\" title=\"Amazon (company)\">Amazon</a>, <a href=\"/wiki/Instagram\" title=\"Instagram\">Instagram</a>,<sup id=\"cite_ref-193\" class=\"reference\"><a href=\"#cite_note-193\"><span class=\"cite-bracket\">&#91;</span>190<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Spotify\" title=\"Spotify\">Spotify</a>,<sup id=\"cite_ref-194\" class=\"reference\"><a href=\"#cite_note-194\"><span class=\"cite-bracket\">&#91;</span>191<span class=\"cite-bracket\">&#93;</span></a></sup> and some smaller entities such as <a href=\"/wiki/Industrial_Light_%26_Magic\" title=\"Industrial Light &amp; Magic\">Industrial Light &amp; Magic</a><sup id=\"cite_ref-AutoNT-32_195-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-32-195\"><span class=\"cite-bracket\">&#91;</span>192<span class=\"cite-bracket\">&#93;</span></a></sup> and <a href=\"/wiki/ITA_Software\" title=\"ITA Software\">ITA</a>.<sup id=\"cite_ref-AutoNT-33_196-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-33-196\"><span class=\"cite-bracket\">&#91;</span>193<span class=\"cite-bracket\">&#93;</span></a></sup> The social news networking site <a href=\"/wiki/Reddit\" title=\"Reddit\">Reddit</a> was developed mostly in Python.<sup id=\"cite_ref-197\" class=\"reference\"><a href=\"#cite_note-197\"><span class=\"cite-bracket\">&#91;</span>194<span class=\"cite-bracket\">&#93;</span></a></sup> Organizations that partly use Python include <a href=\"/wiki/Discord\" title=\"Discord\">Discord</a><sup id=\"cite_ref-198\" class=\"reference\"><a href=\"#cite_note-198\"><span class=\"cite-bracket\">&#91;</span>195<span class=\"cite-bracket\">&#93;</span></a></sup> and <a href=\"/wiki/Baidu\" title=\"Baidu\">Baidu</a>.<sup id=\"cite_ref-199\" class=\"reference\"><a href=\"#cite_note-199\"><span class=\"cite-bracket\">&#91;</span>196<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Types_of_use\">Types of use</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=24\" title=\"Edit section: Types of use\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Further information: <a href=\"/wiki/List_of_Python_software\" title=\"List of Python software\">List of Python software</a></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Python_Powered.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Python_Powered.png/250px-Python_Powered.png\" decoding=\"async\" width=\"250\" height=\"172\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Python_Powered.png/500px-Python_Powered.png 1.5x\" data-file-width=\"1058\" data-file-height=\"728\" /></a><figcaption>Software that is powered by Python</figcaption></figure>\n<p>Python has many uses, including the following:\n</p>\n<ul><li><a href=\"/wiki/Scripting_language\" title=\"Scripting language\">Scripting</a> for <a href=\"/wiki/Web_application\" title=\"Web application\">web applications</a></li>\n<li>Scientific computing</li>\n<li><a href=\"/wiki/Artificial_intelligence\" title=\"Artificial intelligence\">Artificial intelligence</a> and <a href=\"/wiki/Machine_learning\" title=\"Machine learning\">machine learning</a> projects</li>\n<li><a href=\"/wiki/Graphical_user_interface\" title=\"Graphical user interface\">Graphical user interfaces</a> and <a href=\"/wiki/Desktop_environment\" title=\"Desktop environment\">desktop environments</a></li>\n<li>Embedded scripting in software and hardware products</li>\n<li>Operating systems</li>\n<li><a href=\"/wiki/Information_security\" title=\"Information security\">Information security</a></li></ul>\n<p>Python can serve as a scripting language for web applications, e.g., via the <a href=\"/wiki/Mod_wsgi\" title=\"Mod wsgi\">mod_wsgi</a> module for the <a href=\"/wiki/Apache_webserver\" class=\"mw-redirect\" title=\"Apache webserver\">Apache web server</a>.<sup id=\"cite_ref-AutoNT-35_200-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-35-200\"><span class=\"cite-bracket\">&#91;</span>197<span class=\"cite-bracket\">&#93;</span></a></sup> With <a href=\"/wiki/Web_Server_Gateway_Interface\" title=\"Web Server Gateway Interface\">Web Server Gateway Interface</a>, a standard API has evolved to facilitate these applications. <a href=\"/wiki/Web_framework\" title=\"Web framework\">Web frameworks</a> such as <a href=\"/wiki/Django_(web_framework)\" title=\"Django (web framework)\">Django</a>, <a href=\"/wiki/Pylons_(web_framework)\" class=\"mw-redirect\" title=\"Pylons (web framework)\">Pylons</a>, <a href=\"/wiki/Pyramid_(web_framework)\" class=\"mw-redirect\" title=\"Pyramid (web framework)\">Pyramid</a>, <a href=\"/wiki/TurboGears\" title=\"TurboGears\">TurboGears</a>, <a href=\"/wiki/Web2py\" title=\"Web2py\">web2py</a>, <a href=\"/wiki/Tornado_(web_server)\" title=\"Tornado (web server)\">Tornado</a>, <a href=\"/wiki/Flask_(web_framework)\" title=\"Flask (web framework)\">Flask</a>, Bottle, and <a href=\"/wiki/Zope\" title=\"Zope\">Zope</a> support developers in the design and maintenance of complex applications. Pyjs and <a href=\"/wiki/IronPython\" title=\"IronPython\">IronPython</a> can be used to develop the client-side of Ajax-based applications. <a href=\"/wiki/SQLAlchemy\" title=\"SQLAlchemy\">SQLAlchemy</a> can be used as a <a href=\"/wiki/Data_mapper_pattern\" title=\"Data mapper pattern\">data mapper</a> to a relational database. <a href=\"/wiki/Twisted_(software)\" title=\"Twisted (software)\">Twisted</a> is a framework to program communication between computers; this framework is used by <a href=\"/wiki/Dropbox\" title=\"Dropbox\">Dropbox</a>, for example.\n</p><p>Libraries such as <a href=\"/wiki/NumPy\" title=\"NumPy\">NumPy</a>, <a href=\"/wiki/SciPy\" title=\"SciPy\">SciPy</a> and <a href=\"/wiki/Matplotlib\" title=\"Matplotlib\">Matplotlib</a> allow the effective use of Python in scientific computing,<sup id=\"cite_ref-cise_201-0\" class=\"reference\"><a href=\"#cite_note-cise-201\"><span class=\"cite-bracket\">&#91;</span>198<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-millman_202-0\" class=\"reference\"><a href=\"#cite_note-millman-202\"><span class=\"cite-bracket\">&#91;</span>199<span class=\"cite-bracket\">&#93;</span></a></sup> with specialized libraries such as <a href=\"/wiki/Biopython\" title=\"Biopython\">Biopython</a> and <a href=\"/wiki/Astropy\" title=\"Astropy\">Astropy</a> providing domain-specific functionality. <a href=\"/wiki/SageMath\" title=\"SageMath\">SageMath</a> is a <a href=\"/wiki/Computer_algebra_system\" title=\"Computer algebra system\">computer algebra system</a> with a <a href=\"/wiki/Notebook_interface\" title=\"Notebook interface\">notebook interface</a> that is programmable in Python; the SageMath library covers many aspects of <a href=\"/wiki/Mathematics\" title=\"Mathematics\">mathematics</a>, including <a href=\"/wiki/Algebra\" title=\"Algebra\">algebra</a>, <a href=\"/wiki/Combinatorics\" title=\"Combinatorics\">combinatorics</a>, <a href=\"/wiki/Numerical_mathematics\" class=\"mw-redirect\" title=\"Numerical mathematics\">numerical mathematics</a>, <a href=\"/wiki/Number_theory\" title=\"Number theory\">number theory</a>, and <a href=\"/wiki/Calculus\" title=\"Calculus\">calculus</a>.<sup id=\"cite_ref-ICSE_203-0\" class=\"reference\"><a href=\"#cite_note-ICSE-203\"><span class=\"cite-bracket\">&#91;</span>200<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/OpenCV\" title=\"OpenCV\">OpenCV</a> has Python bindings with a rich set of features for <a href=\"/wiki/Computer_vision\" title=\"Computer vision\">computer vision</a> and <a href=\"/wiki/Image_processing\" class=\"mw-redirect\" title=\"Image processing\">image processing</a>.<sup id=\"cite_ref-204\" class=\"reference\"><a href=\"#cite_note-204\"><span class=\"cite-bracket\">&#91;</span>201<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python is commonly used in artificial-intelligence and machine-learning projects, with support from libraries such as <a href=\"/wiki/TensorFlow\" title=\"TensorFlow\">TensorFlow</a>, <a href=\"/wiki/Keras\" title=\"Keras\">Keras</a>, <a href=\"/wiki/Pytorch\" class=\"mw-redirect\" title=\"Pytorch\">Pytorch</a>, <a href=\"/wiki/Scikit-learn\" title=\"Scikit-learn\">scikit-learn</a> and <a href=\"/wiki/ProbLog\" title=\"ProbLog\">ProbLog</a> (a logic language).<sup id=\"cite_ref-whitepaper2015_205-0\" class=\"reference\"><a href=\"#cite_note-whitepaper2015-205\"><span class=\"cite-bracket\">&#91;</span>202<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-206\" class=\"reference\"><a href=\"#cite_note-206\"><span class=\"cite-bracket\">&#91;</span>203<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-207\" class=\"reference\"><a href=\"#cite_note-207\"><span class=\"cite-bracket\">&#91;</span>204<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-208\" class=\"reference\"><a href=\"#cite_note-208\"><span class=\"cite-bracket\">&#91;</span>205<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-ProbLogConcepts_209-0\" class=\"reference\"><a href=\"#cite_note-ProbLogConcepts-209\"><span class=\"cite-bracket\">&#91;</span>206<span class=\"cite-bracket\">&#93;</span></a></sup> As a scripting language with a <a href=\"/wiki/Modular_programming\" title=\"Modular programming\">modular architecture</a>, simple syntax, and rich text processing tools, Python is often used for <a href=\"/wiki/Natural_language_processing\" title=\"Natural language processing\">natural language processing</a>.<sup id=\"cite_ref-AutoNT-47_210-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-47-210\"><span class=\"cite-bracket\">&#91;</span>207<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The combination of Python and <a href=\"/wiki/Prolog\" title=\"Prolog\">Prolog</a> has proven useful for AI applications, with Prolog providing knowledge representation and reasoning capabilities. The Janus system, in particular, exploits similarities between these two languages, in part because of their dynamic typing and their simple, recursive data structures. This combination is typically applied natural language processing, visual query answering, geospatial reasoning, and handling semantic web data.<sup id=\"cite_ref-211\" class=\"reference\"><a href=\"#cite_note-211\"><span class=\"cite-bracket\">&#91;</span>208<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-212\" class=\"reference\"><a href=\"#cite_note-212\"><span class=\"cite-bracket\">&#91;</span>209<span class=\"cite-bracket\">&#93;</span></a></sup>\nThe Natlog system, implemented in Python, uses <a href=\"/wiki/Definite_clause_grammar\" title=\"Definite clause grammar\">Definite Clause Grammars</a> (DCGs) to create prompts for two types of generators: text-to-text generators such as GPT3, and text-to-image generators such as DALL-E or Stable Diffusion.<sup id=\"cite_ref-213\" class=\"reference\"><a href=\"#cite_note-213\"><span class=\"cite-bracket\">&#91;</span>210<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python can be used for graphical user interfaces (GUIs), by using libraries such as <a href=\"/wiki/Tkinter\" title=\"Tkinter\">Tkinter</a>.<sup id=\"cite_ref-214\" class=\"reference\"><a href=\"#cite_note-214\"><span class=\"cite-bracket\">&#91;</span>211<span class=\"cite-bracket\">&#93;</span></a></sup> Similarly, for the <a href=\"/wiki/One_Laptop_per_Child\" title=\"One Laptop per Child\">One Laptop per Child</a> XO computer, most of the <a href=\"/wiki/Sugar_(software)\" class=\"mw-redirect\" title=\"Sugar (software)\">Sugar</a> desktop environment is written in Python (as of 2008).<sup id=\"cite_ref-215\" class=\"reference\"><a href=\"#cite_note-215\"><span class=\"cite-bracket\">&#91;</span>212<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python is embedded in many software products (and some hardware products) as a scripting language. These products include the following:\n</p>\n<ul><li><a href=\"/wiki/Finite_element_method\" title=\"Finite element method\">finite element method</a> software such as <a href=\"/wiki/Abaqus\" title=\"Abaqus\">Abaqus</a>,</li>\n<li>3D parametric modelers such as <a href=\"/wiki/FreeCAD\" title=\"FreeCAD\">FreeCAD</a>,</li>\n<li>3D animation packages such as <a href=\"/wiki/3ds_Max\" class=\"mw-redirect\" title=\"3ds Max\">3ds Max</a>, <a href=\"/wiki/Blender_(software)\" title=\"Blender (software)\">Blender</a>, <a href=\"/wiki/Cinema_4D\" title=\"Cinema 4D\">Cinema 4D</a>, <a href=\"/wiki/LightWave_3D\" title=\"LightWave 3D\">Lightwave</a>, <a href=\"/wiki/Houdini_(software)\" title=\"Houdini (software)\">Houdini</a>, <a href=\"/wiki/Maya_(software)\" class=\"mw-redirect\" title=\"Maya (software)\">Maya</a>, <a href=\"/wiki/Modo_(software)\" title=\"Modo (software)\">modo</a>, <a href=\"/wiki/MotionBuilder\" class=\"mw-redirect\" title=\"MotionBuilder\">MotionBuilder</a>, <a href=\"/wiki/Autodesk_Softimage\" title=\"Autodesk Softimage\">Softimage</a>,</li>\n<li>the visual effects compositor <a href=\"/wiki/Nuke_(software)\" title=\"Nuke (software)\">Nuke</a>,</li>\n<li>2D imaging programs such as <a href=\"/wiki/GIMP\" title=\"GIMP\">GIMP</a>,<sup id=\"cite_ref-216\" class=\"reference\"><a href=\"#cite_note-216\"><span class=\"cite-bracket\">&#91;</span>213<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Inkscape\" title=\"Inkscape\">Inkscape</a>, <a href=\"/wiki/Scribus\" title=\"Scribus\">Scribus</a> and <a href=\"/wiki/Paint_Shop_Pro\" class=\"mw-redirect\" title=\"Paint Shop Pro\">Paint Shop Pro</a>,<sup id=\"cite_ref-AutoNT-38_217-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-38-217\"><span class=\"cite-bracket\">&#91;</span>214<span class=\"cite-bracket\">&#93;</span></a></sup> and</li>\n<li><a href=\"/wiki/Musical_notation\" title=\"Musical notation\">musical notation</a> programs such as <a href=\"/wiki/Scorewriter\" title=\"Scorewriter\">scorewriter</a> and <a href=\"/wiki/Capella_(notation_program)\" title=\"Capella (notation program)\">capella</a>.</li></ul>\n<p>Similarly, <a href=\"/wiki/GNU_Debugger\" title=\"GNU Debugger\">GNU Debugger</a> uses Python as a <a href=\"/wiki/Pretty_printer\" class=\"mw-redirect\" title=\"Pretty printer\">pretty printer</a> to show complex structures such as C++ containers. <a href=\"/wiki/Esri\" title=\"Esri\">Esri</a> promotes Python as the best choice for writing scripts in <a href=\"/wiki/ArcGIS\" title=\"ArcGIS\">ArcGIS</a>.<sup id=\"cite_ref-AutoNT-39_218-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-39-218\"><span class=\"cite-bracket\">&#91;</span>215<span class=\"cite-bracket\">&#93;</span></a></sup> Python has also been used in several video games,<sup id=\"cite_ref-AutoNT-40_219-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-40-219\"><span class=\"cite-bracket\">&#91;</span>216<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-41_220-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-41-220\"><span class=\"cite-bracket\">&#91;</span>217<span class=\"cite-bracket\">&#93;</span></a></sup> and it has been adopted as first of the three <a href=\"/wiki/Programming_language\" title=\"Programming language\">programming languages</a> available in <a href=\"/wiki/Google_App_Engine\" title=\"Google App Engine\">Google App Engine</a> (the other two being <a href=\"/wiki/Java_(software_platform)\" title=\"Java (software platform)\">Java</a> and <a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a>).<sup id=\"cite_ref-AutoNT-42_221-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-42-221\"><span class=\"cite-bracket\">&#91;</span>218<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/LibreOffice\" title=\"LibreOffice\">LibreOffice</a> includes Python, and its developers plan to replace Java with Python; LibreOffice's Python Scripting Provider is a core feature<sup id=\"cite_ref-222\" class=\"reference\"><a href=\"#cite_note-222\"><span class=\"cite-bracket\">&#91;</span>219<span class=\"cite-bracket\">&#93;</span></a></sup> since version 4.0 (from 7 February 2013).\n</p><p>Among hardware products, the <a href=\"/wiki/Raspberry_Pi\" title=\"Raspberry Pi\">Raspberry Pi</a> <a href=\"/wiki/Single-board_computer\" title=\"Single-board computer\">single-board computer</a> project has adopted Python as its main user-programming language.\n</p><p>Many operating systems include Python as a standard component. Python ships with most <a href=\"/wiki/Linux_distribution\" title=\"Linux distribution\">Linux distributions</a>,<sup id=\"cite_ref-223\" class=\"reference\"><a href=\"#cite_note-223\"><span class=\"cite-bracket\">&#91;</span>220<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/AmigaOS_4\" title=\"AmigaOS 4\">AmigaOS 4</a> (using Python&#160;2.7), <a href=\"/wiki/FreeBSD\" title=\"FreeBSD\">FreeBSD</a> (as a package), <a href=\"/wiki/NetBSD\" title=\"NetBSD\">NetBSD</a>, and <a href=\"/wiki/OpenBSD\" title=\"OpenBSD\">OpenBSD</a> (as a package); it can be used from the command line (terminal). Many Linux distributions use installers written in Python: <a href=\"/wiki/Ubuntu\" title=\"Ubuntu\">Ubuntu</a> uses the <a href=\"/wiki/Ubiquity_(software)\" title=\"Ubiquity (software)\">Ubiquity</a> installer, while <a href=\"/wiki/Red_Hat_Linux\" title=\"Red Hat Linux\">Red Hat Linux</a> and <a href=\"/wiki/Fedora_Linux\" title=\"Fedora Linux\">Fedora Linux</a> use the <a href=\"/wiki/Anaconda_(installer)\" title=\"Anaconda (installer)\">Anaconda</a> installer. <a href=\"/wiki/Gentoo_Linux\" title=\"Gentoo Linux\">Gentoo Linux</a> uses Python in its <a href=\"/wiki/Package_management_system\" class=\"mw-redirect\" title=\"Package management system\">package management system</a>, <a href=\"/wiki/Portage_(software)\" title=\"Portage (software)\">Portage</a>.<sup id=\"cite_ref-AutoNT-51_224-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-51-224\"><span class=\"cite-bracket\">&#91;</span>221<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Python is used extensively in the information security industry, including in exploit development.<sup id=\"cite_ref-AutoNT-49_225-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-49-225\"><span class=\"cite-bracket\">&#91;</span>222<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-AutoNT-50_226-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-50-226\"><span class=\"cite-bracket\">&#91;</span>223<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Limitations\">Limitations</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=25\" title=\"Edit section: Limitations\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li>The energy usage of Python is much worse than C by a factor of 75.88.<sup id=\"cite_ref-:1_227-0\" class=\"reference\"><a href=\"#cite_note-:1-227\"><span class=\"cite-bracket\">&#91;</span>224<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Python lacks do while loops.<sup id=\"cite_ref-228\" class=\"reference\"><a href=\"#cite_note-228\"><span class=\"cite-bracket\">&#91;</span>225<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>The throughput of Python is worse than C by a factor of 71.9.<sup id=\"cite_ref-:1_227-1\" class=\"reference\"><a href=\"#cite_note-:1-227\"><span class=\"cite-bracket\">&#91;</span>224<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>The average memory usage of Python is worse than C by a factor of 2.4.<sup id=\"cite_ref-:1_227-2\" class=\"reference\"><a href=\"#cite_note-:1-227\"><span class=\"cite-bracket\">&#91;</span>224<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Creating an executable with Python requires bundling the entire Python interpreter into the executable, which causes binary sizes of small executable to be massive.<sup id=\"cite_ref-229\" class=\"reference\"><a href=\"#cite_note-229\"><span class=\"cite-bracket\">&#91;</span>226<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Significant whitespace causes Python scripts to be difficult to minify. The most compact minification can transform each indent level into a tab.<sup id=\"cite_ref-230\" class=\"reference\"><a href=\"#cite_note-230\"><span class=\"cite-bracket\">&#91;</span>227<span class=\"cite-bracket\">&#93;</span></a></sup> However, this always takes up at least as much or more than braces.</li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Languages_influenced_by_Python\">Languages influenced by Python</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=26\" title=\"Edit section: Languages influenced by Python\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Python's design and philosophy have influenced many other programming languages:\n</p>\n<ul><li><a href=\"/wiki/Boo_(programming_language)\" title=\"Boo (programming language)\">Boo</a> uses indentation, a similar syntax, and a similar object model.<sup id=\"cite_ref-AutoNT-90_231-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-90-231\"><span class=\"cite-bracket\">&#91;</span>228<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Cobra_(programming_language)\" title=\"Cobra (programming language)\">Cobra</a> uses indentation and a similar syntax; its <i>Acknowledgements</i> document lists Python first among influencing languages.<sup id=\"cite_ref-AutoNT-91_232-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-91-232\"><span class=\"cite-bracket\">&#91;</span>229<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/CoffeeScript\" title=\"CoffeeScript\">CoffeeScript</a>, a programming language that cross-compiles to JavaScript, has a Python-inspired syntax.</li>\n<li><a href=\"/wiki/ECMAScript\" title=\"ECMAScript\">ECMAScript</a>–<a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a> borrowed iterators and <a href=\"/wiki/Generator_(computer_science)\" class=\"mw-redirect\" title=\"Generator (computer science)\">generators</a> from Python.<sup id=\"cite_ref-AutoNT-93_233-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-93-233\"><span class=\"cite-bracket\">&#91;</span>230<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/GDScript\" class=\"mw-redirect\" title=\"GDScript\">GDScript</a>, a Python-like scripting language that is built in to the <a href=\"/wiki/Godot_(game_engine)\" title=\"Godot (game engine)\">Godot</a> game engine.<sup id=\"cite_ref-234\" class=\"reference\"><a href=\"#cite_note-234\"><span class=\"cite-bracket\">&#91;</span>231<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a> is designed for \"speed of working in a dynamic language like Python\";<sup id=\"cite_ref-AutoNT-94_235-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-94-235\"><span class=\"cite-bracket\">&#91;</span>232<span class=\"cite-bracket\">&#93;</span></a></sup> Go shares Python's syntax for slicing arrays.</li>\n<li><a href=\"/wiki/Groovy_(programming_language)\" class=\"mw-redirect\" title=\"Groovy (programming language)\">Groovy</a> was motivated by a desire to incorporate the Python design philosophy into <a href=\"/wiki/Java_(programming_language)\" title=\"Java (programming language)\">Java</a>.<sup id=\"cite_ref-AutoNT-95_236-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-95-236\"><span class=\"cite-bracket\">&#91;</span>233<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a> was designed to be \"as usable for general programming as Python\".<sup id=\"cite_ref-Julia_30-1\" class=\"reference\"><a href=\"#cite_note-Julia-30\"><span class=\"cite-bracket\">&#91;</span>28<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Mojo_(programming_language)\" title=\"Mojo (programming language)\">Mojo</a> is a non-strict<sup id=\"cite_ref-Mojo_31-1\" class=\"reference\"><a href=\"#cite_note-Mojo-31\"><span class=\"cite-bracket\">&#91;</span>29<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-237\" class=\"reference\"><a href=\"#cite_note-237\"><span class=\"cite-bracket\">&#91;</span>234<span class=\"cite-bracket\">&#93;</span></a></sup> superset of Python (e.g., omitting classes, and adding <a href=\"/wiki/Struct\" class=\"mw-redirect\" title=\"Struct\">struct</a>).<sup id=\"cite_ref-238\" class=\"reference\"><a href=\"#cite_note-238\"><span class=\"cite-bracket\">&#91;</span>235<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Nim_(programming_language)\" title=\"Nim (programming language)\">Nim</a> uses indentation and a similar syntax.<sup id=\"cite_ref-239\" class=\"reference\"><a href=\"#cite_note-239\"><span class=\"cite-bracket\">&#91;</span>236<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a>'s creator, <a href=\"/wiki/Yukihiro_Matsumoto\" title=\"Yukihiro Matsumoto\">Yukihiro Matsumoto</a>, said that \"I wanted a scripting language that was more powerful than Perl, and more object-oriented than Python. That's why I decided to design my own language.\"<sup id=\"cite_ref-linuxdevcenter_240-0\" class=\"reference\"><a href=\"#cite_note-linuxdevcenter-240\"><span class=\"cite-bracket\">&#91;</span>237<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a>, a programming language developed by Apple, has some Python-inspired syntax.<sup id=\"cite_ref-241\" class=\"reference\"><a href=\"#cite_note-241\"><span class=\"cite-bracket\">&#91;</span>238<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><a href=\"/wiki/Kotlin_(programming_language)\" title=\"Kotlin (programming language)\">Kotlin</a> blends Python and Java features, which minimizes boilerplate code and enhances developer efficiency.<sup id=\"cite_ref-242\" class=\"reference\"><a href=\"#cite_note-242\"><span class=\"cite-bracket\">&#91;</span>239<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n<p>Python's development practices have also been emulated by other languages. For example, Python requires a document that describes the rationale and context for any language change; this document is known as a <i>Python Enhancement Proposal</i> or PEP. This practice is also used by the developers of <a href=\"/wiki/Tcl\" title=\"Tcl\">Tcl</a>,<sup id=\"cite_ref-AutoNT-99_243-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-99-243\"><span class=\"cite-bracket\">&#91;</span>240<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/Erlang_(programming_language)\" title=\"Erlang (programming language)\">Erlang</a>,<sup id=\"cite_ref-AutoNT-100_244-0\" class=\"reference\"><a href=\"#cite_note-AutoNT-100-244\"><span class=\"cite-bracket\">&#91;</span>241<span class=\"cite-bracket\">&#93;</span></a></sup> and Swift.<sup id=\"cite_ref-245\" class=\"reference\"><a href=\"#cite_note-245\"><span class=\"cite-bracket\">&#91;</span>242<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"See_also\">See also</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=27\" title=\"Edit section: See also\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1266661725\">.mw-parser-output .portalbox{padding:0;margin:0.5em 0;display:table;box-sizing:border-box;max-width:175px;list-style:none}.mw-parser-output .portalborder{border:1px solid var(--border-color-base,#a2a9b1);padding:0.1em;background:var(--background-color-neutral-subtle,#f8f9fa)}.mw-parser-output .portalbox-entry{display:table-row;font-size:85%;line-height:110%;height:1.9em;font-style:italic;font-weight:bold}.mw-parser-output .portalbox-image{display:table-cell;padding:0.2em;vertical-align:middle;text-align:center}.mw-parser-output .portalbox-link{display:table-cell;padding:0.2em 0.2em 0.2em 0.3em;vertical-align:middle}@media(min-width:720px){.mw-parser-output .portalleft{margin:0.5em 1em 0.5em 0}.mw-parser-output .portalright{clear:right;float:right;margin:0.5em 0 0.5em 1em}}</style><ul role=\"navigation\" aria-label=\"Portals\" class=\"noprint portalbox portalborder portalright\">\n<li class=\"portalbox-entry\"><span class=\"portalbox-image\"><span class=\"skin-invert-image noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Octicons-terminal.svg\" class=\"mw-file-description\"><img alt=\"icon\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/40px-Octicons-terminal.svg.png\" decoding=\"async\" width=\"24\" height=\"28\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/60px-Octicons-terminal.svg.png 2x\" data-file-width=\"896\" data-file-height=\"1024\" /></a></span></span><span class=\"portalbox-link\"><a href=\"/wiki/Portal:Computer_programming\" title=\"Portal:Computer programming\">Computer programming portal</a></span></li><li class=\"portalbox-entry\"><span class=\"portalbox-image\"><span class=\"noviewer\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Free_and_open-source_software_logo_%282009%29.svg/40px-Free_and_open-source_software_logo_%282009%29.svg.png\" decoding=\"async\" width=\"28\" height=\"28\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Free_and_open-source_software_logo_%282009%29.svg/60px-Free_and_open-source_software_logo_%282009%29.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"512\" /></span></span></span><span class=\"portalbox-link\"><a href=\"/wiki/Portal:Free_and_open-source_software\" title=\"Portal:Free and open-source software\">Free and open-source software portal</a></span></li></ul>\n<ul><li><a href=\"/wiki/Google_Colab\" title=\"Google Colab\">Google Colab</a>&#160;&#8211;&#32; zero setup <a href=\"/wiki/Online_integrated_development_environment\" title=\"Online integrated development environment\">online IDE</a> that runs Python</li>\n<li><a href=\"/wiki/List_of_computer_books#Python\" title=\"List of computer books\">List of Python programming books</a></li>\n<li><a href=\"/wiki/Pip_(package_manager)\" title=\"Pip (package manager)\">pip (package manager)</a></li></ul>\n<div style=\"clear:both;\" class=\"\"></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links\">External links</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=28\" title=\"Edit section: External links\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/\">Python documentation</a></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Notes\">Notes</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=29\" title=\"Edit section: Notes\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1239543626\">.mw-parser-output .reflist{margin-bottom:0.5em;list-style-type:decimal}@media screen{.mw-parser-output .reflist{font-size:90%}}.mw-parser-output .reflist .references{font-size:100%;margin-bottom:0;list-style-type:inherit}.mw-parser-output .reflist-columns-2{column-width:30em}.mw-parser-output .reflist-columns-3{column-width:25em}.mw-parser-output .reflist-columns{margin-top:0.3em}.mw-parser-output .reflist-columns ol{margin-top:0}.mw-parser-output .reflist-columns li{page-break-inside:avoid;break-inside:avoid-column}.mw-parser-output .reflist-upper-alpha{list-style-type:upper-alpha}.mw-parser-output .reflist-upper-roman{list-style-type:upper-roman}.mw-parser-output .reflist-lower-alpha{list-style-type:lower-alpha}.mw-parser-output .reflist-lower-greek{list-style-type:lower-greek}.mw-parser-output .reflist-lower-roman{list-style-type:lower-roman}</style><div class=\"reflist reflist-lower-alpha\">\n<div class=\"mw-references-wrap\"><ol class=\"references\" data-mw-group=\"lower-alpha\">\n<li id=\"cite_note-6\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-6\">^</a></b></span> <span class=\"reference-text\">since 3.5, but those hints are ignored, except with unofficial tools<sup id=\"cite_ref-type_hint-PEP_5-0\" class=\"reference\"><a href=\"#cite_note-type_hint-PEP-5\"><span class=\"cite-bracket\">&#91;</span>5<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n<li id=\"cite_note-12\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-12\">^</a></b></span> <span class=\"reference-text\">\n<ul><li><b>Tier 1</b>: 64-bit <a href=\"/wiki/Linux\" title=\"Linux\">Linux</a>, <a href=\"/wiki/MacOS\" title=\"MacOS\">macOS</a>; 64- and 32-bit <a href=\"/wiki/Windows\" class=\"mw-redirect\" title=\"Windows\">Windows</a> 10+<sup id=\"cite_ref-7\" class=\"reference\"><a href=\"#cite_note-7\"><span class=\"cite-bracket\">&#91;</span>6<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li><b>Tier 2</b>: E.g. 32-bit <a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a> (WASI)</li>\n<li><b>Tier 3</b>: 64-bit <a href=\"/wiki/Android_(operating_system)\" title=\"Android (operating system)\">Android</a>,<sup id=\"cite_ref-8\" class=\"reference\"><a href=\"#cite_note-8\"><span class=\"cite-bracket\">&#91;</span>7<span class=\"cite-bracket\">&#93;</span></a></sup> <a href=\"/wiki/IOS\" title=\"IOS\">iOS</a>, <a href=\"/wiki/FreeBSD\" title=\"FreeBSD\">FreeBSD</a>, and (32-bit) <a href=\"/wiki/Raspberry_Pi_OS\" title=\"Raspberry Pi OS\">Raspberry Pi OS</a><br />Unofficial (or has been known to work): Other <a href=\"/wiki/Unix-like\" title=\"Unix-like\">Unix-like</a>/<a href=\"/wiki/BSD\" class=\"mw-redirect\" title=\"BSD\">BSD</a> variants) and a few other platforms<sup id=\"cite_ref-9\" class=\"reference\"><a href=\"#cite_note-9\"><span class=\"cite-bracket\">&#91;</span>8<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-10\" class=\"reference\"><a href=\"#cite_note-10\"><span class=\"cite-bracket\">&#91;</span>9<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-11\" class=\"reference\"><a href=\"#cite_note-11\"><span class=\"cite-bracket\">&#91;</span>10<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n</span></li>\n<li id=\"cite_note-87\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-87\">^</a></b></span> <span class=\"reference-text\"><code>del</code> in Python does not behave the same way <code>delete</code> in languages such as <a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a> does, where such a word is used to call the <a href=\"/wiki/Destructor_(computer_programming)\" title=\"Destructor (computer programming)\">destructor</a> and deallocate heap memory.</span>\n</li>\n</ol></div></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"References\">References</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=30\" title=\"Edit section: References\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239543626\" /><div class=\"reflist reflist-columns references-column-width\" style=\"column-width: 25em;\">\n<ol class=\"references\">\n<li id=\"cite_note-1\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-1\">^</a></b></span> <span class=\"reference-text\"><style data-mw-deduplicate=\"TemplateStyles:r1238218222\">.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}.mw-parser-output .citation q{quotes:\"\\\"\"\"\\\"\"\"'\"\"'\"}.mw-parser-output .citation:target{background-color:rgba(0,127,255,0.133)}.mw-parser-output .id-lock-free.id-lock-free a{background:url(\"//upload.wikimedia.org/wikipedia/commons/6/65/Lock-green.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-limited.id-lock-limited a,.mw-parser-output .id-lock-registration.id-lock-registration a{background:url(\"//upload.wikimedia.org/wikipedia/commons/d/d6/Lock-gray-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-subscription.id-lock-subscription a{background:url(\"//upload.wikimedia.org/wikipedia/commons/a/aa/Lock-red-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .cs1-ws-icon a{background:url(\"//upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg\")right 0.1em center/12px no-repeat}body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-free a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-limited a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-registration a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-subscription a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .cs1-ws-icon a{background-size:contain;padding:0 1em 0 0}.mw-parser-output .cs1-code{color:inherit;background:inherit;border:none;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;color:var(--color-error,#d33)}.mw-parser-output .cs1-visible-error{color:var(--color-error,#d33)}.mw-parser-output .cs1-maint{display:none;color:#085;margin-left:0.3em}.mw-parser-output .cs1-kern-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right{padding-right:0.2em}.mw-parser-output .citation .mw-selflink{font-weight:inherit}@media screen{.mw-parser-output .cs1-format{font-size:95%}html.skin-theme-clientpref-night .mw-parser-output .cs1-maint{color:#18911f}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .cs1-maint{color:#18911f}}</style><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/faq/general.html#what-is-python\">\"General Python FAQ – Python 3 documentation\"</a>. <i>docs.python.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 July</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=General+Python+FAQ+%E2%80%93+Python+3+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Ffaq%2Fgeneral.html%23what-is-python&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-alt-sources-history-2\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-alt-sources-history_2-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.tuhs.org/Usenet/alt.sources/1991-February/001749.html\">\"Python 0.9.1 part 01/21\"</a>. alt.sources archives. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210811171015/https://www.tuhs.org/Usenet/alt.sources/1991-February/001749.html\">Archived</a> from the original on 11 August 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 August</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+0.9.1+part+01%2F21&amp;rft.pub=alt.sources+archives&amp;rft_id=https%3A%2F%2Fwww.tuhs.org%2FUsenet%2Falt.sources%2F1991-February%2F001749.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-wikidata-94d5b2798914e50f7103b872a798e54cb7cdf1fe-v20-3\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-wikidata-94d5b2798914e50f7103b872a798e54cb7cdf1fe-v20_3-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://blog.python.org/2025/08/python-3140rc2-and-3137-are-go.html\">\"Python Insider: Python 3.14.0rc2 and 3.13.7 are go!\"</a>. 14 August 2025<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">14 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+Insider%3A+Python+3.14.0rc2+and+3.13.7+are+go%21&amp;rft.date=2025-08-14&amp;rft_id=https%3A%2F%2Fblog.python.org%2F2025%2F08%2Fpython-3140rc2-and-3137-are-go.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-4\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-4\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.python.org/moin/Why%20is%20Python%20a%20dynamic%20language%20and%20also%20a%20strongly%20typed%20language\">\"Why is Python a dynamic language and also a strongly typed language\"</a>. <i>Python Wiki</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210314173706/https://wiki.python.org/moin/Why%20is%20Python%20a%20dynamic%20language%20and%20also%20a%20strongly%20typed%20language\">Archived</a> from the original on 14 March 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Wiki&amp;rft.atitle=Why+is+Python+a+dynamic+language+and+also+a+strongly+typed+language&amp;rft_id=https%3A%2F%2Fwiki.python.org%2Fmoin%2FWhy%2520is%2520Python%2520a%2520dynamic%2520language%2520and%2520also%2520a%2520strongly%2520typed%2520language&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-type_hint-PEP-5\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-type_hint-PEP_5-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-type_hint-PEP_5-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0483/\">\"PEP 483 – The Theory of Type Hints\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614153558/https://www.python.org/dev/peps/pep-0483/\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">14 June</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+483+%E2%80%93+The+Theory+of+Type+Hints&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0483%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-7\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-7\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://peps.python.org/pep-0011/\">\"PEP 11 – CPython platform support | peps.python.org\"</a>. <i>Python Enhancement Proposals (PEPs)</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 April</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals+%28PEPs%29&amp;rft.atitle=PEP+11+%E2%80%93+CPython+platform+support+%7C+peps.python.org&amp;rft_id=https%3A%2F%2Fpeps.python.org%2Fpep-0011%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-8\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-8\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://peps.python.org/pep-0738/\">\"PEP 738 – Adding Android as a supported platform | peps.python.org\"</a>. <i>Python Enhancement Proposals (PEPs)</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 May</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals+%28PEPs%29&amp;rft.atitle=PEP+738+%E2%80%93+Adding+Android+as+a+supported+platform+%7C+peps.python.org&amp;rft_id=https%3A%2F%2Fpeps.python.org%2Fpep-0738%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-9\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-9\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/download/other/\">\"Download Python for Other Platforms\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201127015815/https://www.python.org/download/other/\">Archived</a> from the original on 27 November 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 August</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Download+Python+for+Other+Platforms&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownload%2Fother%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-10\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-10\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.7/library/test.html?highlight=android#test.support.is_android\">\"test – Regression tests package for Python – Python 3.7.13 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220517151240/https://docs.python.org/3.7/library/test.html?highlight=android#test.support.is_android\">Archived</a> from the original on 17 May 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 May</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=test+%E2%80%93+Regression+tests+package+for+Python+%E2%80%93+Python+3.7.13+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.7%2Flibrary%2Ftest.html%3Fhighlight%3Dandroid%23test.support.is_android&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-11\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-11\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/platform.html?highlight=android\">\"platform – Access to underlying platform's identifying data – Python 3.10.4 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220517150826/https://docs.python.org/3/library/platform.html?highlight=android\">Archived</a> from the original on 17 May 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 May</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=platform+%E2%80%93+Access+to+underlying+platform%27s+identifying+data+%E2%80%93+Python+3.10.4+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fplatform.html%3Fhighlight%3Dandroid&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-13\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-13\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHolth2014\" class=\"citation web cs1\">Holth, Moore (30 March 2014). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0441/\">\"PEP 0441 – Improving Python ZIP Application Support\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181226141117/https://www.python.org/dev/peps/pep-0441/%20\">Archived</a> from the original on 26 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">12 November</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PEP+0441+%E2%80%93+Improving+Python+ZIP+Application+Support&amp;rft.date=2014-03-30&amp;rft.aulast=Holth&amp;rft.aufirst=Moore&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0441%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-14\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-14\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.bazel.build/versions/master/skylark/language.html\">\"Starlark Language\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615140534/https://docs.bazel.build/versions/master/skylark/language.html\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Starlark+Language&amp;rft_id=https%3A%2F%2Fdocs.bazel.build%2Fversions%2Fmaster%2Fskylark%2Flanguage.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-faq-created-15\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-faq-created_15-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-faq-created_15-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/faq/general.html#why-was-python-created-in-the-first-place\">\"Why was Python created in the first place?\"</a>. <i>General Python FAQ</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121024164224/http://docs.python.org/faq/general.html#why-was-python-created-in-the-first-place\">Archived</a> from the original on 24 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 March</span> 2007</span>. <q>I had extensive experience with implementing an interpreted language in the ABC group at CWI, and from working with this group I had learned a lot about language design. This is the origin of many Python features, including the use of indentation for statement grouping and the inclusion of very high-level data types (although the details are all different in Python).</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=General+Python+FAQ&amp;rft.atitle=Why+was+Python+created+in+the+first+place%3F&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Ffaq%2Fgeneral.html%23why-was-python-created-in-the-first-place&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-16\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-16\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191022155758/http://archive.adaic.com/standards/83lrm/html/lrm-11-03.html#11.3\">\"Ada 83 Reference Manual (raise statement)\"</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://archive.adaic.com/standards/83lrm/html/lrm-11-03.html#11.3\">the original</a> on 22 October 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 January</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Ada+83+Reference+Manual+%28raise+statement%29&amp;rft_id=https%3A%2F%2Farchive.adaic.com%2Fstandards%2F83lrm%2Fhtml%2Flrm-11-03.html%2311.3&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-98-interview-17\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-98-interview_17-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-98-interview_17-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKuchling2006\" class=\"citation web cs1\">Kuchling, Andrew M. (22 December 2006). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20070501105422/http://www.amk.ca/python/writing/gvr-interview\">\"Interview with Guido van Rossum (July 1998)\"</a>. <i>amk.ca</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.amk.ca/python/writing/gvr-interview\">the original</a> on 1 May 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">12 March</span> 2012</span>. <q>I'd spent a summer at DEC's Systems Research Center, which introduced me to Modula-2+; the Modula-3 final report was being written there at about the same time. What I learned there later showed up in Python's exception handling, modules, and the fact that methods explicitly contain 'self' in their parameter list. String slicing came from Algol-68 and Icon.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=amk.ca&amp;rft.atitle=Interview+with+Guido+van+Rossum+%28July+1998%29&amp;rft.date=2006-12-22&amp;rft.aulast=Kuchling&amp;rft.aufirst=Andrew+M.&amp;rft_id=http%3A%2F%2Fwww.amk.ca%2Fpython%2Fwriting%2Fgvr-interview&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-python.org-18\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-python.org_18-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-python.org_18-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-python.org_18-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/itertools.html\">\"itertools – Functions creating iterators for efficient looping – Python 3.7.1 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614153629/https://docs.python.org/3/library/itertools.html\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 November</span> 2016</span>. <q>This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=itertools+%E2%80%93+Functions+creating+iterators+for+efficient+looping+%E2%80%93+Python+3.7.1+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fitertools.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-1-19\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-1_19-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum1993\" class=\"citation journal cs1\">van Rossum, Guido (1993). \"An Introduction to Python for UNIX/C Programmers\". <i>Proceedings of the NLUUG Najaarsconferentie (Dutch UNIX Users Group)</i>. <a href=\"/wiki/CiteSeerX_(identifier)\" class=\"mw-redirect\" title=\"CiteSeerX (identifier)\">CiteSeerX</a>&#160;<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.38.2023\">10.1.1.38.2023</a></span>. <q>even though the design of C is far from ideal, its influence on Python is considerable.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Proceedings+of+the+NLUUG+Najaarsconferentie+%28Dutch+UNIX+Users+Group%29&amp;rft.atitle=An+Introduction+to+Python+for+UNIX%2FC+Programmers&amp;rft.date=1993&amp;rft_id=https%3A%2F%2Fciteseerx.ist.psu.edu%2Fviewdoc%2Fsummary%3Fdoi%3D10.1.1.38.2023%23id-name%3DCiteSeerX&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-classmix-20\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-classmix_20-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-classmix_20-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/tutorial/classes.html\">\"Classes\"</a>. <i>The Python Tutorial</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121023030209/http://docs.python.org/tutorial/classes.html\">Archived</a> from the original on 23 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 February</span> 2012</span>. <q>It is a mixture of the class mechanisms found in C++ and Modula-3</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Python+Tutorial&amp;rft.atitle=Classes&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Ftutorial%2Fclasses.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-effbot-call-by-object-21\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-effbot-call-by-object_21-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLundh\" class=\"citation web cs1\">Lundh, Fredrik. <a rel=\"nofollow\" class=\"external text\" href=\"http://effbot.org/zone/call-by-object.htm\">\"Call By Object\"</a>. <i>effbot.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191123043655/http://effbot.org/zone/call-by-object.htm\">Archived</a> from the original on 23 November 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">21 November</span> 2017</span>. <q>replace \"CLU\" with \"Python\", \"record\" with \"instance\", and \"procedure\" with \"function or method\", and you get a pretty accurate description of Python's object model.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=effbot.org&amp;rft.atitle=Call+By+Object&amp;rft.aulast=Lundh&amp;rft.aufirst=Fredrik&amp;rft_id=http%3A%2F%2Feffbot.org%2Fzone%2Fcall-by-object.htm&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-2-22\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-2_22-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSimionato\" class=\"citation web cs1\">Simionato, Michele. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/download/releases/2.3/mro/\">\"The Python 2.3 Method Resolution Order\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200820231854/https://www.python.org/download/releases/2.3/mro/\">Archived</a> from the original on 20 August 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 July</span> 2014</span>. <q>The C3 method itself has nothing to do with Python, since it was invented by people working on Dylan and it is described in a paper intended for lispers</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Python+2.3+Method+Resolution+Order&amp;rft.pub=Python+Software+Foundation&amp;rft.aulast=Simionato&amp;rft.aufirst=Michele&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownload%2Freleases%2F2.3%2Fmro%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-3-23\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-3_23-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKuchling\" class=\"citation web cs1\">Kuchling, A. M. <a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/howto/functional.html\">\"Functional Programming HOWTO\"</a>. <i>Python v2.7.2 documentation</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121024163217/http://docs.python.org/howto/functional.html\">Archived</a> from the original on 24 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 February</span> 2012</span>. <q>List comprehensions and generator expressions [...] are a concise notation for such operations, borrowed from the functional programming language Haskell.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+v2.7.2+documentation&amp;rft.atitle=Functional+Programming+HOWTO&amp;rft.aulast=Kuchling&amp;rft.aufirst=A.+M.&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Fhowto%2Ffunctional.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-4-24\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-4_24-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSchemenauerPetersHetland2001\" class=\"citation web cs1\">Schemenauer, Neil; Peters, Tim; Hetland, Magnus Lie (18 May 2001). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0255/\">\"PEP 255&#160;– Simple Generators\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605012926/https://www.python.org/dev/peps/pep-0255/\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+255+%E2%80%93+Simple+Generators&amp;rft.date=2001-05-18&amp;rft.aulast=Schemenauer&amp;rft.aufirst=Neil&amp;rft.au=Peters%2C+Tim&amp;rft.au=Hetland%2C+Magnus+Lie&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0255%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-6-25\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-6_25-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.2/tutorial/controlflow.html\">\"More Control Flow Tools\"</a>. <i>Python 3 documentation</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160604080843/https://docs.python.org/3.2/tutorial/controlflow.html\">Archived</a> from the original on 4 June 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 July</span> 2015</span>. <q>By popular demand, a few features commonly found in functional programming languages like Lisp have been added to Python. With the lambda keyword, small anonymous functions can be created.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+3+documentation&amp;rft.atitle=More+Control+Flow+Tools&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.2%2Ftutorial%2Fcontrolflow.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-26\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-26\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/re.html\">\"re – Regular expression operations – Python 3.10.6 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180718132241/https://docs.python.org/3/library/re.html\">Archived</a> from the original on 18 July 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 September</span> 2022</span>. <q>This module provides regular expression matching operations similar to those found in Perl.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=re+%E2%80%93+Regular+expression+operations+%E2%80%93+Python+3.10.6+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fre.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-27\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-27\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://coffeescript.org/\">\"CoffeeScript\"</a>. <i>coffeescript.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200612100004/http://coffeescript.org/\">Archived</a> from the original on 12 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 July</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=coffeescript.org&amp;rft.atitle=CoffeeScript&amp;rft_id=https%3A%2F%2Fcoffeescript.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-28\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-28\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.2ality.com/2013/02/javascript-influences.html\">\"Perl and Python influences in JavaScript\"</a>. <i>www.2ality.com</i>. 24 February 2013. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181226141121/http://2ality.com/2013/02/javascript-influences.html%0A\">Archived</a> from the original on 26 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 May</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=www.2ality.com&amp;rft.atitle=Perl+and+Python+influences+in+JavaScript&amp;rft.date=2013-02-24&amp;rft_id=https%3A%2F%2Fwww.2ality.com%2F2013%2F02%2Fjavascript-influences.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-29\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-29\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRauschmayer\" class=\"citation web cs1\">Rauschmayer, Axel. <a rel=\"nofollow\" class=\"external text\" href=\"https://speakingjs.com/es5/ch03.html\">\"Chapter 3: The Nature of JavaScript; Influences\"</a>. <i>O'Reilly, Speaking JavaScript</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181226141123/http://speakingjs.com/es5/ch03.html%0A\">Archived</a> from the original on 26 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 May</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=O%27Reilly%2C+Speaking+JavaScript&amp;rft.atitle=Chapter+3%3A+The+Nature+of+JavaScript%3B+Influences&amp;rft.aulast=Rauschmayer&amp;rft.aufirst=Axel&amp;rft_id=https%3A%2F%2Fspeakingjs.com%2Fes5%2Fch03.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Julia-30\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-Julia_30-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-Julia_30-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://julialang.org/blog/2012/02/why-we-created-julia\">\"Why We Created Julia\"</a>. <i>Julia website</i>. February 2012. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200502144010/https://julialang.org/blog/2012/02/why-we-created-julia/\">Archived</a> from the original on 2 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 June</span> 2014</span>. <q>We want something as usable for general programming as Python [...]</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Julia+website&amp;rft.atitle=Why+We+Created+Julia&amp;rft.date=2012-02&amp;rft_id=https%3A%2F%2Fjulialang.org%2Fblog%2F2012%2F02%2Fwhy-we-created-julia&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Mojo-31\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-Mojo_31-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-Mojo_31-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKrill2023\" class=\"citation web cs1\">Krill, Paul (4 May 2023). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3695588/mojo-language-marries-python-and-mlir-for-ai-development.html\">\"Mojo language marries Python and MLIR for AI development\"</a>. <i>InfoWorld</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230505064554/https://www.infoworld.com/article/3695588/mojo-language-marries-python-and-mlir-for-ai-development.html\">Archived</a> from the original on 5 May 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 May</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoWorld&amp;rft.atitle=Mojo+language+marries+Python+and+MLIR+for+AI+development&amp;rft.date=2023-05-04&amp;rft.aulast=Krill&amp;rft.aufirst=Paul&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3695588%2Fmojo-language-marries-python-and-mlir-for-ai-development.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-The_Ring_programming_language_and_other_languages-32\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-The_Ring_programming_language_and_other_languages_32-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRing_Team2017\" class=\"citation web cs1\">Ring Team (4 December 2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://ring-lang.sourceforge.net/doc1.6/introduction.html#ring-and-other-languages\">\"Ring and other languages\"</a>. <i>ring-lang.net</i>. <a href=\"/w/index.php?title=Ring-lang&amp;action=edit&amp;redlink=1\" class=\"new\" title=\"Ring-lang (page does not exist)\">ring-lang</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181225175312/http://ring-lang.sourceforge.net/doc1.6/introduction.html#ring-and-other-languages\">Archived</a> from the original on 25 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 December</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ring-lang.net&amp;rft.atitle=Ring+and+other+languages&amp;rft.date=2017-12-04&amp;rft.au=Ring+Team&amp;rft_id=https%3A%2F%2Fring-lang.sourceforge.net%2Fdoc1.6%2Fintroduction.html%23ring-and-other-languages&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-bini-33\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-bini_33-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBini2007\" class=\"citation book cs1\">Bini, Ola (2007). <span class=\"id-lock-registration\" title=\"Free registration required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/practicaljrubyon0000bini/page/3\"><i>Practical JRuby on Rails Web 2.0 Projects: bringing Ruby on Rails to the Java platform</i></a></span>. Berkeley: APress. p.&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/practicaljrubyon0000bini/page/3\">3</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-59059-881-8\" title=\"Special:BookSources/978-1-59059-881-8\"><bdi>978-1-59059-881-8</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Practical+JRuby+on+Rails+Web+2.0+Projects%3A+bringing+Ruby+on+Rails+to+the+Java+platform&amp;rft.place=Berkeley&amp;rft.pages=3&amp;rft.pub=APress&amp;rft.date=2007&amp;rft.isbn=978-1-59059-881-8&amp;rft.aulast=Bini&amp;rft.aufirst=Ola&amp;rft_id=https%3A%2F%2Farchive.org%2Fdetails%2Fpracticaljrubyon0000bini%2Fpage%2F3&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-lattner2014-34\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-lattner2014_34-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLattner2014\" class=\"citation web cs1\">Lattner, Chris (3 June 2014). <a rel=\"nofollow\" class=\"external text\" href=\"http://nondot.org/sabre/\">\"Chris Lattner's Homepage\"</a>. Chris Lattner. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181225175312/http://nondot.org/sabre/\">Archived</a> from the original on 25 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 June</span> 2014</span>. <q>The Swift language is the product of tireless effort from a team of language experts, documentation gurus, compiler optimization ninjas, and an incredibly important internal dogfooding group who provided feedback to help refine and battle-test ideas. Of course, it also greatly benefited from the experiences hard-won by many other languages in the field, drawing ideas from Objective-C, Rust, Haskell, Ruby, Python, C#, CLU, and far too many others to list.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Chris+Lattner%27s+Homepage&amp;rft.pub=Chris+Lattner&amp;rft.date=2014-06-03&amp;rft.aulast=Lattner&amp;rft.aufirst=Chris&amp;rft_id=http%3A%2F%2Fnondot.org%2Fsabre%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-vpeople-35\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-vpeople_35-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/vlang/v/blob/master/doc/docs.md#introduction\">\"V documentation (Introduction)\"</a>. <i>GitHub</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 December</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=V+documentation+%28Introduction%29&amp;rft_id=https%3A%2F%2Fgithub.com%2Fvlang%2Fv%2Fblob%2Fmaster%2Fdoc%2Fdocs.md%23introduction&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-7-36\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-7_36-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKuhlman\" class=\"citation web cs1\">Kuhlman, Dave. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120623165941/http://cutter.rexx.com/~dkuhlman/python_book_01.html\">\"A Python Book: Beginning Python, Advanced Python, and Python Exercises\"</a>. Section 1.1. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.davekuhlman.org/python_book_01.pdf\">the original</a> <span class=\"cs1-format\">(PDF)</span> on 23 June 2012.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=A+Python+Book%3A+Beginning+Python%2C+Advanced+Python%2C+and+Python+Exercises&amp;rft.pages=Section+1.1&amp;rft.aulast=Kuhlman&amp;rft.aufirst=Dave&amp;rft_id=https%3A%2F%2Fwww.davekuhlman.org%2Fpython_book_01.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-37\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-37\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://mypy-lang.org/\">\"mypy - Optional Static Typing for Python\"</a>. <i>mypy-lang.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=mypy-lang.org&amp;rft.atitle=mypy+-+Optional+Static+Typing+for+Python&amp;rft_id=https%3A%2F%2Fmypy-lang.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-38\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-38\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://survey.stackoverflow.co/2022/\">\"Stack Overflow Developer Survey 2022\"</a>. <i>Stack Overflow</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220627175307/https://survey.stackoverflow.co/2022/\">Archived</a> from the original on 27 June 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">12 August</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Stack+Overflow&amp;rft.atitle=Stack+Overflow+Developer+Survey+2022&amp;rft_id=https%3A%2F%2Fsurvey.stackoverflow.co%2F2022%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-39\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-39\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.jetbrains.com/lp/devecosystem-2020/\">\"The State of Developer Ecosystem in 2020 Infographic\"</a>. <i>JetBrains: Developer Tools for Professionals and Teams</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210301062411/https://www.jetbrains.com/lp/devecosystem-2020/\">Archived</a> from the original on 1 March 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 March</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=JetBrains%3A+Developer+Tools+for+Professionals+and+Teams&amp;rft.atitle=The+State+of+Developer+Ecosystem+in+2020+Infographic&amp;rft_id=https%3A%2F%2Fwww.jetbrains.com%2Flp%2Fdevecosystem-2020%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-tiobecurrent-40\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-tiobecurrent_40-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-tiobecurrent_40-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.tiobe.com/tiobe-index/\">\"TIOBE Index\"</a>. TIOBE. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180225101948/https://www.tiobe.com/tiobe-index/\">Archived</a> from the original on 25 February 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 January</span> 2023</span>. <q>The TIOBE Programming Community index is an indicator of the popularity of programming languages</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=TIOBE+Index&amp;rft.pub=TIOBE&amp;rft_id=https%3A%2F%2Fwww.tiobe.com%2Ftiobe-index%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span> Updated as required.</span>\n</li>\n<li id=\"cite_note-41\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-41\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHealyMcInnesWeir2017\" class=\"citation journal cs1\">Healy, John; McInnes, Leland; Weir, Colin (2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.jstor.org/stable/26267404\">\"Bridging the Cyber-Analysis Gap: The Democratization of Data Science\"</a>. <i>The Cyber Defense Review</i>. <b>2</b> (1): <span class=\"nowrap\">109–</span>118. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/2474-2120\">2474-2120</a>. <a href=\"/wiki/JSTOR_(identifier)\" class=\"mw-redirect\" title=\"JSTOR (identifier)\">JSTOR</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.jstor.org/stable/26267404\">26267404</a>. <q>Python is the lingua franca of data science and machine learning.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Cyber+Defense+Review&amp;rft.atitle=Bridging+the+Cyber-Analysis+Gap%3A+The+Democratization+of+Data+Science&amp;rft.volume=2&amp;rft.issue=1&amp;rft.pages=109-118&amp;rft.date=2017&amp;rft_id=https%3A%2F%2Fwww.jstor.org%2Fstable%2F26267404%23id-name%3DJSTOR&amp;rft.issn=2474-2120&amp;rft.aulast=Healy&amp;rft.aufirst=John&amp;rft.au=McInnes%2C+Leland&amp;rft.au=Weir%2C+Colin&amp;rft_id=https%3A%2F%2Fwww.jstor.org%2Fstable%2F26267404&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-42\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-42\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSultanaReed2017\" class=\"citation journal cs1\">Sultana, Simon G.; Reed, Philip A. (2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.jstor.org/stable/90023144\">\"Curriculum for an Introductory Computer Science Course: Identifying Recommendations from Academia and Industry\"</a>. <i>The Journal of Technology Studies</i>. <b>43</b> (2): <span class=\"nowrap\">80–</span>92. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.21061%2Fjots.v43i2.a.3\">10.21061/jots.v43i2.a.3</a>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/1071-6084\">1071-6084</a>. <a href=\"/wiki/JSTOR_(identifier)\" class=\"mw-redirect\" title=\"JSTOR (identifier)\">JSTOR</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.jstor.org/stable/90023144\">90023144</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Journal+of+Technology+Studies&amp;rft.atitle=Curriculum+for+an+Introductory+Computer+Science+Course%3A+Identifying+Recommendations+from+Academia+and+Industry&amp;rft.volume=43&amp;rft.issue=2&amp;rft.pages=80-92&amp;rft.date=2017&amp;rft.issn=1071-6084&amp;rft_id=https%3A%2F%2Fwww.jstor.org%2Fstable%2F90023144%23id-name%3DJSTOR&amp;rft_id=info%3Adoi%2F10.21061%2Fjots.v43i2.a.3&amp;rft.aulast=Sultana&amp;rft.aufirst=Simon+G.&amp;rft.au=Reed%2C+Philip+A.&amp;rft_id=https%3A%2F%2Fwww.jstor.org%2Fstable%2F90023144&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-venners-interview-pt-1-43\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-venners-interview-pt-1_43-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-venners-interview-pt-1_43-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFVenners2003\" class=\"citation web cs1\">Venners, Bill (13 January 2003). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.artima.com/intv/pythonP.html\">\"The Making of Python\"</a>. <i>Artima Developer</i>. Artima. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160901183332/http://www.artima.com/intv/pythonP.html\">Archived</a> from the original on 1 September 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 March</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Artima+Developer&amp;rft.atitle=The+Making+of+Python&amp;rft.date=2003-01-13&amp;rft.aulast=Venners&amp;rft.aufirst=Bill&amp;rft_id=http%3A%2F%2Fwww.artima.com%2Fintv%2FpythonP.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-:2-44\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-:2_44-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-:2_44-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRossum2009\" class=\"citation web cs1\">Rossum, Guido Van (20 January 2009). <a rel=\"nofollow\" class=\"external text\" href=\"https://python-history.blogspot.com/2009/01/brief-timeline-of-python.html\">\"The History of Python: A Brief Timeline of Python\"</a>. <i>The History of Python</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605032200/https://python-history.blogspot.com/2009/01/brief-timeline-of-python.html\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 March</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+History+of+Python&amp;rft.atitle=The+History+of+Python%3A+A+Brief+Timeline+of+Python&amp;rft.date=2009-01-20&amp;rft.aulast=Rossum&amp;rft.aufirst=Guido+Van&amp;rft_id=https%3A%2F%2Fpython-history.blogspot.com%2F2009%2F01%2Fbrief-timeline-of-python.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-12-45\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-12_45-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum2000\" class=\"citation mailinglist cs1\"><a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">van Rossum, Guido</a> (29 August 2000). <a rel=\"nofollow\" class=\"external text\" href=\"https://mail.python.org/pipermail/python-dev/2000-August/008881.html\">\"SETL (was: Lukewarm about range literals)\"</a>. <i>Python-Dev</i> (Mailing list). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180714064019/https://mail.python.org/pipermail/python-dev/2000-August/008881.html\">Archived</a> from the original on 14 July 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">13 March</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=SETL+%28was%3A+Lukewarm+about+range+literals%29&amp;rft.date=2000-08-29&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft_id=https%3A%2F%2Fmail.python.org%2Fpipermail%2Fpython-dev%2F2000-August%2F008881.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-timeline-of-python-46\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-timeline-of-python_46-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum2009\" class=\"citation web cs1\">van Rossum, Guido (20 January 2009). <a rel=\"nofollow\" class=\"external text\" href=\"https://python-history.blogspot.com/2009/01/brief-timeline-of-python.html\">\"A Brief Timeline of Python\"</a>. <i>The History of Python</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605032200/https://python-history.blogspot.com/2009/01/brief-timeline-of-python.html\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 January</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+History+of+Python&amp;rft.atitle=A+Brief+Timeline+of+Python&amp;rft.date=2009-01-20&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft_id=https%3A%2F%2Fpython-history.blogspot.com%2F2009%2F01%2Fbrief-timeline-of-python.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-lj-bdfl-resignation-47\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-lj-bdfl-resignation_47-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFFairchild2018\" class=\"citation magazine cs1\">Fairchild, Carlie (12 July 2018). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.linuxjournal.com/content/guido-van-rossum-stepping-down-role-pythons-benevolent-dictator-life\">\"Guido van Rossum Stepping Down from Role as Python's Benevolent Dictator For Life\"</a>. <i>Linux Journal</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180713192427/https://www.linuxjournal.com/content/guido-van-rossum-stepping-down-role-pythons-benevolent-dictator-life\">Archived</a> from the original on 13 July 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">13 July</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Linux+Journal&amp;rft.atitle=Guido+van+Rossum+Stepping+Down+from+Role+as+Python%27s+Benevolent+Dictator+For+Life&amp;rft.date=2018-07-12&amp;rft.aulast=Fairchild&amp;rft.aufirst=Carlie&amp;rft_id=https%3A%2F%2Fwww.linuxjournal.com%2Fcontent%2Fguido-van-rossum-stepping-down-role-pythons-benevolent-dictator-life&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-48\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-48\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-8100/\">\"PEP 8100\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604235027/https://www.python.org/dev/peps/pep-8100/\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PEP+8100&amp;rft.pub=Python+Software+Foundation&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-8100%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-49\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-49\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0013/\">\"PEP 13 – Python Language Governance\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210527000035/https://www.python.org/dev/peps/pep-0013/\">Archived</a> from the original on 27 May 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 August</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+13+%E2%80%93+Python+Language+Governance&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0013%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-:0-50\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-:0_50-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBriggsLipovača2013\" class=\"citation book cs1\">Briggs, Jason R.; Lipovača, Miran (2013). <i>Python for kids: a playful introduction to programming</i>. San Francisco, Calif: No Starch Press. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-59327-407-8\" title=\"Special:BookSources/978-1-59327-407-8\"><bdi>978-1-59327-407-8</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Python+for+kids%3A+a+playful+introduction+to+programming&amp;rft.place=San+Francisco%2C+Calif&amp;rft.pub=No+Starch+Press&amp;rft.date=2013&amp;rft.isbn=978-1-59327-407-8&amp;rft.aulast=Briggs&amp;rft.aufirst=Jason+R.&amp;rft.au=Lipova%C4%8Da%2C+Miran&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-newin-2.0-51\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-newin-2.0_51-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKuchlingZadka2000\" class=\"citation web cs1\">Kuchling, A. M.; Zadka, Moshe (16 October 2000). <a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/whatsnew/2.0.html\">\"What's New in Python 2.0\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121023112045/http://docs.python.org/whatsnew/2.0.html\">Archived</a> from the original on 23 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What%27s+New+in+Python+2.0&amp;rft.pub=Python+Software+Foundation&amp;rft.date=2000-10-16&amp;rft.aulast=Kuchling&amp;rft.aufirst=A.+M.&amp;rft.au=Zadka%2C+Moshe&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Fwhatsnew%2F2.0.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-52\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-52\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://legacy.python.org/dev/peps/pep-0373/\">\"PEP 373 – Python 2.7 Release Schedule\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200519075520/https://legacy.python.org/dev/peps/pep-0373/\">Archived</a> from the original on 19 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=PEP+373+%E2%80%93+Python+2.7+Release+Schedule&amp;rft_id=https%3A%2F%2Flegacy.python.org%2Fdev%2Fpeps%2Fpep-0373%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-53\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-53\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0466/\">\"PEP 466 – Network Security Enhancements for Python 2.7.x\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604232833/https://www.python.org/dev/peps/pep-0466/\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=PEP+466+%E2%80%93+Network+Security+Enhancements+for+Python+2.7.x&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0466%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-54\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-54\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/doc/sunset-python-2/\">\"Sunsetting Python 2\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200112080903/https://www.python.org/doc/sunset-python-2/\">Archived</a> from the original on 12 January 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 September</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Sunsetting+Python+2&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdoc%2Fsunset-python-2%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-55\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-55\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0373/\">\"PEP 373 – Python 2.7 Release Schedule\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200113033257/https://www.python.org/dev/peps/pep-0373/\">Archived</a> from the original on 13 January 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 September</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+373+%E2%80%93+Python+2.7+Release+Schedule&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0373%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-56\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-56\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFmattip2023\" class=\"citation web cs1\">mattip (25 December 2023). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.pypy.org/posts/2023/12/pypy-v7314-release.html\">\"PyPy v7.3.14 release\"</a>. <i>PyPy</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240105132820/https://www.pypy.org/posts/2023/12/pypy-v7314-release.html\">Archived</a> from the original on 5 January 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 January</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=PyPy&amp;rft.atitle=PyPy+v7.3.14+release&amp;rft.date=2023-12-25&amp;rft.au=mattip&amp;rft_id=https%3A%2F%2Fwww.pypy.org%2Fposts%2F2023%2F12%2Fpypy-v7314-release.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-57\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-57\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPeterson2020\" class=\"citation web cs1\">Peterson, Benjamin (20 April 2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://pythoninsider.blogspot.com/2020/04/python-2718-last-release-of-python-2.html\">\"Python 2.7.18, the last release of Python 2\"</a>. <i>Python Insider</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200426204118/https://pythoninsider.blogspot.com/2020/04/python-2718-last-release-of-python-2.html\">Archived</a> from the original on 26 April 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 April</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Insider&amp;rft.atitle=Python+2.7.18%2C+the+last+release+of+Python+2&amp;rft.date=2020-04-20&amp;rft.aulast=Peterson&amp;rft.aufirst=Benjamin&amp;rft_id=https%3A%2F%2Fpythoninsider.blogspot.com%2F2020%2F04%2Fpython-2718-last-release-of-python-2.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-58\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-58\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://devguide.python.org/versions/\">\"Status of Python versions\"</a>. <i>Python Developer's Guide</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Developer%27s+Guide&amp;rft.atitle=Status+of+Python+versions&amp;rft_id=https%3A%2F%2Fdevguide.python.org%2Fversions%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-13-59\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-13_59-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFThe_Cain_Gang_Ltd.\" class=\"citation web cs1\">The Cain Gang Ltd. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20090530030205/http://www.python.org/community/pycon/dc2004/papers/24/metaclasses-pycon.pdf\">\"Python Metaclasses: Who? Why? When?\"</a> <span class=\"cs1-format\">(PDF)</span>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/community/pycon/dc2004/papers/24/metaclasses-pycon.pdf\">the original</a> <span class=\"cs1-format\">(PDF)</span> on 30 May 2009<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 June</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+Metaclasses%3A+Who%3F+Why%3F+When%3F&amp;rft.au=The+Cain+Gang+Ltd.&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fcommunity%2Fpycon%2Fdc2004%2Fpapers%2F24%2Fmetaclasses-pycon.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-14-60\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-14_60-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.0/reference/datamodel.html#special-method-names\">\"3.3. Special method names\"</a>. <i>The Python Language Reference</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181215123146/https://docs.python.org/3.0/reference/datamodel.html#special-method-names\">Archived</a> from the original on 15 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 June</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Python+Language+Reference&amp;rft.atitle=3.3.+Special+method+names&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.0%2Freference%2Fdatamodel.html%23special-method-names&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-15-61\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-15_61-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.nongnu.org/pydbc/\">\"PyDBC: method preconditions, method postconditions and class invariants for Python\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191123231931/http://www.nongnu.org/pydbc/\">Archived</a> from the original on 23 November 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PyDBC%3A+method+preconditions%2C+method+postconditions+and+class+invariants+for+Python&amp;rft_id=http%3A%2F%2Fwww.nongnu.org%2Fpydbc%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-16-62\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-16_62-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.wayforward.net/pycontract/\">\"Contracts for Python\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615173404/http://www.wayforward.net/pycontract/\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Contracts+for+Python&amp;rft_id=http%3A%2F%2Fwww.wayforward.net%2Fpycontract%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-17-63\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-17_63-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://sites.google.com/site/pydatalog/\">\"PyDatalog\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200613160231/https://sites.google.com/site/pydatalog/\">Archived</a> from the original on 13 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 July</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PyDatalog&amp;rft_id=https%3A%2F%2Fsites.google.com%2Fsite%2Fpydatalog%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-64\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-64\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/doc/essays/omg-darpa-mcc-position/\">\"Glue it all together with Python\"</a>. <i>Python.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 September</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Glue+it+all+together+with+Python&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdoc%2Fessays%2Fomg-darpa-mcc-position%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Reference_counting-65\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Reference_counting_65-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/extending/extending.html#reference-counts\">\"Reference counts\"</a>. Extending and embedding the Python interpreter. <i>Docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121018063230/http://docs.python.org/extending/extending.html#reference-counts\">Archived</a> from the original on 18 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 June</span> 2020</span>. <q>Since Python makes heavy use of <code>malloc()</code> and <code>free()}</code>, it needs a strategy to avoid memory leaks as well as the re‑use of freed memory. The method chosen is called <i>reference counting</i>.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Docs.python.org&amp;rft.atitle=Reference+counts&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Fextending%2Fextending.html%23reference-counts&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-59-66\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-AutoNT-59_66-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-AutoNT-59_66-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHettinger2002\" class=\"citation web cs1\">Hettinger, Raymond (30 January 2002). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0289/\">\"PEP 289&#160;– Generator Expressions\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614153717/https://www.python.org/dev/peps/pep-0289/\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+289+%E2%80%93+Generator+Expressions&amp;rft.date=2002-01-30&amp;rft.aulast=Hettinger&amp;rft.aufirst=Raymond&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0289%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-18-67\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-18_67-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/itertools.html\">\"6.5 itertools&#160;– Functions creating iterators for efficient looping\"</a>. Docs.python.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614153629/https://docs.python.org/3/library/itertools.html\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 November</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=6.5+itertools+%E2%80%93+Functions+creating+iterators+for+efficient+looping&amp;rft.pub=Docs.python.org&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fitertools.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-PEP20-68\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-PEP20_68-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-PEP20_68-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPeters2004\" class=\"citation web cs1\">Peters, Tim (19 August 2004). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0020/\">\"PEP 20&#160;– The Zen of Python\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181226141127/https://www.python.org/dev/peps/pep-0020/\">Archived</a> from the original on 26 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+20+%E2%80%93+The+Zen+of+Python&amp;rft.date=2004-08-19&amp;rft.aulast=Peters&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0020%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Python-Changes-2014-69\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Python-Changes-2014_69-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLutz2022\" class=\"citation web cs1\">Lutz, Mark (January 2022). <a rel=\"nofollow\" class=\"external text\" href=\"https://learning-python.com/python-changes-2014-plus.html\">\"Python changes 2014+\"</a>. <i>Learning Python</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240315075935/https://learning-python.com/python-changes-2014-plus.html\">Archived</a> from the original on 15 March 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 February</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Learning+Python&amp;rft.atitle=Python+changes+2014%2B&amp;rft.date=2022-01&amp;rft.aulast=Lutz&amp;rft.aufirst=Mark&amp;rft_id=https%3A%2F%2Flearning-python.com%2Fpython-changes-2014-plus.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Confusion-regarding-a-rule-in-the-Zen-of-Python-70\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Confusion-regarding-a-rule-in-the-Zen-of-Python_70-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://discuss.python.org/t/confusion-regarding-a-rule-in-the-zen-of-python/15927\">\"Confusion regarding a rule in 'the Zen of Python'<span class=\"cs1-kern-right\"></span>\"</a>. Discussions. <i>Python.org</i>. Python help. 3 May 2022. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240225221142/https://discuss.python.org/t/confusion-regarding-a-rule-in-the-zen-of-python/15927\">Archived</a> from the original on 25 February 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 February</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Confusion+regarding+a+rule+in+%27the+Zen+of+Python%27&amp;rft.date=2022-05-03&amp;rft_id=https%3A%2F%2Fdiscuss.python.org%2Ft%2Fconfusion-regarding-a-rule-in-the-zen-of-python%2F15927&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-The-Most-Controversial-Python-Walrus-Operator-71\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-The-Most-Controversial-Python-Walrus-Operator_71-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAmbi2021\" class=\"citation web cs1\">Ambi, Chetan (4 July 2021). <a rel=\"nofollow\" class=\"external text\" href=\"https://pythonsimplified.com/the-most-controversial-python-walrus-operator/\">\"The most controversial Python 'walrus operator'<span class=\"cs1-kern-right\"></span>\"</a>. <i>Python simplified (pythonsimplified.com)</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230827154931/https://pythonsimplified.com/the-most-controversial-python-walrus-operator/\">Archived</a> from the original on 27 August 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 February</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+simplified+%28pythonsimplified.com%29&amp;rft.atitle=The+most+controversial+Python+%27walrus+operator%27&amp;rft.date=2021-07-04&amp;rft.aulast=Ambi&amp;rft.aufirst=Chetan&amp;rft_id=https%3A%2F%2Fpythonsimplified.com%2Fthe-most-controversial-python-walrus-operator%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-The-Controversy-Behind-The-Walrus-Operator-in-Python-72\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-The-Controversy-Behind-The-Walrus-Operator-in-Python_72-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGrifski2020\" class=\"citation web cs1\">Grifski, Jeremy (24 May 2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://therenegadecoder.com/code/the-controversy-behind-the-walrus-operator-in-python/\">\"The controversy behind the 'walrus operator' in Python\"</a>. <i>The Renegade Coder (therenegadecoder.com)</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231228135749/https://therenegadecoder.com/code/the-controversy-behind-the-walrus-operator-in-python/\">Archived</a> from the original on 28 December 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 February</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Renegade+Coder+%28therenegadecoder.com%29&amp;rft.atitle=The+controversy+behind+the+%27walrus+operator%27+in+Python&amp;rft.date=2020-05-24&amp;rft.aulast=Grifski&amp;rft.aufirst=Jeremy&amp;rft_id=https%3A%2F%2Ftherenegadecoder.com%2Fcode%2Fthe-controversy-behind-the-walrus-operator-in-python%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Python-String-Formatting-Best-Practices-73\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Python-String-Formatting-Best-Practices_73-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBader\" class=\"citation web cs1\">Bader, Dan. <a rel=\"nofollow\" class=\"external text\" href=\"https://realpython.com/python-string-formatting/\">\"Python string formatting best practices\"</a>. <i>Real Python (realpython.com)</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240218083506/https://realpython.com/python-string-formatting/\">Archived</a> from the original on 18 February 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 February</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Real+Python+%28realpython.com%29&amp;rft.atitle=Python+string+formatting+best+practices&amp;rft.aulast=Bader&amp;rft.aufirst=Dan&amp;rft_id=https%3A%2F%2Frealpython.com%2Fpython-string-formatting%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-19-74\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-19_74-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFMartelliRavenscroftAscher2005\" class=\"citation book cs1\">Martelli, Alex; Ravenscroft, Anna; Ascher, David (2005). <a rel=\"nofollow\" class=\"external text\" href=\"http://shop.oreilly.com/product/9780596007973.do\"><i>Python Cookbook, 2nd Edition</i></a>. <a href=\"/wiki/O%27Reilly_Media\" title=\"O&#39;Reilly Media\">O'Reilly Media</a>. p.&#160;230. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-596-00797-3\" title=\"Special:BookSources/978-0-596-00797-3\"><bdi>978-0-596-00797-3</bdi></a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200223171254/http://shop.oreilly.com/product/9780596007973.do\">Archived</a> from the original on 23 February 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">14 November</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Python+Cookbook%2C+2nd+Edition&amp;rft.pages=230&amp;rft.pub=O%27Reilly+Media&amp;rft.date=2005&amp;rft.isbn=978-0-596-00797-3&amp;rft.aulast=Martelli&amp;rft.aufirst=Alex&amp;rft.au=Ravenscroft%2C+Anna&amp;rft.au=Ascher%2C+David&amp;rft_id=http%3A%2F%2Fshop.oreilly.com%2Fproduct%2F9780596007973.do&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-20-75\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-20_75-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20140130021902/http://ebeab.com/2014/01/21/python-culture/\">\"Python Culture\"</a>. <i>ebeab</i>. 21 January 2014. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://ebeab.com/2014/01/21/python-culture/\">the original</a> on 30 January 2014.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ebeab&amp;rft.atitle=Python+Culture&amp;rft.date=2014-01-21&amp;rft_id=http%3A%2F%2Febeab.com%2F2014%2F01%2F21%2Fpython-culture%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-PyJL-76\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-PyJL_76-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-PyJL_76-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.ist.utl.pt/antonio.menezes.leitao/ADA/documents/publications_docs/2022_TranspilingPythonToJuliaUsingPyJL.pdf\">\"Transpiling Python to Julia using PyJL\"</a> <span class=\"cs1-format\">(PDF)</span>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231119071525/https://web.ist.utl.pt/antonio.menezes.leitao/ADA/documents/publications_docs/2022_TranspilingPythonToJuliaUsingPyJL.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 19 November 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 September</span> 2023</span>. <q>After manually modifying one line of code by specifying the necessary type information, we obtained a speedup of 52.6×, making the translated Julia code 19.5× faster than the original Python code.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Transpiling+Python+to+Julia+using+PyJL&amp;rft_id=https%3A%2F%2Fweb.ist.utl.pt%2Fantonio.menezes.leitao%2FADA%2Fdocuments%2Fpublications_docs%2F2022_TranspilingPythonToJuliaUsingPyJL.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-whyname-77\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-whyname_77-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/faq/general.html#why-is-it-called-python\">\"Why is it called Python?\"</a>. <i>General Python FAQ</i>. Docs.python.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121024164224/http://docs.python.org/faq/general.html#why-is-it-called-python\">Archived</a> from the original on 24 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 January</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=General+Python+FAQ&amp;rft.atitle=Why+is+it+called+Python%3F&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Ffaq%2Fgeneral.html%23why-is-it-called-python&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-78\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-78\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190511065650/http://insidetech.monster.com/training/articles/8114-15-ways-python-is-a-powerful-force-on-the-web\">\"15&#160;ways Python is a powerful force on the web\"</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://insidetech.monster.com/training/articles/8114-15-ways-python-is-a-powerful-force-on-the-web\">the original</a> on 11 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 July</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=15+ways+Python+is+a+powerful+force+on+the+web&amp;rft_id=https%3A%2F%2Finsidetech.monster.com%2Ftraining%2Farticles%2F8114-15-ways-python-is-a-powerful-force-on-the-web&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-pprint-doc-79\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-pprint-doc_79-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/pprint.html\">\"<code>pprint</code> – data pretty printer – <span class=\"nowrap\">Python 3.11.0</span> documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210122224848/https://docs.python.org/3/library/pprint.html\">Archived</a> from the original on 22 January 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 November</span> 2022</span>. <q><code>stuff = ['spam', 'eggs', 'lumberjack', 'knights', 'ni']</code></q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=%3Ccode%3Epprint%3C%2Fcode%3E+%E2%80%93+data+pretty+printer+%E2%80%93+%3Cspan+class%3D%22nowrap%22%3EPython+3.11.0%3C%2Fspan%3E+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fpprint.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-80\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-80\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python-guide.org/writing/style\">\"Code style\"</a>. The hitchhiker's guide to Python. <i>docs.python-guide.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210127154341/https://docs.python-guide.org/writing/style/\">Archived</a> from the original on 27 January 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python-guide.org&amp;rft.atitle=Code+style&amp;rft_id=https%3A%2F%2Fdocs.python-guide.org%2Fwriting%2Fstyle&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-52-81\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-52_81-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/faq/general.html#is-python-a-good-language-for-beginning-programmers\">\"Is Python a good language for beginning programmers?\"</a>. <i>General Python FAQ</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121024164224/http://docs.python.org/faq/general.html#is-python-a-good-language-for-beginning-programmers\">Archived</a> from the original on 24 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">21 March</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=General+Python+FAQ&amp;rft.atitle=Is+Python+a+good+language+for+beginning+programmers%3F&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Ffaq%2Fgeneral.html%23is-python-a-good-language-for-beginning-programmers&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-53-82\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-53_82-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180218162410/http://www.secnetix.de/~olli/Python/block_indentation.hawk\">\"Myths about indentation in Python\"</a>. Secnetix.de. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.secnetix.de/~olli/Python/block_indentation.hawk\">the original</a> on 18 February 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 April</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Myths+about+indentation+in+Python&amp;rft.pub=Secnetix.de&amp;rft_id=http%3A%2F%2Fwww.secnetix.de%2F~olli%2FPython%2Fblock_indentation.hawk&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-guttag-83\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-guttag_83-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGuttag2016\" class=\"citation book cs1\">Guttag, John V. (12 August 2016). <i>Introduction to Computation and Programming Using Python: With Application to Understanding Data</i>. MIT Press. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-262-52962-4\" title=\"Special:BookSources/978-0-262-52962-4\"><bdi>978-0-262-52962-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Introduction+to+Computation+and+Programming+Using+Python%3A+With+Application+to+Understanding+Data&amp;rft.pub=MIT+Press&amp;rft.date=2016-08-12&amp;rft.isbn=978-0-262-52962-4&amp;rft.aulast=Guttag&amp;rft.aufirst=John+V.&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-84\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-84\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0008/\">\"PEP 8 – Style Guide for Python Code\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190417223549/https://www.python.org/dev/peps/pep-0008/\">Archived</a> from the original on 17 April 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">26 March</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+8+%E2%80%93+Style+Guide+for+Python+Code&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0008%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-85\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-85\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.11/tutorial/errors.html\">\"8. Errors and Exceptions – Python 3.12.0a0 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220509145745/https://docs.python.org/3.11/tutorial/errors.html\">Archived</a> from the original on 9 May 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 May</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=8.+Errors+and+Exceptions+%E2%80%93+Python+3.12.0a0+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.11%2Ftutorial%2Ferrors.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-86\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-86\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/download/releases/2.5/highlights/\">\"Highlights: Python 2.5\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190804120408/https://www.python.org/download/releases/2.5/highlights/\">Archived</a> from the original on 4 August 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 March</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Highlights%3A+Python+2.5&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownload%2Freleases%2F2.5%2Fhighlights%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-55-88\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-55_88-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum2009\" class=\"citation web cs1\">van Rossum, Guido (22 April 2009). <a rel=\"nofollow\" class=\"external text\" href=\"http://neopythonic.blogspot.be/2009/04/tail-recursion-elimination.html\">\"Tail Recursion Elimination\"</a>. Neopythonic.blogspot.be. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180519225253/http://neopythonic.blogspot.be/2009/04/tail-recursion-elimination.html\">Archived</a> from the original on 19 May 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Tail+Recursion+Elimination&amp;rft.pub=Neopythonic.blogspot.be&amp;rft.date=2009-04-22&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft_id=http%3A%2F%2Fneopythonic.blogspot.be%2F2009%2F04%2Ftail-recursion-elimination.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-56-89\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-56_89-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum2006\" class=\"citation web cs1\">van Rossum, Guido (9 February 2006). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.artima.com/weblogs/viewpost.jsp?thread=147358\">\"Language Design Is Not Just Solving Puzzles\"</a>. <i>Artima forums</i>. Artima. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200117182525/https://www.artima.com/weblogs/viewpost.jsp?thread=147358\">Archived</a> from the original on 17 January 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">21 March</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Artima+forums&amp;rft.atitle=Language+Design+Is+Not+Just+Solving+Puzzles&amp;rft.date=2006-02-09&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft_id=http%3A%2F%2Fwww.artima.com%2Fweblogs%2Fviewpost.jsp%3Fthread%3D147358&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-57-90\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-57_90-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_RossumEby2005\" class=\"citation web cs1\">van Rossum, Guido; Eby, Phillip J. (10 May 2005). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0342/\">\"PEP 342&#160;– Coroutines via Enhanced Generators\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200529003739/https://www.python.org/dev/peps/pep-0342/\">Archived</a> from the original on 29 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+342+%E2%80%93+Coroutines+via+Enhanced+Generators&amp;rft.date=2005-05-10&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft.au=Eby%2C+Phillip+J.&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0342%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-58-91\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-58_91-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0380/\">\"PEP 380\"</a>. Python.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604233821/https://www.python.org/dev/peps/pep-0380/\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PEP+380&amp;rft.pub=Python.org&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0380%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-92\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-92\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org\">\"division\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20060720033244/http://docs.python.org/\">Archived</a> from the original on 20 July 2006<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 July</span> 2014</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=division&amp;rft_id=https%3A%2F%2Fdocs.python.org&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-PEP465-93\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-PEP465_93-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0465/\">\"PEP 0465 – A dedicated infix operator for matrix multiplication\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604224255/https://www.python.org/dev/peps/pep-0465/\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">1 January</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=PEP+0465+%E2%80%93+A+dedicated+infix+operator+for+matrix+multiplication&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0465%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Python3.5Changelog-94\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Python3.5Changelog_94-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/downloads/release/python-351/\">\"Python 3.5.1 Release and Changelog\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200514034938/https://www.python.org/downloads/release/python-351/\">Archived</a> from the original on 14 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">1 January</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=Python+3.5.1+Release+and+Changelog&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownloads%2Frelease%2Fpython-351%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Python3.8Changelog-95\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Python3.8Changelog_95-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.8/whatsnew/3.8.html\">\"What's New in Python 3.8\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200608124345/https://docs.python.org/3.8/whatsnew/3.8.html\">Archived</a> from the original on 8 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">14 October</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What%27s+New+in+Python+3.8&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.8%2Fwhatsnew%2F3.8.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-60-96\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-60_96-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_RossumHettinger2003\" class=\"citation web cs1\">van Rossum, Guido; Hettinger, Raymond (7 February 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0308/\">\"PEP 308&#160;– Conditional Expressions\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160313113147/https://www.python.org/dev/peps/pep-0308/\">Archived</a> from the original on 13 March 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">13 July</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+308+%E2%80%93+Conditional+Expressions&amp;rft.date=2003-02-07&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft.au=Hettinger%2C+Raymond&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0308%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-97\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-97\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/stdtypes.html#tuple\">\"4. Built-in Types – Python 3.6.3rc1 documentation\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614194325/https://docs.python.org/3/library/stdtypes.html#tuple\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">1 October</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=4.+Built-in+Types+%E2%80%93+Python+3.6.3rc1+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fstdtypes.html%23tuple&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-98\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-98\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences\">\"5.3. Tuples and Sequences – Python 3.7.1rc2 documentation\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200610050047/https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences\">Archived</a> from the original on 10 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 October</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=5.3.+Tuples+and+Sequences+%E2%80%93+Python+3.7.1rc2+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Ftutorial%2Fdatastructures.html%23tuples-and-sequences&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-pep-0498-99\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-pep-0498_99-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-pep-0498_99-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0498/\">\"PEP 498 – Literal String Interpolation\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615184141/https://www.python.org/dev/peps/pep-0498/\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">8 March</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=PEP+498+%E2%80%93+Literal+String+Interpolation&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0498%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-61-100\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-61_100-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/faq/design.html#why-must-self-be-used-explicitly-in-method-definitions-and-calls\">\"Why must 'self' be used explicitly in method definitions and calls?\"</a>. <i>Design and History FAQ</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121024164243/http://docs.python.org/faq/design.html#why-must-self-be-used-explicitly-in-method-definitions-and-calls\">Archived</a> from the original on 24 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Design+and+History+FAQ&amp;rft.atitle=Why+must+%27self%27+be+used+explicitly+in+method+definitions+and+calls%3F&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Ffaq%2Fdesign.html%23why-must-self-be-used-explicitly-in-method-definitions-and-calls&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-101\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-101\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSweigart2020\" class=\"citation book cs1\">Sweigart, Al (2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://books.google.com/books?id=7GUKEAAAQBAJ&amp;pg=PA322\"><i>Beyond the Basic Stuff with Python: Best Practices for Writing Clean Code</i></a>. No Starch Press. p.&#160;322. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-59327-966-0\" title=\"Special:BookSources/978-1-59327-966-0\"><bdi>978-1-59327-966-0</bdi></a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210813194312/https://books.google.com/books?id=7GUKEAAAQBAJ&amp;pg=PA322\">Archived</a> from the original on 13 August 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 July</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Beyond+the+Basic+Stuff+with+Python%3A+Best+Practices+for+Writing+Clean+Code&amp;rft.pages=322&amp;rft.pub=No+Starch+Press&amp;rft.date=2020&amp;rft.isbn=978-1-59327-966-0&amp;rft.aulast=Sweigart&amp;rft.aufirst=Al&amp;rft_id=https%3A%2F%2Fbooks.google.com%2Fbooks%3Fid%3D7GUKEAAAQBAJ%26pg%3DPA322&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-classy-102\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-classy_102-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121026063834/http://docs.python.org/reference/datamodel.html#new-style-and-classic-classes\">\"The Python Language Reference, section 3.3. New-style and classic classes, for release 2.7.1\"</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/reference/datamodel.html#new-style-and-classic-classes\">the original</a> on 26 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">12 January</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Python+Language+Reference%2C+section+3.3.+New-style+and+classic+classes%2C+for+release+2.7.1&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Freference%2Fdatamodel.html%23new-style-and-classic-classes&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-103\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-103\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://peps.python.org/pep-0484/\">\"PEP 484 – Type Hints | peps.python.org\"</a>. <i>peps.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231127205023/https://peps.python.org/pep-0484/\">Archived</a> from the original on 27 November 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 November</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=peps.python.org&amp;rft.atitle=PEP+484+%E2%80%93+Type+Hints+%7C+peps.python.org&amp;rft_id=https%3A%2F%2Fpeps.python.org%2Fpep-0484%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-104\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-104\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/typing.html\">\"typing — Support for type hints\"</a>. <i>Python documentation</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200221184042/https://docs.python.org/3/library/typing.html\">Archived</a> from the original on 21 February 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 December</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+documentation&amp;rft.atitle=typing+%E2%80%94+Support+for+type+hints&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Ftyping.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-105\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-105\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://mypy-lang.org/\">\"mypy – Optional Static Typing for Python\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200606192012/http://mypy-lang.org/\">Archived</a> from the original on 6 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">28 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=mypy+%E2%80%93+Optional+Static+Typing+for+Python&amp;rft_id=http%3A%2F%2Fmypy-lang.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-106\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-106\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://mypyc.readthedocs.io/en/latest/introduction.html\">\"Introduction\"</a>. <i>mypyc.readthedocs.io</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231222000457/https://mypyc.readthedocs.io/en/latest/introduction.html\">Archived</a> from the original on 22 December 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 December</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=mypyc.readthedocs.io&amp;rft.atitle=Introduction&amp;rft_id=https%3A%2F%2Fmypyc.readthedocs.io%2Fen%2Flatest%2Fintroduction.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-107\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-107\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3.8/tutorial/floatingpoint.html#representation-error\">\"15. Floating Point Arithmetic: Issues and Limitations – Python 3.8.3 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200606113842/https://docs.python.org/3.8/tutorial/floatingpoint.html#representation-error\">Archived</a> from the original on 6 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 June</span> 2020</span>. <q>Almost all machines today (November 2000) use IEEE-754 floating point arithmetic, and almost all platforms map Python floats to IEEE-754 \"double precision\".</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=15.+Floating+Point+Arithmetic%3A+Issues+and+Limitations+%E2%80%93+Python+3.8.3+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3.8%2Ftutorial%2Ffloatingpoint.html%23representation-error&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-pep0237-108\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-pep0237_108-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFZadkavan_Rossum2001\" class=\"citation web cs1\">Zadka, Moshe; van Rossum, Guido (11 March 2001). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0237/\">\"PEP 237&#160;– Unifying Long Integers and Integers\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200528063237/https://www.python.org/dev/peps/pep-0237/\">Archived</a> from the original on 28 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+237+%E2%80%93+Unifying+Long+Integers+and+Integers&amp;rft.date=2001-03-11&amp;rft.aulast=Zadka&amp;rft.aufirst=Moshe&amp;rft.au=van+Rossum%2C+Guido&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0237%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-109\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-109\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/stdtypes.html#typesseq-range\">\"Built-in Types\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614194325/https://docs.python.org/3/library/stdtypes.html#typesseq-range\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 October</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Built-in+Types&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fstdtypes.html%23typesseq-range&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-110\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-110\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://legacy.python.org/dev/peps/pep-0465/\">\"PEP 465 – A dedicated infix operator for matrix multiplication\"</a>. <i>python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200529200310/https://legacy.python.org/dev/peps/pep-0465/\">Archived</a> from the original on 29 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 July</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=PEP+465+%E2%80%93+A+dedicated+infix+operator+for+matrix+multiplication&amp;rft_id=https%3A%2F%2Flegacy.python.org%2Fdev%2Fpeps%2Fpep-0465%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-pep0238-111\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-pep0238_111-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-pep0238_111-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFZadkavan_Rossum2001\" class=\"citation web cs1\">Zadka, Moshe; van Rossum, Guido (11 March 2001). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0238/\">\"PEP 238&#160;– Changing the Division Operator\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200528115550/https://www.python.org/dev/peps/pep-0238/\">Archived</a> from the original on 28 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">23 October</span> 2013</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+238+%E2%80%93+Changing+the+Division+Operator&amp;rft.date=2001-03-11&amp;rft.aulast=Zadka&amp;rft.aufirst=Moshe&amp;rft.au=van+Rossum%2C+Guido&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0238%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-62-112\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-62_112-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html\">\"Why Python's Integer Division Floors\"</a>. 24 August 2010. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605151500/https://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 August</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Why+Python%27s+Integer+Division+Floors&amp;rft.date=2010-08-24&amp;rft_id=https%3A%2F%2Fpython-history.blogspot.com%2F2010%2F08%2Fwhy-pythons-integer-division-floors.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-64-113\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-64_113-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation cs2\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/py3k/library/functions.html#round\">\"round\"</a>, <i>The Python standard library, release 3.2, §2: Built-in functions</i>, <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121025141808/http://docs.python.org/py3k/library/functions.html#round\">archived</a> from the original on 25 October 2012<span class=\"reference-accessdate\">, retrieved <span class=\"nowrap\">14 August</span> 2011</span></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Python+standard+library%2C+release+3.2%2C+%C2%A72%3A+Built-in+functions&amp;rft.atitle=round&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Fpy3k%2Flibrary%2Ffunctions.html%23round&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-63-114\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-63_114-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation cs2\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/library/functions.html#round\">\"round\"</a>, <i>The Python standard library, release 2.7, §2: Built-in functions</i>, <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121027081602/http://docs.python.org/library/functions.html#round\">archived</a> from the original on 27 October 2012<span class=\"reference-accessdate\">, retrieved <span class=\"nowrap\">14 August</span> 2011</span></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=The+Python+standard+library%2C+release+2.7%2C+%C2%A72%3A+Built-in+functions&amp;rft.atitle=round&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Flibrary%2Ffunctions.html%23round&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-65-115\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-65_115-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBeazley2009\" class=\"citation book cs1\">Beazley, David M. (2009). <span class=\"id-lock-limited\" title=\"Free access subject to limited trial, subscription normally required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/pythonessentialr00beaz_036\"><i>Python Essential Reference</i></a></span> (4th&#160;ed.). Addison-Wesley Professional. p.&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/pythonessentialr00beaz_036/page/n90\">66</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/9780672329784\" title=\"Special:BookSources/9780672329784\"><bdi>9780672329784</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Python+Essential+Reference&amp;rft.pages=66&amp;rft.edition=4th&amp;rft.pub=Addison-Wesley+Professional&amp;rft.date=2009&amp;rft.isbn=9780672329784&amp;rft.aulast=Beazley&amp;rft.aufirst=David+M.&amp;rft_id=https%3A%2F%2Farchive.org%2Fdetails%2Fpythonessentialr00beaz_036&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-CPL-116\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-CPL_116-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKernighanRitchie1988\" class=\"citation book cs1\">Kernighan, Brian W.; Ritchie, Dennis M. (1988). <a href=\"/wiki/The_C_Programming_Language\" title=\"The C Programming Language\"><i>The C Programming Language</i></a> (2nd&#160;ed.). p.&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/cprogramminglang00bria/page/206\">206</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=The+C+Programming+Language&amp;rft.pages=206&amp;rft.edition=2nd&amp;rft.date=1988&amp;rft.aulast=Kernighan&amp;rft.aufirst=Brian+W.&amp;rft.au=Ritchie%2C+Dennis+M.&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-88-117\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-AutoNT-88_117-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-AutoNT-88_117-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBatista2003\" class=\"citation web cs1\">Batista, Facundo (17 October 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0327/\">\"PEP 327&#160;– Decimal Data Type\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604234830/https://www.python.org/dev/peps/pep-0327/\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+327+%E2%80%93+Decimal+Data+Type&amp;rft.date=2003-10-17&amp;rft.aulast=Batista&amp;rft.aufirst=Facundo&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0327%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-118\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-118\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/2.6/whatsnew/2.6.html\">\"What's New in Python 2.6\"</a>. <i>Python v2.6.9 documentation</i>. 29 October 2013. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191223213856/https://docs.python.org/2.6/whatsnew/2.6.html\">Archived</a> from the original on 23 December 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">26 September</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+v2.6.9+documentation&amp;rft.atitle=What%27s+New+in+Python+2.6&amp;rft.date=2013-10-29&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F2.6%2Fwhatsnew%2F2.6.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-119\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-119\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200531211840/https://www.stat.washington.edu/~hoytak/blog/whypython.html\">\"10 Reasons Python Rocks for Research (And a Few Reasons it Doesn't) – Hoyt Koepke\"</a>. <i>University of Washington Department of Statistics</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.stat.washington.edu/~hoytak/blog/whypython.html\">the original</a> on 31 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 February</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=University+of+Washington+Department+of+Statistics&amp;rft.atitle=10+Reasons+Python+Rocks+for+Research+%28And+a+Few+Reasons+it+Doesn%27t%29+%E2%80%93+Hoyt+Koepke&amp;rft_id=https%3A%2F%2Fwww.stat.washington.edu%2F~hoytak%2Fblog%2Fwhypython.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-120\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-120\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFShell2014\" class=\"citation web cs1\">Shell, Scott (17 June 2014). <a rel=\"nofollow\" class=\"external text\" href=\"https://engineering.ucsb.edu/~shell/che210d/python.pdf\">\"An introduction to Python for scientific computing\"</a> <span class=\"cs1-format\">(PDF)</span>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190204014642/https://engineering.ucsb.edu/~shell/che210d/python.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 4 February 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 February</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=An+introduction+to+Python+for+scientific+computing&amp;rft.date=2014-06-17&amp;rft.aulast=Shell&amp;rft.aufirst=Scott&amp;rft_id=https%3A%2F%2Fengineering.ucsb.edu%2F~shell%2Fche210d%2Fpython.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-86-121\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-86_121-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPiotrowski2006\" class=\"citation web cs1\">Piotrowski, Przemyslaw (July 2006). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.oracle.com/technetwork/articles/piotrowski-pythoncore-084049.html\">\"Build a Rapid Web Development Environment for Python Server Pages and Oracle\"</a>. <i>Oracle Technology Network</i>. Oracle. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190402124435/https://www.oracle.com/technetwork/articles/piotrowski-pythoncore-084049.html\">Archived</a> from the original on 2 April 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">12 March</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Oracle+Technology+Network&amp;rft.atitle=Build+a+Rapid+Web+Development+Environment+for+Python+Server+Pages+and+Oracle&amp;rft.date=2006-07&amp;rft.aulast=Piotrowski&amp;rft.aufirst=Przemyslaw&amp;rft_id=http%3A%2F%2Fwww.oracle.com%2Ftechnetwork%2Farticles%2Fpiotrowski-pythoncore-084049.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-89-122\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-89_122-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEby2003\" class=\"citation web cs1\">Eby, Phillip J. (7 December 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0333/\">\"PEP 333&#160;– Python Web Server Gateway Interface v1.0\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614170344/https://www.python.org/dev/peps/pep-0333/\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+333+%E2%80%93+Python+Web+Server+Gateway+Interface+v1.0&amp;rft.date=2003-12-07&amp;rft.aulast=Eby&amp;rft.aufirst=Phillip+J.&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0333%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-PyPI-123\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-PyPI_123-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://pypi.org/\">\"PyPI\"</a>. <i>PyPI</i>. 13 March 2025. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20250222013445/https://pypi.org/\">Archived</a> from the original on 22 February 2025.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=PyPI&amp;rft.atitle=PyPI&amp;rft.date=2025-03-13&amp;rft_id=https%3A%2F%2Fpypi.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-124\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-124\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/glossary.html#term-interactive\">\"Glossary: interactive\"</a>. <i>Python documentation</i>. v3.13.7<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+documentation&amp;rft.atitle=Glossary%3A+interactive&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Fglossary.html%23term-interactive&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-idle-125\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-idle_125-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-idle_125-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/idle.html\">\"IDLE — Python editor and shell\"</a>. <i>Python documentation</i>. v3.13.7<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 August</span> 2025</span>. <q>IDLE is Python's Integrated Development and Learning Environment.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+documentation&amp;rft.atitle=IDLE+%E2%80%94+Python+editor+and+shell&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fidle.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-126\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-126\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://ipython.readthedocs.io/en/stable/\">\"IPython Documentation\"</a>. v9.5.0. 29 August 2025. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20250831204721/https://ipython.readthedocs.io/en/stable/\">Archived</a> from the original on 31 August 2025<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=IPython+Documentation&amp;rft.series=v9.5.0&amp;rft.date=2025-08-29&amp;rft_id=https%3A%2F%2Fipython.readthedocs.io%2Fen%2Fstable%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-127\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-127\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://jupyter.org\">\"Project Jupyter\"</a>. <i>Jupyter.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20231012055917/https://jupyter.org/\">Archived</a> from the original on 12 October 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2 April</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Jupyter.org&amp;rft.atitle=Project+Jupyter&amp;rft_id=https%3A%2F%2Fjupyter.org&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-128\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-128\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHarper2024\" class=\"citation web cs1\">Harper, Doug (Spring 2024). <a rel=\"nofollow\" class=\"external text\" href=\"http://physics.wku.edu/phys316/software/canopy/\">\"Enthought Canopy\"</a>. <i>WKU Physics 316</i>. <a href=\"/wiki/Western_Kentucky_University\" title=\"Western Kentucky University\">Western Kentucky University</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240818041226/http://physics.wku.edu/phys316/software/canopy/\">Archived</a> from the original on 18 August 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=WKU+Physics+316&amp;rft.atitle=Enthought+Canopy&amp;rft.ssn=spring&amp;rft.date=2024&amp;rft.aulast=Harper&amp;rft.aufirst=Doug&amp;rft_id=http%3A%2F%2Fphysics.wku.edu%2Fphys316%2Fsoftware%2Fcanopy%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-129\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-129\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170715151703/https://www.enthought.com/products/canopy/\">\"Enthought Canopy\"</a>. <i><a href=\"/wiki/Enthought\" title=\"Enthought\">Enthought</a></i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.enthought.com/products/canopy/\">the original</a> on 15 July 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 August</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Enthought&amp;rft.atitle=Enthought+Canopy&amp;rft_id=https%3A%2F%2Fwww.enthought.com%2Fproducts%2Fcanopy%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-130\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-130\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://peps.python.org/pep-0007/\">\"PEP 7 – Style Guide for C Code | peps.python.org\"</a>. <i>peps.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220424202827/https://peps.python.org/pep-0007/\">Archived</a> from the original on 24 April 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">28 April</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=peps.python.org&amp;rft.atitle=PEP+7+%E2%80%93+Style+Guide+for+C+Code+%7C+peps.python.org&amp;rft_id=https%3A%2F%2Fpeps.python.org%2Fpep-0007%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-131\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-131\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/extending/building.html\">\"4. Building C and C++ Extensions – Python 3.9.2 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210303002519/https://docs.python.org/3/extending/building.html\">Archived</a> from the original on 3 March 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">1 March</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=4.+Building+C+and+C%2B%2B+Extensions+%E2%80%93+Python+3.9.2+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Fextending%2Fbuilding.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-66-132\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-66_132-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFvan_Rossum2001\" class=\"citation web cs1\">van Rossum, Guido (5 June 2001). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0007/\">\"PEP 7&#160;– Style Guide for C Code\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200601203908/https://www.python.org/dev/peps/pep-0007/\">Archived</a> from the original on 1 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+7+%E2%80%93+Style+Guide+for+C+Code&amp;rft.date=2001-06-05&amp;rft.aulast=van+Rossum&amp;rft.aufirst=Guido&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0007%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-67-133\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-67_133-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/dis.html#python-bytecode-instructions\">\"CPython byte code\"</a>. Docs.python.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605151542/https://docs.python.org/3/library/dis.html#python-bytecode-instructions\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">16 February</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=CPython+byte+code&amp;rft.pub=Docs.python.org&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fdis.html%23python-bytecode-instructions&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-68-134\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-68_134-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.troeger.eu/teaching/pythonvm08.pdf\">\"Python 2.5 internals\"</a> <span class=\"cs1-format\">(PDF)</span>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120806094951/http://www.troeger.eu/teaching/pythonvm08.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 6 August 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 April</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+2.5+internals&amp;rft_id=http%3A%2F%2Fwww.troeger.eu%2Fteaching%2Fpythonvm08.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-135\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-135\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/release/3.9.0/whatsnew/changelog.html#changelog\">\"Changelog – Python 3.9.0 documentation\"</a>. <i>docs.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210207001142/https://docs.python.org/release/3.9.0/whatsnew/changelog.html#changelog\">Archived</a> from the original on 7 February 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">8 February</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.python.org&amp;rft.atitle=Changelog+%E2%80%93+Python+3.9.0+documentation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Frelease%2F3.9.0%2Fwhatsnew%2Fchangelog.html%23changelog&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-136\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-136\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/downloads/release/python-391\">\"Download Python\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201208045225/https://www.python.org/downloads/release/python-391/\">Archived</a> from the original on 8 December 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">13 December</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Download+Python&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownloads%2Frelease%2Fpython-391&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-137\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-137\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.vmspython.org/doku.php?id=history\">\"history &#91;vmspython&#93;\"</a>. <i>www.vmspython.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201202194743/https://www.vmspython.org/doku.php?id=history\">Archived</a> from the original on 2 December 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 December</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=www.vmspython.org&amp;rft.atitle=history+%5Bvmspython%5D&amp;rft_id=https%3A%2F%2Fwww.vmspython.org%2Fdoku.php%3Fid%3Dhistory&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-69-138\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-69_138-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.oreilly.com/pub/a/oreilly/frank/rossum_1099.html\">\"An Interview with Guido van Rossum\"</a>. Oreilly.com. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20140716222652/http://oreilly.com/pub/a/oreilly/frank/rossum_1099.html\">Archived</a> from the original on 16 July 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=An+Interview+with+Guido+van+Rossum&amp;rft.pub=Oreilly.com&amp;rft_id=http%3A%2F%2Fwww.oreilly.com%2Fpub%2Fa%2Foreilly%2Ffrank%2Frossum_1099.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-139\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-139\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/download/other/\">\"Download Python for Other Platforms\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201127015815/https://www.python.org/download/other/\">Archived</a> from the original on 27 November 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 December</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Download+Python+for+Other+Platforms&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdownload%2Fother%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-70-140\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-70_140-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://pypy.org/compat.html\">\"PyPy compatibility\"</a>. Pypy.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200606041845/https://www.pypy.org/compat.html\">Archived</a> from the original on 6 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PyPy+compatibility&amp;rft.pub=Pypy.org&amp;rft_id=https%3A%2F%2Fpypy.org%2Fcompat.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-141\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-141\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTeam2019\" class=\"citation web cs1\">Team, The PyPy (28 December 2019). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.pypy.org/download.html\">\"Download and Install\"</a>. <i>PyPy</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220108212951/https://www.pypy.org/download.html\">Archived</a> from the original on 8 January 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">8 January</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=PyPy&amp;rft.atitle=Download+and+Install&amp;rft.date=2019-12-28&amp;rft.aulast=Team&amp;rft.aufirst=The+PyPy&amp;rft_id=https%3A%2F%2Fwww.pypy.org%2Fdownload.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-71-142\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-71_142-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://speed.pypy.org/\">\"speed comparison between CPython and Pypy\"</a>. Speed.pypy.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210510014902/https://speed.pypy.org/\">Archived</a> from the original on 10 May 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=speed+comparison+between+CPython+and+Pypy&amp;rft.pub=Speed.pypy.org&amp;rft_id=https%3A%2F%2Fspeed.pypy.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-143\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-143\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.exaloop.io/codon/general/differences\">\"Codon: Differences with Python\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230525002540/https://docs.exaloop.io/codon/general/differences\">Archived</a> from the original on 25 May 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">28 August</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Codon%3A+Differences+with+Python&amp;rft_id=https%3A%2F%2Fdocs.exaloop.io%2Fcodon%2Fgeneral%2Fdifferences&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-144\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-144\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLawson2023\" class=\"citation web cs1\">Lawson, Loraine (14 March 2023). <a rel=\"nofollow\" class=\"external text\" href=\"https://thenewstack.io/mit-created-compiler-speeds-up-python-code/\">\"MIT-Created Compiler Speeds up Python Code\"</a>. <i>The New Stack</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230406054200/https://thenewstack.io/mit-created-compiler-speeds-up-python-code/\">Archived</a> from the original on 6 April 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">28 August</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+New+Stack&amp;rft.atitle=MIT-Created+Compiler+Speeds+up+Python+Code&amp;rft.date=2023-03-14&amp;rft.aulast=Lawson&amp;rft.aufirst=Loraine&amp;rft_id=https%3A%2F%2Fthenewstack.io%2Fmit-created-compiler-speeds-up-python-code%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-145\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-145\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://education.lego.com/en-us/support/mindstorms-ev3/python-for-ev3\">\"Python-for-EV3\"</a>. <i>LEGO Education</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200607234814/https://education.lego.com/en-us/support/mindstorms-ev3/python-for-ev3\">Archived</a> from the original on 7 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 April</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LEGO+Education&amp;rft.atitle=Python-for-EV3&amp;rft_id=https%3A%2F%2Feducation.lego.com%2Fen-us%2Fsupport%2Fmindstorms-ev3%2Fpython-for-ev3&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-146\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-146\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFYegulalp2020\" class=\"citation news cs1\">Yegulalp, Serdar (29 October 2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3587591/pyston-returns-from-the-dead-to-speed-python.html\">\"Pyston returns from the dead to speed Python\"</a>. <i><a href=\"/wiki/InfoWorld\" title=\"InfoWorld\">InfoWorld</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210127113233/https://www.infoworld.com/article/3587591/pyston-returns-from-the-dead-to-speed-python.html\">Archived</a> from the original on 27 January 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">26 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=InfoWorld&amp;rft.atitle=Pyston+returns+from+the+dead+to+speed+Python&amp;rft.date=2020-10-29&amp;rft.aulast=Yegulalp&amp;rft.aufirst=Serdar&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3587591%2Fpyston-returns-from-the-dead-to-speed-python.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-147\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-147\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/facebookincubator/cinder\">\"cinder: Instagram's performance-oriented fork of CPython\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210504112500/https://github.com/facebookincubator/cinder\">Archived</a> from the original on 4 May 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 May</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=cinder%3A+Instagram%27s+performance-oriented+fork+of+CPython.&amp;rft_id=https%3A%2F%2Fgithub.com%2Ffacebookincubator%2Fcinder&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-148\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-148\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAroca2021\" class=\"citation web cs1\">Aroca, Rafael (7 August 2021). <a rel=\"nofollow\" class=\"external text\" href=\"https://rafaelaroca.wordpress.com/2021/08/07/snek-lang-feels-like-python-on-arduinos/\">\"Snek Lang: feels like Python on Arduinos\"</a>. <i>Yet Another Technology Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240105001031/https://rafaelaroca.wordpress.com/2021/08/07/snek-lang-feels-like-python-on-arduinos/\">Archived</a> from the original on 5 January 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 January</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Yet+Another+Technology+Blog&amp;rft.atitle=Snek+Lang%3A+feels+like+Python+on+Arduinos&amp;rft.date=2021-08-07&amp;rft.aulast=Aroca&amp;rft.aufirst=Rafael&amp;rft_id=https%3A%2F%2Frafaelaroca.wordpress.com%2F2021%2F08%2F07%2Fsnek-lang-feels-like-python-on-arduinos%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-149\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-149\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAufranc_(CNXSoft)2020\" class=\"citation web cs1\">Aufranc (CNXSoft), Jean-Luc (16 January 2020). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.cnx-software.com/2020/01/16/snekboard-controls-lego-power-functions-with-circuitpython-or-snek-programming-languages/\">\"Snekboard Controls LEGO Power Functions with CircuitPython or Snek Programming Languages (Crowdfunding) – CNX Software\"</a>. <i>CNX Software – Embedded Systems News</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240105001031/https://www.cnx-software.com/2020/01/16/snekboard-controls-lego-power-functions-with-circuitpython-or-snek-programming-languages/\">Archived</a> from the original on 5 January 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 January</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=CNX+Software+%E2%80%93+Embedded+Systems+News&amp;rft.atitle=Snekboard+Controls+LEGO+Power+Functions+with+CircuitPython+or+Snek+Programming+Languages+%28Crowdfunding%29+%E2%80%93+CNX+Software&amp;rft.date=2020-01-16&amp;rft.aulast=Aufranc+%28CNXSoft%29&amp;rft.aufirst=Jean-Luc&amp;rft_id=https%3A%2F%2Fwww.cnx-software.com%2F2020%2F01%2F16%2Fsnekboard-controls-lego-power-functions-with-circuitpython-or-snek-programming-languages%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-150\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-150\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKennedy_(@mkennedy)\" class=\"citation web cs1\">Kennedy (@mkennedy), Michael. <a rel=\"nofollow\" class=\"external text\" href=\"https://pythonbytes.fm/episodes/show/187/ready-to-find-out-if-youre-git-famous\">\"Ready to find out if you're git famous?\"</a>. <i>pythonbytes.fm</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240105001031/https://pythonbytes.fm/episodes/show/187/ready-to-find-out-if-youre-git-famous\">Archived</a> from the original on 5 January 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 January</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=pythonbytes.fm&amp;rft.atitle=Ready+to+find+out+if+you%27re+git+famous%3F&amp;rft.aulast=Kennedy+%28%40mkennedy%29&amp;rft.aufirst=Michael&amp;rft_id=https%3A%2F%2Fpythonbytes.fm%2Fepisodes%2Fshow%2F187%2Fready-to-find-out-if-youre-git-famous&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-151\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-151\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPackard2022\" class=\"citation web cs1\">Packard, Keith (20 December 2022). <a rel=\"nofollow\" class=\"external text\" href=\"https://sneklang.org/doc/snek.pdf\">\"The Snek Programming Language: A Python-inspired Embedded Computing Language\"</a> <span class=\"cs1-format\">(PDF)</span>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240104162458/https://sneklang.org/doc/snek.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 4 January 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">4 January</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Snek+Programming+Language%3A+A+Python-inspired+Embedded+Computing+Language&amp;rft.date=2022-12-20&amp;rft.aulast=Packard&amp;rft.aufirst=Keith&amp;rft_id=https%3A%2F%2Fsneklang.org%2Fdoc%2Fsnek.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-73-152\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-73_152-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://doc.pypy.org/en/latest/stackless.html\">\"Application-level Stackless features – PyPy 2.0.2 documentation\"</a>. Doc.pypy.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200604231513/https://doc.pypy.org/en/latest/stackless.html\">Archived</a> from the original on 4 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 July</span> 2013</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Application-level+Stackless+features+%E2%80%93+PyPy+2.0.2+documentation&amp;rft.pub=Doc.pypy.org&amp;rft_id=http%3A%2F%2Fdoc.pypy.org%2Fen%2Flatest%2Fstackless.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-74-153\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-74_153-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://code.google.com/p/unladen-swallow/wiki/ProjectPlan\">\"Plans for optimizing Python\"</a>. <i>Google Project Hosting</i>. 15 December 2009. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160411181848/https://code.google.com/p/unladen-swallow/wiki/ProjectPlan\">Archived</a> from the original on 11 April 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Google+Project+Hosting&amp;rft.atitle=Plans+for+optimizing+Python&amp;rft.date=2009-12-15&amp;rft_id=https%3A%2F%2Fcode.google.com%2Fp%2Funladen-swallow%2Fwiki%2FProjectPlan&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-154\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-154\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.stochasticgeometry.ie/2010/04/29/python-on-the-nokia-n900/\">\"Python on the Nokia N900\"</a>. <i>Stochastic Geometry</i>. 29 April 2010. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190620000053/http://www.stochasticgeometry.ie/2010/04/29/python-on-the-nokia-n900/\">Archived</a> from the original on 20 June 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 July</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Stochastic+Geometry&amp;rft.atitle=Python+on+the+Nokia+N900&amp;rft.date=2010-04-29&amp;rft_id=http%3A%2F%2Fwww.stochasticgeometry.ie%2F2010%2F04%2F29%2Fpython-on-the-nokia-n900%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-155\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-155\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://brython.info/\">\"Brython\"</a>. <i>brython.info</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180803065954/http://brython.info/\">Archived</a> from the original on 3 August 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">21 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=brython.info&amp;rft.atitle=Brython&amp;rft_id=https%3A%2F%2Fbrython.info%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-156\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-156\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.transcrypt.org\">\"Transcrypt – Python in the browser\"</a>. <i>transcrypt.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180819133303/http://www.transcrypt.org/\">Archived</a> from the original on 19 August 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 December</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=transcrypt.org&amp;rft.atitle=Transcrypt+%E2%80%93+Python+in+the+browser&amp;rft_id=https%3A%2F%2Fwww.transcrypt.org&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-157\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-157\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoq.com/articles/transcrypt-python-javascript-compiler/\">\"Transcrypt: Anatomy of a Python to JavaScript Compiler\"</a>. <i>InfoQ</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201205193339/https://www.infoq.com/articles/transcrypt-python-javascript-compiler/\">Archived</a> from the original on 5 December 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoQ&amp;rft.atitle=Transcrypt%3A+Anatomy+of+a+Python+to+JavaScript+Compiler&amp;rft_id=https%3A%2F%2Fwww.infoq.com%2Farticles%2Ftranscrypt-python-javascript-compiler%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-158\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-158\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://nuitka.net/\">\"Nuitka Home | Nuitka Home\"</a>. <i>nuitka.net</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200530211233/https://nuitka.net/\">Archived</a> from the original on 30 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 August</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=nuitka.net&amp;rft.atitle=Nuitka+Home+%7C+Nuitka+Home&amp;rft_id=http%3A%2F%2Fnuitka.net%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Guelton_Brunet_Amini_Merlini_2015_p=014001-159\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Guelton_Brunet_Amini_Merlini_2015_p=014001_159-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGueltonBrunetAminiMerlini2015\" class=\"citation journal cs1\">Guelton, Serge; Brunet, Pierrick; Amini, Mehdi; Merlini, Adrien; Corbillon, Xavier; Raynaud, Alan (16 March 2015). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1088%2F1749-4680%2F8%2F1%2F014001\">\"Pythran: enabling static optimization of scientific Python programs\"</a>. <i>Computational Science &amp; Discovery</i>. <b>8</b> (1). IOP Publishing: 014001. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2015CS&amp;D....8a4001G\">2015CS&#38;D....8a4001G</a>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1088%2F1749-4680%2F8%2F1%2F014001\">10.1088/1749-4680/8/1/014001</a></span>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/1749-4699\">1749-4699</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Computational+Science+%26+Discovery&amp;rft.atitle=Pythran%3A+enabling+static+optimization+of+scientific+Python+programs&amp;rft.volume=8&amp;rft.issue=1&amp;rft.pages=014001&amp;rft.date=2015-03-16&amp;rft.issn=1749-4699&amp;rft_id=info%3Adoi%2F10.1088%2F1749-4680%2F8%2F1%2F014001&amp;rft_id=info%3Abibcode%2F2015CS%26D....8a4001G&amp;rft.aulast=Guelton&amp;rft.aufirst=Serge&amp;rft.au=Brunet%2C+Pierrick&amp;rft.au=Amini%2C+Mehdi&amp;rft.au=Merlini%2C+Adrien&amp;rft.au=Corbillon%2C+Xavier&amp;rft.au=Raynaud%2C+Alan&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1088%252F1749-4680%252F8%252F1%252F014001&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span><span class=\"cs1-maint citation-comment\"><code class=\"cs1-code\">{{<a href=\"/wiki/Template:Cite_journal\" title=\"Template:Cite journal\">cite journal</a>}}</code>:  CS1 maint: article number as page number (<a href=\"/wiki/Category:CS1_maint:_article_number_as_page_number\" title=\"Category:CS1 maint: article number as page number\">link</a>)</span></span>\n</li>\n<li id=\"cite_note-160\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-160\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://11l-lang.org/transpiler\">\"The Python → 11l → C++ transpiler\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220924233728/https://11l-lang.org/transpiler/\">Archived</a> from the original on 24 September 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 July</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Python+%E2%86%92+11l+%E2%86%92+C%2B%2B+transpiler&amp;rft_id=https%3A%2F%2F11l-lang.org%2Ftranspiler&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-161\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-161\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/google/grumpy\">\"google/grumpy\"</a>. 10 April 2020. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200415054919/https://github.com/google/grumpy\">Archived</a> from the original on 15 April 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 March</span> 2020</span> &#8211; via GitHub.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=google%2Fgrumpy&amp;rft.date=2020-04-10&amp;rft_id=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgrumpy&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-162\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-162\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://opensource.google/projects/\">\"Projects\"</a>. <i>opensource.google</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200424191248/https://opensource.google/projects/\">Archived</a> from the original on 24 April 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 March</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=opensource.google&amp;rft.atitle=Projects&amp;rft_id=https%3A%2F%2Fopensource.google%2Fprojects%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-163\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-163\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFFrancisco\" class=\"citation news cs1\">Francisco, Thomas Claburn in San. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.theregister.com/2017/01/05/googles_grumpy_makes_python_go/\">\"Google's Grumpy code makes Python Go\"</a>. <i>www.theregister.com</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210307165521/https://www.theregister.com/2017/01/05/googles_grumpy_makes_python_go/\">Archived</a> from the original on 7 March 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 January</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=www.theregister.com&amp;rft.atitle=Google%27s+Grumpy+code+makes+Python+Go&amp;rft.aulast=Francisco&amp;rft.aufirst=Thomas+Claburn+in+San&amp;rft_id=https%3A%2F%2Fwww.theregister.com%2F2017%2F01%2F05%2Fgoogles_grumpy_makes_python_go%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-164\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-164\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://ironpython.net/\">\"IronPython.net /\"</a>. <i>ironpython.net</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210417064418/https://ironpython.net/\">Archived</a> from the original on 17 April 2021.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ironpython.net&amp;rft.atitle=IronPython.net+%2F&amp;rft_id=https%3A%2F%2Fironpython.net%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-165\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-165\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/IronLanguages/ironpython3\">\"GitHub – IronLanguages/ironpython3: Implementation of Python 3.x for .NET Framework that is built on top of the Dynamic Language Runtime\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210928101250/https://github.com/IronLanguages/ironpython3\">Archived</a> from the original on 28 September 2021.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=GitHub+%E2%80%93+IronLanguages%2Fironpython3%3A+Implementation+of+Python+3.x+for+.NET+Framework+that+is+built+on+top+of+the+Dynamic+Language+Runtime&amp;rft_id=https%3A%2F%2Fgithub.com%2FIronLanguages%2Fironpython3&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-166\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-166\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.jython.org/jython-old-sites/archive/22/userfaq.html\">\"Jython FAQ\"</a>. <i>www.jython.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210422055726/https://www.jython.org/jython-old-sites/archive/22/userfaq.html\">Archived</a> from the original on 22 April 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 April</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=www.jython.org&amp;rft.atitle=Jython+FAQ&amp;rft_id=https%3A%2F%2Fwww.jython.org%2Fjython-old-sites%2Farchive%2F22%2Fuserfaq.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-167\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-167\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFMurri2013\" class=\"citation conference cs1\">Murri, Riccardo (2013). <i>Performance of Python runtimes on a non-numeric scientific code</i>. European Conference on Python in Science (EuroSciPy). <a href=\"/wiki/ArXiv_(identifier)\" class=\"mw-redirect\" title=\"ArXiv (identifier)\">arXiv</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://arxiv.org/abs/1404.6388\">1404.6388</a></span>. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2014arXiv1404.6388M\">2014arXiv1404.6388M</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=conference&amp;rft.btitle=Performance+of+Python+runtimes+on+a+non-numeric+scientific+code&amp;rft.date=2013&amp;rft_id=info%3Aarxiv%2F1404.6388&amp;rft_id=info%3Abibcode%2F2014arXiv1404.6388M&amp;rft.aulast=Murri&amp;rft.aufirst=Riccardo&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-168\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-168\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/python.html\">\"The Computer Language Benchmarks Game\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614210246/https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/python.html\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 April</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Computer+Language+Benchmarks+Game&amp;rft_id=https%3A%2F%2Fbenchmarksgame-team.pages.debian.net%2Fbenchmarksgame%2Ffastest%2Fpython.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-PepCite000-169\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-PepCite000_169-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-PepCite000_169-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFWarsawHyltonGoodger2000\" class=\"citation web cs1\">Warsaw, Barry; Hylton, Jeremy; Goodger, David (13 June 2000). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0001/\">\"PEP 1&#160;– PEP Purpose and Guidelines\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200606042011/https://www.python.org/dev/peps/pep-0001/\">Archived</a> from the original on 6 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 April</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+1+%E2%80%93+PEP+Purpose+and+Guidelines&amp;rft.date=2000-06-13&amp;rft.aulast=Warsaw&amp;rft.aufirst=Barry&amp;rft.au=Hylton%2C+Jeremy&amp;rft.au=Goodger%2C+David&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0001%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-170\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-170\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0008/\">\"PEP 8 – Style Guide for Python Code\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190417223549/https://www.python.org/dev/peps/pep-0008/\">Archived</a> from the original on 17 April 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">26 March</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+8+%E2%80%93+Style+Guide+for+Python+Code&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0008%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-21-171\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-21_171-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCannon\" class=\"citation web cs1\">Cannon, Brett. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20090601134342/http://www.python.org/dev/intro/\">\"Guido, Some Guys, and a Mailing List: How Python is Developed\"</a>. <i>python.org</i>. Python Software Foundation. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/intro/\">the original</a> on 1 June 2009<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 June</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=python.org&amp;rft.atitle=Guido%2C+Some+Guys%2C+and+a+Mailing+List%3A+How+Python+is+Developed&amp;rft.aulast=Cannon&amp;rft.aufirst=Brett&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fintro%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-172\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-172\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEdge2022\" class=\"citation web cs1\">Edge, Jake (23 February 2022). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/885854/\">\"Moving Python's bugs to GitHub &#91;LWN.net&#93;\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20221002183818/https://lwn.net/Articles/885854/\">Archived</a> from the original on 2 October 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2 October</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Moving+Python%27s+bugs+to+GitHub+%26%2391%3BLWN.net%26%2393%3B&amp;rft.date=2022-02-23&amp;rft.aulast=Edge&amp;rft.aufirst=Jake&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F885854%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-py_dev_guide-173\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-py_dev_guide_173-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://devguide.python.org/\">\"Python Developer's Guide – Python Developer's Guide\"</a>. <i>devguide.python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201109032501/https://devguide.python.org/\">Archived</a> from the original on 9 November 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 December</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=devguide.python.org&amp;rft.atitle=Python+Developer%27s+Guide+%E2%80%93+Python+Developer%27s+Guide&amp;rft_id=https%3A%2F%2Fdevguide.python.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-174\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-174\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHughes2021\" class=\"citation web cs1\">Hughes, Owen (24 May 2021). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.techrepublic.com/article/programming-languages-why-python-4-0-will-probably-never-arrive-according-to-its-creator/\">\"Programming languages: Why Python 4.0 might never arrive, according to its creator\"</a>. <i>TechRepublic</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20220714201302/https://www.techrepublic.com/article/programming-languages-why-python-4-0-will-probably-never-arrive-according-to-its-creator/\">Archived</a> from the original on 14 July 2022<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">16 May</span> 2022</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=TechRepublic&amp;rft.atitle=Programming+languages%3A+Why+Python+4.0+might+never+arrive%2C+according+to+its+creator&amp;rft.date=2021-05-24&amp;rft.aulast=Hughes&amp;rft.aufirst=Owen&amp;rft_id=https%3A%2F%2Fwww.techrepublic.com%2Farticle%2Fprogramming-languages-why-python-4-0-will-probably-never-arrive-according-to-its-creator%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-175\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-175\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0602/\">\"PEP 602 – Annual Release Cycle for Python\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200614202755/https://www.python.org/dev/peps/pep-0602/\">Archived</a> from the original on 14 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 November</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=PEP+602+%E2%80%93+Annual+Release+Cycle+for+Python&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0602%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-176\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-176\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEdge2019\" class=\"citation web cs1\">Edge, Jake (23 October 2019). <a rel=\"nofollow\" class=\"external text\" href=\"https://lwn.net/Articles/802777/\">\"Changing the Python release cadence &#91;LWN.net&#93;\"</a>. <i>lwn.net</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191106170153/https://lwn.net/Articles/802777/\">Archived</a> from the original on 6 November 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 November</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=lwn.net&amp;rft.atitle=Changing+the+Python+release+cadence+%5BLWN.net%5D&amp;rft.date=2019-10-23&amp;rft.aulast=Edge&amp;rft.aufirst=Jake&amp;rft_id=https%3A%2F%2Flwn.net%2FArticles%2F802777%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-release-schedule-177\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-release-schedule_177-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFNorwitz2002\" class=\"citation web cs1\">Norwitz, Neal (8 April 2002). <a rel=\"nofollow\" class=\"external text\" href=\"https://mail.python.org/pipermail/python-dev/2002-April/022739.html\">\"&#91;Python-Dev&#93; Release Schedules (was Stability &amp; change)\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181215122750/https://mail.python.org/pipermail/python-dev/2002-April/022739.html\">Archived</a> from the original on 15 December 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 June</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=%26%2391%3BPython-Dev%26%2393%3B+Release+Schedules+%28was+Stability+%26+change%29&amp;rft.date=2002-04-08&amp;rft.aulast=Norwitz&amp;rft.aufirst=Neal&amp;rft_id=https%3A%2F%2Fmail.python.org%2Fpipermail%2Fpython-dev%2F2002-April%2F022739.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-22-178\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-AutoNT-22_178-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-AutoNT-22_178-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFAahzBaxter2001\" class=\"citation web cs1\">Aahz; Baxter, Anthony (15 March 2001). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/peps/pep-0006/\">\"PEP 6&#160;– Bug Fix Releases\"</a>. <i>Python Enhancement Proposals</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605001318/https://www.python.org/dev/peps/pep-0006/\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 June</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Enhancement+Proposals&amp;rft.atitle=PEP+6+%E2%80%93+Bug+Fix+Releases&amp;rft.date=2001-03-15&amp;rft.au=Aahz&amp;rft.au=Baxter%2C+Anthony&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0006%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-23-179\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-23_179-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/dev/buildbot/\">\"Python Buildbot\"</a>. <i>Python Developer's Guide</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605001322/https://www.python.org/dev/buildbot/\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python+Developer%27s+Guide&amp;rft.atitle=Python+Buildbot&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fdev%2Fbuildbot%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-180\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-180\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.python.org/moin/DocumentationTools\">\"Documentation Tools\"</a>. <i>Python.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20201111173635/https://wiki.python.org/moin/DocumentationTools\">Archived</a> from the original on 11 November 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 March</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Python.org&amp;rft.atitle=Documentation+Tools&amp;rft_id=https%3A%2F%2Fwiki.python.org%2Fmoin%2FDocumentationTools&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-tutorial-chapter1-181\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-tutorial-chapter1_181-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-tutorial-chapter1_181-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/tutorial/appetite.html\">\"Whetting Your Appetite\"</a>. <i>The Python Tutorial</i>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121026063559/http://docs.python.org/tutorial/appetite.html\">Archived</a> from the original on 26 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">20 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+Python+Tutorial&amp;rft.atitle=Whetting+Your+Appetite&amp;rft_id=https%3A%2F%2Fdocs.python.org%2Ftutorial%2Fappetite.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-26-182\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-26_182-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://stackoverflow.com/questions/5033906/in-python-should-i-use-else-after-a-return-in-an-if-block\">\"In Python, should I use else after a return in an if block?\"</a>. <i><a href=\"/wiki/Stack_Overflow\" title=\"Stack Overflow\">Stack Overflow</a></i>. Stack Exchange. 17 February 2011. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190620000050/https://stackoverflow.com/questions/5033906/in-python-should-i-use-else-after-a-return-in-an-if-block\">Archived</a> from the original on 20 June 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 May</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Stack+Overflow&amp;rft.atitle=In+Python%2C+should+I+use+else+after+a+return+in+an+if+block%3F&amp;rft.date=2011-02-17&amp;rft_id=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F5033906%2Fin-python-should-i-use-else-after-a-return-in-an-if-block&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-183\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-183\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLutz2009\" class=\"citation book cs1\">Lutz, Mark (2009). <a rel=\"nofollow\" class=\"external text\" href=\"https://books.google.com/books?id=1HxWGezDZcgC&amp;pg=PA17\"><i>Learning Python: Powerful Object-Oriented Programming</i></a>. O'Reilly Media, Inc. p.&#160;17. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/9781449379322\" title=\"Special:BookSources/9781449379322\"><bdi>9781449379322</bdi></a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170717044012/https://books.google.com/books?id=1HxWGezDZcgC&amp;pg=PA17\">Archived</a> from the original on 17 July 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 May</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Learning+Python%3A+Powerful+Object-Oriented+Programming&amp;rft.pages=17&amp;rft.pub=O%27Reilly+Media%2C+Inc.&amp;rft.date=2009&amp;rft.isbn=9781449379322&amp;rft.aulast=Lutz&amp;rft.aufirst=Mark&amp;rft_id=https%3A%2F%2Fbooks.google.com%2Fbooks%3Fid%3D1HxWGezDZcgC%26pg%3DPA17&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-184\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-184\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFFehily2002\" class=\"citation book cs1\">Fehily, Chris (2002). <a rel=\"nofollow\" class=\"external text\" href=\"https://books.google.com/books?id=carqdIdfVlYC&amp;pg=PR15\"><i>Python</i></a>. Peachpit Press. p.&#160;xv. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/9780201748840\" title=\"Special:BookSources/9780201748840\"><bdi>9780201748840</bdi></a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170717044040/https://books.google.com/books?id=carqdIdfVlYC&amp;pg=PR15\">Archived</a> from the original on 17 July 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 May</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Python&amp;rft.pages=xv&amp;rft.pub=Peachpit+Press&amp;rft.date=2002&amp;rft.isbn=9780201748840&amp;rft.aulast=Fehily&amp;rft.aufirst=Chris&amp;rft_id=https%3A%2F%2Fbooks.google.com%2Fbooks%3Fid%3DcarqdIdfVlYC%26pg%3DPR15&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-introducing_python-185\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-introducing_python_185-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLubanovic2014\" class=\"citation book cs1\">Lubanovic, Bill (2014). <a rel=\"nofollow\" class=\"external text\" href=\"http://archive.org/details/introducingpytho0000luba\"><i>Introducing Python</i></a>. Sebastopol, CA&#160;: O'Reilly Media. p.&#160;305. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4493-5936-2\" title=\"Special:BookSources/978-1-4493-5936-2\"><bdi>978-1-4493-5936-2</bdi></a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 July</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Introducing+Python&amp;rft.pages=305&amp;rft.pub=Sebastopol%2C+CA+%3A+O%27Reilly+Media&amp;rft.date=2014&amp;rft.isbn=978-1-4493-5936-2&amp;rft.aulast=Lubanovic&amp;rft.aufirst=Bill&amp;rft_id=http%3A%2F%2Farchive.org%2Fdetails%2Fintroducingpytho0000luba&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-186\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-186\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.tiobe.com/tiobe-index/\">\"TIOBE Index for August 2025\"</a>. 30 August 2025<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 August</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=TIOBE+Index+for+August+2025&amp;rft.date=2025-08-30&amp;rft_id=https%3A%2F%2Fwww.tiobe.com%2Ftiobe-index%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-187\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-187\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.tiobe.com/tiobe-index/\">\"TIOBE Index\"</a>. <i>TIOBE</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 March</span> 2025</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=TIOBE&amp;rft.atitle=TIOBE+Index&amp;rft_id=https%3A%2F%2Fwww.tiobe.com%2Ftiobe-index%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-quotes-about-python-188\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-quotes-about-python_188-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/about/quotes/\">\"Quotes about Python\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200603135201/https://www.python.org/about/quotes/\">Archived</a> from the original on 3 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">8 January</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Quotes+about+Python&amp;rft.pub=Python+Software+Foundation&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fabout%2Fquotes%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-29-189\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-29_189-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.python.org/moin/OrganizationsUsingPython\">\"Organizations Using Python\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180821075931/https://wiki.python.org/moin/OrganizationsUsingPython\">Archived</a> from the original on 21 August 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 January</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Organizations+Using+Python&amp;rft.pub=Python+Software+Foundation&amp;rft_id=https%3A%2F%2Fwiki.python.org%2Fmoin%2FOrganizationsUsingPython&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-30-190\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-30_190-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation journal cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://cdsweb.cern.ch/journal/CERNBulletin/2006/31/News%20Articles/974627?ln=en\">\"Python&#160;: the holy grail of programming\"</a>. <i>CERN Bulletin</i> (31/2006). CERN Publications. 31 July 2006. <a rel=\"nofollow\" class=\"external text\" href=\"https://archive.today/20130115191843/http://cdsweb.cern.ch/journal/CERNBulletin/2006/31/News%20Articles/974627?ln=en\">Archived</a> from the original on 15 January 2013<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=CERN+Bulletin&amp;rft.atitle=Python+%3A+the+holy+grail+of+programming&amp;rft.issue=31%2F2006&amp;rft.date=2006-07-31&amp;rft_id=http%3A%2F%2Fcdsweb.cern.ch%2Fjournal%2FCERNBulletin%2F2006%2F31%2FNews%2520Articles%2F974627%3Fln%3Den&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-31-191\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-31_191-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFShafer2003\" class=\"citation web cs1\">Shafer, Daniel G. (17 January 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/about/success/usa/\">\"Python Streamlines Space Shuttle Mission Design\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605093424/https://www.python.org/about/success/usa/\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+Streamlines+Space+Shuttle+Mission+Design&amp;rft.pub=Python+Software+Foundation&amp;rft.date=2003-01-17&amp;rft.aulast=Shafer&amp;rft.aufirst=Daniel+G.&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fabout%2Fsuccess%2Fusa%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-192\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-192\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://developers.facebook.com/blog/post/301\">\"Tornado: Facebook's Real-Time Web Framework for Python – Facebook for Developers\"</a>. <i>Facebook for Developers</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190219031313/https://developers.facebook.com/blog/post/301\">Archived</a> from the original on 19 February 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 June</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Facebook+for+Developers&amp;rft.atitle=Tornado%3A+Facebook%27s+Real-Time+Web+Framework+for+Python+%E2%80%93+Facebook+for+Developers&amp;rft_id=https%3A%2F%2Fdevelopers.facebook.com%2Fblog%2Fpost%2F301&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-193\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-193\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://instagram-engineering.com/what-powers-instagram-hundreds-of-instances-dozens-of-technologies-adf2e22da2ad\">\"What Powers Instagram: Hundreds of Instances, Dozens of Technologies\"</a>. Instagram Engineering. 11 December 2016. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615183410/https://instagram-engineering.com/what-powers-instagram-hundreds-of-instances-dozens-of-technologies-adf2e22da2ad\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What+Powers+Instagram%3A+Hundreds+of+Instances%2C+Dozens+of+Technologies&amp;rft.pub=Instagram+Engineering&amp;rft.date=2016-12-11&amp;rft_id=https%3A%2F%2Finstagram-engineering.com%2Fwhat-powers-instagram-hundreds-of-instances-dozens-of-technologies-adf2e22da2ad&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-194\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-194\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://labs.spotify.com/2013/03/20/how-we-use-python-at-spotify/\">\"How we use Python at Spotify\"</a>. <i>Spotify Labs</i>. 20 March 2013. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200610005143/https://labs.spotify.com/2013/03/20/how-we-use-python-at-spotify/\">Archived</a> from the original on 10 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 July</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Spotify+Labs&amp;rft.atitle=How+we+use+Python+at+Spotify&amp;rft.date=2013-03-20&amp;rft_id=https%3A%2F%2Flabs.spotify.com%2F2013%2F03%2F20%2Fhow-we-use-python-at-spotify%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-32-195\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-32_195-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFFortenberry2003\" class=\"citation web cs1\">Fortenberry, Tim (17 January 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/about/success/ilm/\">\"Industrial Light &amp; Magic Runs on Python\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200606042020/https://www.python.org/about/success/ilm/\">Archived</a> from the original on 6 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Industrial+Light+%26+Magic+Runs+on+Python&amp;rft.pub=Python+Software+Foundation&amp;rft.date=2003-01-17&amp;rft.aulast=Fortenberry&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.python.org%2Fabout%2Fsuccess%2Film%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-33-196\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-33_196-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFTaft2007\" class=\"citation web cs1\">Taft, Darryl K. (5 March 2007). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.eweek.com/c/a/Application-Development/Python-Slithers-into-Systems/\">\"Python Slithers into Systems\"</a>. <i>eWeek.com</i>. Ziff Davis Holdings. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210813194304/https://www.eweek.com/development/python-slithers-into-systems/\">Archived</a> from the original on 13 August 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 September</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=eWeek.com&amp;rft.atitle=Python+Slithers+into+Systems&amp;rft.date=2007-03-05&amp;rft.aulast=Taft&amp;rft.aufirst=Darryl+K.&amp;rft_id=http%3A%2F%2Fwww.eweek.com%2Fc%2Fa%2FApplication-Development%2FPython-Slithers-into-Systems%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-197\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-197\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation cs2\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/reddit-archive/reddit\"><i>GitHub – reddit-archive/reddit: historical code from reddit.com.</i></a>, The Reddit Archives, <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200601104939/https://github.com/reddit-archive/reddit\">archived</a> from the original on 1 June 2020<span class=\"reference-accessdate\">, retrieved <span class=\"nowrap\">20 March</span> 2019</span></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=GitHub+%E2%80%93+reddit-archive%2Freddit%3A+historical+code+from+reddit.com.&amp;rft.pub=The+Reddit+Archives&amp;rft_id=https%3A%2F%2Fgithub.com%2Freddit-archive%2Freddit&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-198\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-198\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://elixir-lang.org/blog/2020/10/08/real-time-communication-at-scale-with-elixir-at-discord/\">\"Real time communication at scale with Elixir at Discord\"</a>. 8 October 2020.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Real+time+communication+at+scale+with+Elixir+at+Discord&amp;rft.date=2020-10-08&amp;rft_id=https%3A%2F%2Felixir-lang.org%2Fblog%2F2020%2F10%2F08%2Freal-time-communication-at-scale-with-elixir-at-discord%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-199\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-199\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.freelancinggig.com/blog/2018/07/05/what-programming-language-is-baidu-built-in/#:~:text=Even%20though%20Baidu%20has%20used,part%20JavaScript%20has%20been%20applied\">\"What Programming Language is Baidu Built In?\"</a>. 5 July 2018.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What+Programming+Language+is+Baidu+Built+In%3F&amp;rft.date=2018-07-05&amp;rft_id=https%3A%2F%2Fwww.freelancinggig.com%2Fblog%2F2018%2F07%2F05%2Fwhat-programming-language-is-baidu-built-in%2F%23%3A~%3Atext%3DEven%2520though%2520Baidu%2520has%2520used%2Cpart%2520JavaScript%2520has%2520been%2520applied&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-35-200\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-35_200-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://w3techs.com/technologies/details/pl-python/all/all\">\"Usage statistics and market share of Python for websites\"</a>. 2012. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210813194305/https://w3techs.com/technologies/details/pl-python\">Archived</a> from the original on 13 August 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Usage+statistics+and+market+share+of+Python+for+websites&amp;rft.date=2012&amp;rft_id=http%3A%2F%2Fw3techs.com%2Ftechnologies%2Fdetails%2Fpl-python%2Fall%2Fall&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-cise-201\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-cise_201-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFOliphant2007\" class=\"citation journal cs1\">Oliphant, Travis (2007). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.h2desk.com/blog/python-scientific-computing/\">\"Python for Scientific Computing\"</a>. <i>Computing in Science and Engineering</i>. <b>9</b> (3): <span class=\"nowrap\">10–</span>20. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2007CSE.....9c..10O\">2007CSE.....9c..10O</a>. <a href=\"/wiki/CiteSeerX_(identifier)\" class=\"mw-redirect\" title=\"CiteSeerX (identifier)\">CiteSeerX</a>&#160;<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.474.6460\">10.1.1.474.6460</a></span>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1109%2FMCSE.2007.58\">10.1109/MCSE.2007.58</a>. <a href=\"/wiki/ISSN_(identifier)\" class=\"mw-redirect\" title=\"ISSN (identifier)\">ISSN</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://search.worldcat.org/issn/1521-9615\">1521-9615</a>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:206457124\">206457124</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615193226/https://www.h2desk.com/blog/python-scientific-computing/\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 April</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Computing+in+Science+and+Engineering&amp;rft.atitle=Python+for+Scientific+Computing&amp;rft.volume=9&amp;rft.issue=3&amp;rft.pages=10-20&amp;rft.date=2007&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A206457124%23id-name%3DS2CID&amp;rft_id=info%3Abibcode%2F2007CSE.....9c..10O&amp;rft_id=https%3A%2F%2Fciteseerx.ist.psu.edu%2Fviewdoc%2Fsummary%3Fdoi%3D10.1.1.474.6460%23id-name%3DCiteSeerX&amp;rft.issn=1521-9615&amp;rft_id=info%3Adoi%2F10.1109%2FMCSE.2007.58&amp;rft.aulast=Oliphant&amp;rft.aufirst=Travis&amp;rft_id=https%3A%2F%2Fwww.h2desk.com%2Fblog%2Fpython-scientific-computing%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-millman-202\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-millman_202-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFMillmanAivazis2011\" class=\"citation journal cs1\">Millman, K. Jarrod; Aivazis, Michael (2011). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.computer.org/csdl/mags/cs/2011/02/mcs2011020009.html\">\"Python for Scientists and Engineers\"</a>. <i>Computing in Science and Engineering</i>. <b>13</b> (2): <span class=\"nowrap\">9–</span>12. <a href=\"/wiki/Bibcode_(identifier)\" class=\"mw-redirect\" title=\"Bibcode (identifier)\">Bibcode</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://ui.adsabs.harvard.edu/abs/2011CSE....13b...9M\">2011CSE....13b...9M</a>. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1109%2FMCSE.2011.36\">10.1109/MCSE.2011.36</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190219031439/https://www.computer.org/csdl/mags/cs/2011/02/mcs2011020009.html\">Archived</a> from the original on 19 February 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 July</span> 2014</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Computing+in+Science+and+Engineering&amp;rft.atitle=Python+for+Scientists+and+Engineers&amp;rft.volume=13&amp;rft.issue=2&amp;rft.pages=9-12&amp;rft.date=2011&amp;rft_id=info%3Adoi%2F10.1109%2FMCSE.2011.36&amp;rft_id=info%3Abibcode%2F2011CSE....13b...9M&amp;rft.aulast=Millman&amp;rft.aufirst=K.+Jarrod&amp;rft.au=Aivazis%2C+Michael&amp;rft_id=http%3A%2F%2Fwww.computer.org%2Fcsdl%2Fmags%2Fcs%2F2011%2F02%2Fmcs2011020009.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ICSE-203\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-ICSE_203-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation cs2\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615180428/http://visual.icse.us.edu.pl/methodology/why_Sage.html\"><i>Science education with SageMath</i></a>, Innovative Computing in Science Education, archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://visual.icse.us.edu.pl/methodology/why_Sage.html\">the original</a> on 15 June 2020<span class=\"reference-accessdate\">, retrieved <span class=\"nowrap\">22 April</span> 2019</span></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Science+education+with+SageMath&amp;rft.pub=Innovative+Computing+in+Science+Education&amp;rft_id=http%3A%2F%2Fvisual.icse.us.edu.pl%2Fmethodology%2Fwhy_Sage.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-204\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-204\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.opencv.org/3.4.9/d6/d00/tutorial_py_root.html\">\"OpenCV: OpenCV-Python Tutorials\"</a>. <i>docs.opencv.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200923063145/https://docs.opencv.org/3.4.9/d6/d00/tutorial_py_root.html\">Archived</a> from the original on 23 September 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">14 September</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.opencv.org&amp;rft.atitle=OpenCV%3A+OpenCV-Python+Tutorials&amp;rft_id=https%3A%2F%2Fdocs.opencv.org%2F3.4.9%2Fd6%2Fd00%2Ftutorial_py_root.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-whitepaper2015-205\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-whitepaper2015_205-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFDeanMongaGhemawat2015\" class=\"citation web cs1\"><a href=\"/wiki/Jeff_Dean_(computer_scientist)\" class=\"mw-redirect\" title=\"Jeff Dean (computer scientist)\">Dean, Jeff</a>; Monga, Rajat; et&#160;al. (9 November 2015). <a rel=\"nofollow\" class=\"external text\" href=\"http://download.tensorflow.org/paper/whitepaper2015.pdf\">\"TensorFlow: Large-scale machine learning on heterogeneous systems\"</a> <span class=\"cs1-format\">(PDF)</span>. <i>TensorFlow.org</i>. Google Research. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20151120004649/http://download.tensorflow.org/paper/whitepaper2015.pdf\">Archived</a> <span class=\"cs1-format\">(PDF)</span> from the original on 20 November 2015<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 November</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=TensorFlow.org&amp;rft.atitle=TensorFlow%3A+Large-scale+machine+learning+on+heterogeneous+systems&amp;rft.date=2015-11-09&amp;rft.aulast=Dean&amp;rft.aufirst=Jeff&amp;rft.au=Monga%2C+Rajat&amp;rft.au=Ghemawat%2C+Sanjay&amp;rft_id=http%3A%2F%2Fdownload.tensorflow.org%2Fpaper%2Fwhitepaper2015.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-206\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-206\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPiatetsky\" class=\"citation web cs1\">Piatetsky, Gregory. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.kdnuggets.com/2018/05/poll-tools-analytics-data-science-machine-learning-results.html/2\">\"Python eats away at R: Top Software for Analytics, Data Science, Machine Learning in 2018: Trends and Analysis\"</a>. <i>KDnuggets</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191115234216/https://www.kdnuggets.com/2018/05/poll-tools-analytics-data-science-machine-learning-results.html/2\">Archived</a> from the original on 15 November 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 May</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=KDnuggets&amp;rft.atitle=Python+eats+away+at+R%3A+Top+Software+for+Analytics%2C+Data+Science%2C+Machine+Learning+in+2018%3A+Trends+and+Analysis&amp;rft.aulast=Piatetsky&amp;rft.aufirst=Gregory&amp;rft_id=https%3A%2F%2Fwww.kdnuggets.com%2F2018%2F05%2Fpoll-tools-analytics-data-science-machine-learning-results.html%2F2&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-207\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-207\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://scikit-learn.org/stable/testimonials/testimonials.html\">\"Who is using scikit-learn? – scikit-learn 0.20.1 documentation\"</a>. <i>scikit-learn.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200506210716/https://scikit-learn.org/stable/testimonials/testimonials.html\">Archived</a> from the original on 6 May 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">30 November</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=scikit-learn.org&amp;rft.atitle=Who+is+using+scikit-learn%3F+%E2%80%93+scikit-learn+0.20.1+documentation&amp;rft_id=https%3A%2F%2Fscikit-learn.org%2Fstable%2Ftestimonials%2Ftestimonials.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-208\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-208\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJouppi\" class=\"citation web cs1\"><a href=\"/wiki/Norman_Jouppi\" title=\"Norman Jouppi\">Jouppi, Norm</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://cloudplatform.googleblog.com/2016/05/Google-supercharges-machine-learning-tasks-with-custom-chip.html\">\"Google supercharges machine learning tasks with TPU custom chip\"</a>. <i>Google Cloud Platform Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20160518201516/https://cloudplatform.googleblog.com/2016/05/Google-supercharges-machine-learning-tasks-with-custom-chip.html\">Archived</a> from the original on 18 May 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 May</span> 2016</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Google+Cloud+Platform+Blog&amp;rft.atitle=Google+supercharges+machine+learning+tasks+with+TPU+custom+chip&amp;rft.aulast=Jouppi&amp;rft.aufirst=Norm&amp;rft_id=https%3A%2F%2Fcloudplatform.googleblog.com%2F2016%2F05%2FGoogle-supercharges-machine-learning-tasks-with-custom-chip.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ProbLogConcepts-209\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-ProbLogConcepts_209-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFDe_RaedtKimmig2015\" class=\"citation journal cs1\">De Raedt, Luc; Kimmig, Angelika (2015). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007%2Fs10994-015-5494-z\">\"Probabilistic (logic) programming concepts\"</a>. <i>Machine Learning</i>. <b>100</b> (1): <span class=\"nowrap\">5–</span>47. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1007%2Fs10994-015-5494-z\">10.1007/s10994-015-5494-z</a></span>. <a href=\"/wiki/S2CID_(identifier)\" class=\"mw-redirect\" title=\"S2CID (identifier)\">S2CID</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://api.semanticscholar.org/CorpusID:3166992\">3166992</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Machine+Learning&amp;rft.atitle=Probabilistic+%28logic%29+programming+concepts&amp;rft.volume=100&amp;rft.issue=1&amp;rft.pages=5-47&amp;rft.date=2015&amp;rft_id=info%3Adoi%2F10.1007%2Fs10994-015-5494-z&amp;rft_id=https%3A%2F%2Fapi.semanticscholar.org%2FCorpusID%3A3166992%23id-name%3DS2CID&amp;rft.aulast=De+Raedt&amp;rft.aufirst=Luc&amp;rft.au=Kimmig%2C+Angelika&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1007%252Fs10994-015-5494-z&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-47-210\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-47_210-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.nltk.org/\">\"Natural Language Toolkit – NLTK 3.5b1 documentation\"</a>. <i>www.nltk.org</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200613003911/http://www.nltk.org/\">Archived</a> from the original on 13 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 April</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=www.nltk.org&amp;rft.atitle=Natural+Language+Toolkit+%E2%80%93+NLTK+3.5b1+documentation&amp;rft_id=http%3A%2F%2Fwww.nltk.org%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-211\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-211\">^</a></b></span> <span class=\"reference-text\">Andersen, C. and Swift, T., 2023. The Janus System: a bridge to new prolog applications. In Prolog: The Next 50 Years (pp. 93–104). Cham: Springer Nature Switzerland.</span>\n</li>\n<li id=\"cite_note-212\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-212\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.swi-prolog.org/pldoc/doc_for?object=section(%27packages/janus.html%27)\">\"SWI-Prolog Python interface\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20240315162046/https://www.swi-prolog.org/pldoc/doc_for?object=section%28%27packages%2Fjanus.html%27%29\">Archived</a> from the original on 15 March 2024<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 March</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=SWI-Prolog+Python+interface&amp;rft_id=https%3A%2F%2Fwww.swi-prolog.org%2Fpldoc%2Fdoc_for%3Fobject%3Dsection%28%2527packages%2Fjanus.html%2527%29&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-213\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-213\">^</a></b></span> <span class=\"reference-text\">Tarau, P., 2023. Reflections on automation, learnability and expressiveness in logic-based programming languages. In Prolog: The Next 50 Years (pp. 359–371). Cham: Springer Nature Switzerland.</span>\n</li>\n<li id=\"cite_note-214\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-214\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/library/tkinter.html\">\"Tkinter — Python interface to TCL/Tk\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121018043136/http://docs.python.org/library/tkinter.html\">Archived</a> from the original on 18 October 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 June</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Tkinter+%E2%80%94+Python+interface+to+TCL%2FTk&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Ftkinter.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-215\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-215\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.geeksforgeeks.org/python-tkinter-tutorial/\">\"Python Tkinter Tutorial\"</a>. 3 June 2020. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230609031631/https://www.geeksforgeeks.org/python-tkinter-tutorial/\">Archived</a> from the original on 9 June 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 June</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+Tkinter+Tutorial&amp;rft.date=2020-06-03&amp;rft_id=https%3A%2F%2Fwww.geeksforgeeks.org%2Fpython-tkinter-tutorial%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-216\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-216\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20130717070814/http://gimp-win.sourceforge.net/faq.html\">\"Installers for GIMP for Windows – Frequently Asked Questions\"</a>. 26 July 2013. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://gimp-win.sourceforge.net/faq.html\">the original</a> on 17 July 2013<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">26 July</span> 2013</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Installers+for+GIMP+for+Windows+%E2%80%93+Frequently+Asked+Questions&amp;rft.date=2013-07-26&amp;rft_id=https%3A%2F%2Fgimp-win.sourceforge.net%2Ffaq.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-38-217\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-38_217-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20080319061519/http://www.jasc.com/support/customercare/articles/psp9components.asp\">\"jasc psp9components\"</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.jasc.com/support/customercare/articles/psp9components.asp\">the original</a> on 19 March 2008.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=jasc+psp9components&amp;rft_id=http%3A%2F%2Fwww.jasc.com%2Fsupport%2Fcustomercare%2Farticles%2Fpsp9components.asp&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-39-218\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-39_218-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://webhelp.esri.com/arcgisdesktop/9.2/index.cfm?TopicName=About_getting_started_with_writing_geoprocessing_scripts\">\"About getting started with writing geoprocessing scripts\"</a>. <i>ArcGIS Desktop Help 9.2</i>. Environmental Systems Research Institute. 17 November 2006. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200605144616/http://webhelp.esri.com/arcgisdesktop/9.2/index.cfm?TopicName=About_getting_started_with_writing_geoprocessing_scripts\">Archived</a> from the original on 5 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=ArcGIS+Desktop+Help+9.2&amp;rft.atitle=About+getting+started+with+writing+geoprocessing+scripts&amp;rft.date=2006-11-17&amp;rft_id=http%3A%2F%2Fwebhelp.esri.com%2Farcgisdesktop%2F9.2%2Findex.cfm%3FTopicName%3DAbout_getting_started_with_writing_geoprocessing_scripts&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-40-219\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-40_219-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCCP_porkbelly2010\" class=\"citation web cs1\">CCP porkbelly (24 August 2010). <a rel=\"nofollow\" class=\"external text\" href=\"https://community.eveonline.com/news/dev-blogs/stackless-python-2.7/\">\"Stackless Python 2.7\"</a>. <i>EVE Community Dev Blogs</i>. <a href=\"/wiki/CCP_Games\" title=\"CCP Games\">CCP Games</a>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20140111155537/http://community.eveonline.com/news/dev-blogs/stackless-python-2.7/\">Archived</a> from the original on 11 January 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 January</span> 2014</span>. <q>As you may know, EVE has at its core the programming language known as Stackless Python.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=EVE+Community+Dev+Blogs&amp;rft.atitle=Stackless+Python+2.7&amp;rft.date=2010-08-24&amp;rft.au=CCP+porkbelly&amp;rft_id=http%3A%2F%2Fcommunity.eveonline.com%2Fnews%2Fdev-blogs%2Fstackless-python-2.7%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-41-220\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-41_220-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCaudill2005\" class=\"citation web cs1\">Caudill, Barry (20 September 2005). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20101202164144/http://www.2kgames.com/civ4/blog_03.htm\">\"Modding Sid Meier's Civilization IV\"</a>. <i>Sid Meier's Civilization IV Developer Blog</i>. <a href=\"/wiki/Firaxis_Games\" title=\"Firaxis Games\">Firaxis Games</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.2kgames.com/civ4/blog_03.htm\">the original</a> on 2 December 2010. <q>we created three levels of tools ... The next level offers Python and XML support, letting modders with more experience manipulate the game world and everything in it.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Sid+Meier%27s+Civilization+IV+Developer+Blog&amp;rft.atitle=Modding+Sid+Meier%27s+Civilization+IV&amp;rft.date=2005-09-20&amp;rft.aulast=Caudill&amp;rft.aufirst=Barry&amp;rft_id=http%3A%2F%2Fwww.2kgames.com%2Fciv4%2Fblog_03.htm&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-42-221\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-42_221-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20100715145616/http://code.google.com/apis/documents/docs/1.0/developers_guide_python.html\">\"Python Language Guide (v1.0)\"</a>. <i>Google Documents List Data API v1.0</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://code.google.com/apis/documents/docs/1.0/developers_guide_python.html\">the original</a> on 15 July 2010.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Google+Documents+List+Data+API+v1.0&amp;rft.atitle=Python+Language+Guide+%28v1.0%29&amp;rft_id=https%3A%2F%2Fcode.google.com%2Fapis%2Fdocuments%2Fdocs%2F1.0%2Fdevelopers_guide_python.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-222\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-222\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.libreoffice.org/download/4-0-new-features-and-fixes/\">\"4.0 New Features and Fixes\"</a>. <i>LibreOffice.org</i>. <a href=\"/wiki/The_Document_Foundation\" title=\"The Document Foundation\">The Document Foundation</a>. 2013. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20140209184807/http://www.libreoffice.org/download/4-0-new-features-and-fixes/\">Archived</a> from the original on 9 February 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">25 February</span> 2013</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=LibreOffice.org&amp;rft.atitle=4.0+New+Features+and+Fixes&amp;rft.date=2013&amp;rft_id=http%3A%2F%2Fwww.libreoffice.org%2Fdownload%2F4-0-new-features-and-fixes%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-223\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-223\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/using/unix.html\">\"Python Setup and Usage\"</a>. Python Software Foundation. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200617143505/https://docs.python.org/3/using/unix.html\">Archived</a> from the original on 17 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 January</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+Setup+and+Usage&amp;rft.pub=Python+Software+Foundation&amp;rft_id=https%3A%2F%2Fdocs.python.org%2F3%2Fusing%2Funix.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-51-224\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-51_224-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://sugarlabs.org/go/Sugar\">\"What is Sugar?\"</a>. Sugar Labs. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20090109025944/http://sugarlabs.org/go/Sugar\">Archived</a> from the original on 9 January 2009<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What+is+Sugar%3F&amp;rft.pub=Sugar+Labs&amp;rft_id=http%3A%2F%2Fsugarlabs.org%2Fgo%2FSugar&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-49-225\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-49_225-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1 cs1-prop-unfit\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20090216134332/http://immunitysec.com/products-immdbg.shtml\">\"Immunity: Knowing You're Secure\"</a>. Archived from the original on 16 February 2009.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Immunity%3A+Knowing+You%27re+Secure&amp;rft_id=http%3A%2F%2Fwww.immunitysec.com%2Fproducts-immdbg.shtml&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-50-226\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-50_226-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.coresecurity.com/\">\"Core Security\"</a>. <i>Core Security</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200609165041/http://www.coresecurity.com/\">Archived</a> from the original on 9 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 April</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Core+Security&amp;rft.atitle=Core+Security&amp;rft_id=https%3A%2F%2Fwww.coresecurity.com%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-:1-227\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-:1_227-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-:1_227-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-:1_227-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPereiraCoutoRibeiroRua2017\" class=\"citation book cs1\">Pereira, Rui; Couto, Marco; Ribeiro, Francisco; Rua, Rui; Cunha, Jácome; Fernandes, João Paulo; Saraiva, João (23 October 2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145/3136014.3136031\">\"Energy efficiency across programming languages: How do energy, time, and memory relate?\"</a>. <a rel=\"nofollow\" class=\"external text\" href=\"http://repositorio.inesctec.pt/handle/123456789/5492\"><i>Proceedings of the 10th ACM SIGPLAN International Conference on Software Language Engineering</i></a>. SLE 2017. New York, NY, USA: Association for Computing Machinery. pp.&#160;<span class=\"nowrap\">256–</span>267. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3136014.3136031\">10.1145/3136014.3136031</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4503-5525-4\" title=\"Special:BookSources/978-1-4503-5525-4\"><bdi>978-1-4503-5525-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Energy+efficiency+across+programming+languages%3A+How+do+energy%2C+time%2C+and+memory+relate%3F&amp;rft.btitle=Proceedings+of+the+10th+ACM+SIGPLAN+International+Conference+on+Software+Language+Engineering&amp;rft.place=New+York%2C+NY%2C+USA&amp;rft.series=SLE+2017&amp;rft.pages=256-267&amp;rft.pub=Association+for+Computing+Machinery&amp;rft.date=2017-10-23&amp;rft_id=info%3Adoi%2F10.1145%2F3136014.3136031&amp;rft.isbn=978-1-4503-5525-4&amp;rft.aulast=Pereira&amp;rft.aufirst=Rui&amp;rft.au=Couto%2C+Marco&amp;rft.au=Ribeiro%2C+Francisco&amp;rft.au=Rua%2C+Rui&amp;rft.au=Cunha%2C+J%C3%A1come&amp;rft.au=Fernandes%2C+Jo%C3%A3o+Paulo&amp;rft.au=Saraiva%2C+Jo%C3%A3o&amp;rft_id=https%3A%2F%2Fdoi.org%2F10.1145%2F3136014.3136031&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-228\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-228\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://mail.python.org/pipermail/python-ideas/2013-June/021610.html\">\"&#91;Python-ideas&#93; PEP 315: do-while\"</a>. 26 June 2013.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=%5BPython-ideas%5D+PEP+315%3A+do-while&amp;rft.date=2013-06-26&amp;rft_id=https%3A%2F%2Fmail.python.org%2Fpipermail%2Fpython-ideas%2F2013-June%2F021610.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-229\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-229\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://pyinstaller.org/en/stable/operating-mode.html\">\"What PyInstaller Does and How It Does It\"</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=What+PyInstaller+Does+and+How+It+Does+It&amp;rft_id=https%3A%2F%2Fpyinstaller.org%2Fen%2Fstable%2Foperating-mode.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-230\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-230\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/dflook/python-minifier/issues/130\">\"Feature Request: Whitespace Minification and Line Merging Options #130\"</a>. <i><a href=\"/wiki/GitHub\" title=\"GitHub\">GitHub</a></i>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=GitHub&amp;rft.atitle=Feature+Request%3A+Whitespace+Minification+and+Line+Merging+Options+%23130&amp;rft_id=https%3A%2F%2Fgithub.com%2Fdflook%2Fpython-minifier%2Fissues%2F130&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-90-231\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-90_231-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20081211062108/http://boo.codehaus.org/Gotchas+for+Python+Users\">\"Gotchas for Python Users\"</a>. <i>boo.codehaus.org</i>. Codehaus Foundation. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://boo.codehaus.org/Gotchas+for+Python+Users\">the original</a> on 11 December 2008<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=boo.codehaus.org&amp;rft.atitle=Gotchas+for+Python+Users&amp;rft_id=http%3A%2F%2Fboo.codehaus.org%2FGotchas%2Bfor%2BPython%2BUsers&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-91-232\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-91_232-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEsterbrook\" class=\"citation web cs1\">Esterbrook, Charles. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20080208141002/http://cobra-language.com/docs/acknowledgements/\">\"Acknowledgements\"</a>. <i>cobra-language.com</i>. Cobra Language. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://cobra-language.com/docs/acknowledgements/\">the original</a> on 8 February 2008<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 April</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=cobra-language.com&amp;rft.atitle=Acknowledgements&amp;rft.aulast=Esterbrook&amp;rft.aufirst=Charles&amp;rft_id=http%3A%2F%2Fcobra-language.com%2Fdocs%2Facknowledgements%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-93-233\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-93_233-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20071020082650/http://wiki.ecmascript.org/doku.php?id=proposals:iterators_and_generators\">\"Proposals: iterators and generators &#91;ES4 Wiki&#93;\"</a>. wiki.ecmascript.org. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://wiki.ecmascript.org/doku.php?id=proposals:iterators_and_generators\">the original</a> on 20 October 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Proposals%3A+iterators+and+generators+%5BES4+Wiki%26%2393%3B&amp;rft.pub=wiki.ecmascript.org&amp;rft_id=http%3A%2F%2Fwiki.ecmascript.org%2Fdoku.php%3Fid%3Dproposals%3Aiterators_and_generators&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-234\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-234\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.godotengine.org/en/stable/about/faq.html\">\"Frequently asked questions\"</a>. <i>Godot Engine documentation</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20210428053339/https://docs.godotengine.org/en/stable/about/faq.html\">Archived</a> from the original on 28 April 2021<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">10 May</span> 2021</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Godot+Engine+documentation&amp;rft.atitle=Frequently+asked+questions&amp;rft_id=https%3A%2F%2Fdocs.godotengine.org%2Fen%2Fstable%2Fabout%2Ffaq.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-94-235\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-94_235-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKincaid2009\" class=\"citation news cs1\">Kincaid, Jason (10 November 2009). <a rel=\"nofollow\" class=\"external text\" href=\"https://techcrunch.com/2009/11/10/google-go-language/\">\"Google's Go: A New Programming Language That's Python Meets C++\"</a>. <i>TechCrunch</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20100118014358/http://www.techcrunch.com/2009/11/10/google-go-language/\">Archived</a> from the original on 18 January 2010<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 January</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=TechCrunch&amp;rft.atitle=Google%27s+Go%3A+A+New+Programming+Language+That%27s+Python+Meets+C%2B%2B&amp;rft.date=2009-11-10&amp;rft.aulast=Kincaid&amp;rft.aufirst=Jason&amp;rft_id=https%3A%2F%2Ftechcrunch.com%2F2009%2F11%2F10%2Fgoogle-go-language%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-95-236\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-95_236-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFStrachan2003\" class=\"citation web cs1\">Strachan, James (29 August 2003). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20070405085722/http://radio.weblogs.com/0112098/2003/08/29.html\">\"Groovy&#160;– the birth of a new dynamic language for the Java platform\"</a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://radio.weblogs.com/0112098/2003/08/29.html\">the original</a> on 5 April 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 June</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Groovy+%E2%80%93+the+birth+of+a+new+dynamic+language+for+the+Java+platform&amp;rft.date=2003-08-29&amp;rft.aulast=Strachan&amp;rft.aufirst=James&amp;rft_id=http%3A%2F%2Fradio.weblogs.com%2F0112098%2F2003%2F08%2F29.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-237\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-237\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.modular.com/mojo/why-mojo.html\">\"Modular Docs – Why Mojo\"</a>. <i>docs.modular.com</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230505083518/https://docs.modular.com/mojo/why-mojo.html\">Archived</a> from the original on 5 May 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 May</span> 2023</span>. <q>Mojo as a member of the Python family [..] Embracing Python massively simplifies our design efforts, because most of the syntax is already specified.  [..] we decided that the right long-term goal for Mojo is to provide a superset of Python (i.e. be compatible with existing programs) and to embrace the CPython immediately for long-tail ecosystem enablement. To a Python programmer, we expect and hope that Mojo will be immediately familiar, while also providing new tools for developing systems-level code that enable you to do things that Python falls back to C and C++ for.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=docs.modular.com&amp;rft.atitle=Modular+Docs+%E2%80%93+Why+Mojo&amp;rft_id=https%3A%2F%2Fdocs.modular.com%2Fmojo%2Fwhy-mojo.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-238\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-238\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSpencer2023\" class=\"citation web cs1\">Spencer, Michael (4 May 2023). <a rel=\"nofollow\" class=\"external text\" href=\"https://datasciencelearningcenter.substack.com/p/what-is-mojo-programming-language\">\"What is Mojo Programming Language?\"</a>. <i>datasciencelearningcenter.substack.com</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20230505090408/https://datasciencelearningcenter.substack.com/p/what-is-mojo-programming-language\">Archived</a> from the original on 5 May 2023<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">5 May</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=datasciencelearningcenter.substack.com&amp;rft.atitle=What+is+Mojo+Programming+Language%3F&amp;rft.date=2023-05-04&amp;rft.aulast=Spencer&amp;rft.aufirst=Michael&amp;rft_id=https%3A%2F%2Fdatasciencelearningcenter.substack.com%2Fp%2Fwhat-is-mojo-programming-language&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-239\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-239\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFYegulalp2017\" class=\"citation web cs1\">Yegulalp, Serdar (16 January 2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.infoworld.com/article/3157745/application-development/nim-language-draws-from-best-of-python-rust-go-and-lisp.html\">\"Nim language draws from best of Python, Rust, Go, and Lisp\"</a>. <i>InfoWorld</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20181013211847/https://www.infoworld.com/article/3157745/application-development/nim-language-draws-from-best-of-python-rust-go-and-lisp.html\">Archived</a> from the original on 13 October 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">7 June</span> 2020</span>. <q>Nim's syntax is strongly reminiscent of Python's, as it uses indented code blocks and some of the same syntax (such as the way if/elif/then/else blocks are constructed).</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=InfoWorld&amp;rft.atitle=Nim+language+draws+from+best+of+Python%2C+Rust%2C+Go%2C+and+Lisp&amp;rft.date=2017-01-16&amp;rft.aulast=Yegulalp&amp;rft.aufirst=Serdar&amp;rft_id=https%3A%2F%2Fwww.infoworld.com%2Farticle%2F3157745%2Fapplication-development%2Fnim-language-draws-from-best-of-python-rust-go-and-lisp.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-linuxdevcenter-240\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-linuxdevcenter_240-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.linuxdevcenter.com/pub/a/linux/2001/11/29/ruby.html\">\"An Interview with the Creator of Ruby\"</a>. Linuxdevcenter.com. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20180428150410/http://www.linuxdevcenter.com/pub/a/linux/2001/11/29/ruby.html\">Archived</a> from the original on 28 April 2018<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=An+Interview+with+the+Creator+of+Ruby&amp;rft.pub=Linuxdevcenter.com&amp;rft_id=http%3A%2F%2Fwww.linuxdevcenter.com%2Fpub%2Fa%2Flinux%2F2001%2F11%2F29%2Fruby.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-241\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-241\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLattner2014\" class=\"citation web cs1\"><a href=\"/wiki/Chris_Lattner\" title=\"Chris Lattner\">Lattner, Chris</a> (3 June 2014). <a rel=\"nofollow\" class=\"external text\" href=\"http://nondot.org/sabre\">\"Chris Lattner's Homepage\"</a>. Chris Lattner. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20151222150510/http://nondot.org/sabre/\">Archived</a> from the original on 22 December 2015<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 June</span> 2014</span>. <q>I started work on the Swift Programming Language in July of 2010. I implemented much of the basic language structure, with only a few people knowing of its existence. A few other (amazing) people started contributing in earnest late in 2011, and it became a major focus for the Apple Developer Tools group in July 2013 [...] drawing ideas from Objective-C, Rust, Haskell, Ruby, Python, C#, CLU, and far too many others to list.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Chris+Lattner%27s+Homepage&amp;rft.pub=Chris+Lattner&amp;rft.date=2014-06-03&amp;rft.aulast=Lattner&amp;rft.aufirst=Chris&amp;rft_id=http%3A%2F%2Fnondot.org%2Fsabre&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-242\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-242\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJalan2022\" class=\"citation web cs1\">Jalan, Nishant Aanjaney (10 November 2022). <a rel=\"nofollow\" class=\"external text\" href=\"https://medium.com/codex/programming-in-kotlin-934bdb3659cf\">\"Programming in Kotlin\"</a>. <i>CodeX</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 April</span> 2024</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=CodeX&amp;rft.atitle=Programming+in+Kotlin&amp;rft.date=2022-11-10&amp;rft.aulast=Jalan&amp;rft.aufirst=Nishant+Aanjaney&amp;rft_id=https%3A%2F%2Fmedium.com%2Fcodex%2Fprogramming-in-kotlin-934bdb3659cf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-99-243\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-99_243-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKupriesFellows2000\" class=\"citation web cs1\">Kupries, Andreas; Fellows, Donal K. (14 September 2000). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.tcl.tk/cgi-bin/tct/tip/3.html\">\"TIP #3: TIP Format\"</a>. <i>tcl.tk</i>. Tcl Developer Xchange. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170713233954/http://tcl.tk/cgi-bin/tct/tip/3.html\">Archived</a> from the original on 13 July 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 November</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=tcl.tk&amp;rft.atitle=TIP+%233%3A+TIP+Format&amp;rft.date=2000-09-14&amp;rft.aulast=Kupries&amp;rft.aufirst=Andreas&amp;rft.au=Fellows%2C+Donal+K.&amp;rft_id=http%3A%2F%2Fwww.tcl.tk%2Fcgi-bin%2Ftct%2Ftip%2F3.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-AutoNT-100-244\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-AutoNT-100_244-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGustafssonNiskanen2007\" class=\"citation web cs1\">Gustafsson, Per; Niskanen, Raimo (29 January 2007). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.erlang.org/eeps/eep-0001.html\">\"EEP 1: EEP Purpose and Guidelines\"</a>. erlang.org. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200615153206/http://erlang.org/eeps/eep-0001.html\">Archived</a> from the original on 15 June 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">19 April</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=EEP+1%3A+EEP+Purpose+and+Guidelines&amp;rft.pub=erlang.org&amp;rft.date=2007-01-29&amp;rft.aulast=Gustafsson&amp;rft.aufirst=Per&amp;rft.au=Niskanen%2C+Raimo&amp;rft_id=http%3A%2F%2Fwww.erlang.org%2Feeps%2Feep-0001.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-245\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-245\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://github.com/apple/swift-evolution/blob/master/process.md\">\"Swift Evolution Process\"</a>. <i>Swift Programming Language Evolution repository on GitHub</i>. 18 February 2020. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20200427182556/https://github.com/apple/swift-evolution/blob/master/process.md\">Archived</a> from the original on 27 April 2020<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 April</span> 2020</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Swift+Programming+Language+Evolution+repository+on+GitHub&amp;rft.atitle=Swift+Evolution+Process&amp;rft.date=2020-02-18&amp;rft_id=https%3A%2F%2Fgithub.com%2Fapple%2Fswift-evolution%2Fblob%2Fmaster%2Fprocess.md&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></span>\n</li>\n</ol></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Sources\">Sources</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=31\" title=\"Edit section: Sources\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20121101045354/http://wiki.python.org/moin/PythonForArtificialIntelligence\">\"Python for Artificial Intelligence\"</a>. Python Wiki. 19 July 2012. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.python.org/moin/PythonForArtificialIntelligence\">the original</a> on 1 November 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">3 December</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Python+for+Artificial+Intelligence&amp;rft.pub=Python+Wiki&amp;rft.date=2012-07-19&amp;rft_id=https%3A%2F%2Fwiki.python.org%2Fmoin%2FPythonForArtificialIntelligence&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPaine2005\" class=\"citation journal cs1\">Paine, Jocelyn, ed. (August 2005). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120326105810/http://www.ainewsletter.com/newsletters/aix_0508.htm#python_ai_ai\">\"AI in Python\"</a>. <i>AI Expert Newsletter</i>. Amzi!. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.ainewsletter.com/newsletters/aix_0508.htm#python_ai_ai\">the original</a> on 26 March 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">11 February</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=AI+Expert+Newsletter&amp;rft.atitle=AI+in+Python&amp;rft.date=2005-08&amp;rft_id=http%3A%2F%2Fwww.ainewsletter.com%2Fnewsletters%2Faix_0508.htm%23python_ai_ai&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://pypi.python.org/pypi/PyAIML\">\"PyAIML 0.8.5&#160;: Python Package Index\"</a>. Pypi.python.org<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 July</span> 2013</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=PyAIML+0.8.5+%3A+Python+Package+Index&amp;rft.pub=Pypi.python.org&amp;rft_id=https%3A%2F%2Fpypi.python.org%2Fpypi%2FPyAIML&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRussellNorvig2009\" class=\"citation book cs1\"><a href=\"/wiki/Stuart_J._Russell\" title=\"Stuart J. Russell\">Russell, Stuart J.</a> &amp; <a href=\"/wiki/Peter_Norvig\" title=\"Peter Norvig\">Norvig, Peter</a> (2009). <i>Artificial Intelligence: A Modern Approach</i> (3rd&#160;ed.). Upper Saddle River, NJ: Prentice Hall. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-13-604259-4\" title=\"Special:BookSources/978-0-13-604259-4\"><bdi>978-0-13-604259-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Artificial+Intelligence%3A+A+Modern+Approach&amp;rft.place=Upper+Saddle+River%2C+NJ&amp;rft.edition=3rd&amp;rft.pub=Prentice+Hall&amp;rft.date=2009&amp;rft.isbn=978-0-13-604259-4&amp;rft.aulast=Russell&amp;rft.aufirst=Stuart+J.&amp;rft.au=Norvig%2C+Peter&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Further_reading\">Further reading</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=32\" title=\"Edit section: Further reading\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFDowney2024\" class=\"citation book cs1\">Downey, Allen (July 2024). <a rel=\"nofollow\" class=\"external text\" href=\"https://allendowney.github.io/ThinkPython/\"><i>Think Python: How to Think Like a Computer Scientist</i></a> (3rd&#160;ed.). O'Reilly Media. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1098155438\" title=\"Special:BookSources/978-1098155438\"><bdi>978-1098155438</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Think+Python%3A+How+to+Think+Like+a+Computer+Scientist&amp;rft.edition=3rd&amp;rft.pub=O%27Reilly+Media&amp;rft.date=2024-07&amp;rft.isbn=978-1098155438&amp;rft.aulast=Downey&amp;rft.aufirst=Allen&amp;rft_id=https%3A%2F%2Fallendowney.github.io%2FThinkPython%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLutz2013\" class=\"citation book cs1\">Lutz, Mark (2013). <i>Learning Python</i> (5th&#160;ed.). O'Reilly Media. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-596-15806-4\" title=\"Special:BookSources/978-0-596-15806-4\"><bdi>978-0-596-15806-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Learning+Python&amp;rft.edition=5th&amp;rft.pub=O%27Reilly+Media&amp;rft.date=2013&amp;rft.isbn=978-0-596-15806-4&amp;rft.aulast=Lutz&amp;rft.aufirst=Mark&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSummerfield2009\" class=\"citation book cs1\">Summerfield, Mark (2009). <i>Programming in Python 3</i> (2nd&#160;ed.). Addison-Wesley Professional. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-321-68056-3\" title=\"Special:BookSources/978-0-321-68056-3\"><bdi>978-0-321-68056-3</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Programming+in+Python+3&amp;rft.edition=2nd&amp;rft.pub=Addison-Wesley+Professional&amp;rft.date=2009&amp;rft.isbn=978-0-321-68056-3&amp;rft.aulast=Summerfield&amp;rft.aufirst=Mark&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRamalho2022\" class=\"citation book cs1\">Ramalho, Luciano (May 2022). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.thoughtworks.com/insights/books/fluent-python-2nd-edition\"><i>Fluent Python</i></a>. O'Reilly Media. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-1-4920-5632-4\" title=\"Special:BookSources/978-1-4920-5632-4\"><bdi>978-1-4920-5632-4</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Fluent+Python&amp;rft.pub=O%27Reilly+Media&amp;rft.date=2022-05&amp;rft.isbn=978-1-4920-5632-4&amp;rft.aulast=Ramalho&amp;rft.aufirst=Luciano&amp;rft_id=https%3A%2F%2Fwww.thoughtworks.com%2Finsights%2Fbooks%2Ffluent-python-2nd-edition&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3APython+%28programming+language%29\" class=\"Z3988\"></span></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links_2\">External links</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=Python_(programming_language)&amp;action=edit&amp;section=33\" title=\"Edit section: External links\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1308029216\">.mw-parser-output .side-box{margin:4px 0;box-sizing:border-box;border:1px solid #aaa;font-size:88%;line-height:1.25em;background-color:var(--background-color-interactive-subtle,#f8f9fa);display:flow-root}.mw-parser-output .infobox .side-box{font-size:100%}.mw-parser-output .side-box-abovebelow,.mw-parser-output .side-box-text{padding:0.25em 0.9em}.mw-parser-output .side-box-image{padding:2px 0 2px 0.9em;text-align:center}.mw-parser-output .side-box-imageright{padding:2px 0.9em 2px 0;text-align:center}@media(min-width:500px){.mw-parser-output .side-box-flex{display:flex;align-items:center}.mw-parser-output .side-box-text{flex:1;min-width:0}}@media(min-width:640px){.mw-parser-output .side-box{width:238px}.mw-parser-output .side-box-right{clear:right;float:right;margin-left:1em}.mw-parser-output .side-box-left{margin-right:1em}}</style><style data-mw-deduplicate=\"TemplateStyles:r1307723979\">.mw-parser-output .sister-box .side-box-abovebelow{padding:0.75em 0;text-align:center}.mw-parser-output .sister-box .side-box-abovebelow>b{display:block}.mw-parser-output .sister-box .side-box-text>ul{border-top:1px solid #aaa;padding:0.75em 0;width:220px;margin:0 auto}.mw-parser-output .sister-box .side-box-text>ul>li{min-height:31px}.mw-parser-output .sister-logo{display:inline-block;width:31px;line-height:31px;vertical-align:middle;text-align:center}.mw-parser-output .sister-link{display:inline-block;margin-left:7px;width:182px;vertical-align:middle}@media print{body.ns-0 .mw-parser-output .sistersitebox{display:none!important}}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}</style><div role=\"navigation\" aria-labelledby=\"sister-projects\" class=\"side-box metadata side-box-right sister-box sistersitebox plainlinks\"><style data-mw-deduplicate=\"TemplateStyles:r1126788409\">.mw-parser-output .plainlist ol,.mw-parser-output .plainlist ul{line-height:inherit;list-style:none;margin:0;padding:0}.mw-parser-output .plainlist ol li,.mw-parser-output .plainlist ul li{margin-bottom:0}</style>\n<div class=\"side-box-abovebelow\">\n<b>Python</b>  at Wikipedia's <a href=\"/wiki/Wikipedia:Wikimedia_sister_projects\" title=\"Wikipedia:Wikimedia sister projects\"><span id=\"sister-projects\">sister projects</span></a></div>\n<div class=\"side-box-flex\">\n<div class=\"side-box-text plainlist\"><ul><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Commons-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/20px-Commons-logo.svg.png\" decoding=\"async\" width=\"20\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/40px-Commons-logo.svg.png 1.5x\" data-file-width=\"1024\" data-file-height=\"1376\" /></a></span></span><span class=\"sister-link\"><a href=\"https://commons.wikimedia.org/wiki/Category:Python_(programming_language)\" class=\"extiw\" title=\"c:Category:Python (programming language)\">Media</a> from Commons</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikiquote-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikiquote-logo.svg/40px-Wikiquote-logo.svg.png\" decoding=\"async\" width=\"23\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikiquote-logo.svg/60px-Wikiquote-logo.svg.png 2x\" data-file-width=\"300\" data-file-height=\"355\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikiquote.org/wiki/Python\" class=\"extiw\" title=\"q:Python\">Quotations</a> from Wikiquote</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikibooks-logo.svg/40px-Wikibooks-logo.svg.png\" decoding=\"async\" width=\"27\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikibooks-logo.svg/60px-Wikibooks-logo.svg.png 1.5x\" data-file-width=\"300\" data-file-height=\"300\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikibooks.org/wiki/Python_Programming\" class=\"extiw\" title=\"b:Python Programming\">Textbooks</a> from Wikibooks</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikiversity_logo_2017.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/40px-Wikiversity_logo_2017.svg.png\" decoding=\"async\" width=\"27\" height=\"22\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/60px-Wikiversity_logo_2017.svg.png 1.5x\" data-file-width=\"626\" data-file-height=\"512\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikiversity.org/wiki/Python\" class=\"extiw\" title=\"v:Python\">Resources</a> from Wikiversity</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikidata-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/40px-Wikidata-logo.svg.png\" decoding=\"async\" width=\"27\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/60px-Wikidata-logo.svg.png 1.5x\" data-file-width=\"1050\" data-file-height=\"590\" /></a></span></span><span class=\"sister-link\"><a href=\"https://www.wikidata.org/wiki/Q28865\" class=\"extiw\" title=\"d:Q28865\">Data</a> from Wikidata</span></li></ul></div></div>\n</div>\n<ul><li><span class=\"official-website\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.python.org/\">Official website</a></span></span> <span class=\"mw-valign-text-top\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q28865#P856\" title=\"Edit this at Wikidata\"><img alt=\"Edit this at Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://docs.python.org/3/tutorial/\">The Python Tutorial</a></li></ul>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1236075235\">.mw-parser-output .navbox{box-sizing:border-box;border:1px solid #a2a9b1;width:100%;clear:both;font-size:88%;text-align:center;padding:1px;margin:1em auto 0}.mw-parser-output .navbox .navbox{margin-top:0}.mw-parser-output .navbox+.navbox,.mw-parser-output .navbox+.navbox-styles+.navbox{margin-top:-1px}.mw-parser-output .navbox-inner,.mw-parser-output .navbox-subgroup{width:100%}.mw-parser-output .navbox-group,.mw-parser-output .navbox-title,.mw-parser-output .navbox-abovebelow{padding:0.25em 1em;line-height:1.5em;text-align:center}.mw-parser-output .navbox-group{white-space:nowrap;text-align:right}.mw-parser-output .navbox,.mw-parser-output .navbox-subgroup{background-color:#fdfdfd}.mw-parser-output .navbox-list{line-height:1.5em;border-color:#fdfdfd}.mw-parser-output .navbox-list-with-group{text-align:left;border-left-width:2px;border-left-style:solid}.mw-parser-output tr+tr>.navbox-abovebelow,.mw-parser-output tr+tr>.navbox-group,.mw-parser-output tr+tr>.navbox-image,.mw-parser-output tr+tr>.navbox-list{border-top:2px solid #fdfdfd}.mw-parser-output .navbox-title{background-color:#ccf}.mw-parser-output .navbox-abovebelow,.mw-parser-output .navbox-group,.mw-parser-output .navbox-subgroup .navbox-title{background-color:#ddf}.mw-parser-output .navbox-subgroup .navbox-group,.mw-parser-output .navbox-subgroup .navbox-abovebelow{background-color:#e6e6ff}.mw-parser-output .navbox-even{background-color:#f7f7f7}.mw-parser-output .navbox-odd{background-color:transparent}.mw-parser-output .navbox .hlist td dl,.mw-parser-output .navbox .hlist td ol,.mw-parser-output .navbox .hlist td ul,.mw-parser-output .navbox td.hlist dl,.mw-parser-output .navbox td.hlist ol,.mw-parser-output .navbox td.hlist ul{padding:0.125em 0}.mw-parser-output .navbox .navbar{display:block;font-size:100%}.mw-parser-output .navbox-title .navbar{float:left;text-align:left;margin-right:0.5em}body.skin--responsive .mw-parser-output .navbox-image img{max-width:none!important}@media print{body.ns-0 .mw-parser-output .navbox{display:none!important}}</style><style data-mw-deduplicate=\"TemplateStyles:r1239334494\">@media screen{html.skin-theme-clientpref-night .mw-parser-output div:not(.notheme)>.tmp-color,html.skin-theme-clientpref-night .mw-parser-output p>.tmp-color,html.skin-theme-clientpref-night .mw-parser-output table:not(.notheme) .tmp-color{color:inherit!important}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output div:not(.notheme)>.tmp-color,html.skin-theme-clientpref-os .mw-parser-output p>.tmp-color,html.skin-theme-clientpref-os .mw-parser-output table:not(.notheme) .tmp-color{color:inherit!important}}</style></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Python1124\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible expanded navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"3\" style=\"background: #3467AC; color: #FCFCFC;\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1239400231\">.mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}</style><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Python_(programming_language)\" title=\"Template:Python (programming language)\"><abbr title=\"View this template\" style=\"color: #FCFCFC\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Python_(programming_language)\" title=\"Template talk:Python (programming language)\"><abbr title=\"Discuss this template\" style=\"color: #FCFCFC\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Python_(programming_language)\" title=\"Special:EditPage/Template:Python (programming language)\"><abbr title=\"Edit this template\" style=\"color: #FCFCFC\">e</abbr></a></li></ul></div><div id=\"Python1124\" style=\"font-size:114%;margin:0 4em\"><a class=\"mw-selflink selflink\"><span class=\"tmp-color\" style=\"color:#FCFCFC\">Python</span></a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%;background: #FFCC55;\"><a href=\"/wiki/Programming_language_implementation\" title=\"Programming language implementation\">Implementations</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/CircuitPython\" title=\"CircuitPython\">CircuitPython</a></li>\n<li><a href=\"/wiki/CLPython\" title=\"CLPython\">CLPython</a></li>\n<li><a href=\"/wiki/CPython\" title=\"CPython\">CPython</a></li>\n<li><a href=\"/wiki/Cython\" title=\"Cython\">Cython</a></li>\n<li><a href=\"/wiki/MicroPython\" title=\"MicroPython\">MicroPython</a></li>\n<li><a href=\"/wiki/Numba\" title=\"Numba\">Numba</a></li>\n<li><a href=\"/wiki/IronPython\" title=\"IronPython\">IronPython</a></li>\n<li><a href=\"/wiki/Jython\" title=\"Jython\">Jython</a></li>\n<li><a href=\"/wiki/Psyco\" title=\"Psyco\">Psyco</a></li>\n<li><a href=\"/wiki/PyPy\" title=\"PyPy\">PyPy</a></li>\n<li><a href=\"/wiki/Python_for_S60\" title=\"Python for S60\">Python for S60</a></li>\n<li><a href=\"/wiki/Shed_Skin\" title=\"Shed Skin\">Shed Skin</a></li>\n<li><a href=\"/wiki/Stackless_Python\" title=\"Stackless Python\">Stackless Python</a></li>\n<li><a href=\"/wiki/Unladen_Swallow\" title=\"Unladen Swallow\">Unladen Swallow</a></li>\n<li><i><a href=\"/wiki/List_of_Python_software#Python_implementations\" title=\"List of Python software\">more</a>...</i></li></ul>\n</div></td><td class=\"noviewer navbox-image\" rowspan=\"4\" style=\"width:1px;padding:0 0 0 2px\"><div><span typeof=\"mw:File\"><a href=\"/wiki/File:Python-logo-notext.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/60px-Python-logo-notext.svg.png\" decoding=\"async\" width=\"55\" height=\"55\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/83px-Python-logo-notext.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/110px-Python-logo-notext.svg.png 2x\" data-file-width=\"110\" data-file-height=\"110\" /></a></span></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%;background: #FFCC55;\"><a href=\"/wiki/Integrated_development_environment\" title=\"Integrated development environment\">IDEs</a></th><td class=\"navbox-list-with-group navbox-list navbox-even hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Eric_(software)\" title=\"Eric (software)\">eric</a></li>\n<li><a href=\"/wiki/IDLE\" title=\"IDLE\">IDLE</a></li>\n<li><a href=\"/wiki/Ninja-IDE\" title=\"Ninja-IDE\">Ninja-IDE</a></li>\n<li><a href=\"/wiki/PyCharm\" title=\"PyCharm\">PyCharm</a></li>\n<li><a href=\"/wiki/PyDev\" title=\"PyDev\">PyDev</a></li>\n<li><a href=\"/wiki/Spyder_(software)\" title=\"Spyder (software)\">Spyder</a></li>\n<li><i><a href=\"/wiki/List_of_integrated_development_environments_for_Python#Python\" class=\"mw-redirect\" title=\"List of integrated development environments for Python\">more</a>...</i></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%;background: #FFCC55;\">Topics</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Web_Server_Gateway_Interface\" title=\"Web Server Gateway Interface\">WSGI</a></li>\n<li><a href=\"/wiki/Asynchronous_Server_Gateway_Interface\" title=\"Asynchronous Server Gateway Interface\">ASGI</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%;background: #FFCC55;\"><a href=\"/wiki/Software_development\" title=\"Software development\">Designer</a></th><td class=\"navbox-list-with-group navbox-list navbox-even hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Guido_van_Rossum\" title=\"Guido van Rossum\">Guido van Rossum</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow hlist\" colspan=\"3\" style=\"background: #FFCC55;\"><div>\n<ul><li><a href=\"/wiki/List_of_Python_software\" title=\"List of Python software\">Software</a> (list)</li>\n<li><a href=\"/wiki/Python_Software_Foundation\" title=\"Python Software Foundation\">Python Software Foundation</a></li>\n<li><a href=\"/wiki/Python_Conference\" title=\"Python Conference\">Python Conference</a> (PyCon)</li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Programming_languages1944\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible expanded navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Programming_languages\" title=\"Template:Programming languages\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Programming_languages\" title=\"Template talk:Programming languages\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Programming_languages\" title=\"Special:EditPage/Template:Programming languages\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Programming_languages1944\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Programming_language\" title=\"Programming language\">Programming languages</a></div></th></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><a href=\"/wiki/Comparison_of_programming_languages\" title=\"Comparison of programming languages\">Comparison</a></li>\n<li><a href=\"/wiki/Timeline_of_programming_languages\" title=\"Timeline of programming languages\">Timeline</a></li>\n<li><a href=\"/wiki/History_of_programming_languages\" title=\"History of programming languages\">History</a></li></ul>\n</div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Ada_(programming_language)\" title=\"Ada (programming language)\">Ada</a></li>\n<li><a href=\"/wiki/ALGOL\" title=\"ALGOL\">ALGOL</a>\n<ul><li><a href=\"/wiki/Simula\" title=\"Simula\">Simula</a></li></ul></li>\n<li><a href=\"/wiki/APL_(programming_language)\" title=\"APL (programming language)\">APL</a></li>\n<li><a href=\"/wiki/Assembly_language\" title=\"Assembly language\">Assembly</a></li>\n<li><a href=\"/wiki/BASIC\" title=\"BASIC\">BASIC</a>\n<ul><li><a href=\"/wiki/Visual_Basic\" title=\"Visual Basic\">Visual Basic</a>\n<ul><li><a href=\"/wiki/Visual_Basic_(classic)\" title=\"Visual Basic (classic)\">classic</a></li>\n<li><a href=\"/wiki/Visual_Basic_(.NET)\" title=\"Visual Basic (.NET)\">.NET</a></li></ul></li></ul></li>\n<li><a href=\"/wiki/C_(programming_language)\" title=\"C (programming language)\">C</a></li>\n<li><a href=\"/wiki/C%2B%2B\" title=\"C++\">C++</a></li>\n<li><a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">C#</a></li>\n<li><a href=\"/wiki/COBOL\" title=\"COBOL\">COBOL</a></li>\n<li><a href=\"/wiki/Erlang_(programming_language)\" title=\"Erlang (programming language)\">Erlang</a>\n<ul><li><a href=\"/wiki/Elixir_(programming_language)\" title=\"Elixir (programming language)\">Elixir</a></li></ul></li>\n<li><a href=\"/wiki/Forth_(programming_language)\" title=\"Forth (programming language)\">Forth</a></li>\n<li><a href=\"/wiki/Fortran\" title=\"Fortran\">Fortran</a></li>\n<li><a href=\"/wiki/Go_(programming_language)\" title=\"Go (programming language)\">Go</a></li>\n<li><a href=\"/wiki/Haskell\" title=\"Haskell\">Haskell</a></li>\n<li><a href=\"/wiki/Java_(programming_language)\" title=\"Java (programming language)\">Java</a></li>\n<li><a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a></li>\n<li><a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a></li>\n<li><a href=\"/wiki/Kotlin_(programming_language)\" title=\"Kotlin (programming language)\">Kotlin</a></li>\n<li><a href=\"/wiki/Lisp_(programming_language)\" title=\"Lisp (programming language)\">Lisp</a></li>\n<li><a href=\"/wiki/Lua\" title=\"Lua\">Lua</a></li>\n<li><a href=\"/wiki/MATLAB\" title=\"MATLAB\">MATLAB</a></li>\n<li><a href=\"/wiki/ML_(programming_language)\" title=\"ML (programming language)\">ML</a>\n<ul><li><a href=\"/wiki/Caml\" title=\"Caml\">Caml </a>\n<ul><li><a href=\"/wiki/OCaml\" title=\"OCaml\">OCaml</a></li></ul></li></ul></li>\n<li><a href=\"/wiki/Pascal_(programming_language)\" title=\"Pascal (programming language)\">Pascal</a>\n<ul><li><a href=\"/wiki/Object_Pascal\" title=\"Object Pascal\">Object Pascal</a></li></ul></li>\n<li><a href=\"/wiki/Perl\" title=\"Perl\">Perl </a>\n<ul><li><a href=\"/wiki/Raku_(programming_language)\" title=\"Raku (programming language)\">Raku</a></li></ul></li>\n<li><a href=\"/wiki/PHP\" title=\"PHP\">PHP</a></li>\n<li><a href=\"/wiki/Prolog\" title=\"Prolog\">Prolog</a></li>\n<li><a class=\"mw-selflink selflink\">Python</a></li>\n<li><a href=\"/wiki/R_(programming_language)\" title=\"R (programming language)\">R</a></li>\n<li><a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">Ruby</a></li>\n<li><a href=\"/wiki/Rust_(programming_language)\" title=\"Rust (programming language)\">Rust</a></li>\n<li><a href=\"/wiki/SAS_language\" title=\"SAS language\">SAS</a></li>\n<li><a href=\"/wiki/SQL\" title=\"SQL\">SQL</a></li>\n<li><a href=\"/wiki/Scratch_(programming_language)\" title=\"Scratch (programming language)\">Scratch</a></li>\n<li><a href=\"/wiki/Shell_script\" title=\"Shell script\">Shell</a></li>\n<li><a href=\"/wiki/Smalltalk\" title=\"Smalltalk\">Smalltalk</a></li>\n<li><a href=\"/wiki/Swift_(programming_language)\" title=\"Swift (programming language)\">Swift</a></li>\n<li><i><a href=\"/wiki/List_of_programming_languages\" title=\"List of programming languages\">more...</a></i></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"List-Class article\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/d/db/Symbol_list_class.svg/20px-Symbol_list_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/d/db/Symbol_list_class.svg/40px-Symbol_list_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <b>Lists:</b> <a href=\"/wiki/List_of_programming_languages\" title=\"List of programming languages\">Alphabetical</a></li>\n<li><a href=\"/wiki/List_of_programming_languages_by_type\" title=\"List of programming languages by type\">Categorical</a></li>\n<li><a href=\"/wiki/Generational_list_of_programming_languages\" title=\"Generational list of programming languages\">Generational</a></li>\n<li><a href=\"/wiki/Non-English-based_programming_languages\" title=\"Non-English-based programming languages\">Non-English-based</a></li>\n<li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"Category\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/20px-Symbol_category_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/40px-Symbol_category_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <a href=\"/wiki/Category:Programming_languages\" title=\"Category:Programming languages\">Category</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Python_web_frameworks613\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Python_web_frameworks\" title=\"Template:Python web frameworks\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Python_web_frameworks\" title=\"Template talk:Python web frameworks\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Python_web_frameworks\" title=\"Special:EditPage/Template:Python web frameworks\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Python_web_frameworks613\" style=\"font-size:114%;margin:0 4em\"><a class=\"mw-selflink selflink\">Python</a> <a href=\"/wiki/Web_framework\" title=\"Web framework\">web frameworks</a></div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/CherryPy\" title=\"CherryPy\">CherryPy</a></li>\n<li><a href=\"/wiki/CubicWeb\" title=\"CubicWeb\">CubicWeb</a></li>\n<li><a href=\"/wiki/Django_(web_framework)\" title=\"Django (web framework)\">Django</a></li>\n<li><a href=\"/wiki/FastAPI\" title=\"FastAPI\">FastAPI</a></li>\n<li><a href=\"/wiki/Flask_(web_framework)\" title=\"Flask (web framework)\">Flask</a></li>\n<li><a href=\"/wiki/Grok_(web_framework)\" class=\"mw-redirect\" title=\"Grok (web framework)\">Grok</a></li>\n<li><a href=\"/wiki/Nevow\" class=\"mw-redirect\" title=\"Nevow\">Nevow</a></li>\n<li><a href=\"/wiki/Pylons_project#Pylons_Framework\" title=\"Pylons project\">Pylons</a></li>\n<li><a href=\"/wiki/Pylons_project#Pyramid\" title=\"Pylons project\">Pyramid</a></li>\n<li><a href=\"/wiki/Quixote_(web_framework)\" title=\"Quixote (web framework)\">Quixote</a></li>\n<li><a href=\"/wiki/Tornado_(web_server)\" title=\"Tornado (web server)\">Tornado</a></li>\n<li><a href=\"/wiki/TurboGears\" title=\"TurboGears\">TurboGears</a></li>\n<li><a href=\"/wiki/Twisted_(software)\" title=\"Twisted (software)\">TwistedWeb</a></li>\n<li><a href=\"/wiki/Web2py\" title=\"Web2py\">web2py</a></li>\n<li><a href=\"/wiki/Zope#Zope_2\" title=\"Zope\">Zope 2</a></li>\n<li><i><a href=\"/wiki/Category:Python_(programming_language)_web_frameworks\" title=\"Category:Python (programming language) web frameworks\">more</a></i>...</li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow hlist\" colspan=\"2\"><div>\n<ul><li><a href=\"/wiki/Comparison_of_server-side_web_frameworks#Python\" title=\"Comparison of server-side web frameworks\">Comparison</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Differentiable_computing838\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Differentiable_computing\" title=\"Template:Differentiable computing\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Differentiable_computing\" title=\"Template talk:Differentiable computing\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Differentiable_computing\" title=\"Special:EditPage/Template:Differentiable computing\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Differentiable_computing838\" style=\"font-size:114%;margin:0 4em\">Differentiable computing</div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Differentiable_function\" title=\"Differentiable function\">General</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><b><a href=\"/wiki/Differentiable_programming\" title=\"Differentiable programming\">Differentiable programming</a></b></li>\n<li><a href=\"/wiki/Information_geometry\" title=\"Information geometry\">Information geometry</a></li>\n<li><a href=\"/wiki/Statistical_manifold\" title=\"Statistical manifold\">Statistical manifold</a></li>\n<li><a href=\"/wiki/Automatic_differentiation\" title=\"Automatic differentiation\">Automatic differentiation</a></li>\n<li><a href=\"/wiki/Neuromorphic_computing\" title=\"Neuromorphic computing\">Neuromorphic computing</a></li>\n<li><a href=\"/wiki/Pattern_recognition\" title=\"Pattern recognition\">Pattern recognition</a></li>\n<li><a href=\"/wiki/Ricci_calculus\" title=\"Ricci calculus\">Ricci calculus</a></li>\n<li><a href=\"/wiki/Computational_learning_theory\" title=\"Computational learning theory\">Computational learning theory</a></li>\n<li><a href=\"/wiki/Inductive_bias\" title=\"Inductive bias\">Inductive bias</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Hardware</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Graphcore\" title=\"Graphcore\">IPU</a></li>\n<li><a href=\"/wiki/Tensor_Processing_Unit\" title=\"Tensor Processing Unit\">TPU</a></li>\n<li><a href=\"/wiki/Vision_processing_unit\" title=\"Vision processing unit\">VPU</a></li>\n<li><a href=\"/wiki/Memristor\" title=\"Memristor\">Memristor</a></li>\n<li><a href=\"/wiki/SpiNNaker\" title=\"SpiNNaker\">SpiNNaker</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Software libraries</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/TensorFlow\" title=\"TensorFlow\">TensorFlow</a></li>\n<li><a href=\"/wiki/PyTorch\" title=\"PyTorch\">PyTorch</a></li>\n<li><a href=\"/wiki/Keras\" title=\"Keras\">Keras</a></li>\n<li><a href=\"/wiki/Scikit-learn\" title=\"Scikit-learn\">scikit-learn</a></li>\n<li><a href=\"/wiki/Theano_(software)\" title=\"Theano (software)\">Theano</a></li>\n<li><a href=\"/wiki/JAX_(software)\" title=\"JAX (software)\">JAX</a></li>\n<li><a href=\"/wiki/Flux_(machine-learning_framework)\" title=\"Flux (machine-learning framework)\">Flux.jl</a></li>\n<li><a href=\"/wiki/MindSpore\" title=\"MindSpore\">MindSpore</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Symbol_portal_class.svg\" class=\"mw-file-description\" title=\"Portal\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/e/e2/Symbol_portal_class.svg/20px-Symbol_portal_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/e/e2/Symbol_portal_class.svg/40px-Symbol_portal_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></a></span> Portals\n<ul><li><a href=\"/wiki/Portal:Computer_programming\" title=\"Portal:Computer programming\">Computer programming</a></li>\n<li><a href=\"/wiki/Portal:Technology\" title=\"Portal:Technology\">Technology</a></li></ul></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Free_and_open-source_software4621\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:FOSS\" title=\"Template:FOSS\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:FOSS\" title=\"Template talk:FOSS\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:FOSS\" title=\"Special:EditPage/Template:FOSS\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Free_and_open-source_software4621\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Free_and_open-source_software\" title=\"Free and open-source software\">Free and open-source software</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">General</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Alternative_terms_for_free_software\" title=\"Alternative terms for free software\">Alternative terms for free software</a></li>\n<li><a href=\"/wiki/Comparison_of_open-source_and_closed-source_software\" title=\"Comparison of open-source and closed-source software\">Comparison of open-source and closed-source software</a></li>\n<li><a href=\"/wiki/Comparison_of_source-code-hosting_facilities\" title=\"Comparison of source-code-hosting facilities\">Comparison of source-code-hosting facilities</a></li>\n<li><a href=\"/wiki/Free_software\" title=\"Free software\">Free software</a></li>\n<li><a href=\"/wiki/List_of_free_software_project_directories\" title=\"List of free software project directories\">Free software project directories</a></li>\n<li><a href=\"/wiki/Gratis_versus_libre\" title=\"Gratis versus libre\">Gratis versus libre</a></li>\n<li><a href=\"/wiki/Long-term_support\" title=\"Long-term support\">Long-term support</a></li>\n<li><a href=\"/wiki/Open-source_software\" title=\"Open-source software\">Open-source software</a></li>\n<li><a href=\"/wiki/Open-source_software_development\" title=\"Open-source software development\">Open-source software development</a></li>\n<li><a href=\"/wiki/Outline_of_free_software\" title=\"Outline of free software\">Outline</a></li>\n<li><a href=\"/wiki/Timeline_of_free_and_open-source_software\" title=\"Timeline of free and open-source software\">Timeline</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/List_of_free_and_open-source_software_packages\" title=\"List of free and open-source software packages\">Software<br />packages</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Comparison_of_free_software_for_audio\" title=\"Comparison of free software for audio\">Audio</a></li>\n<li><a href=\"/wiki/List_of_open-source_bioinformatics_software\" title=\"List of open-source bioinformatics software\">Bioinformatics</a></li>\n<li><a href=\"/wiki/List_of_open-source_codecs\" title=\"List of open-source codecs\">Codecs</a></li>\n<li><a href=\"/wiki/Comparison_of_open-source_configuration_management_software\" title=\"Comparison of open-source configuration management software\">Configuration management</a></li>\n<li><a href=\"/wiki/Device_driver\" title=\"Device driver\">Drivers</a>\n<ul><li><a href=\"/wiki/Free_and_open-source_graphics_device_driver\" title=\"Free and open-source graphics device driver\">Graphics</a></li>\n<li><a href=\"/wiki/Comparison_of_open-source_wireless_drivers\" title=\"Comparison of open-source wireless drivers\">Wireless</a></li></ul></li>\n<li><a href=\"/wiki/List_of_open-source_health_software\" title=\"List of open-source health software\">Health</a></li>\n<li><a href=\"/wiki/List_of_open-source_software_for_mathematics\" title=\"List of open-source software for mathematics\">Mathematics</a></li>\n<li><a href=\"/wiki/List_of_office_suites\" title=\"List of office suites\">Office suites</a></li>\n<li><a href=\"/wiki/Comparison_of_open-source_operating_systems\" title=\"Comparison of open-source operating systems\">Operating systems</a></li>\n<li><a href=\"/wiki/List_of_open-source_routing_platforms\" title=\"List of open-source routing platforms\">Routing</a></li>\n<li><a href=\"/wiki/List_of_free_television_software\" title=\"List of free television software\">Television</a></li>\n<li><a href=\"/wiki/List_of_open-source_video_games\" title=\"List of open-source video games\">Video games</a></li>\n<li><a href=\"/wiki/List_of_free_and_open-source_web_applications\" title=\"List of free and open-source web applications\">Web applications</a>\n<ul><li><a href=\"/wiki/Comparison_of_shopping_cart_software\" title=\"Comparison of shopping cart software\">E-commerce</a></li></ul></li>\n<li><a href=\"/wiki/List_of_free_and_open-source_Android_applications\" title=\"List of free and open-source Android applications\">Android apps</a></li>\n<li><a href=\"/wiki/List_of_free_and_open-source_iOS_applications\" title=\"List of free and open-source iOS applications\">iOS apps</a></li>\n<li><a href=\"/wiki/List_of_commercial_open-source_applications_and_services\" title=\"List of commercial open-source applications and services\">Commercial</a></li>\n<li><a href=\"/wiki/List_of_formerly_proprietary_software\" title=\"List of formerly proprietary software\">Formerly proprietary</a></li>\n<li><a href=\"/wiki/List_of_formerly_free_and_open-source_software\" class=\"mw-redirect\" title=\"List of formerly free and open-source software\">Formerly open-source</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Community_of_practice\" title=\"Community of practice\">Community</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Free_software_movement\" title=\"Free software movement\">Free software movement</a></li>\n<li><a href=\"/wiki/History_of_free_and_open-source_software\" title=\"History of free and open-source software\">History</a></li>\n<li><a href=\"/wiki/Open-source-software_movement\" class=\"mw-redirect\" title=\"Open-source-software movement\">Open-source-software movement</a></li>\n<li><a href=\"/wiki/List_of_free-software_events\" title=\"List of free-software events\">Events</a></li>\n<li><a href=\"/wiki/Open-source_software_advocacy\" title=\"Open-source software advocacy\">Advocacy</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/List_of_free_and_open-source_software_organizations\" title=\"List of free and open-source software organizations\">Organisations</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Free_Software_Movement_of_India\" title=\"Free Software Movement of India\">Free Software Movement of India</a></li>\n<li><a href=\"/wiki/Free_Software_Foundation\" title=\"Free Software Foundation\">Free Software Foundation</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Free-software_license\" title=\"Free-software license\">Licenses</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Academic_Free_License\" title=\"Academic Free License\">AFL</a></li>\n<li><a href=\"/wiki/Apache_License\" title=\"Apache License\">Apache</a></li>\n<li><a href=\"/wiki/Apple_Public_Source_License\" title=\"Apple Public Source License\">APSL</a></li>\n<li><a href=\"/wiki/Artistic_License\" title=\"Artistic License\">Artistic</a></li>\n<li><a href=\"/wiki/Beerware\" class=\"mw-redirect\" title=\"Beerware\">Beerware</a></li>\n<li><a href=\"/wiki/BSD_licenses\" title=\"BSD licenses\">BSD</a></li>\n<li><a href=\"/wiki/Creative_Commons_license\" title=\"Creative Commons license\">Creative Commons</a></li>\n<li><a href=\"/wiki/Common_Development_and_Distribution_License\" title=\"Common Development and Distribution License\">CDDL</a></li>\n<li><a href=\"/wiki/Eclipse_Public_License\" title=\"Eclipse Public License\">EPL</a></li>\n<li><a href=\"/wiki/Free_Software_Foundation\" title=\"Free Software Foundation\">Free Software Foundation</a>\n<ul><li><a href=\"/wiki/GNU_General_Public_License\" title=\"GNU General Public License\">GNU GPL</a></li>\n<li><a href=\"/wiki/GNU_Affero_General_Public_License\" title=\"GNU Affero General Public License\">GNU AGPL</a></li>\n<li><a href=\"/wiki/GNU_Lesser_General_Public_License\" title=\"GNU Lesser General Public License\">GNU LGPL</a></li></ul></li>\n<li><a href=\"/wiki/ISC_license\" title=\"ISC license\">ISC</a></li>\n<li><a href=\"/wiki/MIT_License\" title=\"MIT License\">MIT</a></li>\n<li><a href=\"/wiki/Mozilla_Public_License\" title=\"Mozilla Public License\">MPL</a></li>\n<li><a href=\"/wiki/Python_License\" title=\"Python License\">Python</a></li>\n<li><a href=\"/wiki/Python_Software_Foundation_License\" title=\"Python Software Foundation License\">Python Software Foundation License</a></li>\n<li><a href=\"/wiki/Shared_Source_Initiative\" title=\"Shared Source Initiative\">Shared Source Initiative</a></li>\n<li><a href=\"/wiki/Sleepycat_License\" class=\"mw-redirect\" title=\"Sleepycat License\">Sleepycat</a></li>\n<li><a href=\"/wiki/Unlicense\" title=\"Unlicense\">Unlicense</a></li>\n<li><a href=\"/wiki/WTFPL\" title=\"WTFPL\">WTFPL</a></li>\n<li><a href=\"/wiki/Zlib_License\" title=\"Zlib License\">zlib</a></li></ul>\n</div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th id=\"Types_and_standards397\" scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Types and<br /> standards</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Comparison_of_free_and_open-source_software_licenses\" title=\"Comparison of free and open-source software licenses\">Comparison of licenses</a></li>\n<li><a href=\"/wiki/Contributor_License_Agreement\" class=\"mw-redirect\" title=\"Contributor License Agreement\">Contributor License Agreement</a></li>\n<li><a href=\"/wiki/Copyleft\" title=\"Copyleft\">Copyleft</a></li>\n<li><a href=\"/wiki/Debian_Free_Software_Guidelines\" class=\"mw-redirect\" title=\"Debian Free Software Guidelines\">Debian Free Software Guidelines</a></li>\n<li><a href=\"/wiki/Definition_of_Free_Cultural_Works\" title=\"Definition of Free Cultural Works\">Definition of Free Cultural Works</a></li>\n<li><a href=\"/wiki/Free_license\" title=\"Free license\">Free license</a></li>\n<li><a href=\"/wiki/The_Free_Software_Definition\" title=\"The Free Software Definition\">The Free Software Definition</a></li>\n<li><a href=\"/wiki/The_Open_Source_Definition\" title=\"The Open Source Definition\">The Open Source Definition</a></li>\n<li><a href=\"/wiki/Open-source_license\" title=\"Open-source license\">Open-source license</a></li>\n<li><a href=\"/wiki/Permissive_software_license\" title=\"Permissive software license\">Permissive software license</a></li>\n<li><a href=\"/wiki/Public_domain\" title=\"Public domain\">Public domain</a></li></ul>\n</div></td></tr></tbody></table><div>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Challenges</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Digital_rights_management\" title=\"Digital rights management\">Digital rights management</a></li>\n<li><a href=\"/wiki/License_proliferation\" title=\"License proliferation\">License proliferation</a></li>\n<li><a href=\"/wiki/Mozilla_software_rebranded_by_Debian\" class=\"mw-redirect\" title=\"Mozilla software rebranded by Debian\">Mozilla software rebranding</a></li>\n<li><a href=\"/wiki/Proprietary_device_driver\" class=\"mw-redirect\" title=\"Proprietary device driver\">Proprietary device drivers</a></li>\n<li><a href=\"/wiki/Proprietary_firmware\" title=\"Proprietary firmware\">Proprietary firmware</a></li>\n<li><a href=\"/wiki/Proprietary_software\" title=\"Proprietary software\">Proprietary software</a></li>\n<li><a href=\"/wiki/SCO%E2%80%93Linux_disputes\" title=\"SCO–Linux disputes\">SCO/Linux controversies</a></li>\n<li><a href=\"/wiki/Software_patents_and_free_software\" title=\"Software patents and free software\">Software patents</a></li>\n<li><a href=\"/wiki/Open-source_software_security\" title=\"Open-source software security\">Software security</a></li>\n<li><a href=\"/wiki/Tivoization\" title=\"Tivoization\">Tivoization</a></li>\n<li><a href=\"/wiki/Trusted_Computing\" title=\"Trusted Computing\">Trusted Computing</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Related <br />topics</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Fork_(software_development)\" title=\"Fork (software development)\">Forking</a></li>\n<li><i><a href=\"/wiki/GNU_Manifesto\" title=\"GNU Manifesto\">GNU Manifesto</a></i></li>\n<li><a href=\"/wiki/Microsoft_Open_Specification_Promise\" title=\"Microsoft Open Specification Promise\">Microsoft Open Specification Promise</a></li>\n<li><a href=\"/wiki/Open-core_model\" title=\"Open-core model\">Open-core model</a></li>\n<li><a href=\"/wiki/Open-source_hardware\" title=\"Open-source hardware\">Open-source hardware</a></li>\n<li><a href=\"/wiki/Shared_Source_Initiative\" title=\"Shared Source Initiative\">Shared Source Initiative</a></li>\n<li><a href=\"/wiki/Source-available_software\" title=\"Source-available software\">Source-available software</a></li>\n<li><i><a href=\"/wiki/The_Cathedral_and_the_Bazaar\" title=\"The Cathedral and the Bazaar\">The Cathedral and the Bazaar</a></i></li>\n<li><i><a href=\"/wiki/Revolution_OS\" title=\"Revolution OS\">Revolution OS</a></i></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\" style=\"font-weight:bold\"><div>\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Symbol_portal_class.svg\" class=\"mw-file-description\" title=\"Portal\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/e/e2/Symbol_portal_class.svg/20px-Symbol_portal_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/e/e2/Symbol_portal_class.svg/40px-Symbol_portal_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></a></span> <a href=\"/wiki/Portal:Free_and_open-source_software\" title=\"Portal:Free and open-source software\">Portal</a></li>\n<li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"Category\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/20px-Symbol_category_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/40px-Symbol_category_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <a href=\"/wiki/Category:Free_software\" title=\"Category:Free software\">Category</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Statistical_software2683\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible mw-collapsed navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Statistical_software\" title=\"Template:Statistical software\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Statistical_software\" title=\"Template talk:Statistical software\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Statistical_software\" title=\"Special:EditPage/Template:Statistical software\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Statistical_software2683\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/List_of_statistical_software\" title=\"List of statistical software\">Statistical software</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Public-domain_software\" title=\"Public-domain software\">Public domain</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Dataplot\" title=\"Dataplot\">Dataplot</a></li>\n<li><a href=\"/wiki/Epi_Info\" title=\"Epi Info\">Epi Info</a></li>\n<li><a href=\"/wiki/CSPro\" title=\"CSPro\">CSPro</a></li>\n<li><a href=\"/wiki/X-12-ARIMA\" class=\"mw-redirect\" title=\"X-12-ARIMA\">X-12-ARIMA</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Open-source_software\" title=\"Open-source software\">Open-source</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ADMB\" title=\"ADMB\">ADMB</a></li>\n<li><a href=\"/wiki/DAP_(software)\" title=\"DAP (software)\">DAP</a></li>\n<li><a href=\"/wiki/Gretl\" title=\"Gretl\">gretl</a></li>\n<li><a href=\"/wiki/Jamovi\" title=\"Jamovi\">jamovi</a></li>\n<li><a href=\"/wiki/JASP\" title=\"JASP\">JASP</a></li>\n<li><a href=\"/wiki/Just_another_Gibbs_sampler\" title=\"Just another Gibbs sampler\">JAGS</a></li>\n<li><a href=\"/wiki/JMulTi\" title=\"JMulTi\">JMulTi</a></li>\n<li><a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a></li>\n<li><a href=\"/wiki/Project_Jupyter\" title=\"Project Jupyter\">Jupyter</a> (<i>Ju</i>lia, <i>Py</i>thon, <i>R</i>)</li>\n<li><a href=\"/wiki/GNU_Octave\" title=\"GNU Octave\">GNU Octave</a></li>\n<li><a href=\"/wiki/OpenBUGS\" title=\"OpenBUGS\">OpenBUGS</a></li>\n<li><a href=\"/wiki/Orange_(software)\" title=\"Orange (software)\">Orange</a></li>\n<li><a href=\"/wiki/PSPP\" title=\"PSPP\">PSPP</a></li>\n<li><a class=\"mw-selflink selflink\">Python</a> (statsmodels, <a href=\"/wiki/PyMC\" title=\"PyMC\">PyMC</a>, <a href=\"/wiki/IPython\" title=\"IPython\">IPython</a>, <a href=\"/wiki/IDLE\" title=\"IDLE\">IDLE</a>)</li>\n<li><a href=\"/wiki/R_(programming_language)\" title=\"R (programming language)\">R</a> (<a href=\"/wiki/RStudio\" title=\"RStudio\">RStudio</a>)</li>\n<li><a href=\"/wiki/SageMath\" title=\"SageMath\">SageMath</a></li>\n<li><a href=\"/wiki/SimFiT\" title=\"SimFiT\">SimFiT</a></li>\n<li><a href=\"/wiki/SOFA_Statistics\" title=\"SOFA Statistics\">SOFA Statistics</a></li>\n<li><a href=\"/wiki/Stan_(software)\" title=\"Stan (software)\">Stan</a></li>\n<li><a href=\"/wiki/XLispStat\" title=\"XLispStat\">XLispStat</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Freeware\" title=\"Freeware\">Freeware</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/BV4.1_(software)\" title=\"BV4.1 (software)\">BV4.1</a></li>\n<li><a href=\"/wiki/CumFreq\" title=\"CumFreq\">CumFreq</a></li>\n<li><a href=\"/wiki/SegReg\" title=\"SegReg\">SegReg</a></li>\n<li><a href=\"/wiki/XploRe\" title=\"XploRe\">XploRe</a></li>\n<li><a href=\"/wiki/WinBUGS\" title=\"WinBUGS\">WinBUGS</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Commercial_software\" title=\"Commercial software\">Commercial</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Cross-platform_software\" title=\"Cross-platform software\">Cross-platform</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Data_Desk\" title=\"Data Desk\">Data Desk</a></li>\n<li><a href=\"/wiki/GAUSS_(software)\" title=\"GAUSS (software)\">GAUSS</a></li>\n<li><a href=\"/wiki/GraphPad_InStat\" class=\"mw-redirect\" title=\"GraphPad InStat\">GraphPad InStat</a></li>\n<li><a href=\"/wiki/GraphPad_Prism\" class=\"mw-redirect\" title=\"GraphPad Prism\">GraphPad Prism</a></li>\n<li>IBM <a href=\"/wiki/SPSS\" title=\"SPSS\">SPSS</a> Statistics</li>\n<li>IBM <a href=\"/wiki/SPSS_Modeler\" title=\"SPSS Modeler\">SPSS Modeler</a></li>\n<li><a href=\"/wiki/JMP_(statistical_software)\" title=\"JMP (statistical software)\">JMP</a></li>\n<li><a href=\"/wiki/Maple_(software)\" title=\"Maple (software)\">Maple</a></li>\n<li><a href=\"/wiki/Mathcad\" title=\"Mathcad\">Mathcad</a></li>\n<li><a href=\"/wiki/Wolfram_Mathematica\" title=\"Wolfram Mathematica\">Mathematica</a></li>\n<li><a href=\"/wiki/MATLAB\" title=\"MATLAB\">MATLAB</a></li>\n<li><a href=\"/wiki/OxMetrics\" title=\"OxMetrics\">OxMetrics</a></li>\n<li><a href=\"/wiki/RATS_(software)\" title=\"RATS (software)\">RATS</a></li>\n<li><a href=\"/wiki/Revolution_Analytics\" title=\"Revolution Analytics\">Revolution Analytics</a></li>\n<li><a href=\"/wiki/SAS_(software)\" title=\"SAS (software)\">SAS</a> (<a href=\"/wiki/SAS_Viya\" title=\"SAS Viya\">SAS Viya</a>)</li>\n<li><a href=\"/wiki/SmartPLS\" title=\"SmartPLS\">SmartPLS</a></li>\n<li><a href=\"/wiki/Stata\" title=\"Stata\">Stata</a></li>\n<li><a href=\"/wiki/StatView\" title=\"StatView\">StatView</a></li>\n<li><a href=\"/wiki/SUDAAN\" title=\"SUDAAN\">SUDAAN</a></li>\n<li><a href=\"/wiki/S-PLUS\" title=\"S-PLUS\">S-PLUS</a></li>\n<li><a href=\"/wiki/TSP_(econometrics_software)\" title=\"TSP (econometrics software)\">TSP</a></li>\n<li><a href=\"/wiki/World_Programming_System\" title=\"World Programming System\">World Programming System</a> (WPS)</li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Microsoft_Windows\" title=\"Microsoft Windows\">Windows</a> only</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/BMDP\" title=\"BMDP\">BMDP</a></li>\n<li><a href=\"/wiki/EViews\" title=\"EViews\">EViews</a></li>\n<li><a href=\"/wiki/Genstat\" title=\"Genstat\">GenStat</a></li>\n<li><a href=\"/wiki/LIMDEP\" title=\"LIMDEP\">LIMDEP</a></li>\n<li><a href=\"/wiki/LISREL\" title=\"LISREL\">LISREL</a></li>\n<li><a href=\"/wiki/MedCalc\" title=\"MedCalc\">MedCalc</a></li>\n<li><a href=\"/wiki/Microfit\" title=\"Microfit\">Microfit</a></li>\n<li><a href=\"/wiki/Minitab\" title=\"Minitab\">Minitab</a></li>\n<li><a href=\"/wiki/MLwiN\" title=\"MLwiN\">MLwiN</a></li>\n<li><a href=\"/wiki/NCSS_(statistical_software)\" title=\"NCSS (statistical software)\">NCSS</a></li>\n<li><a href=\"/wiki/Shazam_(econometrics_software)\" title=\"Shazam (econometrics software)\">Shazam</a></li>\n<li><a href=\"/wiki/SigmaStat\" title=\"SigmaStat\">SigmaStat</a></li>\n<li><a href=\"/wiki/Statistica\" title=\"Statistica\">Statistica</a></li>\n<li><a href=\"/wiki/StatsDirect\" title=\"StatsDirect\">StatsDirect</a></li>\n<li><a href=\"/wiki/StatXact\" title=\"StatXact\">StatXact</a></li>\n<li><a href=\"/wiki/SYSTAT_(statistics_package)\" title=\"SYSTAT (statistics package)\">SYSTAT</a></li>\n<li><a href=\"/wiki/The_Unscrambler\" title=\"The Unscrambler\">The Unscrambler</a></li>\n<li>Unistat</li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Microsoft_Excel\" title=\"Microsoft Excel\">Excel</a> add-ons</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Analyse-it\" title=\"Analyse-it\">Analyse-it</a></li>\n<li>Unistat for Excel</li>\n<li><a href=\"/wiki/XLfit\" title=\"XLfit\">XLfit</a></li>\n<li><a href=\"/wiki/RExcel\" title=\"RExcel\">RExcel</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div><b><a href=\"/wiki/Comparison_of_statistical_packages\" title=\"Comparison of statistical packages\">Comparison</a></b> • <b><a href=\"/wiki/Category:Statistical_software\" title=\"Category:Statistical software\">Category</a></b></div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Numerical-analysis_software1433\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Numerical_analysis_software\" title=\"Template:Numerical analysis software\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Numerical_analysis_software\" title=\"Template talk:Numerical analysis software\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Numerical_analysis_software\" title=\"Special:EditPage/Template:Numerical analysis software\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Numerical-analysis_software1433\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/List_of_numerical-analysis_software\" title=\"List of numerical-analysis software\">Numerical-analysis software</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Free</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Advanced_Simulation_Library\" title=\"Advanced Simulation Library\">Advanced Simulation Library</a></li>\n<li><a href=\"/wiki/ADMB\" title=\"ADMB\">ADMB</a></li>\n<li><a href=\"/wiki/Chapel_(programming_language)\" title=\"Chapel (programming language)\">Chapel</a></li>\n<li><a href=\"/wiki/Euler_Mathematical_Toolbox\" title=\"Euler Mathematical Toolbox\">Euler Mathematical Toolbox</a></li>\n<li><a href=\"/wiki/FreeFem%2B%2B\" title=\"FreeFem++\">FreeFem++</a></li>\n<li><a href=\"/wiki/FreeMat\" title=\"FreeMat\">FreeMat</a></li>\n<li><a href=\"/wiki/Genius_(mathematics_software)\" title=\"Genius (mathematics software)\">Genius</a></li>\n<li><a href=\"/wiki/Gmsh\" title=\"Gmsh\">Gmsh</a></li>\n<li><a href=\"/wiki/GNU_Octave\" title=\"GNU Octave\">GNU Octave</a></li>\n<li><a href=\"/wiki/Gretl\" title=\"Gretl\">gretl</a></li>\n<li><a href=\"/wiki/Julia_(programming_language)\" title=\"Julia (programming language)\">Julia</a></li>\n<li><a href=\"/wiki/Project_Jupyter\" title=\"Project Jupyter\">Jupyter</a> (<i>Ju</i>lia, <i>Pyt</i>hon, <i>R</i>; <a href=\"/wiki/IPython\" title=\"IPython\">IPython</a>)</li>\n<li><a href=\"/wiki/MFEM\" title=\"MFEM\">MFEM</a></li>\n<li><a href=\"/wiki/OpenFOAM\" title=\"OpenFOAM\">OpenFOAM</a></li>\n<li><a class=\"mw-selflink selflink\">Python</a></li>\n<li><a href=\"/wiki/R_(programming_language)\" title=\"R (programming language)\">R</a></li>\n<li><a href=\"/wiki/SageMath\" title=\"SageMath\">SageMath</a></li>\n<li><a href=\"/wiki/Salome_(software)\" title=\"Salome (software)\">Salome</a></li>\n<li><a href=\"/wiki/ScicosLab\" title=\"ScicosLab\">ScicosLab</a></li>\n<li><a href=\"/wiki/Scilab\" title=\"Scilab\">Scilab</a></li>\n<li><a href=\"/wiki/X10_(programming_language)\" title=\"X10 (programming language)\">X10</a></li>\n<li><a href=\"/wiki/Weka_(software)\" title=\"Weka (software)\">Weka</a></li></ul>\n</div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th id=\"Discontinued62\" scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Discontinued</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Fortress_(programming_language)\" title=\"Fortress (programming language)\">Fortress</a></li></ul>\n</div></td></tr></tbody></table><div>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Proprietary</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/DADiSP\" title=\"DADiSP\">DADiSP</a></li>\n<li><a href=\"/wiki/FEATool_Multiphysics\" title=\"FEATool Multiphysics\">FEATool Multiphysics</a></li>\n<li><a href=\"/wiki/GAUSS_(software)\" title=\"GAUSS (software)\">GAUSS</a></li>\n<li><a href=\"/wiki/LabVIEW\" title=\"LabVIEW\">LabVIEW</a></li>\n<li><a href=\"/wiki/Maple_(software)\" title=\"Maple (software)\">Maple</a></li>\n<li><a href=\"/wiki/Mathcad\" title=\"Mathcad\">Mathcad</a></li>\n<li><a href=\"/wiki/Wolfram_Mathematica\" title=\"Wolfram Mathematica\">Mathematica</a></li>\n<li><a href=\"/wiki/MATLAB\" title=\"MATLAB\">MATLAB</a></li>\n<li><a href=\"/wiki/MWorks\" title=\"MWorks\">MWorks</a></li>\n<li><a href=\"/wiki/SAS_(software)\" title=\"SAS (software)\">SAS</a> (<a href=\"/wiki/SAS_Viya\" title=\"SAS Viya\">SAS Viya</a>)</li>\n<li><a href=\"/wiki/Speakeasy_(computational_environment)\" title=\"Speakeasy (computational environment)\">Speakeasy</a></li>\n<li><a href=\"/wiki/VisSim\" title=\"VisSim\">VisSim</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow hlist\" colspan=\"2\"><div>\n<ul><li><b><a href=\"/wiki/Comparison_of_numerical-analysis_software\" title=\"Comparison of numerical-analysis software\">Comparison</a></b></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /><style data-mw-deduplicate=\"TemplateStyles:r1038841319\">.mw-parser-output .tooltip-dotted{border-bottom:1px dotted;cursor:help}</style></div><div role=\"navigation\" class=\"navbox authority-control\" aria-labelledby=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q28865#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1346\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q28865#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1346\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Help:Authority_control\" title=\"Help:Authority control\">Authority control databases</a> <span class=\"mw-valign-text-top noprint\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q28865#identifiers\" title=\"Edit this at Wikidata\"><img alt=\"Edit this at Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">International</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://d-nb.info/gnd/4434275-5\">GND</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://id.worldcat.org/fast/1084736\">FAST</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">National</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://id.loc.gov/authorities/sh96008834\">United States</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://catalogue.bnf.fr/ark:/12148/cb13560465c\">France</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://data.bnf.fr/ark:/12148/cb13560465c\">BnF data</a></span></li><li><span class=\"uid\"><span class=\"rt-commentedText tooltip tooltip-dotted\" title=\"Python (programovací jazyk)\"><a rel=\"nofollow\" class=\"external text\" href=\"https://aleph.nkp.cz/F/?func=find-c&amp;local_base=aut&amp;ccl_term=ica=ph170668&amp;CON_LNG=ENG\">Czech Republic</a></span></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.nli.org.il/en/authorities/987007563637105171\">Israel</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.idref.fr/051626225\">IdRef</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://lux.collections.yale.edu/view/concept/c274a087-484b-4995-8a3c-dde45cfdd7e1\">Yale LUX</a></span></li></ul></div></td></tr></tbody></table></div>\n<!--\nNewPP limit report\nParsed by mw‐web.codfw.main‐558cd6dccc‐5f9pv\nCached time: 20251001170429\nCache expiry: 2592000\nReduced expiry: false\nComplications: [vary‐revision‐sha1, show‐toc]\nCPU time usage: 2.340 seconds\nReal time usage: 2.615 seconds\nPreprocessor visited node count: 18834/1000000\nRevision size: 168887/2097152 bytes\nPost‐expand include size: 584661/2097152 bytes\nTemplate argument size: 21676/2097152 bytes\nHighest expansion depth: 27/100\nExpensive parser function count: 80/500\nUnstrip recursion depth: 1/20\nUnstrip post‐expand size: 965151/5000000 bytes\nLua time usage: 1.438/10.000 seconds\nLua memory usage: 14934939/52428800 bytes\nNumber of Wikibase entities loaded: 1/500\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 2191.341      1 -total\n 49.37% 1081.865      2 Template:Reflist\n 35.68%  781.813    212 Template:Cite_web\n 22.36%  489.961      2 Template:Infobox\n 20.35%  446.029      1 Template:Infobox_programming_language\n 12.73%  278.886      1 Template:Infobox_software/simple\n 11.27%  246.921      2 Template:Wikidata\n  5.56%  121.942     10 Template:Navbox\n  3.79%   83.076     16 Template:Cite_book\n  3.72%   81.526      1 Template:Python_(programming_language)\n-->\n\n<!-- Saved in parser cache with key enwiki:pcache:23862:|#|:idhash:canonical and timestamp 20251001170429 and revision id 1314465879. Rendering was triggered because: page_view\n -->\n</div><noscript><img src=\"https://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1&amp;usesul3=1\" alt=\"\" width=\"1\" height=\"1\" style=\"border: none; position: absolute;\"></noscript>\n<div class=\"printfooter\" data-nosnippet=\"\">Retrieved from \"<a dir=\"ltr\" href=\"https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;oldid=1314465879\">https://en.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;oldid=1314465879</a>\"</div></div>\n\t\t\t\t\t<div id=\"catlinks\" class=\"catlinks\" data-mw=\"interface\"><div id=\"mw-normal-catlinks\" class=\"mw-normal-catlinks\"><a href=\"/wiki/Help:Category\" title=\"Help:Category\">Categories</a>: <ul><li><a href=\"/wiki/Category:Python_(programming_language)\" title=\"Category:Python (programming language)\">Python (programming language)</a></li><li><a href=\"/wiki/Category:Class-based_programming_languages\" title=\"Category:Class-based programming languages\">Class-based programming languages</a></li><li><a href=\"/wiki/Category:Notebook_interface\" title=\"Category:Notebook interface\">Notebook interface</a></li><li><a href=\"/wiki/Category:Computer_science_in_the_Netherlands\" title=\"Category:Computer science in the Netherlands\">Computer science in the Netherlands</a></li><li><a href=\"/wiki/Category:Concurrent_programming_languages\" title=\"Category:Concurrent programming languages\">Concurrent programming languages</a></li><li><a href=\"/wiki/Category:Cross-platform_free_software\" title=\"Category:Cross-platform free software\">Cross-platform free software</a></li><li><a href=\"/wiki/Category:Cross-platform_software\" title=\"Category:Cross-platform software\">Cross-platform software</a></li><li><a href=\"/wiki/Category:Dutch_inventions\" title=\"Category:Dutch inventions\">Dutch inventions</a></li><li><a href=\"/wiki/Category:Dynamically_typed_programming_languages\" title=\"Category:Dynamically typed programming languages\">Dynamically typed programming languages</a></li><li><a href=\"/wiki/Category:Educational_programming_languages\" title=\"Category:Educational programming languages\">Educational programming languages</a></li><li><a href=\"/wiki/Category:High-level_programming_languages\" title=\"Category:High-level programming languages\">High-level programming languages</a></li><li><a href=\"/wiki/Category:Information_technology_in_the_Netherlands\" title=\"Category:Information technology in the Netherlands\">Information technology in the Netherlands</a></li><li><a href=\"/wiki/Category:Multi-paradigm_programming_languages\" title=\"Category:Multi-paradigm programming languages\">Multi-paradigm programming languages</a></li><li><a href=\"/wiki/Category:Object-oriented_programming_languages\" title=\"Category:Object-oriented programming languages\">Object-oriented programming languages</a></li><li><a href=\"/wiki/Category:Pattern_matching_programming_languages\" title=\"Category:Pattern matching programming languages\">Pattern matching programming languages</a></li><li><a href=\"/wiki/Category:Programming_languages\" title=\"Category:Programming languages\">Programming languages</a></li><li><a href=\"/wiki/Category:Programming_languages_created_in_1991\" title=\"Category:Programming languages created in 1991\">Programming languages created in 1991</a></li><li><a href=\"/wiki/Category:Scripting_languages\" title=\"Category:Scripting languages\">Scripting languages</a></li><li><a href=\"/wiki/Category:Text-oriented_programming_languages\" title=\"Category:Text-oriented programming languages\">Text-oriented programming languages</a></li><li><a href=\"/wiki/Category:Monty_Python_references\" title=\"Category:Monty Python references\">Monty Python references</a></li></ul></div><div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks mw-hidden-cats-hidden\">Hidden categories: <ul><li><a href=\"/wiki/Category:CS1_maint:_article_number_as_page_number\" title=\"Category:CS1 maint: article number as page number\">CS1 maint: article number as page number</a></li><li><a href=\"/wiki/Category:CS1:_unfit_URL\" title=\"Category:CS1: unfit URL\">CS1: unfit URL</a></li><li><a href=\"/wiki/Category:Articles_with_short_description\" title=\"Category:Articles with short description\">Articles with short description</a></li><li><a href=\"/wiki/Category:Short_description_matches_Wikidata\" title=\"Category:Short description matches Wikidata\">Short description matches Wikidata</a></li><li><a href=\"/wiki/Category:Use_dmy_dates_from_November_2021\" title=\"Category:Use dmy dates from November 2021\">Use dmy dates from November 2021</a></li><li><a href=\"/wiki/Category:Use_American_English_from_December_2024\" title=\"Category:Use American English from December 2024\">Use American English from December 2024</a></li><li><a href=\"/wiki/Category:All_Wikipedia_articles_written_in_American_English\" title=\"Category:All Wikipedia articles written in American English\">All Wikipedia articles written in American English</a></li><li><a href=\"/wiki/Category:All_articles_with_failed_verification\" title=\"Category:All articles with failed verification\">All articles with failed verification</a></li><li><a href=\"/wiki/Category:Articles_with_failed_verification_from_August_2025\" title=\"Category:Articles with failed verification from August 2025\">Articles with failed verification from August 2025</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_August_2025\" title=\"Category:Articles containing potentially dated statements from August 2025\">Articles containing potentially dated statements from August 2025</a></li><li><a href=\"/wiki/Category:All_articles_containing_potentially_dated_statements\" title=\"Category:All articles containing potentially dated statements\">All articles containing potentially dated statements</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_March_2025\" title=\"Category:Articles containing potentially dated statements from March 2025\">Articles containing potentially dated statements from March 2025</a></li><li><a href=\"/wiki/Category:All_articles_with_specifically_marked_weasel-worded_phrases\" title=\"Category:All articles with specifically marked weasel-worded phrases\">All articles with specifically marked weasel-worded phrases</a></li><li><a href=\"/wiki/Category:Articles_with_specifically_marked_weasel-worded_phrases_from_August_2025\" title=\"Category:Articles with specifically marked weasel-worded phrases from August 2025\">Articles with specifically marked weasel-worded phrases from August 2025</a></li><li><a href=\"/wiki/Category:All_articles_with_unsourced_statements\" title=\"Category:All articles with unsourced statements\">All articles with unsourced statements</a></li><li><a href=\"/wiki/Category:Articles_with_unsourced_statements_from_August_2025\" title=\"Category:Articles with unsourced statements from August 2025\">Articles with unsourced statements from August 2025</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_December_2022\" title=\"Category:Articles containing potentially dated statements from December 2022\">Articles containing potentially dated statements from December 2022</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_2025\" title=\"Category:Articles containing potentially dated statements from 2025\">Articles containing potentially dated statements from 2025</a></li><li><a href=\"/wiki/Category:Pages_using_Sister_project_links_with_wikidata_namespace_mismatch\" title=\"Category:Pages using Sister project links with wikidata namespace mismatch\">Pages using Sister project links with wikidata namespace mismatch</a></li><li><a href=\"/wiki/Category:Pages_using_Sister_project_links_with_hidden_wikidata\" title=\"Category:Pages using Sister project links with hidden wikidata\">Pages using Sister project links with hidden wikidata</a></li><li><a href=\"/wiki/Category:Articles_with_example_Python_(programming_language)_code\" title=\"Category:Articles with example Python (programming language) code\">Articles with example Python (programming language) code</a></li></ul></div></div>\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t</div>\n\t\t<div class=\"mw-footer-container\">\n\n<footer id=\"footer\" class=\"mw-footer\" >\n\t<ul id=\"footer-info\">\n\t<li id=\"footer-info-lastmod\"> This page was last edited on 1 October 2025, at 15:22<span class=\"anonymous-show\">&#160;(UTC)</span>.</li>\n\t<li id=\"footer-info-copyright\">Text is available under the <a href=\"/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License\" title=\"Wikipedia:Text of the Creative Commons Attribution-ShareAlike 4.0 International License\">Creative Commons Attribution-ShareAlike 4.0 License</a>;\nadditional terms may apply. By using this site, you agree to the <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Terms of Use\">Terms of Use</a> and <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Privacy policy\">Privacy Policy</a>. Wikipedia® is a registered trademark of the <a rel=\"nofollow\" class=\"external text\" href=\"https://wikimediafoundation.org/\">Wikimedia Foundation, Inc.</a>, a non-profit organization.</li>\n</ul>\n\n\t<ul id=\"footer-places\">\n\t<li id=\"footer-places-privacy\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\">Privacy policy</a></li>\n\t<li id=\"footer-places-about\"><a href=\"/wiki/Wikipedia:About\">About Wikipedia</a></li>\n\t<li id=\"footer-places-disclaimers\"><a href=\"/wiki/Wikipedia:General_disclaimer\">Disclaimers</a></li>\n\t<li id=\"footer-places-contact\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\">Contact Wikipedia</a></li>\n\t<li id=\"footer-places-wm-codeofconduct\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Universal_Code_of_Conduct\">Code of Conduct</a></li>\n\t<li id=\"footer-places-developers\"><a href=\"https://developer.wikimedia.org\">Developers</a></li>\n\t<li id=\"footer-places-statslink\"><a href=\"https://stats.wikimedia.org/#/en.wikipedia.org\">Statistics</a></li>\n\t<li id=\"footer-places-cookiestatement\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Cookie_statement\">Cookie statement</a></li>\n\t<li id=\"footer-places-mobileview\"><a href=\"//en.m.wikipedia.org/w/index.php?title=Python_(programming_language)&amp;mobileaction=toggle_view_mobile\" class=\"noprint stopMobileRedirectToggle\">Mobile view</a></li>\n</ul>\n\n\t<ul id=\"footer-icons\" class=\"noprint\">\n\t<li id=\"footer-copyrightico\"><a href=\"https://www.wikimedia.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/static/images/footer/wikimedia-button.svg\" width=\"84\" height=\"29\"><img src=\"/static/images/footer/wikimedia.svg\" width=\"25\" height=\"25\" alt=\"Wikimedia Foundation\" lang=\"en\" loading=\"lazy\"></picture></a></li>\n\t<li id=\"footer-poweredbyico\"><a href=\"https://www.mediawiki.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/w/resources/assets/poweredby_mediawiki.svg\" width=\"88\" height=\"31\"><img src=\"/w/resources/assets/mediawiki_compact.svg\" alt=\"Powered by MediaWiki\" lang=\"en\" width=\"25\" height=\"25\" loading=\"lazy\"></picture></a></li>\n</ul>\n\n</footer>\n\n\t\t</div>\n\t</div>\n</div>\n<div class=\"vector-header-container vector-sticky-header-container no-font-mode-scale\">\n\t<div id=\"vector-sticky-header\" class=\"vector-sticky-header\">\n\t\t<div class=\"vector-sticky-header-start\">\n\t\t\t<div class=\"vector-sticky-header-icon-start vector-button-flush-left vector-button-flush-right\" aria-hidden=\"true\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-sticky-header-search-toggle\" tabindex=\"-1\" data-event-name=\"ui.vector-sticky-search-form.icon\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div role=\"search\" class=\"vector-search-box-vue  vector-search-box-show-thumbnail vector-search-box\">\n\t\t\t<div class=\"vector-typeahead-search-container\">\n\t\t\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail\">\n\t\t\t\t\t<form action=\"/w/index.php\" id=\"vector-sticky-search-form\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t\t\t<div  class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\n\t\t\t\t\t\t\t\t\ttype=\"search\" name=\"search\" placeholder=\"Search Wikipedia\">\n\t\t\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-context-bar\">\n\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n\t\t\t\t\t<div id=\"vector-sticky-header-toc\" class=\"vector-dropdown mw-portlet mw-portlet-sticky-header-toc vector-sticky-header-toc vector-button-flush-left\"  >\n\t\t\t\t\t\t<input type=\"checkbox\" id=\"vector-sticky-header-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-sticky-header-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t\t\t\t\t\t<label id=\"vector-sticky-header-toc-label\" for=\"vector-sticky-header-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<div class=\"vector-dropdown-content\">\n\n\t\t\t\t\t\t<div id=\"vector-sticky-header-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t</nav>\n\t\t\t\t<div class=\"vector-sticky-header-context-bar-primary\" aria-hidden=\"true\" ><span class=\"mw-page-title-main\">Python (programming language)</span></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-end\" aria-hidden=\"true\">\n\t\t\t<div class=\"vector-sticky-header-icons\">\n\t\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-talk-sticky-header\" tabindex=\"-1\" data-event-name=\"talk-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbles mw-ui-icon-wikimedia-speechBubbles\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-subject-sticky-header\" tabindex=\"-1\" data-event-name=\"subject-sticky-header\"><span class=\"vector-icon mw-ui-icon-article mw-ui-icon-wikimedia-article\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-history-sticky-header\" tabindex=\"-1\" data-event-name=\"history-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-history mw-ui-icon-wikimedia-wikimedia-history\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only mw-watchlink\" id=\"ca-watchstar-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-star mw-ui-icon-wikimedia-wikimedia-star\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only reading-lists-bookmark\" id=\"ca-bookmark-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-bookmark\"><span class=\"vector-icon mw-ui-icon-wikimedia-bookmarkOutline mw-ui-icon-wikimedia-wikimedia-bookmarkOutline\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"wikitext-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-wikiText mw-ui-icon-wikimedia-wikimedia-wikiText\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-ve-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-edit mw-ui-icon-wikimedia-wikimedia-edit\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-viewsource-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-protected-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-editLock mw-ui-icon-wikimedia-wikimedia-editLock\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-buttons\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet mw-interlanguage-selector\" id=\"p-lang-btn-sticky-header\" tabindex=\"-1\" data-event-name=\"ui.dropdown-p-lang-btn-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-language mw-ui-icon-wikimedia-wikimedia-language\"></span>\n\n<span>116 languages</span>\n\t\t\t</button>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive\" id=\"ca-addsection-sticky-header\" tabindex=\"-1\" data-event-name=\"addsection-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbleAdd-progressive mw-ui-icon-wikimedia-speechBubbleAdd-progressive\"></span>\n\n<span>Add topic</span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-icon-end\">\n\t\t\t\t<div class=\"vector-user-links\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"mw-portlet mw-portlet-dock-bottom emptyPortlet\" id=\"p-dock-bottom\">\n\t<ul>\n\n\t</ul>\n</div>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({\"wgHostname\":\"mw-web.codfw.main-558cd6dccc-wgb97\",\"wgBackendResponseTime\":211,\"wgPageParseReport\":{\"limitreport\":{\"cputime\":\"2.340\",\"walltime\":\"2.615\",\"ppvisitednodes\":{\"value\":18834,\"limit\":1000000},\"revisionsize\":{\"value\":168887,\"limit\":2097152},\"postexpandincludesize\":{\"value\":584661,\"limit\":2097152},\"templateargumentsize\":{\"value\":21676,\"limit\":2097152},\"expansiondepth\":{\"value\":27,\"limit\":100},\"expensivefunctioncount\":{\"value\":80,\"limit\":500},\"unstrip-depth\":{\"value\":1,\"limit\":20},\"unstrip-size\":{\"value\":965151,\"limit\":5000000},\"entityaccesscount\":{\"value\":1,\"limit\":500},\"timingprofile\":[\"100.00% 2191.341      1 -total\",\" 49.37% 1081.865      2 Template:Reflist\",\" 35.68%  781.813    212 Template:Cite_web\",\" 22.36%  489.961      2 Template:Infobox\",\" 20.35%  446.029      1 Template:Infobox_programming_language\",\" 12.73%  278.886      1 Template:Infobox_software/simple\",\" 11.27%  246.921      2 Template:Wikidata\",\"  5.56%  121.942     10 Template:Navbox\",\"  3.79%   83.076     16 Template:Cite_book\",\"  3.72%   81.526      1 Template:Python_(programming_language)\"]},\"scribunto\":{\"limitreport-timeusage\":{\"value\":\"1.438\",\"limit\":\"10.000\"},\"limitreport-memusage\":{\"value\":14934939,\"limit\":52428800}},\"cachereport\":{\"origin\":\"mw-web.codfw.main-558cd6dccc-5f9pv\",\"timestamp\":\"20251001170429\",\"ttl\":2592000,\"transientcontent\":false}}});});</script>\n<script type=\"application/ld+json\">{\"@context\":\"https:\\/\\/schema.org\",\"@type\":\"Article\",\"name\":\"Python (programming language)\",\"url\":\"https:\\/\\/en.wikipedia.org\\/wiki\\/Python_(programming_language)\",\"sameAs\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q28865\",\"mainEntity\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q28865\",\"author\":{\"@type\":\"Organization\",\"name\":\"Contributors to Wikimedia projects\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Wikimedia Foundation, Inc.\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/www.wikimedia.org\\/static\\/images\\/wmf-hor-googpub.png\"}},\"datePublished\":\"2001-10-29T18:24:39Z\",\"dateModified\":\"2025-10-01T15:22:56Z\",\"image\":\"https:\\/\\/upload.wikimedia.org\\/wikipedia\\/commons\\/c\\/c3\\/Python-logo-notext.svg\",\"headline\":\"general-purpose programming language\"}</script>\n</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/wikipedia/small_html.html",
    "content": "<!DOCTYPE html>\n<html class=\"client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\" lang=\"en\" dir=\"ltr\">\n<head>\n<meta charset=\"UTF-8\">\n<title>HTML - Wikipedia</title>\n<script>(function(){var className=\"client-js vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\";var cookie=document.cookie.match(/(?:^|; )enwikimwclientpreferences=([^;]+)/);if(cookie){cookie[1].split('%2C').forEach(function(pref){className=className.replace(new RegExp('(^| )'+pref.replace(/-clientpref-\\w+$|[^\\w-]+/g,'')+'-clientpref-\\\\w+( |$)'),'$1'+pref+'$2');});}document.documentElement.className=className;}());RLCONF={\"wgBreakFrames\":false,\"wgSeparatorTransformTable\":[\"\",\"\"],\"wgDigitTransformTable\":[\"\",\"\"],\"wgDefaultDateFormat\":\"dmy\",\"wgMonthNames\":[\"\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"wgRequestId\":\"ab8deebe-f76a-4a8f-b495-a865a1ef32a5\",\"wgCanonicalNamespace\":\"\",\"wgCanonicalSpecialPageName\":false,\"wgNamespaceNumber\":0,\"wgPageName\":\"HTML\",\"wgTitle\":\"HTML\",\"wgCurRevisionId\":1314044013,\"wgRevisionId\":1314044013,\"wgArticleId\":13191,\"wgIsArticle\":true,\"wgIsRedirect\":false,\"wgAction\":\"view\",\"wgUserName\":null,\"wgUserGroups\":[\"*\"],\"wgCategories\":[\"Webarchive template wayback links\",\"Articles with short description\",\"Short description is different from Wikidata\",\"Wikipedia pages semi-protected against vandalism\",\"Articles containing potentially dated statements from 1997\",\"All articles containing potentially dated statements\",\"Articles containing potentially dated statements from 1996\",\"All articles lacking reliable references\",\"Articles lacking reliable references from February 2019\",\"All articles with unsourced statements\",\"Articles with unsourced statements from February 2025\",\"All articles with vague or ambiguous time\",\"Vague or ambiguous time from March 2022\",\"Articles to be expanded from January 2021\",\"All articles with specifically marked weasel-worded phrases\",\"Articles with specifically marked weasel-worded phrases from June 2020\",\"Pages using Sister project links with wikidata namespace mismatch\",\"Pages using Sister project links with hidden wikidata\",\"Articles with example code\",\"HTML\",\"Computer-related introductions in 1990\",\"Markup languages\",\"Open formats\",\"Technical communication\",\"World Wide Web Consortium standards\",\"SGML\"],\"wgPageViewLanguage\":\"en\",\"wgPageContentLanguage\":\"en\",\"wgPageContentModel\":\"wikitext\",\"wgRelevantPageName\":\"HTML\",\"wgRelevantArticleId\":13191,\"wgIsProbablyEditable\":false,\"wgRelevantPageIsProbablyEditable\":false,\"wgRestrictionEdit\":[\"autoconfirmed\"],\"wgRestrictionMove\":[],\"wgNoticeProject\":\"wikipedia\",\"wgFlaggedRevsParams\":{\"tags\":{\"status\":{\"levels\":1}}},\"wgMediaViewerOnClick\":true,\"wgMediaViewerEnabledByDefault\":true,\"wgPopupsFlags\":0,\"wgVisualEditor\":{\"pageLanguageCode\":\"en\",\"pageLanguageDir\":\"ltr\",\"pageVariantFallbacks\":\"en\"},\"wgMFDisplayWikibaseDescriptions\":{\"search\":true,\"watchlist\":true,\"tagline\":false,\"nearby\":true},\"wgWMESchemaEditAttemptStepOversample\":false,\"wgWMEPageLength\":90000,\"wgMetricsPlatformUserExperiments\":{\"active_experiments\":[],\"overrides\":[],\"enrolled\":[],\"assigned\":[],\"subject_ids\":[],\"sampling_units\":[]},\"wgEditSubmitButtonLabelPublish\":true,\"wgULSPosition\":\"interlanguage\",\"wgULSisCompactLinksEnabled\":false,\"wgVector2022LanguageInHeader\":true,\"wgULSisLanguageSelectorEmpty\":false,\"wgWikibaseItemId\":\"Q8811\",\"wgCheckUserClientHintsHeadersJsApi\":[\"brands\",\"architecture\",\"bitness\",\"fullVersionList\",\"mobile\",\"model\",\"platform\",\"platformVersion\"],\"GEHomepageSuggestedEditsEnableTopics\":true,\"wgGESuggestedEditsTaskTypes\":{\"taskTypes\":[\"copyedit\",\"link-recommendation\"],\"unavailableTaskTypes\":[]},\"wgGETopicsMatchModeEnabled\":false,\"wgGELevelingUpEnabledForUser\":false};\nRLSTATE={\"ext.globalCssJs.user.styles\":\"ready\",\"site.styles\":\"ready\",\"user.styles\":\"ready\",\"ext.globalCssJs.user\":\"ready\",\"user\":\"ready\",\"user.options\":\"loading\",\"ext.cite.styles\":\"ready\",\"ext.pygments\":\"ready\",\"ext.wikimediamessages.styles\":\"ready\",\"skins.vector.search.codex.styles\":\"ready\",\"skins.vector.styles\":\"ready\",\"skins.vector.icons\":\"ready\",\"jquery.makeCollapsible.styles\":\"ready\",\"ext.visualEditor.desktopArticleTarget.noscript\":\"ready\",\"ext.uls.interlanguage\":\"ready\",\"wikibase.client.init\":\"ready\"};RLPAGEMODULES=[\"ext.xLab\",\"ext.cite.ux-enhancements\",\"ext.pygments.view\",\"mediawiki.page.media\",\"site\",\"mediawiki.page.ready\",\"jquery.makeCollapsible\",\"mediawiki.toc\",\"skins.vector.js\",\"ext.centralNotice.geoIP\",\"ext.centralNotice.startUp\",\"ext.gadget.ReferenceTooltips\",\"ext.gadget.switcher\",\"ext.urlShortener.toolbar\",\"ext.centralauth.centralautologin\",\"mmv.bootstrap\",\"ext.popups\",\"ext.visualEditor.desktopArticleTarget.init\",\"ext.visualEditor.targetLoader\",\"ext.echo.centralauth\",\"ext.eventLogging\",\"ext.wikimediaEvents\",\"ext.navigationTiming\",\"ext.uls.interface\",\"ext.cx.eventlogging.campaigns\",\"ext.cx.uls.quick.actions\",\"wikibase.client.vector-2022\",\"ext.checkUser.clientHints\",\"ext.quicksurveys.init\",\"ext.growthExperiments.SuggestedEditSession\"];</script>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return[\"user.options@12s5i\",function($,jQuery,require,module){mw.user.tokens.set({\"patrolToken\":\"+\\\\\",\"watchToken\":\"+\\\\\",\"csrfToken\":\"+\\\\\"});\n}];});});</script>\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=ext.cite.styles%7Cext.pygments%7Cext.uls.interlanguage%7Cext.visualEditor.desktopArticleTarget.noscript%7Cext.wikimediamessages.styles%7Cjquery.makeCollapsible.styles%7Cskins.vector.icons%2Cstyles%7Cskins.vector.search.codex.styles%7Cwikibase.client.init&amp;only=styles&amp;skin=vector-2022\">\n<script async=\"\" src=\"/w/load.php?lang=en&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;skin=vector-2022\"></script>\n<meta name=\"ResourceLoaderDynamicStyles\" content=\"\">\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=vector-2022\">\n<meta name=\"generator\" content=\"MediaWiki 1.45.0-wmf.20\">\n<meta name=\"referrer\" content=\"origin\">\n<meta name=\"referrer\" content=\"origin-when-cross-origin\">\n<meta name=\"robots\" content=\"max-image-preview:standard\">\n<meta name=\"format-detection\" content=\"telephone=no\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/1200px-HTML5_logo_and_wordmark.svg.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"1200\">\n<meta name=\"viewport\" content=\"width=1120\">\n<meta property=\"og:title\" content=\"HTML - Wikipedia\">\n<meta property=\"og:type\" content=\"website\">\n<link rel=\"preconnect\" href=\"//upload.wikimedia.org\">\n<link rel=\"alternate\" media=\"only screen and (max-width: 640px)\" href=\"//en.m.wikipedia.org/wiki/HTML\">\n<link rel=\"apple-touch-icon\" href=\"/static/apple-touch/wikipedia.png\">\n<link rel=\"icon\" href=\"/static/favicon/wikipedia.ico\">\n<link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"/w/rest.php/v1/search\" title=\"Wikipedia (en)\">\n<link rel=\"EditURI\" type=\"application/rsd+xml\" href=\"//en.wikipedia.org/w/api.php?action=rsd\">\n<link rel=\"canonical\" href=\"https://en.wikipedia.org/wiki/HTML\">\n<link rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/4.0/deed.en\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Wikipedia Atom feed\" href=\"/w/index.php?title=Special:RecentChanges&amp;feed=atom\">\n<link rel=\"dns-prefetch\" href=\"//meta.wikimedia.org\" />\n<link rel=\"dns-prefetch\" href=\"auth.wikimedia.org\">\n</head>\n<body class=\"skin--responsive skin-vector skin-vector-search-vue mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject page-HTML rootpage-HTML skin-vector-2022 action-view\"><a class=\"mw-jump-link\" href=\"#bodyContent\">Jump to content</a>\n<div class=\"vector-header-container\">\n\t<header class=\"vector-header mw-header no-font-mode-scale\">\n\t\t<div class=\"vector-header-start\">\n\t\t\t<nav class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\n<div id=\"vector-main-menu-dropdown\" class=\"vector-dropdown vector-main-menu-dropdown vector-button-flush-left vector-button-flush-right\"  title=\"Main menu\" >\n\t<input type=\"checkbox\" id=\"vector-main-menu-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-main-menu-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Main menu\"  >\n\t<label id=\"vector-main-menu-dropdown-label\" for=\"vector-main-menu-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu\"></span>\n\n<span class=\"vector-dropdown-label-text\">Main menu</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t<div id=\"vector-main-menu-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-main-menu\" class=\"vector-main-menu vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-main-menu-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"main-menu-pinned\"\n\tdata-pinnable-element-id=\"vector-main-menu\"\n\tdata-pinned-container-id=\"vector-main-menu-pinned-container\"\n\tdata-unpinned-container-id=\"vector-main-menu-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Main menu</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-main-menu.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-main-menu.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-navigation\" class=\"vector-menu mw-portlet mw-portlet-navigation\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tNavigation\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-mainpage-description\" class=\"mw-list-item\"><a href=\"/wiki/Main_Page\" title=\"Visit the main page [z]\" accesskey=\"z\"><span>Main page</span></a></li><li id=\"n-contents\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Contents\" title=\"Guides to browsing Wikipedia\"><span>Contents</span></a></li><li id=\"n-currentevents\" class=\"mw-list-item\"><a href=\"/wiki/Portal:Current_events\" title=\"Articles related to current events\"><span>Current events</span></a></li><li id=\"n-randompage\" class=\"mw-list-item\"><a href=\"/wiki/Special:Random\" title=\"Visit a randomly selected article [x]\" accesskey=\"x\"><span>Random article</span></a></li><li id=\"n-aboutsite\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:About\" title=\"Learn about Wikipedia and how it works\"><span>About Wikipedia</span></a></li><li id=\"n-contactpage\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\" title=\"How to contact Wikipedia\"><span>Contact us</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\n<div id=\"p-interaction\" class=\"vector-menu mw-portlet mw-portlet-interaction\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tContribute\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-help\" class=\"mw-list-item\"><a href=\"/wiki/Help:Contents\" title=\"Guidance on how to use and edit Wikipedia\"><span>Help</span></a></li><li id=\"n-introduction\" class=\"mw-list-item\"><a href=\"/wiki/Help:Introduction\" title=\"Learn how to edit Wikipedia\"><span>Learn to edit</span></a></li><li id=\"n-portal\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Community_portal\" title=\"The hub for editors\"><span>Community portal</span></a></li><li id=\"n-recentchanges\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChanges\" title=\"A list of recent changes to Wikipedia [r]\" accesskey=\"r\"><span>Recent changes</span></a></li><li id=\"n-upload\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:File_upload_wizard\" title=\"Add images or other media for use on Wikipedia\"><span>Upload file</span></a></li><li id=\"n-specialpages\" class=\"mw-list-item\"><a href=\"/wiki/Special:SpecialPages\"><span>Special pages</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t</nav>\n\n<a href=\"/wiki/Main_Page\" class=\"mw-logo\">\n\t<img class=\"mw-logo-icon\" src=\"/static/images/icons/wikipedia.png\" alt=\"\" aria-hidden=\"true\" height=\"50\" width=\"50\">\n\t<span class=\"mw-logo-container skin-invert\">\n\t\t<img class=\"mw-logo-wordmark\" alt=\"Wikipedia\" src=\"/static/images/mobile/copyright/wikipedia-wordmark-en.svg\" style=\"width: 7.5em; height: 1.125em;\">\n\t\t<img class=\"mw-logo-tagline\" alt=\"The Free Encyclopedia\" src=\"/static/images/mobile/copyright/wikipedia-tagline-en.svg\" width=\"117\" height=\"13\" style=\"width: 7.3125em; height: 0.8125em;\">\n\t</span>\n</a>\n\n\t\t</div>\n\t\t<div class=\"vector-header-end\">\n\n<div id=\"p-search\" role=\"search\" class=\"vector-search-box-vue  vector-search-box-collapses vector-search-box-show-thumbnail vector-search-box-auto-expand-width vector-search-box\">\n\t<a href=\"/wiki/Special:Search\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only search-toggle\" title=\"Search Wikipedia [f]\" accesskey=\"f\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t</a>\n\t<div class=\"vector-typeahead-search-container\">\n\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail cdx-typeahead-search--auto-expand-width\">\n\t\t\t<form action=\"/w/index.php\" id=\"searchform\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t<div id=\"simpleSearch\" class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\t\t\t\t\t\t\t type=\"search\" name=\"search\" placeholder=\"Search Wikipedia\" aria-label=\"Search Wikipedia\" autocapitalize=\"sentences\" spellcheck=\"false\" title=\"Search Wikipedia [f]\" accesskey=\"f\" id=\"searchInput\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t</div>\n\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n\t\t\t<nav class=\"vector-user-links vector-user-links-wide\" aria-label=\"Personal tools\">\n\t<div class=\"vector-user-links-main\">\n\n<div id=\"p-vector-user-menu-preferences\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-userpage\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\n<div id=\"vector-appearance-dropdown\" class=\"vector-dropdown \"  title=\"Change the appearance of the page&#039;s font size, width, and color\" >\n\t<input type=\"checkbox\" id=\"vector-appearance-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-appearance-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Appearance\"  >\n\t<label id=\"vector-appearance-dropdown-label\" for=\"vector-appearance-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-appearance mw-ui-icon-wikimedia-appearance\"></span>\n\n<span class=\"vector-dropdown-label-text\">Appearance</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t<div id=\"vector-appearance-unpinned-container\" class=\"vector-unpinned-container\">\n\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t</nav>\n\n<div id=\"p-vector-user-menu-notifications\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-overflow\" class=\"vector-menu mw-portlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\t\t\t<li id=\"pt-sitesupport-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\" class=\"\"><span>Donate</span></a>\n</li>\n<li id=\"pt-createaccount-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=HTML\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\" class=\"\"><span>Create account</span></a>\n</li>\n<li id=\"pt-login-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:UserLogin&amp;returnto=HTML\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\" class=\"\"><span>Log in</span></a>\n</li>\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t</div>\n\n<div id=\"vector-user-links-dropdown\" class=\"vector-dropdown vector-user-menu vector-button-flush-right vector-user-menu-logged-out\"  title=\"Log in and more options\" >\n\t<input type=\"checkbox\" id=\"vector-user-links-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-user-links-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Personal tools\"  >\n\t<label id=\"vector-user-links-dropdown-label\" for=\"vector-user-links-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-ellipsis mw-ui-icon-wikimedia-ellipsis\"></span>\n\n<span class=\"vector-dropdown-label-text\">Personal tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-personal\" class=\"vector-menu mw-portlet mw-portlet-personal user-links-collapsible-item\"  title=\"User menu\" >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-sitesupport\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\"><span>Donate</span></a></li><li id=\"pt-createaccount\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=HTML\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\"><span class=\"vector-icon mw-ui-icon-userAdd mw-ui-icon-wikimedia-userAdd\"></span> <span>Create account</span></a></li><li id=\"pt-login\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:UserLogin&amp;returnto=HTML\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\"><span class=\"vector-icon mw-ui-icon-logIn mw-ui-icon-wikimedia-logIn\"></span> <span>Log in</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-user-menu-anon-editor\" class=\"vector-menu mw-portlet mw-portlet-user-menu-anon-editor\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPages for logged out editors <a href=\"/wiki/Help:Introduction\" aria-label=\"Learn more about editing\"><span>learn more</span></a>\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-anoncontribs\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyContributions\" title=\"A list of edits made from this IP address [y]\" accesskey=\"y\"><span>Contributions</span></a></li><li id=\"pt-anontalk\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyTalk\" title=\"Discussion about edits from this IP address [n]\" accesskey=\"n\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n</nav>\n\n\t\t</div>\n\t</header>\n</div>\n<div class=\"mw-page-container\">\n\t<div class=\"mw-page-container-inner\">\n\t\t<div class=\"vector-sitenotice-container\">\n\t\t\t<div id=\"siteNotice\"><!-- CentralNotice --></div>\n\t\t</div>\n\t\t<div class=\"vector-column-start\">\n\t\t\t<div class=\"vector-main-menu-container\">\n\t\t<div id=\"mw-navigation\">\n\t\t\t<nav id=\"mw-panel\" class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\t\t\t\t<div id=\"vector-main-menu-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t</div>\n\t\t</nav>\n\t\t</div>\n\t</div>\n\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t<nav id=\"mw-panel-toc\" aria-label=\"Contents\" data-event-name=\"ui.sidebar-toc\" class=\"mw-table-of-contents-container vector-toc-landmark\">\n\t\t\t\t\t<div id=\"vector-toc-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t\t<div id=\"vector-toc\" class=\"vector-toc vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"toc-pinned\"\n\tdata-pinnable-element-id=\"vector-toc\"\n\n\n>\n\t<h2 class=\"vector-pinnable-header-label\">Contents</h2>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-toc.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-toc.unpin\">hide</button>\n</div>\n\n\n\t<ul class=\"vector-toc-contents\" id=\"mw-panel-toc-list\">\n\t\t<li id=\"toc-mw-content-text\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t\t<a href=\"#\" class=\"vector-toc-link\">\n\t\t\t\t<div class=\"vector-toc-text\">(Top)</div>\n\t\t\t</a>\n\t\t</li>\n\t\t<li id=\"toc-History\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#History\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">1</span>\n\t\t\t\t<span>History</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-History-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle History subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-History-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Development\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Development\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.1</span>\n\t\t\t\t\t<span>Development</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Development-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_version_timeline\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_version_timeline\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.2</span>\n\t\t\t\t\t<span>HTML version timeline</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_version_timeline-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-HTML_2\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_2\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.2.1</span>\n\t\t\t\t\t<span>HTML 2</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_2-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_3\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_3\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.2.2</span>\n\t\t\t\t\t<span>HTML 3</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_3-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_4\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_4\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.2.3</span>\n\t\t\t\t\t<span>HTML 4</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_4-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_5\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_5\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.2.4</span>\n\t\t\t\t\t<span>HTML 5</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_5-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_draft_version_timeline\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_draft_version_timeline\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.3</span>\n\t\t\t\t\t<span>HTML draft version timeline</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_draft_version_timeline-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-XHTML_versions\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#XHTML_versions\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.3.1</span>\n\t\t\t\t\t<span>XHTML versions</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-XHTML_versions-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Transition_of_HTML_publication_to_WHATWG\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Transition_of_HTML_publication_to_WHATWG\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">1.4</span>\n\t\t\t\t\t<span>Transition of HTML publication to WHATWG</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Transition_of_HTML_publication_to_WHATWG-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Markup\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Markup\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">2</span>\n\t\t\t\t<span>Markup</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Markup-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Markup subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Markup-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-Elements\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Elements\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1</span>\n\t\t\t\t\t<span>Elements</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Elements-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Element_examples\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Element_examples\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.1</span>\n\t\t\t\t\t<span>Element examples</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Element_examples-sublist\" class=\"vector-toc-list\">\n\t\t\t\t<li id=\"toc-Headings\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-4\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Headings\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.1.1</span>\n\t\t\t\t\t<span>Headings</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Headings-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Line_breaks\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-4\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Line_breaks\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.1.2</span>\n\t\t\t\t\t<span>Line breaks</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Line_breaks-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Links\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-4\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Links\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.1.3</span>\n\t\t\t\t\t<span>Links</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Links-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Inputs\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-4\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Inputs\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.1.4</span>\n\t\t\t\t\t<span>Inputs</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Inputs-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Attributes\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-3\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Attributes\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1.2</span>\n\t\t\t\t\t<span>Attributes</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Attributes-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Character_and_entity_references\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Character_and_entity_references\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.2</span>\n\t\t\t\t\t<span>Character and entity references</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Character_and_entity_references-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Data_types\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Data_types\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.3</span>\n\t\t\t\t\t<span>Data types</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Data_types-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Document_type_declaration\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Document_type_declaration\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.4</span>\n\t\t\t\t\t<span>Document type declaration</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Document_type_declaration-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Semantic_HTML\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Semantic_HTML\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">3</span>\n\t\t\t\t<span>Semantic HTML</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Semantic_HTML-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Delivery\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Delivery\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">4</span>\n\t\t\t\t<span>Delivery</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-Delivery-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle Delivery subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-Delivery-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-HTTP\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTTP\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.1</span>\n\t\t\t\t\t<span>HTTP</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTTP-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_e-mail\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_e-mail\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.2</span>\n\t\t\t\t\t<span>HTML e-mail</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_e-mail-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Naming_conventions\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Naming_conventions\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.3</span>\n\t\t\t\t\t<span>Naming conventions</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Naming_conventions-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-HTML_Application\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#HTML_Application\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">4.4</span>\n\t\t\t\t\t<span>HTML Application</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-HTML_Application-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-HTML4_variations\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#HTML4_variations\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">5</span>\n\t\t\t\t<span>HTML4 variations</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-HTML4_variations-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle HTML4 variations subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-HTML4_variations-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-SGML-based_versus_XML-based_HTML\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#SGML-based_versus_XML-based_HTML\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">5.1</span>\n\t\t\t\t\t<span>SGML-based versus XML-based HTML</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-SGML-based_versus_XML-based_HTML-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Transitional_versus_strict\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Transitional_versus_strict\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">5.2</span>\n\t\t\t\t\t<span>Transitional versus strict</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Transitional_versus_strict-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Frameset_versus_transitional\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Frameset_versus_transitional\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">5.3</span>\n\t\t\t\t\t<span>Frameset versus transitional</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Frameset_versus_transitional-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Summary_of_specification_versions\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Summary_of_specification_versions\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">5.4</span>\n\t\t\t\t\t<span>Summary of specification versions</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Summary_of_specification_versions-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-WHATWG_HTML_versus_HTML5\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#WHATWG_HTML_versus_HTML5\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">6</span>\n\t\t\t\t<span>WHATWG HTML versus HTML5</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-WHATWG_HTML_versus_HTML5-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-WYSIWYG_editors\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#WYSIWYG_editors\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">7</span>\n\t\t\t\t<span>WYSIWYG editors</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-WYSIWYG_editors-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-See_also\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#See_also\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">8</span>\n\t\t\t\t<span>See also</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-See_also-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-Notes\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#Notes\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">9</span>\n\t\t\t\t<span>Notes</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Notes-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-References\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#References\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">10</span>\n\t\t\t\t<span>References</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-References-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-External_links\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">11</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n</ul>\n</div>\n\n\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mw-content-container\">\n\t\t\t<main id=\"content\" class=\"mw-body\">\n\t\t\t\t<header class=\"mw-body-header vector-page-titlebar no-font-mode-scale\">\n\t\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n<div id=\"vector-page-titlebar-toc\" class=\"vector-dropdown vector-page-titlebar-toc vector-button-flush-left\"  title=\"Table of Contents\" >\n\t<input type=\"checkbox\" id=\"vector-page-titlebar-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-titlebar-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t<label id=\"vector-page-titlebar-toc-label\" for=\"vector-page-titlebar-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t<div id=\"vector-page-titlebar-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t</nav>\n\t\t\t\t\t<h1 id=\"firstHeading\" class=\"firstHeading mw-first-heading\"><span class=\"mw-page-title-main\">HTML</span></h1>\n\n<div id=\"p-lang-btn\" class=\"vector-dropdown mw-portlet mw-portlet-lang\"  >\n\t<input type=\"checkbox\" id=\"p-lang-btn-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-p-lang-btn\" class=\"vector-dropdown-checkbox mw-interlanguage-selector\" aria-label=\"Go to an article in another language. Available in 139 languages\"   >\n\t<label id=\"p-lang-btn-label\" for=\"p-lang-btn-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive mw-portlet-lang-heading-139\" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-language-progressive mw-ui-icon-wikimedia-language-progressive\"></span>\n\n<span class=\"vector-dropdown-label-text\">139 languages</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\t\t<div class=\"vector-menu-content\">\n\n\t\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t\t<li class=\"interlanguage-link interwiki-af mw-list-item\"><a href=\"https://af.wikipedia.org/wiki/HTML\" title=\"HTML – Afrikaans\" lang=\"af\" hreflang=\"af\" data-title=\"HTML\" data-language-autonym=\"Afrikaans\" data-language-local-name=\"Afrikaans\" class=\"interlanguage-link-target\"><span>Afrikaans</span></a></li><li class=\"interlanguage-link interwiki-als mw-list-item\"><a href=\"https://als.wikipedia.org/wiki/HTML\" title=\"HTML – Alemannic\" lang=\"gsw\" hreflang=\"gsw\" data-title=\"HTML\" data-language-autonym=\"Alemannisch\" data-language-local-name=\"Alemannic\" class=\"interlanguage-link-target\"><span>Alemannisch</span></a></li><li class=\"interlanguage-link interwiki-ang mw-list-item\"><a href=\"https://ang.wikipedia.org/wiki/Oferwrit_mearc_gereord\" title=\"Oferwrit mearc gereord – Old English\" lang=\"ang\" hreflang=\"ang\" data-title=\"Oferwrit mearc gereord\" data-language-autonym=\"Ænglisc\" data-language-local-name=\"Old English\" class=\"interlanguage-link-target\"><span>Ænglisc</span></a></li><li class=\"interlanguage-link interwiki-ar mw-list-item\"><a href=\"https://ar.wikipedia.org/wiki/%D9%84%D8%BA%D8%A9_%D8%AA%D8%A3%D8%B4%D9%8A%D8%B1_%D8%A7%D9%84%D9%86%D8%B5_%D8%A7%D9%84%D8%AA%D8%B1%D8%A7%D8%A8%D8%B7%D9%8A\" title=\"لغة تأشير النص الترابطي – Arabic\" lang=\"ar\" hreflang=\"ar\" data-title=\"لغة تأشير النص الترابطي\" data-language-autonym=\"العربية\" data-language-local-name=\"Arabic\" class=\"interlanguage-link-target\"><span>العربية</span></a></li><li class=\"interlanguage-link interwiki-an mw-list-item\"><a href=\"https://an.wikipedia.org/wiki/HTML\" title=\"HTML – Aragonese\" lang=\"an\" hreflang=\"an\" data-title=\"HTML\" data-language-autonym=\"Aragonés\" data-language-local-name=\"Aragonese\" class=\"interlanguage-link-target\"><span>Aragonés</span></a></li><li class=\"interlanguage-link interwiki-as mw-list-item\"><a href=\"https://as.wikipedia.org/wiki/%E0%A6%8F%E0%A6%87%E0%A6%9B%E0%A6%9F%E0%A6%BF%E0%A6%8F%E0%A6%AE%E0%A6%8F%E0%A6%B2\" title=\"এইছটিএমএল – Assamese\" lang=\"as\" hreflang=\"as\" data-title=\"এইছটিএমএল\" data-language-autonym=\"অসমীয়া\" data-language-local-name=\"Assamese\" class=\"interlanguage-link-target\"><span>অসমীয়া</span></a></li><li class=\"interlanguage-link interwiki-ast mw-list-item\"><a href=\"https://ast.wikipedia.org/wiki/HTML\" title=\"HTML – Asturian\" lang=\"ast\" hreflang=\"ast\" data-title=\"HTML\" data-language-autonym=\"Asturianu\" data-language-local-name=\"Asturian\" class=\"interlanguage-link-target\"><span>Asturianu</span></a></li><li class=\"interlanguage-link interwiki-az mw-list-item\"><a href=\"https://az.wikipedia.org/wiki/HTML\" title=\"HTML – Azerbaijani\" lang=\"az\" hreflang=\"az\" data-title=\"HTML\" data-language-autonym=\"Azərbaycanca\" data-language-local-name=\"Azerbaijani\" class=\"interlanguage-link-target\"><span>Azərbaycanca</span></a></li><li class=\"interlanguage-link interwiki-azb mw-list-item\"><a href=\"https://azb.wikipedia.org/wiki/%D8%A7%DA%86%E2%80%8C%D8%AA%DB%8C%E2%80%8C%D8%A7%D9%85%E2%80%8C%D8%A7%D9%84\" title=\"اچ‌تی‌ام‌ال – South Azerbaijani\" lang=\"azb\" hreflang=\"azb\" data-title=\"اچ‌تی‌ام‌ال\" data-language-autonym=\"تۆرکجه\" data-language-local-name=\"South Azerbaijani\" class=\"interlanguage-link-target\"><span>تۆرکجه</span></a></li><li class=\"interlanguage-link interwiki-ban mw-list-item\"><a href=\"https://ban.wikipedia.org/wiki/HTML\" title=\"HTML – Balinese\" lang=\"ban\" hreflang=\"ban\" data-title=\"HTML\" data-language-autonym=\"Basa Bali\" data-language-local-name=\"Balinese\" class=\"interlanguage-link-target\"><span>Basa Bali</span></a></li><li class=\"interlanguage-link interwiki-bn mw-list-item\"><a href=\"https://bn.wikipedia.org/wiki/%E0%A6%8F%E0%A6%87%E0%A6%9A%E0%A6%9F%E0%A6%BF%E0%A6%8F%E0%A6%AE%E0%A6%8F%E0%A6%B2\" title=\"এইচটিএমএল – Bangla\" lang=\"bn\" hreflang=\"bn\" data-title=\"এইচটিএমএল\" data-language-autonym=\"বাংলা\" data-language-local-name=\"Bangla\" class=\"interlanguage-link-target\"><span>বাংলা</span></a></li><li class=\"interlanguage-link interwiki-zh-min-nan mw-list-item\"><a href=\"https://zh-min-nan.wikipedia.org/wiki/HTML\" title=\"HTML – Minnan\" lang=\"nan\" hreflang=\"nan\" data-title=\"HTML\" data-language-autonym=\"閩南語 / Bân-lâm-gí\" data-language-local-name=\"Minnan\" class=\"interlanguage-link-target\"><span>閩南語 / Bân-lâm-gí</span></a></li><li class=\"interlanguage-link interwiki-ba mw-list-item\"><a href=\"https://ba.wikipedia.org/wiki/HTML\" title=\"HTML – Bashkir\" lang=\"ba\" hreflang=\"ba\" data-title=\"HTML\" data-language-autonym=\"Башҡортса\" data-language-local-name=\"Bashkir\" class=\"interlanguage-link-target\"><span>Башҡортса</span></a></li><li class=\"interlanguage-link interwiki-be mw-list-item\"><a href=\"https://be.wikipedia.org/wiki/HTML\" title=\"HTML – Belarusian\" lang=\"be\" hreflang=\"be\" data-title=\"HTML\" data-language-autonym=\"Беларуская\" data-language-local-name=\"Belarusian\" class=\"interlanguage-link-target\"><span>Беларуская</span></a></li><li class=\"interlanguage-link interwiki-be-x-old mw-list-item\"><a href=\"https://be-tarask.wikipedia.org/wiki/HTML\" title=\"HTML – Belarusian (Taraškievica orthography)\" lang=\"be-tarask\" hreflang=\"be-tarask\" data-title=\"HTML\" data-language-autonym=\"Беларуская (тарашкевіца)\" data-language-local-name=\"Belarusian (Taraškievica orthography)\" class=\"interlanguage-link-target\"><span>Беларуская (тарашкевіца)</span></a></li><li class=\"interlanguage-link interwiki-bg mw-list-item\"><a href=\"https://bg.wikipedia.org/wiki/HTML\" title=\"HTML – Bulgarian\" lang=\"bg\" hreflang=\"bg\" data-title=\"HTML\" data-language-autonym=\"Български\" data-language-local-name=\"Bulgarian\" class=\"interlanguage-link-target\"><span>Български</span></a></li><li class=\"interlanguage-link interwiki-bar mw-list-item\"><a href=\"https://bar.wikipedia.org/wiki/HTML\" title=\"HTML – Bavarian\" lang=\"bar\" hreflang=\"bar\" data-title=\"HTML\" data-language-autonym=\"Boarisch\" data-language-local-name=\"Bavarian\" class=\"interlanguage-link-target\"><span>Boarisch</span></a></li><li class=\"interlanguage-link interwiki-bs mw-list-item\"><a href=\"https://bs.wikipedia.org/wiki/HTML\" title=\"HTML – Bosnian\" lang=\"bs\" hreflang=\"bs\" data-title=\"HTML\" data-language-autonym=\"Bosanski\" data-language-local-name=\"Bosnian\" class=\"interlanguage-link-target\"><span>Bosanski</span></a></li><li class=\"interlanguage-link interwiki-br mw-list-item\"><a href=\"https://br.wikipedia.org/wiki/HTML\" title=\"HTML – Breton\" lang=\"br\" hreflang=\"br\" data-title=\"HTML\" data-language-autonym=\"Brezhoneg\" data-language-local-name=\"Breton\" class=\"interlanguage-link-target\"><span>Brezhoneg</span></a></li><li class=\"interlanguage-link interwiki-ca mw-list-item\"><a href=\"https://ca.wikipedia.org/wiki/Hyper_Text_Markup_Language\" title=\"Hyper Text Markup Language – Catalan\" lang=\"ca\" hreflang=\"ca\" data-title=\"Hyper Text Markup Language\" data-language-autonym=\"Català\" data-language-local-name=\"Catalan\" class=\"interlanguage-link-target\"><span>Català</span></a></li><li class=\"interlanguage-link interwiki-cv mw-list-item\"><a href=\"https://cv.wikipedia.org/wiki/HTML\" title=\"HTML – Chuvash\" lang=\"cv\" hreflang=\"cv\" data-title=\"HTML\" data-language-autonym=\"Чӑвашла\" data-language-local-name=\"Chuvash\" class=\"interlanguage-link-target\"><span>Чӑвашла</span></a></li><li class=\"interlanguage-link interwiki-cs mw-list-item\"><a href=\"https://cs.wikipedia.org/wiki/Hypertext_Markup_Language\" title=\"Hypertext Markup Language – Czech\" lang=\"cs\" hreflang=\"cs\" data-title=\"Hypertext Markup Language\" data-language-autonym=\"Čeština\" data-language-local-name=\"Czech\" class=\"interlanguage-link-target\"><span>Čeština</span></a></li><li class=\"interlanguage-link interwiki-tum mw-list-item\"><a href=\"https://tum.wikipedia.org/wiki/HTML\" title=\"HTML – Tumbuka\" lang=\"tum\" hreflang=\"tum\" data-title=\"HTML\" data-language-autonym=\"ChiTumbuka\" data-language-local-name=\"Tumbuka\" class=\"interlanguage-link-target\"><span>ChiTumbuka</span></a></li><li class=\"interlanguage-link interwiki-co mw-list-item\"><a href=\"https://co.wikipedia.org/wiki/HTML\" title=\"HTML – Corsican\" lang=\"co\" hreflang=\"co\" data-title=\"HTML\" data-language-autonym=\"Corsu\" data-language-local-name=\"Corsican\" class=\"interlanguage-link-target\"><span>Corsu</span></a></li><li class=\"interlanguage-link interwiki-cy mw-list-item\"><a href=\"https://cy.wikipedia.org/wiki/HTML\" title=\"HTML – Welsh\" lang=\"cy\" hreflang=\"cy\" data-title=\"HTML\" data-language-autonym=\"Cymraeg\" data-language-local-name=\"Welsh\" class=\"interlanguage-link-target\"><span>Cymraeg</span></a></li><li class=\"interlanguage-link interwiki-da mw-list-item\"><a href=\"https://da.wikipedia.org/wiki/HTML\" title=\"HTML – Danish\" lang=\"da\" hreflang=\"da\" data-title=\"HTML\" data-language-autonym=\"Dansk\" data-language-local-name=\"Danish\" class=\"interlanguage-link-target\"><span>Dansk</span></a></li><li class=\"interlanguage-link interwiki-se mw-list-item\"><a href=\"https://se.wikipedia.org/wiki/HTML\" title=\"HTML – Northern Sami\" lang=\"se\" hreflang=\"se\" data-title=\"HTML\" data-language-autonym=\"Davvisámegiella\" data-language-local-name=\"Northern Sami\" class=\"interlanguage-link-target\"><span>Davvisámegiella</span></a></li><li class=\"interlanguage-link interwiki-de mw-list-item\"><a href=\"https://de.wikipedia.org/wiki/Hypertext_Markup_Language\" title=\"Hypertext Markup Language – German\" lang=\"de\" hreflang=\"de\" data-title=\"Hypertext Markup Language\" data-language-autonym=\"Deutsch\" data-language-local-name=\"German\" class=\"interlanguage-link-target\"><span>Deutsch</span></a></li><li class=\"interlanguage-link interwiki-dsb mw-list-item\"><a href=\"https://dsb.wikipedia.org/wiki/HTML\" title=\"HTML – Lower Sorbian\" lang=\"dsb\" hreflang=\"dsb\" data-title=\"HTML\" data-language-autonym=\"Dolnoserbski\" data-language-local-name=\"Lower Sorbian\" class=\"interlanguage-link-target\"><span>Dolnoserbski</span></a></li><li class=\"interlanguage-link interwiki-et mw-list-item\"><a href=\"https://et.wikipedia.org/wiki/HTML\" title=\"HTML – Estonian\" lang=\"et\" hreflang=\"et\" data-title=\"HTML\" data-language-autonym=\"Eesti\" data-language-local-name=\"Estonian\" class=\"interlanguage-link-target\"><span>Eesti</span></a></li><li class=\"interlanguage-link interwiki-el mw-list-item\"><a href=\"https://el.wikipedia.org/wiki/HTML\" title=\"HTML – Greek\" lang=\"el\" hreflang=\"el\" data-title=\"HTML\" data-language-autonym=\"Ελληνικά\" data-language-local-name=\"Greek\" class=\"interlanguage-link-target\"><span>Ελληνικά</span></a></li><li class=\"interlanguage-link interwiki-es mw-list-item\"><a href=\"https://es.wikipedia.org/wiki/HTML\" title=\"HTML – Spanish\" lang=\"es\" hreflang=\"es\" data-title=\"HTML\" data-language-autonym=\"Español\" data-language-local-name=\"Spanish\" class=\"interlanguage-link-target\"><span>Español</span></a></li><li class=\"interlanguage-link interwiki-eo mw-list-item\"><a href=\"https://eo.wikipedia.org/wiki/HTML\" title=\"HTML – Esperanto\" lang=\"eo\" hreflang=\"eo\" data-title=\"HTML\" data-language-autonym=\"Esperanto\" data-language-local-name=\"Esperanto\" class=\"interlanguage-link-target\"><span>Esperanto</span></a></li><li class=\"interlanguage-link interwiki-eu mw-list-item\"><a href=\"https://eu.wikipedia.org/wiki/HTML\" title=\"HTML – Basque\" lang=\"eu\" hreflang=\"eu\" data-title=\"HTML\" data-language-autonym=\"Euskara\" data-language-local-name=\"Basque\" class=\"interlanguage-link-target\"><span>Euskara</span></a></li><li class=\"interlanguage-link interwiki-fa mw-list-item\"><a href=\"https://fa.wikipedia.org/wiki/%D8%A7%DA%86%E2%80%8C%D8%AA%DB%8C%E2%80%8C%D8%A7%D9%85%E2%80%8C%D8%A7%D9%84\" title=\"اچ‌تی‌ام‌ال – Persian\" lang=\"fa\" hreflang=\"fa\" data-title=\"اچ‌تی‌ام‌ال\" data-language-autonym=\"فارسی\" data-language-local-name=\"Persian\" class=\"interlanguage-link-target\"><span>فارسی</span></a></li><li class=\"interlanguage-link interwiki-hif mw-list-item\"><a href=\"https://hif.wikipedia.org/wiki/HTML\" title=\"HTML – Fiji Hindi\" lang=\"hif\" hreflang=\"hif\" data-title=\"HTML\" data-language-autonym=\"Fiji Hindi\" data-language-local-name=\"Fiji Hindi\" class=\"interlanguage-link-target\"><span>Fiji Hindi</span></a></li><li class=\"interlanguage-link interwiki-fo mw-list-item\"><a href=\"https://fo.wikipedia.org/wiki/HTML\" title=\"HTML – Faroese\" lang=\"fo\" hreflang=\"fo\" data-title=\"HTML\" data-language-autonym=\"Føroyskt\" data-language-local-name=\"Faroese\" class=\"interlanguage-link-target\"><span>Føroyskt</span></a></li><li class=\"interlanguage-link interwiki-fr mw-list-item\"><a href=\"https://fr.wikipedia.org/wiki/Hypertext_Markup_Language\" title=\"Hypertext Markup Language – French\" lang=\"fr\" hreflang=\"fr\" data-title=\"Hypertext Markup Language\" data-language-autonym=\"Français\" data-language-local-name=\"French\" class=\"interlanguage-link-target\"><span>Français</span></a></li><li class=\"interlanguage-link interwiki-fy mw-list-item\"><a href=\"https://fy.wikipedia.org/wiki/HTML\" title=\"HTML – Western Frisian\" lang=\"fy\" hreflang=\"fy\" data-title=\"HTML\" data-language-autonym=\"Frysk\" data-language-local-name=\"Western Frisian\" class=\"interlanguage-link-target\"><span>Frysk</span></a></li><li class=\"interlanguage-link interwiki-fur mw-list-item\"><a href=\"https://fur.wikipedia.org/wiki/HTML\" title=\"HTML – Friulian\" lang=\"fur\" hreflang=\"fur\" data-title=\"HTML\" data-language-autonym=\"Furlan\" data-language-local-name=\"Friulian\" class=\"interlanguage-link-target\"><span>Furlan</span></a></li><li class=\"interlanguage-link interwiki-ga mw-list-item\"><a href=\"https://ga.wikipedia.org/wiki/HTML\" title=\"HTML – Irish\" lang=\"ga\" hreflang=\"ga\" data-title=\"HTML\" data-language-autonym=\"Gaeilge\" data-language-local-name=\"Irish\" class=\"interlanguage-link-target\"><span>Gaeilge</span></a></li><li class=\"interlanguage-link interwiki-gd mw-list-item\"><a href=\"https://gd.wikipedia.org/wiki/HTML\" title=\"HTML – Scottish Gaelic\" lang=\"gd\" hreflang=\"gd\" data-title=\"HTML\" data-language-autonym=\"Gàidhlig\" data-language-local-name=\"Scottish Gaelic\" class=\"interlanguage-link-target\"><span>Gàidhlig</span></a></li><li class=\"interlanguage-link interwiki-gl mw-list-item\"><a href=\"https://gl.wikipedia.org/wiki/HTML\" title=\"HTML – Galician\" lang=\"gl\" hreflang=\"gl\" data-title=\"HTML\" data-language-autonym=\"Galego\" data-language-local-name=\"Galician\" class=\"interlanguage-link-target\"><span>Galego</span></a></li><li class=\"interlanguage-link interwiki-glk mw-list-item\"><a href=\"https://glk.wikipedia.org/wiki/%D8%A6%DA%86.%D8%AA%D9%8A.%D8%A6%D9%85.%D8%A6%D9%84\" title=\"ئچ.تي.ئم.ئل – Gilaki\" lang=\"glk\" hreflang=\"glk\" data-title=\"ئچ.تي.ئم.ئل\" data-language-autonym=\"گیلکی\" data-language-local-name=\"Gilaki\" class=\"interlanguage-link-target\"><span>گیلکی</span></a></li><li class=\"interlanguage-link interwiki-gu mw-list-item\"><a href=\"https://gu.wikipedia.org/wiki/HTML\" title=\"HTML – Gujarati\" lang=\"gu\" hreflang=\"gu\" data-title=\"HTML\" data-language-autonym=\"ગુજરાતી\" data-language-local-name=\"Gujarati\" class=\"interlanguage-link-target\"><span>ગુજરાતી</span></a></li><li class=\"interlanguage-link interwiki-ko mw-list-item\"><a href=\"https://ko.wikipedia.org/wiki/HTML\" title=\"HTML – Korean\" lang=\"ko\" hreflang=\"ko\" data-title=\"HTML\" data-language-autonym=\"한국어\" data-language-local-name=\"Korean\" class=\"interlanguage-link-target\"><span>한국어</span></a></li><li class=\"interlanguage-link interwiki-haw mw-list-item\"><a href=\"https://haw.wikipedia.org/wiki/HTML\" title=\"HTML – Hawaiian\" lang=\"haw\" hreflang=\"haw\" data-title=\"HTML\" data-language-autonym=\"Hawaiʻi\" data-language-local-name=\"Hawaiian\" class=\"interlanguage-link-target\"><span>Hawaiʻi</span></a></li><li class=\"interlanguage-link interwiki-hy mw-list-item\"><a href=\"https://hy.wikipedia.org/wiki/HTML\" title=\"HTML – Armenian\" lang=\"hy\" hreflang=\"hy\" data-title=\"HTML\" data-language-autonym=\"Հայերեն\" data-language-local-name=\"Armenian\" class=\"interlanguage-link-target\"><span>Հայերեն</span></a></li><li class=\"interlanguage-link interwiki-hi mw-list-item\"><a href=\"https://hi.wikipedia.org/wiki/%E0%A4%8F%E0%A4%9A%E0%A4%9F%E0%A5%80%E0%A4%8F%E0%A4%AE%E0%A4%8F%E0%A4%B2\" title=\"एचटीएमएल – Hindi\" lang=\"hi\" hreflang=\"hi\" data-title=\"एचटीएमएल\" data-language-autonym=\"हिन्दी\" data-language-local-name=\"Hindi\" class=\"interlanguage-link-target\"><span>हिन्दी</span></a></li><li class=\"interlanguage-link interwiki-hsb mw-list-item\"><a href=\"https://hsb.wikipedia.org/wiki/HTML\" title=\"HTML – Upper Sorbian\" lang=\"hsb\" hreflang=\"hsb\" data-title=\"HTML\" data-language-autonym=\"Hornjoserbsce\" data-language-local-name=\"Upper Sorbian\" class=\"interlanguage-link-target\"><span>Hornjoserbsce</span></a></li><li class=\"interlanguage-link interwiki-hr mw-list-item\"><a href=\"https://hr.wikipedia.org/wiki/HTML\" title=\"HTML – Croatian\" lang=\"hr\" hreflang=\"hr\" data-title=\"HTML\" data-language-autonym=\"Hrvatski\" data-language-local-name=\"Croatian\" class=\"interlanguage-link-target\"><span>Hrvatski</span></a></li><li class=\"interlanguage-link interwiki-io mw-list-item\"><a href=\"https://io.wikipedia.org/wiki/HTML\" title=\"HTML – Ido\" lang=\"io\" hreflang=\"io\" data-title=\"HTML\" data-language-autonym=\"Ido\" data-language-local-name=\"Ido\" class=\"interlanguage-link-target\"><span>Ido</span></a></li><li class=\"interlanguage-link interwiki-id mw-list-item\"><a href=\"https://id.wikipedia.org/wiki/HTML\" title=\"HTML – Indonesian\" lang=\"id\" hreflang=\"id\" data-title=\"HTML\" data-language-autonym=\"Bahasa Indonesia\" data-language-local-name=\"Indonesian\" class=\"interlanguage-link-target\"><span>Bahasa Indonesia</span></a></li><li class=\"interlanguage-link interwiki-ia mw-list-item\"><a href=\"https://ia.wikipedia.org/wiki/HTML\" title=\"HTML – Interlingua\" lang=\"ia\" hreflang=\"ia\" data-title=\"HTML\" data-language-autonym=\"Interlingua\" data-language-local-name=\"Interlingua\" class=\"interlanguage-link-target\"><span>Interlingua</span></a></li><li class=\"interlanguage-link interwiki-is mw-list-item\"><a href=\"https://is.wikipedia.org/wiki/HTML\" title=\"HTML – Icelandic\" lang=\"is\" hreflang=\"is\" data-title=\"HTML\" data-language-autonym=\"Íslenska\" data-language-local-name=\"Icelandic\" class=\"interlanguage-link-target\"><span>Íslenska</span></a></li><li class=\"interlanguage-link interwiki-it mw-list-item\"><a href=\"https://it.wikipedia.org/wiki/HTML\" title=\"HTML – Italian\" lang=\"it\" hreflang=\"it\" data-title=\"HTML\" data-language-autonym=\"Italiano\" data-language-local-name=\"Italian\" class=\"interlanguage-link-target\"><span>Italiano</span></a></li><li class=\"interlanguage-link interwiki-he mw-list-item\"><a href=\"https://he.wikipedia.org/wiki/HTML\" title=\"HTML – Hebrew\" lang=\"he\" hreflang=\"he\" data-title=\"HTML\" data-language-autonym=\"עברית\" data-language-local-name=\"Hebrew\" class=\"interlanguage-link-target\"><span>עברית</span></a></li><li class=\"interlanguage-link interwiki-jv mw-list-item\"><a href=\"https://jv.wikipedia.org/wiki/Hypertext_markup_language\" title=\"Hypertext markup language – Javanese\" lang=\"jv\" hreflang=\"jv\" data-title=\"Hypertext markup language\" data-language-autonym=\"Jawa\" data-language-local-name=\"Javanese\" class=\"interlanguage-link-target\"><span>Jawa</span></a></li><li class=\"interlanguage-link interwiki-ka mw-list-item\"><a href=\"https://ka.wikipedia.org/wiki/%E1%83%B0%E1%83%98%E1%83%9E%E1%83%94%E1%83%A0%E1%83%A2%E1%83%94%E1%83%A5%E1%83%A1%E1%83%A2%E1%83%A3%E1%83%A0%E1%83%98_%E1%83%9B%E1%83%90%E1%83%A0%E1%83%99%E1%83%98%E1%83%A0%E1%83%94%E1%83%91%E1%83%98%E1%83%A1_%E1%83%94%E1%83%9C%E1%83%90\" title=\"ჰიპერტექსტური მარკირების ენა – Georgian\" lang=\"ka\" hreflang=\"ka\" data-title=\"ჰიპერტექსტური მარკირების ენა\" data-language-autonym=\"ქართული\" data-language-local-name=\"Georgian\" class=\"interlanguage-link-target\"><span>ქართული</span></a></li><li class=\"interlanguage-link interwiki-kk mw-list-item\"><a href=\"https://kk.wikipedia.org/wiki/HTML\" title=\"HTML – Kazakh\" lang=\"kk\" hreflang=\"kk\" data-title=\"HTML\" data-language-autonym=\"Қазақша\" data-language-local-name=\"Kazakh\" class=\"interlanguage-link-target\"><span>Қазақша</span></a></li><li class=\"interlanguage-link interwiki-sw mw-list-item\"><a href=\"https://sw.wikipedia.org/wiki/HTML\" title=\"HTML – Swahili\" lang=\"sw\" hreflang=\"sw\" data-title=\"HTML\" data-language-autonym=\"Kiswahili\" data-language-local-name=\"Swahili\" class=\"interlanguage-link-target\"><span>Kiswahili</span></a></li><li class=\"interlanguage-link interwiki-ku mw-list-item\"><a href=\"https://ku.wikipedia.org/wiki/HTML\" title=\"HTML – Kurdish\" lang=\"ku\" hreflang=\"ku\" data-title=\"HTML\" data-language-autonym=\"Kurdî\" data-language-local-name=\"Kurdish\" class=\"interlanguage-link-target\"><span>Kurdî</span></a></li><li class=\"interlanguage-link interwiki-ky mw-list-item\"><a href=\"https://ky.wikipedia.org/wiki/HTML\" title=\"HTML – Kyrgyz\" lang=\"ky\" hreflang=\"ky\" data-title=\"HTML\" data-language-autonym=\"Кыргызча\" data-language-local-name=\"Kyrgyz\" class=\"interlanguage-link-target\"><span>Кыргызча</span></a></li><li class=\"interlanguage-link interwiki-lo mw-list-item\"><a href=\"https://lo.wikipedia.org/wiki/%E0%BB%80%E0%BA%AE%E0%BA%B1%E0%BA%94%E0%BA%94%E0%BA%B5%E0%BB%80%E0%BA%AD%E0%BA%B1%E0%BA%A1%E0%BB%81%E0%BA%AD%E0%BA%A7\" title=\"ເຮັດດີເອັມແອວ – Lao\" lang=\"lo\" hreflang=\"lo\" data-title=\"ເຮັດດີເອັມແອວ\" data-language-autonym=\"ລາວ\" data-language-local-name=\"Lao\" class=\"interlanguage-link-target\"><span>ລາວ</span></a></li><li class=\"interlanguage-link interwiki-ltg mw-list-item\"><a href=\"https://ltg.wikipedia.org/wiki/HTML\" title=\"HTML – Latgalian\" lang=\"ltg\" hreflang=\"ltg\" data-title=\"HTML\" data-language-autonym=\"Latgaļu\" data-language-local-name=\"Latgalian\" class=\"interlanguage-link-target\"><span>Latgaļu</span></a></li><li class=\"interlanguage-link interwiki-la mw-list-item\"><a href=\"https://la.wikipedia.org/wiki/HTML\" title=\"HTML – Latin\" lang=\"la\" hreflang=\"la\" data-title=\"HTML\" data-language-autonym=\"Latina\" data-language-local-name=\"Latin\" class=\"interlanguage-link-target\"><span>Latina</span></a></li><li class=\"interlanguage-link interwiki-lv mw-list-item\"><a href=\"https://lv.wikipedia.org/wiki/HTML\" title=\"HTML – Latvian\" lang=\"lv\" hreflang=\"lv\" data-title=\"HTML\" data-language-autonym=\"Latviešu\" data-language-local-name=\"Latvian\" class=\"interlanguage-link-target\"><span>Latviešu</span></a></li><li class=\"interlanguage-link interwiki-lb mw-list-item\"><a href=\"https://lb.wikipedia.org/wiki/Hypertext_Markup_Language\" title=\"Hypertext Markup Language – Luxembourgish\" lang=\"lb\" hreflang=\"lb\" data-title=\"Hypertext Markup Language\" data-language-autonym=\"Lëtzebuergesch\" data-language-local-name=\"Luxembourgish\" class=\"interlanguage-link-target\"><span>Lëtzebuergesch</span></a></li><li class=\"interlanguage-link interwiki-lt mw-list-item\"><a href=\"https://lt.wikipedia.org/wiki/HTML\" title=\"HTML – Lithuanian\" lang=\"lt\" hreflang=\"lt\" data-title=\"HTML\" data-language-autonym=\"Lietuvių\" data-language-local-name=\"Lithuanian\" class=\"interlanguage-link-target\"><span>Lietuvių</span></a></li><li class=\"interlanguage-link interwiki-lij mw-list-item\"><a href=\"https://lij.wikipedia.org/wiki/HTML\" title=\"HTML – Ligurian\" lang=\"lij\" hreflang=\"lij\" data-title=\"HTML\" data-language-autonym=\"Ligure\" data-language-local-name=\"Ligurian\" class=\"interlanguage-link-target\"><span>Ligure</span></a></li><li class=\"interlanguage-link interwiki-lfn mw-list-item\"><a href=\"https://lfn.wikipedia.org/wiki/HTML\" title=\"HTML – Lingua Franca Nova\" lang=\"lfn\" hreflang=\"lfn\" data-title=\"HTML\" data-language-autonym=\"Lingua Franca Nova\" data-language-local-name=\"Lingua Franca Nova\" class=\"interlanguage-link-target\"><span>Lingua Franca Nova</span></a></li><li class=\"interlanguage-link interwiki-lmo mw-list-item\"><a href=\"https://lmo.wikipedia.org/wiki/HTML\" title=\"HTML – Lombard\" lang=\"lmo\" hreflang=\"lmo\" data-title=\"HTML\" data-language-autonym=\"Lombard\" data-language-local-name=\"Lombard\" class=\"interlanguage-link-target\"><span>Lombard</span></a></li><li class=\"interlanguage-link interwiki-hu mw-list-item\"><a href=\"https://hu.wikipedia.org/wiki/HTML\" title=\"HTML – Hungarian\" lang=\"hu\" hreflang=\"hu\" data-title=\"HTML\" data-language-autonym=\"Magyar\" data-language-local-name=\"Hungarian\" class=\"interlanguage-link-target\"><span>Magyar</span></a></li><li class=\"interlanguage-link interwiki-mai mw-list-item\"><a href=\"https://mai.wikipedia.org/wiki/%E0%A4%8F%E0%A4%9A%E0%A4%9F%E0%A4%BF%E0%A4%8F%E0%A4%AE%E0%A4%8F%E0%A4%B2\" title=\"एचटिएमएल – Maithili\" lang=\"mai\" hreflang=\"mai\" data-title=\"एचटिएमएल\" data-language-autonym=\"मैथिली\" data-language-local-name=\"Maithili\" class=\"interlanguage-link-target\"><span>मैथिली</span></a></li><li class=\"interlanguage-link interwiki-mk mw-list-item\"><a href=\"https://mk.wikipedia.org/wiki/HTML\" title=\"HTML – Macedonian\" lang=\"mk\" hreflang=\"mk\" data-title=\"HTML\" data-language-autonym=\"Македонски\" data-language-local-name=\"Macedonian\" class=\"interlanguage-link-target\"><span>Македонски</span></a></li><li class=\"interlanguage-link interwiki-mg mw-list-item\"><a href=\"https://mg.wikipedia.org/wiki/Hypertext_Markup_Language\" title=\"Hypertext Markup Language – Malagasy\" lang=\"mg\" hreflang=\"mg\" data-title=\"Hypertext Markup Language\" data-language-autonym=\"Malagasy\" data-language-local-name=\"Malagasy\" class=\"interlanguage-link-target\"><span>Malagasy</span></a></li><li class=\"interlanguage-link interwiki-ml mw-list-item\"><a href=\"https://ml.wikipedia.org/wiki/%E0%B4%8E%E0%B4%9A%E0%B5%8D.%E0%B4%9F%E0%B4%BF.%E0%B4%8E%E0%B4%82.%E0%B4%8E%E0%B5%BD.\" title=\"എച്.ടി.എം.എൽ. – Malayalam\" lang=\"ml\" hreflang=\"ml\" data-title=\"എച്.ടി.എം.എൽ.\" data-language-autonym=\"മലയാളം\" data-language-local-name=\"Malayalam\" class=\"interlanguage-link-target\"><span>മലയാളം</span></a></li><li class=\"interlanguage-link interwiki-mr mw-list-item\"><a href=\"https://mr.wikipedia.org/wiki/%E0%A4%8F%E0%A4%9A.%E0%A4%9F%E0%A5%80.%E0%A4%8F%E0%A4%AE.%E0%A4%8F%E0%A4%B2.\" title=\"एच.टी.एम.एल. – Marathi\" lang=\"mr\" hreflang=\"mr\" data-title=\"एच.टी.एम.एल.\" data-language-autonym=\"मराठी\" data-language-local-name=\"Marathi\" class=\"interlanguage-link-target\"><span>मराठी</span></a></li><li class=\"interlanguage-link interwiki-arz mw-list-item\"><a href=\"https://arz.wikipedia.org/wiki/%D8%A7%D8%AA%D8%B4_%D8%AA%D9%89_%D8%A7%D9%85_%D8%A7%D9%84\" title=\"اتش تى ام ال – Egyptian Arabic\" lang=\"arz\" hreflang=\"arz\" data-title=\"اتش تى ام ال\" data-language-autonym=\"مصرى\" data-language-local-name=\"Egyptian Arabic\" class=\"interlanguage-link-target\"><span>مصرى</span></a></li><li class=\"interlanguage-link interwiki-ms mw-list-item\"><a href=\"https://ms.wikipedia.org/wiki/HTML\" title=\"HTML – Malay\" lang=\"ms\" hreflang=\"ms\" data-title=\"HTML\" data-language-autonym=\"Bahasa Melayu\" data-language-local-name=\"Malay\" class=\"interlanguage-link-target\"><span>Bahasa Melayu</span></a></li><li class=\"interlanguage-link interwiki-min mw-list-item\"><a href=\"https://min.wikipedia.org/wiki/HTML\" title=\"HTML – Minangkabau\" lang=\"min\" hreflang=\"min\" data-title=\"HTML\" data-language-autonym=\"Minangkabau\" data-language-local-name=\"Minangkabau\" class=\"interlanguage-link-target\"><span>Minangkabau</span></a></li><li class=\"interlanguage-link interwiki-cdo mw-list-item\"><a href=\"https://cdo.wikipedia.org/wiki/HTML\" title=\"HTML – Mindong\" lang=\"cdo\" hreflang=\"cdo\" data-title=\"HTML\" data-language-autonym=\"閩東語 / Mìng-dĕ̤ng-ngṳ̄\" data-language-local-name=\"Mindong\" class=\"interlanguage-link-target\"><span>閩東語 / Mìng-dĕ̤ng-ngṳ̄</span></a></li><li class=\"interlanguage-link interwiki-mn mw-list-item\"><a href=\"https://mn.wikipedia.org/wiki/HTML\" title=\"HTML – Mongolian\" lang=\"mn\" hreflang=\"mn\" data-title=\"HTML\" data-language-autonym=\"Монгол\" data-language-local-name=\"Mongolian\" class=\"interlanguage-link-target\"><span>Монгол</span></a></li><li class=\"interlanguage-link interwiki-my mw-list-item\"><a href=\"https://my.wikipedia.org/wiki/HTML\" title=\"HTML – Burmese\" lang=\"my\" hreflang=\"my\" data-title=\"HTML\" data-language-autonym=\"မြန်မာဘာသာ\" data-language-local-name=\"Burmese\" class=\"interlanguage-link-target\"><span>မြန်မာဘာသာ</span></a></li><li class=\"interlanguage-link interwiki-nl mw-list-item\"><a href=\"https://nl.wikipedia.org/wiki/HyperText_Markup_Language\" title=\"HyperText Markup Language – Dutch\" lang=\"nl\" hreflang=\"nl\" data-title=\"HyperText Markup Language\" data-language-autonym=\"Nederlands\" data-language-local-name=\"Dutch\" class=\"interlanguage-link-target\"><span>Nederlands</span></a></li><li class=\"interlanguage-link interwiki-ne mw-list-item\"><a href=\"https://ne.wikipedia.org/wiki/%E0%A4%8F%E0%A4%9A%E0%A4%9F%E0%A4%BF%E0%A4%8F%E0%A4%AE%E0%A4%8F%E0%A4%B2\" title=\"एचटिएमएल – Nepali\" lang=\"ne\" hreflang=\"ne\" data-title=\"एचटिएमएल\" data-language-autonym=\"नेपाली\" data-language-local-name=\"Nepali\" class=\"interlanguage-link-target\"><span>नेपाली</span></a></li><li class=\"interlanguage-link interwiki-new mw-list-item\"><a href=\"https://new.wikipedia.org/wiki/%E0%A4%8F%E0%A4%9A_%E0%A4%9F%E0%A5%80_%E0%A4%8F%E0%A4%AE%E0%A5%8D_%E0%A4%8F%E0%A4%B2%E0%A5%8D\" title=\"एच टी एम् एल् – Newari\" lang=\"new\" hreflang=\"new\" data-title=\"एच टी एम् एल्\" data-language-autonym=\"नेपाल भाषा\" data-language-local-name=\"Newari\" class=\"interlanguage-link-target\"><span>नेपाल भाषा</span></a></li><li class=\"interlanguage-link interwiki-ja mw-list-item\"><a href=\"https://ja.wikipedia.org/wiki/HyperText_Markup_Language\" title=\"HyperText Markup Language – Japanese\" lang=\"ja\" hreflang=\"ja\" data-title=\"HyperText Markup Language\" data-language-autonym=\"日本語\" data-language-local-name=\"Japanese\" class=\"interlanguage-link-target\"><span>日本語</span></a></li><li class=\"interlanguage-link interwiki-ce mw-list-item\"><a href=\"https://ce.wikipedia.org/wiki/HTML\" title=\"HTML – Chechen\" lang=\"ce\" hreflang=\"ce\" data-title=\"HTML\" data-language-autonym=\"Нохчийн\" data-language-local-name=\"Chechen\" class=\"interlanguage-link-target\"><span>Нохчийн</span></a></li><li class=\"interlanguage-link interwiki-frr mw-list-item\"><a href=\"https://frr.wikipedia.org/wiki/HTML\" title=\"HTML – Northern Frisian\" lang=\"frr\" hreflang=\"frr\" data-title=\"HTML\" data-language-autonym=\"Nordfriisk\" data-language-local-name=\"Northern Frisian\" class=\"interlanguage-link-target\"><span>Nordfriisk</span></a></li><li class=\"interlanguage-link interwiki-no mw-list-item\"><a href=\"https://no.wikipedia.org/wiki/HTML\" title=\"HTML – Norwegian Bokmål\" lang=\"nb\" hreflang=\"nb\" data-title=\"HTML\" data-language-autonym=\"Norsk bokmål\" data-language-local-name=\"Norwegian Bokmål\" class=\"interlanguage-link-target\"><span>Norsk bokmål</span></a></li><li class=\"interlanguage-link interwiki-nn mw-list-item\"><a href=\"https://nn.wikipedia.org/wiki/HTML\" title=\"HTML – Norwegian Nynorsk\" lang=\"nn\" hreflang=\"nn\" data-title=\"HTML\" data-language-autonym=\"Norsk nynorsk\" data-language-local-name=\"Norwegian Nynorsk\" class=\"interlanguage-link-target\"><span>Norsk nynorsk</span></a></li><li class=\"interlanguage-link interwiki-mhr mw-list-item\"><a href=\"https://mhr.wikipedia.org/wiki/HTML\" title=\"HTML – Eastern Mari\" lang=\"mhr\" hreflang=\"mhr\" data-title=\"HTML\" data-language-autonym=\"Олык марий\" data-language-local-name=\"Eastern Mari\" class=\"interlanguage-link-target\"><span>Олык марий</span></a></li><li class=\"interlanguage-link interwiki-uz mw-list-item\"><a href=\"https://uz.wikipedia.org/wiki/HTML\" title=\"HTML – Uzbek\" lang=\"uz\" hreflang=\"uz\" data-title=\"HTML\" data-language-autonym=\"Oʻzbekcha / ўзбекча\" data-language-local-name=\"Uzbek\" class=\"interlanguage-link-target\"><span>Oʻzbekcha / ўзбекча</span></a></li><li class=\"interlanguage-link interwiki-pa mw-list-item\"><a href=\"https://pa.wikipedia.org/wiki/%E0%A8%90%E0%A8%9A.%E0%A8%9F%E0%A9%80.%E0%A8%90%E0%A8%AE.%E0%A8%90%E0%A8%B2\" title=\"ਐਚ.ਟੀ.ਐਮ.ਐਲ – Punjabi\" lang=\"pa\" hreflang=\"pa\" data-title=\"ਐਚ.ਟੀ.ਐਮ.ਐਲ\" data-language-autonym=\"ਪੰਜਾਬੀ\" data-language-local-name=\"Punjabi\" class=\"interlanguage-link-target\"><span>ਪੰਜਾਬੀ</span></a></li><li class=\"interlanguage-link interwiki-pnb mw-list-item\"><a href=\"https://pnb.wikipedia.org/wiki/%D8%A7%DB%8C%DA%86_%D9%B9%DB%8C_%D8%A7%DB%8C%D9%85_%D8%A7%DB%8C%D9%84\" title=\"ایچ ٹی ایم ایل – Western Punjabi\" lang=\"pnb\" hreflang=\"pnb\" data-title=\"ایچ ٹی ایم ایل\" data-language-autonym=\"پنجابی\" data-language-local-name=\"Western Punjabi\" class=\"interlanguage-link-target\"><span>پنجابی</span></a></li><li class=\"interlanguage-link interwiki-blk mw-list-item\"><a href=\"https://blk.wikipedia.org/wiki/HTML\" title=\"HTML – Pa&#039;O\" lang=\"blk\" hreflang=\"blk\" data-title=\"HTML\" data-language-autonym=\"ပအိုဝ်ႏဘာႏသာႏ\" data-language-local-name=\"Pa&#039;O\" class=\"interlanguage-link-target\"><span>ပအိုဝ်ႏဘာႏသာႏ</span></a></li><li class=\"interlanguage-link interwiki-km mw-list-item\"><a href=\"https://km.wikipedia.org/wiki/HTML\" title=\"HTML – Khmer\" lang=\"km\" hreflang=\"km\" data-title=\"HTML\" data-language-autonym=\"ភាសាខ្មែរ\" data-language-local-name=\"Khmer\" class=\"interlanguage-link-target\"><span>ភាសាខ្មែរ</span></a></li><li class=\"interlanguage-link interwiki-pms mw-list-item\"><a href=\"https://pms.wikipedia.org/wiki/HTML\" title=\"HTML – Piedmontese\" lang=\"pms\" hreflang=\"pms\" data-title=\"HTML\" data-language-autonym=\"Piemontèis\" data-language-local-name=\"Piedmontese\" class=\"interlanguage-link-target\"><span>Piemontèis</span></a></li><li class=\"interlanguage-link interwiki-pl mw-list-item\"><a href=\"https://pl.wikipedia.org/wiki/HTML\" title=\"HTML – Polish\" lang=\"pl\" hreflang=\"pl\" data-title=\"HTML\" data-language-autonym=\"Polski\" data-language-local-name=\"Polish\" class=\"interlanguage-link-target\"><span>Polski</span></a></li><li class=\"interlanguage-link interwiki-pt mw-list-item\"><a href=\"https://pt.wikipedia.org/wiki/HTML\" title=\"HTML – Portuguese\" lang=\"pt\" hreflang=\"pt\" data-title=\"HTML\" data-language-autonym=\"Português\" data-language-local-name=\"Portuguese\" class=\"interlanguage-link-target\"><span>Português</span></a></li><li class=\"interlanguage-link interwiki-kaa mw-list-item\"><a href=\"https://kaa.wikipedia.org/wiki/HTML\" title=\"HTML – Kara-Kalpak\" lang=\"kaa\" hreflang=\"kaa\" data-title=\"HTML\" data-language-autonym=\"Qaraqalpaqsha\" data-language-local-name=\"Kara-Kalpak\" class=\"interlanguage-link-target\"><span>Qaraqalpaqsha</span></a></li><li class=\"interlanguage-link interwiki-ro mw-list-item\"><a href=\"https://ro.wikipedia.org/wiki/HTML\" title=\"HTML – Romanian\" lang=\"ro\" hreflang=\"ro\" data-title=\"HTML\" data-language-autonym=\"Română\" data-language-local-name=\"Romanian\" class=\"interlanguage-link-target\"><span>Română</span></a></li><li class=\"interlanguage-link interwiki-qu mw-list-item\"><a href=\"https://qu.wikipedia.org/wiki/HTML\" title=\"HTML – Quechua\" lang=\"qu\" hreflang=\"qu\" data-title=\"HTML\" data-language-autonym=\"Runa Simi\" data-language-local-name=\"Quechua\" class=\"interlanguage-link-target\"><span>Runa Simi</span></a></li><li class=\"interlanguage-link interwiki-rue mw-list-item\"><a href=\"https://rue.wikipedia.org/wiki/HTML\" title=\"HTML – Rusyn\" lang=\"rue\" hreflang=\"rue\" data-title=\"HTML\" data-language-autonym=\"Русиньскый\" data-language-local-name=\"Rusyn\" class=\"interlanguage-link-target\"><span>Русиньскый</span></a></li><li class=\"interlanguage-link interwiki-ru mw-list-item\"><a href=\"https://ru.wikipedia.org/wiki/HTML\" title=\"HTML – Russian\" lang=\"ru\" hreflang=\"ru\" data-title=\"HTML\" data-language-autonym=\"Русский\" data-language-local-name=\"Russian\" class=\"interlanguage-link-target\"><span>Русский</span></a></li><li class=\"interlanguage-link interwiki-sco mw-list-item\"><a href=\"https://sco.wikipedia.org/wiki/HTML\" title=\"HTML – Scots\" lang=\"sco\" hreflang=\"sco\" data-title=\"HTML\" data-language-autonym=\"Scots\" data-language-local-name=\"Scots\" class=\"interlanguage-link-target\"><span>Scots</span></a></li><li class=\"interlanguage-link interwiki-sq mw-list-item\"><a href=\"https://sq.wikipedia.org/wiki/HTML\" title=\"HTML – Albanian\" lang=\"sq\" hreflang=\"sq\" data-title=\"HTML\" data-language-autonym=\"Shqip\" data-language-local-name=\"Albanian\" class=\"interlanguage-link-target\"><span>Shqip</span></a></li><li class=\"interlanguage-link interwiki-si mw-list-item\"><a href=\"https://si.wikipedia.org/wiki/HTML\" title=\"HTML – Sinhala\" lang=\"si\" hreflang=\"si\" data-title=\"HTML\" data-language-autonym=\"සිංහල\" data-language-local-name=\"Sinhala\" class=\"interlanguage-link-target\"><span>සිංහල</span></a></li><li class=\"interlanguage-link interwiki-simple mw-list-item\"><a href=\"https://simple.wikipedia.org/wiki/HTML\" title=\"HTML – Simple English\" lang=\"en-simple\" hreflang=\"en-simple\" data-title=\"HTML\" data-language-autonym=\"Simple English\" data-language-local-name=\"Simple English\" class=\"interlanguage-link-target\"><span>Simple English</span></a></li><li class=\"interlanguage-link interwiki-sd mw-list-item\"><a href=\"https://sd.wikipedia.org/wiki/%D9%87%D8%A7%D8%A6%D9%BE%D8%B1_%D9%BD%D9%8A%DA%AA%D8%B3%D9%BD_%D9%85%D8%A7%D8%B1%DA%AA_%D8%A7%D9%BE_%D9%84%D9%8A%D9%86%DA%AF%D9%88%D9%8A%D8%AC\" title=\"هائپر ٽيڪسٽ مارڪ اپ لينگويج – Sindhi\" lang=\"sd\" hreflang=\"sd\" data-title=\"هائپر ٽيڪسٽ مارڪ اپ لينگويج\" data-language-autonym=\"سنڌي\" data-language-local-name=\"Sindhi\" class=\"interlanguage-link-target\"><span>سنڌي</span></a></li><li class=\"interlanguage-link interwiki-sk mw-list-item\"><a href=\"https://sk.wikipedia.org/wiki/Hypertextov%C3%BD_zna%C4%8Dkov%C3%BD_jazyk\" title=\"Hypertextový značkový jazyk – Slovak\" lang=\"sk\" hreflang=\"sk\" data-title=\"Hypertextový značkový jazyk\" data-language-autonym=\"Slovenčina\" data-language-local-name=\"Slovak\" class=\"interlanguage-link-target\"><span>Slovenčina</span></a></li><li class=\"interlanguage-link interwiki-sl mw-list-item\"><a href=\"https://sl.wikipedia.org/wiki/HTML\" title=\"HTML – Slovenian\" lang=\"sl\" hreflang=\"sl\" data-title=\"HTML\" data-language-autonym=\"Slovenščina\" data-language-local-name=\"Slovenian\" class=\"interlanguage-link-target\"><span>Slovenščina</span></a></li><li class=\"interlanguage-link interwiki-so mw-list-item\"><a href=\"https://so.wikipedia.org/wiki/HTML\" title=\"HTML – Somali\" lang=\"so\" hreflang=\"so\" data-title=\"HTML\" data-language-autonym=\"Soomaaliga\" data-language-local-name=\"Somali\" class=\"interlanguage-link-target\"><span>Soomaaliga</span></a></li><li class=\"interlanguage-link interwiki-ckb mw-list-item\"><a href=\"https://ckb.wikipedia.org/wiki/%D8%A6%DB%8E%DA%86_%D8%AA%DB%8C_%D8%A6%DB%8E%D9%85_%D8%A6%DB%8E%DA%B5\" title=\"ئێچ تی ئێم ئێڵ – Central Kurdish\" lang=\"ckb\" hreflang=\"ckb\" data-title=\"ئێچ تی ئێم ئێڵ\" data-language-autonym=\"کوردی\" data-language-local-name=\"Central Kurdish\" class=\"interlanguage-link-target\"><span>کوردی</span></a></li><li class=\"interlanguage-link interwiki-sr mw-list-item\"><a href=\"https://sr.wikipedia.org/wiki/HTML\" title=\"HTML – Serbian\" lang=\"sr\" hreflang=\"sr\" data-title=\"HTML\" data-language-autonym=\"Српски / srpski\" data-language-local-name=\"Serbian\" class=\"interlanguage-link-target\"><span>Српски / srpski</span></a></li><li class=\"interlanguage-link interwiki-sh mw-list-item\"><a href=\"https://sh.wikipedia.org/wiki/HTML\" title=\"HTML – Serbo-Croatian\" lang=\"sh\" hreflang=\"sh\" data-title=\"HTML\" data-language-autonym=\"Srpskohrvatski / српскохрватски\" data-language-local-name=\"Serbo-Croatian\" class=\"interlanguage-link-target\"><span>Srpskohrvatski / српскохрватски</span></a></li><li class=\"interlanguage-link interwiki-fi mw-list-item\"><a href=\"https://fi.wikipedia.org/wiki/HTML\" title=\"HTML – Finnish\" lang=\"fi\" hreflang=\"fi\" data-title=\"HTML\" data-language-autonym=\"Suomi\" data-language-local-name=\"Finnish\" class=\"interlanguage-link-target\"><span>Suomi</span></a></li><li class=\"interlanguage-link interwiki-sv mw-list-item\"><a href=\"https://sv.wikipedia.org/wiki/HTML\" title=\"HTML – Swedish\" lang=\"sv\" hreflang=\"sv\" data-title=\"HTML\" data-language-autonym=\"Svenska\" data-language-local-name=\"Swedish\" class=\"interlanguage-link-target\"><span>Svenska</span></a></li><li class=\"interlanguage-link interwiki-tl mw-list-item\"><a href=\"https://tl.wikipedia.org/wiki/HTML\" title=\"HTML – Tagalog\" lang=\"tl\" hreflang=\"tl\" data-title=\"HTML\" data-language-autonym=\"Tagalog\" data-language-local-name=\"Tagalog\" class=\"interlanguage-link-target\"><span>Tagalog</span></a></li><li class=\"interlanguage-link interwiki-ta mw-list-item\"><a href=\"https://ta.wikipedia.org/wiki/%E0%AE%AE%E0%AF%80%E0%AE%AF%E0%AF%81%E0%AE%B0%E0%AF%88%E0%AE%95%E0%AF%8D_%E0%AE%95%E0%AF%81%E0%AE%B1%E0%AE%BF%E0%AE%AF%E0%AE%BF%E0%AE%9F%E0%AF%81_%E0%AE%AE%E0%AF%8A%E0%AE%B4%E0%AE%BF\" title=\"மீயுரைக் குறியிடு மொழி – Tamil\" lang=\"ta\" hreflang=\"ta\" data-title=\"மீயுரைக் குறியிடு மொழி\" data-language-autonym=\"தமிழ்\" data-language-local-name=\"Tamil\" class=\"interlanguage-link-target\"><span>தமிழ்</span></a></li><li class=\"interlanguage-link interwiki-shn mw-list-item\"><a href=\"https://shn.wikipedia.org/wiki/HTML\" title=\"HTML – Shan\" lang=\"shn\" hreflang=\"shn\" data-title=\"HTML\" data-language-autonym=\"တႆး\" data-language-local-name=\"Shan\" class=\"interlanguage-link-target\"><span>တႆး</span></a></li><li class=\"interlanguage-link interwiki-te mw-list-item\"><a href=\"https://te.wikipedia.org/wiki/HTML\" title=\"HTML – Telugu\" lang=\"te\" hreflang=\"te\" data-title=\"HTML\" data-language-autonym=\"తెలుగు\" data-language-local-name=\"Telugu\" class=\"interlanguage-link-target\"><span>తెలుగు</span></a></li><li class=\"interlanguage-link interwiki-th mw-list-item\"><a href=\"https://th.wikipedia.org/wiki/%E0%B9%80%E0%B8%AD%E0%B8%8A%E0%B8%97%E0%B8%B5%E0%B9%80%E0%B8%AD%E0%B9%87%E0%B8%A1%E0%B9%81%E0%B8%AD%E0%B8%A5\" title=\"เอชทีเอ็มแอล – Thai\" lang=\"th\" hreflang=\"th\" data-title=\"เอชทีเอ็มแอล\" data-language-autonym=\"ไทย\" data-language-local-name=\"Thai\" class=\"interlanguage-link-target\"><span>ไทย</span></a></li><li class=\"interlanguage-link interwiki-tg mw-list-item\"><a href=\"https://tg.wikipedia.org/wiki/HTML\" title=\"HTML – Tajik\" lang=\"tg\" hreflang=\"tg\" data-title=\"HTML\" data-language-autonym=\"Тоҷикӣ\" data-language-local-name=\"Tajik\" class=\"interlanguage-link-target\"><span>Тоҷикӣ</span></a></li><li class=\"interlanguage-link interwiki-tr mw-list-item\"><a href=\"https://tr.wikipedia.org/wiki/HTML\" title=\"HTML – Turkish\" lang=\"tr\" hreflang=\"tr\" data-title=\"HTML\" data-language-autonym=\"Türkçe\" data-language-local-name=\"Turkish\" class=\"interlanguage-link-target\"><span>Türkçe</span></a></li><li class=\"interlanguage-link interwiki-tk mw-list-item\"><a href=\"https://tk.wikipedia.org/wiki/HTML\" title=\"HTML – Turkmen\" lang=\"tk\" hreflang=\"tk\" data-title=\"HTML\" data-language-autonym=\"Türkmençe\" data-language-local-name=\"Turkmen\" class=\"interlanguage-link-target\"><span>Türkmençe</span></a></li><li class=\"interlanguage-link interwiki-uk mw-list-item\"><a href=\"https://uk.wikipedia.org/wiki/HTML\" title=\"HTML – Ukrainian\" lang=\"uk\" hreflang=\"uk\" data-title=\"HTML\" data-language-autonym=\"Українська\" data-language-local-name=\"Ukrainian\" class=\"interlanguage-link-target\"><span>Українська</span></a></li><li class=\"interlanguage-link interwiki-ur mw-list-item\"><a href=\"https://ur.wikipedia.org/wiki/%D8%A7%DB%8C%DA%86_%D9%B9%DB%8C_%D8%A7%DB%8C%D9%85_%D8%A7%DB%8C%D9%84\" title=\"ایچ ٹی ایم ایل – Urdu\" lang=\"ur\" hreflang=\"ur\" data-title=\"ایچ ٹی ایم ایل\" data-language-autonym=\"اردو\" data-language-local-name=\"Urdu\" class=\"interlanguage-link-target\"><span>اردو</span></a></li><li class=\"interlanguage-link interwiki-vec mw-list-item\"><a href=\"https://vec.wikipedia.org/wiki/HTML\" title=\"HTML – Venetian\" lang=\"vec\" hreflang=\"vec\" data-title=\"HTML\" data-language-autonym=\"Vèneto\" data-language-local-name=\"Venetian\" class=\"interlanguage-link-target\"><span>Vèneto</span></a></li><li class=\"interlanguage-link interwiki-vi mw-list-item\"><a href=\"https://vi.wikipedia.org/wiki/HTML\" title=\"HTML – Vietnamese\" lang=\"vi\" hreflang=\"vi\" data-title=\"HTML\" data-language-autonym=\"Tiếng Việt\" data-language-local-name=\"Vietnamese\" class=\"interlanguage-link-target\"><span>Tiếng Việt</span></a></li><li class=\"interlanguage-link interwiki-war mw-list-item\"><a href=\"https://war.wikipedia.org/wiki/HTML\" title=\"HTML – Waray\" lang=\"war\" hreflang=\"war\" data-title=\"HTML\" data-language-autonym=\"Winaray\" data-language-local-name=\"Waray\" class=\"interlanguage-link-target\"><span>Winaray</span></a></li><li class=\"interlanguage-link interwiki-wo mw-list-item\"><a href=\"https://wo.wikipedia.org/wiki/HTML\" title=\"HTML – Wolof\" lang=\"wo\" hreflang=\"wo\" data-title=\"HTML\" data-language-autonym=\"Wolof\" data-language-local-name=\"Wolof\" class=\"interlanguage-link-target\"><span>Wolof</span></a></li><li class=\"interlanguage-link interwiki-wuu mw-list-item\"><a href=\"https://wuu.wikipedia.org/wiki/HTML\" title=\"HTML – Wu\" lang=\"wuu\" hreflang=\"wuu\" data-title=\"HTML\" data-language-autonym=\"吴语\" data-language-local-name=\"Wu\" class=\"interlanguage-link-target\"><span>吴语</span></a></li><li class=\"interlanguage-link interwiki-yi mw-list-item\"><a href=\"https://yi.wikipedia.org/wiki/HTML\" title=\"HTML – Yiddish\" lang=\"yi\" hreflang=\"yi\" data-title=\"HTML\" data-language-autonym=\"ייִדיש\" data-language-local-name=\"Yiddish\" class=\"interlanguage-link-target\"><span>ייִדיש</span></a></li><li class=\"interlanguage-link interwiki-yo mw-list-item\"><a href=\"https://yo.wikipedia.org/wiki/HTML\" title=\"HTML – Yoruba\" lang=\"yo\" hreflang=\"yo\" data-title=\"HTML\" data-language-autonym=\"Yorùbá\" data-language-local-name=\"Yoruba\" class=\"interlanguage-link-target\"><span>Yorùbá</span></a></li><li class=\"interlanguage-link interwiki-zh-yue mw-list-item\"><a href=\"https://zh-yue.wikipedia.org/wiki/HTML\" title=\"HTML – Cantonese\" lang=\"yue\" hreflang=\"yue\" data-title=\"HTML\" data-language-autonym=\"粵語\" data-language-local-name=\"Cantonese\" class=\"interlanguage-link-target\"><span>粵語</span></a></li><li class=\"interlanguage-link interwiki-bat-smg mw-list-item\"><a href=\"https://bat-smg.wikipedia.org/wiki/HTML\" title=\"HTML – Samogitian\" lang=\"sgs\" hreflang=\"sgs\" data-title=\"HTML\" data-language-autonym=\"Žemaitėška\" data-language-local-name=\"Samogitian\" class=\"interlanguage-link-target\"><span>Žemaitėška</span></a></li><li class=\"interlanguage-link interwiki-zh mw-list-item\"><a href=\"https://zh.wikipedia.org/wiki/HTML\" title=\"HTML – Chinese\" lang=\"zh\" hreflang=\"zh\" data-title=\"HTML\" data-language-autonym=\"中文\" data-language-local-name=\"Chinese\" class=\"interlanguage-link-target\"><span>中文</span></a></li>\n\t\t\t</ul>\n\t\t\t<div class=\"after-portlet after-portlet-lang\"><span class=\"wb-langlinks-edit wb-langlinks-link\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q8811#sitelinks-wikipedia\" title=\"Edit interlanguage links\" class=\"wbc-editpage\">Edit links</a></span></div>\n\t\t</div>\n\n\t</div>\n</div>\n</header>\n\t\t\t\t<div class=\"vector-page-toolbar vector-feature-custom-font-size-clientpref--excluded\">\n\t\t\t\t\t<div class=\"vector-page-toolbar-container\">\n\t\t\t\t\t\t<div id=\"left-navigation\">\n\t\t\t\t\t\t\t<nav aria-label=\"Namespaces\">\n\n<div id=\"p-associated-pages\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-associated-pages\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-nstab-main\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/HTML\" title=\"View the content page [c]\" accesskey=\"c\"><span>Article</span></a></li><li id=\"ca-talk\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/wiki/Talk:HTML\" rel=\"discussion\" title=\"Discuss improvements to the content page [t]\" accesskey=\"t\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"vector-variants-dropdown\" class=\"vector-dropdown emptyPortlet\"  >\n\t<input type=\"checkbox\" id=\"vector-variants-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-variants-dropdown\" class=\"vector-dropdown-checkbox \" aria-label=\"Change language variant\"   >\n\t<label id=\"vector-variants-dropdown-label\" for=\"vector-variants-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">English</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-variants\" class=\"vector-menu mw-portlet mw-portlet-variants emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div id=\"right-navigation\" class=\"vector-collapsible\">\n\t\t\t\t\t\t\t<nav aria-label=\"Views\">\n\n<div id=\"p-views\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-views\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-view\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/HTML\"><span>Read</span></a></li><li id=\"ca-viewsource\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;action=edit\" title=\"This page is protected.&#10;You can view its source [e]\" accesskey=\"e\"><span>View source</span></a></li><li id=\"ca-history\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;action=history\" title=\"Past revisions of this page [h]\" accesskey=\"h\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\n<div id=\"vector-page-tools-dropdown\" class=\"vector-dropdown vector-page-tools-dropdown\"  >\n\t<input type=\"checkbox\" id=\"vector-page-tools-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-tools-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Tools\"  >\n\t<label id=\"vector-page-tools-dropdown-label\" for=\"vector-page-tools-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">Tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t\t\t<div id=\"vector-page-tools-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-page-tools\" class=\"vector-page-tools vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-page-tools-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"page-tools-pinned\"\n\tdata-pinnable-element-id=\"vector-page-tools\"\n\tdata-pinned-container-id=\"vector-page-tools-pinned-container\"\n\tdata-unpinned-container-id=\"vector-page-tools-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Tools</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-page-tools.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-page-tools.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-cactions\" class=\"vector-menu mw-portlet mw-portlet-cactions emptyPortlet vector-has-collapsible-items\"  title=\"More options\" >\n\t<div class=\"vector-menu-heading\">\n\t\tActions\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-more-view\" class=\"selected vector-more-collapsible-item mw-list-item\"><a href=\"/wiki/HTML\"><span>Read</span></a></li><li id=\"ca-more-viewsource\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;action=edit\"><span>View source</span></a></li><li id=\"ca-more-history\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;action=history\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-tb\" class=\"vector-menu mw-portlet mw-portlet-tb\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tGeneral\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-whatlinkshere\" class=\"mw-list-item\"><a href=\"/wiki/Special:WhatLinksHere/HTML\" title=\"List of all English Wikipedia pages containing links to this page [j]\" accesskey=\"j\"><span>What links here</span></a></li><li id=\"t-recentchangeslinked\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChangesLinked/HTML\" rel=\"nofollow\" title=\"Recent changes in pages linked from this page [k]\" accesskey=\"k\"><span>Related changes</span></a></li><li id=\"t-upload\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:File_Upload_Wizard\" title=\"Upload files [u]\" accesskey=\"u\"><span>Upload file</span></a></li><li id=\"t-permalink\" class=\"mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;oldid=1314044013\" title=\"Permanent link to this revision of this page\"><span>Permanent link</span></a></li><li id=\"t-info\" class=\"mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;action=info\" title=\"More information about this page\"><span>Page information</span></a></li><li id=\"t-cite\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:CiteThisPage&amp;page=HTML&amp;id=1314044013&amp;wpFormIdentifier=titleform\" title=\"Information on how to cite this page\"><span>Cite this page</span></a></li><li id=\"t-urlshortener\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:UrlShortener&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FHTML\"><span>Get shortened URL</span></a></li><li id=\"t-urlshortener-qrcode\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:QrCode&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FHTML\"><span>Download QR code</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-coll-print_export\" class=\"vector-menu mw-portlet mw-portlet-coll-print_export\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPrint/export\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"coll-download-as-rl\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:DownloadAsPdf&amp;page=HTML&amp;action=show-download-screen\" title=\"Download this page as a PDF file\"><span>Download as PDF</span></a></li><li id=\"t-print\" class=\"mw-list-item\"><a href=\"/w/index.php?title=HTML&amp;printable=yes\" title=\"Printable version of this page [p]\" accesskey=\"p\"><span>Printable version</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-wikibase-otherprojects\" class=\"vector-menu mw-portlet mw-portlet-wikibase-otherprojects\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tIn other projects\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li class=\"wb-otherproject-link wb-otherproject-commons mw-list-item\"><a href=\"https://commons.wikimedia.org/wiki/HTML\" hreflang=\"en\"><span>Wikimedia Commons</span></a></li><li class=\"wb-otherproject-link wb-otherproject-mediawiki mw-list-item\"><a href=\"https://www.mediawiki.org/wiki/HTML\" hreflang=\"en\"><span>MediaWiki</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikibooks mw-list-item\"><a href=\"https://en.wikibooks.org/wiki/HyperText_Markup_Language\" hreflang=\"en\"><span>Wikibooks</span></a></li><li class=\"wb-otherproject-link wb-otherproject-wikiversity mw-list-item\"><a href=\"https://en.wikiversity.org/wiki/HTML\" hreflang=\"en\"><span>Wikiversity</span></a></li><li id=\"t-wikibase\" class=\"wb-otherproject-link wb-otherproject-wikibase-dataitem mw-list-item\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q8811\" title=\"Structured data on this page hosted by Wikidata [g]\" accesskey=\"g\"><span>Wikidata item</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"vector-column-end no-font-mode-scale\">\n\t\t\t\t\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\t\t\t\t\t\t\t<div id=\"vector-page-tools-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\t\t\t\t\t\t\t<div id=\"vector-appearance-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t<div id=\"vector-appearance\" class=\"vector-appearance vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-appearance-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"appearance-pinned\"\n\tdata-pinnable-element-id=\"vector-appearance\"\n\tdata-pinned-container-id=\"vector-appearance-pinned-container\"\n\tdata-unpinned-container-id=\"vector-appearance-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Appearance</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-appearance.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-appearance.unpin\">hide</button>\n</div>\n\n\n</div>\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"bodyContent\" class=\"vector-body\" aria-labelledby=\"firstHeading\" data-mw-ve-target-container>\n\t\t\t\t\t<div class=\"vector-body-before-content\">\n\t\t\t\t\t\t\t<div class=\"mw-indicators\">\n\t\t<div id=\"mw-indicator-pp-default\" class=\"mw-indicator\"><div class=\"mw-parser-output\"><span typeof=\"mw:File\"><a href=\"/wiki/Wikipedia:Protection_policy#semi\" title=\"This article is semi-protected due to vandalism\"><img alt=\"Page semi-protected\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/1/1b/Semi-protection-shackle.svg/20px-Semi-protection-shackle.svg.png\" decoding=\"async\" width=\"20\" height=\"20\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/1/1b/Semi-protection-shackle.svg/40px-Semi-protection-shackle.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"512\" /></a></span></div></div>\n\t\t</div>\n\n\t\t\t\t\t\t<div id=\"siteSub\" class=\"noprint\">From Wikipedia, the free encyclopedia</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"contentSub\"><div id=\"mw-content-subtitle\"></div></div>\n\n\n\t\t\t\t\t<div id=\"mw-content-text\" class=\"mw-body-content\"><div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><div class=\"shortdescription nomobile noexcerpt noprint searchaux\" style=\"display:none\">Markup language for documents</div>\n<style data-mw-deduplicate=\"TemplateStyles:r1236090951\">.mw-parser-output .hatnote{font-style:italic}.mw-parser-output div.hatnote{padding-left:1.6em;margin-bottom:0.5em}.mw-parser-output .hatnote i{font-style:normal}.mw-parser-output .hatnote+link+.hatnote{margin-top:-0.5em}@media print{body.ns-0 .mw-parser-output .hatnote{display:none!important}}</style><div role=\"note\" class=\"hatnote navigation-not-searchable\">\".htm\" and \".html\" redirect here. For other uses, see <a href=\"/wiki/HTM_(disambiguation)\" class=\"mw-redirect mw-disambig\" title=\"HTM (disambiguation)\">HTM</a>.</div>\n<p class=\"mw-empty-elt\">\n</p>\n<style data-mw-deduplicate=\"TemplateStyles:r1295905060\">.mw-parser-output .infobox-subbox{padding:0;border:none;margin:-3px;width:auto;min-width:100%;font-size:100%;clear:none;float:none;background-color:transparent}.mw-parser-output .infobox-3cols-child{margin:auto}.mw-parser-output .infobox .navbar{font-size:100%}@media screen{html.skin-theme-clientpref-night .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .infobox-full-data:not(.notheme)>div:not(.notheme)[style]{background:#1f1f23!important;color:#f8f9fa}}@media(min-width:640px){body.skin--responsive .mw-parser-output .infobox-table{display:table!important}body.skin--responsive .mw-parser-output .infobox-table>caption{display:table-caption!important}body.skin--responsive .mw-parser-output .infobox-table>tbody{display:table-row-group}body.skin--responsive .mw-parser-output .infobox-table th,body.skin--responsive .mw-parser-output .infobox-table td{padding-left:inherit;padding-right:inherit}}</style><table class=\"infobox\"><tbody><tr><th colspan=\"2\" class=\"infobox-above\" style=\"padding-bottom: 0.15em;background-color:#e0e0e0;\">HTML</th></tr><tr><td colspan=\"2\" class=\"infobox-image\"><span typeof=\"mw:File\"><a href=\"/wiki/File:HTML5_logo_and_wordmark.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/250px-HTML5_logo_and_wordmark.svg.png\" decoding=\"async\" width=\"150\" height=\"150\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/330px-HTML5_logo_and_wordmark.svg.png 2x\" data-file-width=\"512\" data-file-height=\"512\" /></a></span><div class=\"infobox-caption\">Official logo of <a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a><sup id=\"cite_ref-1\" class=\"reference\"><a href=\"#cite_note-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Filename_extension\" title=\"Filename extension\">Filename extension</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><style data-mw-deduplicate=\"TemplateStyles:r1126788409\">.mw-parser-output .plainlist ol,.mw-parser-output .plainlist ul{line-height:inherit;list-style:none;margin:0;padding:0}.mw-parser-output .plainlist ol li,.mw-parser-output .plainlist ul li{margin-bottom:0}</style><div class=\"plainlist\"><ul><li><code>.html</code></li><li><code>.htm</code></li></ul></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Media_type\" title=\"Media type\">Internet media&#160;type</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><style data-mw-deduplicate=\"TemplateStyles:r886049734\">.mw-parser-output .monospaced{font-family:monospace,monospace}</style><div class=\"monospaced\">\ntext/html</div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Resource_fork#Types\" title=\"Resource fork\">Type code</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\">TEXT</td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Uniform_Type_Identifier\" title=\"Uniform Type Identifier\">Uniform Type Identifier&#160;(UTI)</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\">public.html</td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Developed&#160;by</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" /><div class=\"plainlist\"><ul><li><a href=\"/wiki/WHATWG\" title=\"WHATWG\">WHATWG</a></li><li><a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a> (W3C; formerly)</li></ul></div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Initial release</th><td class=\"infobox-data\" style=\"line-height: 1.35;\">1993<span class=\"noprint\">&#59;&#32;32&#160;years ago</span><span style=\"display:none\">&#160;(<span class=\"bday dtstart published updated\">1993</span>)</span></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Software_release_life_cycle\" title=\"Software release life cycle\">Latest release</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><div style=\"display: inline-block; line-height: 1.2em; padding: .1em 0;\"><a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/multipage/\">Living Standard</a> </div></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Type of format</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><a href=\"/wiki/Document_file_format\" title=\"Document file format\">Document file format</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><a href=\"/wiki/Container_format\" title=\"Container format\">Container&#160;for</a></th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><a href=\"/wiki/HTML_elements\" class=\"mw-redirect\" title=\"HTML elements\">HTML elements</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Contained&#160;by</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><a href=\"/wiki/Web_browser\" title=\"Web browser\">Web browser</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Extended&#160;from</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><a href=\"/wiki/Standard_Generalized_Markup_Language\" title=\"Standard Generalized Markup Language\">SGML</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Extended&#160;to</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a></td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\"><span class=\"nowrap\"><a href=\"/wiki/Open_file_format\" title=\"Open file format\">Open format</a>?</span></th><td class=\"infobox-data\" style=\"line-height: 1.35;\">Yes</td></tr><tr><th scope=\"row\" class=\"infobox-label\" style=\"line-height: 1.2; padding-right: 0.65em;\">Website</th><td class=\"infobox-data\" style=\"line-height: 1.35;\"><span class=\"url\"><a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/\">html<wbr />.spec<wbr />.whatwg<wbr />.org</a></span></td></tr></tbody></table>\n<style data-mw-deduplicate=\"TemplateStyles:r1129693374\">.mw-parser-output .hlist dl,.mw-parser-output .hlist ol,.mw-parser-output .hlist ul{margin:0;padding:0}.mw-parser-output .hlist dd,.mw-parser-output .hlist dt,.mw-parser-output .hlist li{margin:0;display:inline}.mw-parser-output .hlist.inline,.mw-parser-output .hlist.inline dl,.mw-parser-output .hlist.inline ol,.mw-parser-output .hlist.inline ul,.mw-parser-output .hlist dl dl,.mw-parser-output .hlist dl ol,.mw-parser-output .hlist dl ul,.mw-parser-output .hlist ol dl,.mw-parser-output .hlist ol ol,.mw-parser-output .hlist ol ul,.mw-parser-output .hlist ul dl,.mw-parser-output .hlist ul ol,.mw-parser-output .hlist ul ul{display:inline}.mw-parser-output .hlist .mw-empty-li{display:none}.mw-parser-output .hlist dt::after{content:\": \"}.mw-parser-output .hlist dd::after,.mw-parser-output .hlist li::after{content:\" · \";font-weight:bold}.mw-parser-output .hlist dd:last-child::after,.mw-parser-output .hlist dt:last-child::after,.mw-parser-output .hlist li:last-child::after{content:none}.mw-parser-output .hlist dd dd:first-child::before,.mw-parser-output .hlist dd dt:first-child::before,.mw-parser-output .hlist dd li:first-child::before,.mw-parser-output .hlist dt dd:first-child::before,.mw-parser-output .hlist dt dt:first-child::before,.mw-parser-output .hlist dt li:first-child::before,.mw-parser-output .hlist li dd:first-child::before,.mw-parser-output .hlist li dt:first-child::before,.mw-parser-output .hlist li li:first-child::before{content:\" (\";font-weight:normal}.mw-parser-output .hlist dd dd:last-child::after,.mw-parser-output .hlist dd dt:last-child::after,.mw-parser-output .hlist dd li:last-child::after,.mw-parser-output .hlist dt dd:last-child::after,.mw-parser-output .hlist dt dt:last-child::after,.mw-parser-output .hlist dt li:last-child::after,.mw-parser-output .hlist li dd:last-child::after,.mw-parser-output .hlist li dt:last-child::after,.mw-parser-output .hlist li li:last-child::after{content:\")\";font-weight:normal}.mw-parser-output .hlist ol{counter-reset:listitem}.mw-parser-output .hlist ol>li{counter-increment:listitem}.mw-parser-output .hlist ol>li::before{content:\" \"counter(listitem)\"\\a0 \"}.mw-parser-output .hlist dd ol>li:first-child::before,.mw-parser-output .hlist dt ol>li:first-child::before,.mw-parser-output .hlist li ol>li:first-child::before{content:\" (\"counter(listitem)\"\\a0 \"}</style><style data-mw-deduplicate=\"TemplateStyles:r1246091330\">.mw-parser-output .sidebar{width:22em;float:right;clear:right;margin:0.5em 0 1em 1em;background:var(--background-color-neutral-subtle,#f8f9fa);border:1px solid var(--border-color-base,#a2a9b1);padding:0.2em;text-align:center;line-height:1.4em;font-size:88%;border-collapse:collapse;display:table}body.skin-minerva .mw-parser-output .sidebar{display:table!important;float:right!important;margin:0.5em 0 1em 1em!important}.mw-parser-output .sidebar-subgroup{width:100%;margin:0;border-spacing:0}.mw-parser-output .sidebar-left{float:left;clear:left;margin:0.5em 1em 1em 0}.mw-parser-output .sidebar-none{float:none;clear:both;margin:0.5em 1em 1em 0}.mw-parser-output .sidebar-outer-title{padding:0 0.4em 0.2em;font-size:125%;line-height:1.2em;font-weight:bold}.mw-parser-output .sidebar-top-image{padding:0.4em}.mw-parser-output .sidebar-top-caption,.mw-parser-output .sidebar-pretitle-with-top-image,.mw-parser-output .sidebar-caption{padding:0.2em 0.4em 0;line-height:1.2em}.mw-parser-output .sidebar-pretitle{padding:0.4em 0.4em 0;line-height:1.2em}.mw-parser-output .sidebar-title,.mw-parser-output .sidebar-title-with-pretitle{padding:0.2em 0.8em;font-size:145%;line-height:1.2em}.mw-parser-output .sidebar-title-with-pretitle{padding:0.1em 0.4em}.mw-parser-output .sidebar-image{padding:0.2em 0.4em 0.4em}.mw-parser-output .sidebar-heading{padding:0.1em 0.4em}.mw-parser-output .sidebar-content{padding:0 0.5em 0.4em}.mw-parser-output .sidebar-content-with-subgroup{padding:0.1em 0.4em 0.2em}.mw-parser-output .sidebar-above,.mw-parser-output .sidebar-below{padding:0.3em 0.8em;font-weight:bold}.mw-parser-output .sidebar-collapse .sidebar-above,.mw-parser-output .sidebar-collapse .sidebar-below{border-top:1px solid #aaa;border-bottom:1px solid #aaa}.mw-parser-output .sidebar-navbar{text-align:right;font-size:115%;padding:0 0.4em 0.4em}.mw-parser-output .sidebar-list-title{padding:0 0.4em;text-align:left;font-weight:bold;line-height:1.6em;font-size:105%}.mw-parser-output .sidebar-list-title-c{padding:0 0.4em;text-align:center;margin:0 3.3em}@media(max-width:640px){body.mediawiki .mw-parser-output .sidebar{width:100%!important;clear:both;float:none!important;margin-left:0!important;margin-right:0!important}}body.skin--responsive .mw-parser-output .sidebar a>img{max-width:none!important}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-list-title,html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle{background:transparent!important}html.skin-theme-clientpref-night .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle a{color:var(--color-progressive)!important}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-list-title,html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle{background:transparent!important}html.skin-theme-clientpref-os .mw-parser-output .sidebar:not(.notheme) .sidebar-title-with-pretitle a{color:var(--color-progressive)!important}}@media print{body.ns-0 .mw-parser-output .sidebar{display:none!important}}</style><table class=\"sidebar nomobile nowraplinks\"><tbody><tr><th class=\"sidebar-title\"><a class=\"mw-selflink selflink\">HTML</a></th></tr><tr><td class=\"sidebar-image\"><span class=\"noviewer notpageimage\" typeof=\"mw:File\"><a href=\"/wiki/File:HTML5_logo_and_wordmark.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/60px-HTML5_logo_and_wordmark.svg.png\" decoding=\"async\" width=\"50\" height=\"50\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/120px-HTML5_logo_and_wordmark.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"512\" /></a></span></td></tr><tr><th class=\"sidebar-heading\">\nHTML and variants</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/Dynamic_HTML\" title=\"Dynamic HTML\">Dynamic HTML</a></li>\n<li><a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a></li>\n<li><a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a>\n<ul><li><a href=\"/wiki/XHTML_Basic\" title=\"XHTML Basic\">Basic</a></li>\n<li><a href=\"/wiki/XHTML_Mobile_Profile\" title=\"XHTML Mobile Profile\">Mobile Profile</a></li></ul></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nHTML elements and attributes</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/HTML_element\" title=\"HTML element\">HTML element</a>\n<ul><li><a href=\"/wiki/Article_element\" title=\"Article element\">article</a></li>\n<li><a href=\"/wiki/HTML_audio\" title=\"HTML audio\">audio</a></li>\n<li><a href=\"/wiki/Blink_element\" title=\"Blink element\">blink</a></li>\n<li><a href=\"/wiki/Canvas_element\" title=\"Canvas element\">canvas</a></li>\n<li><a href=\"/wiki/Div_and_span\" title=\"Div and span\">div and span</a></li>\n<li><a href=\"/wiki/Marquee_element\" title=\"Marquee element\">marquee</a></li>\n<li><a href=\"/wiki/Meta_element\" title=\"Meta element\">meta</a></li>\n<li><a href=\"/wiki/HTML_video\" title=\"HTML video\">video</a></li></ul></li>\n<li><a href=\"/wiki/HTML_attribute\" title=\"HTML attribute\">HTML attribute</a>\n<ul><li><a href=\"/wiki/Alt_attribute\" title=\"Alt attribute\">alt attribute</a></li></ul></li>\n<li><a href=\"/wiki/Frame_(World_Wide_Web)\" title=\"Frame (World Wide Web)\">HTML frame</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nEditing</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/HTML_editor\" title=\"HTML editor\">HTML editor</a></li>\n<li><a href=\"/wiki/Text_editor\" title=\"Text editor\">Text editor</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nCharacter encodings and language</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/Character_encodings_in_HTML\" title=\"Character encodings in HTML\">Character encodings</a></li>\n<li><a href=\"/wiki/List_of_XML_and_HTML_character_entity_references\" title=\"List of XML and HTML character entity references\">Character entity references (amed characters)</a></li>\n<li><a href=\"/wiki/Unicode_and_HTML\" title=\"Unicode and HTML\">Unicode</a></li>\n<li><a href=\"/wiki/Language_code\" title=\"Language code\">Language code</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nDocument and browser models</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/Document_Object_Model\" title=\"Document Object Model\">Document Object Model</a></li>\n<li><a href=\"/wiki/Browser_Object_Model\" title=\"Browser Object Model\">Browser Object Model</a></li>\n<li><a href=\"/wiki/Style_sheet_(web_development)\" title=\"Style sheet (web development)\">Style sheets</a>\n<ul><li><a href=\"/wiki/CSS\" title=\"CSS\">CSS</a></li></ul></li>\n<li><a href=\"/wiki/Font_family_(HTML)\" title=\"Font family (HTML)\">Font family</a></li>\n<li><a href=\"/wiki/Web_colors\" title=\"Web colors\">Web colors</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nClient-side scripting and APIs</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>\n<ul><li><a href=\"/wiki/WebCL\" title=\"WebCL\">WebCL</a></li>\n<li><a href=\"/wiki/HTMX\" class=\"mw-redirect\" title=\"HTMX\">HTMX</a></li></ul></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nGraphics and Web3D technology</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/Web3D\" title=\"Web3D\">Web3D</a>\n<ul><li><a href=\"/wiki/WebGL\" title=\"WebGL\">WebGL</a></li>\n<li><a href=\"/wiki/WebGPU\" title=\"WebGPU\">WebGPU</a></li>\n<li><a href=\"/wiki/WebXR\" title=\"WebXR\">WebXR</a></li></ul></li>\n<li><a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">W3C</a>\n<ul><li><a href=\"/wiki/W3C_Markup_Validation_Service\" title=\"W3C Markup Validation Service\">Validator</a></li></ul></li>\n<li><a href=\"/wiki/WHATWG\" title=\"WHATWG\">WHATWG</a></li>\n<li><a href=\"/wiki/Quirks_mode\" title=\"Quirks mode\">Quirks mode</a></li>\n<li><a href=\"/wiki/Web_storage\" title=\"Web storage\">Web storage</a></li>\n<li><a href=\"/wiki/Browser_engine\" title=\"Browser engine\">Rendering engine</a></li></ul></td>\n</tr><tr><th class=\"sidebar-heading\">\nComparisons</th></tr><tr><td class=\"sidebar-content hlist\">\n<ul><li><a href=\"/wiki/Comparison_of_document_markup_languages\" title=\"Comparison of document markup languages\">Document markup languages</a></li>\n<li><a href=\"/wiki/Comparison_of_browser_engines\" title=\"Comparison of browser engines\">Comparison of browser engines</a></li></ul></td>\n</tr><tr><td class=\"sidebar-navbar\" style=\"border-top:1px solid #aaa;\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1239400231\">.mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}</style><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:HTML\" title=\"Template:HTML\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:HTML\" title=\"Template talk:HTML\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:HTML\" title=\"Special:EditPage/Template:HTML\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div></td></tr></tbody></table>\n<p><b>Hypertext Markup Language</b> (<b>HTML</b>) is the standard <a href=\"/wiki/Markup_language\" title=\"Markup language\">markup language</a><sup id=\"cite_ref-3\" class=\"reference\"><a href=\"#cite_note-3\"><span class=\"cite-bracket\">&#91;</span>a<span class=\"cite-bracket\">&#93;</span></a></sup> for documents designed to be displayed in a <a href=\"/wiki/Web_browser\" title=\"Web browser\">web browser</a>. It defines the content and structure of <a href=\"/wiki/Web_content\" title=\"Web content\">web content</a>. It is often assisted by technologies such as <a href=\"/wiki/Cascading_Style_Sheets\" class=\"mw-redirect\" title=\"Cascading Style Sheets\">Cascading Style Sheets</a> (CSS) and <a href=\"/wiki/Scripting_language\" title=\"Scripting language\">scripting languages</a> such as <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>.\n</p><p><a href=\"/wiki/Web_browser\" title=\"Web browser\">Web browsers</a> receive HTML documents from a <a href=\"/wiki/Web_server\" title=\"Web server\">web server</a> or from local storage and <a href=\"/wiki/Browser_engine\" title=\"Browser engine\">render</a> the documents into multimedia web pages. HTML describes the structure of a <a href=\"/wiki/Web_page\" title=\"Web page\">web page</a> <a href=\"/wiki/Semantic_Web\" title=\"Semantic Web\">semantically</a> and originally included cues for its appearance.\n</p><p><a href=\"/wiki/HTML_element\" title=\"HTML element\">HTML elements</a> are the building blocks of HTML pages. With HTML constructs, <a href=\"/wiki/HTML_element#Images_and_objects\" title=\"HTML element\">images</a> and other objects such as <a href=\"/wiki/Fieldset\" class=\"mw-redirect\" title=\"Fieldset\">interactive forms</a> may be embedded into the rendered page. HTML provides a means to create <a href=\"/wiki/Structured_document\" title=\"Structured document\">structured documents</a> by denoting structural <a href=\"/wiki/Semantics\" title=\"Semantics\">semantics</a> for text such as headings, paragraphs, lists, <a href=\"/wiki/Hyperlink\" title=\"Hyperlink\">links</a>, quotes, and other items. HTML elements are delineated by <i>tags</i>, written using <a href=\"/wiki/Bracket#Angle_brackets\" title=\"Bracket\">angle brackets</a>. Tags such as <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">img</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">input</span><span class=\"p\">&gt;</span></code> directly introduce content into the page. Other tags such as <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code> surround and provide information about document text and may include sub-element tags. <a href=\"/wiki/Web_browser\" title=\"Web browser\">Browsers</a> do not display the HTML tags, but use them to interpret the content of the page.\n</p><p>HTML can embed programs written in a <a href=\"/wiki/Scripting_language\" title=\"Scripting language\">scripting language</a> such as <a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>, which affects the behavior and content of web pages. The inclusion of CSS defines the look and layout of content. The <a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a> (W3C), former maintainer of the HTML and current maintainer of the CSS standards, has encouraged the use of <a href=\"/wiki/CSS\" title=\"CSS\">CSS</a> over explicit presentational HTML since 1997.<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=HTML&amp;action=edit\">&#91;update&#93;</a></sup><sup id=\"cite_ref-deprecated_4-0\" class=\"reference\"><a href=\"#cite_note-deprecated-4\"><span class=\"cite-bracket\">&#91;</span>3<span class=\"cite-bracket\">&#93;</span></a></sup> A form of HTML, known as <a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a>, is used to display video and audio, primarily using the <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">canvas</span><span class=\"p\">&gt;</span></code> element, together with JavaScript.\n</p>\n<meta property=\"mw:PageProp/toc\" />\n<div class=\"mw-heading mw-heading2\"><h2 id=\"History\">History</h2></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Development\">Development</h3></div>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:Tim_Berners-Lee_April_2009.jpg\" class=\"mw-file-description\"><img alt=\"Photograph of Tim Berners-Lee in April 2009\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Tim_Berners-Lee_April_2009.jpg/250px-Tim_Berners-Lee_April_2009.jpg\" decoding=\"async\" width=\"190\" height=\"262\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Tim_Berners-Lee_April_2009.jpg/330px-Tim_Berners-Lee_April_2009.jpg 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Tim_Berners-Lee_April_2009.jpg/500px-Tim_Berners-Lee_April_2009.jpg 2x\" data-file-width=\"1195\" data-file-height=\"1648\" /></a><figcaption><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Tim Berners-Lee</a> in April 2009</figcaption></figure>\n<p>In 1980, <a href=\"/wiki/Physicist\" title=\"Physicist\">physicist</a> <a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Tim Berners-Lee</a>, a contractor at <a href=\"/wiki/CERN\" title=\"CERN\">CERN</a>, proposed and prototyped <a href=\"/wiki/ENQUIRE\" title=\"ENQUIRE\">ENQUIRE</a>, a system for CERN researchers to use and share documents. In 1989, Berners-Lee wrote a memo proposing an <a href=\"/wiki/Internet\" title=\"Internet\">Internet</a>-based <a href=\"/wiki/Hypertext\" title=\"Hypertext\">hypertext</a> system.<sup id=\"cite_ref-5\" class=\"reference\"><a href=\"#cite_note-5\"><span class=\"cite-bracket\">&#91;</span>4<span class=\"cite-bracket\">&#93;</span></a></sup> Berners-Lee specified HTML and wrote the browser and server software in late 1990. That year, Berners-Lee and CERN <a href=\"/wiki/Data_system\" title=\"Data system\">data systems</a> engineer <a href=\"/wiki/Robert_Cailliau\" title=\"Robert Cailliau\">Robert Cailliau</a> collaborated on a joint request for funding, but the project was not formally adopted by CERN. In his personal notes of 1990, Berners-Lee listed \"some of the many areas in which hypertext is used\"; an <a href=\"/wiki/Encyclopedia\" title=\"Encyclopedia\">encyclopedia</a> is the first entry.<sup id=\"cite_ref-6\" class=\"reference\"><a href=\"#cite_note-6\"><span class=\"cite-bracket\">&#91;</span>5<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>The first publicly available description of HTML was a document called \"HTML Tags\",<sup id=\"cite_ref-7\" class=\"reference\"><a href=\"#cite_note-7\"><span class=\"cite-bracket\">&#91;</span>6<span class=\"cite-bracket\">&#93;</span></a></sup> first mentioned on the Internet by Tim Berners-Lee in late 1991.<sup id=\"cite_ref-tagshtml_8-0\" class=\"reference\"><a href=\"#cite_note-tagshtml-8\"><span class=\"cite-bracket\">&#91;</span>7<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-9\" class=\"reference\"><a href=\"#cite_note-9\"><span class=\"cite-bracket\">&#91;</span>8<span class=\"cite-bracket\">&#93;</span></a></sup> It describes 18 elements comprising the initial, relatively simple design of HTML. Except for the hyperlink tag, these were strongly influenced by <a href=\"/wiki/CERN_SGML\" title=\"CERN SGML\">CERN SGML</a>, an in-house <a href=\"/wiki/Standard_Generalized_Markup_Language\" title=\"Standard Generalized Markup Language\">Standard Generalized Markup Language</a> (SGML)-based documentation format at CERN. Eleven of these elements still exist in HTML 4.<sup id=\"cite_ref-10\" class=\"reference\"><a href=\"#cite_note-10\"><span class=\"cite-bracket\">&#91;</span>9<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>HTML is a <a href=\"/wiki/Markup_language\" title=\"Markup language\">markup language</a> that <a href=\"/wiki/Web_browser\" title=\"Web browser\">web browsers</a> use to interpret and <a href=\"/wiki/Typesetting\" title=\"Typesetting\">compose</a> text, images, and other material into visible or audible web pages. Default characteristics for every item of HTML markup are defined in the browser, and these characteristics can be altered or enhanced by the web page designer's additional use of <a href=\"/wiki/CSS\" title=\"CSS\">CSS</a>. Many of the text elements are mentioned in the 1988 ISO technical report TR 9537 <i>Techniques for using SGML</i>, which describes the features of early text formatting languages such as that used by the <a href=\"/wiki/TYPSET_and_RUNOFF\" title=\"TYPSET and RUNOFF\">RUNOFF command</a> developed in the early 1960s for the <a href=\"/wiki/Compatible_Time-Sharing_System\" title=\"Compatible Time-Sharing System\">CTSS</a> (Compatible Time-Sharing System) operating system. These formatting commands were derived from the commands used by typesetters to manually format documents. However, the SGML concept of generalized markup is based on elements (nested annotated ranges with attributes) rather than merely print effects, with separate structure and markup. HTML has been progressively moved in this direction with CSS.\n</p><p>Berners-Lee considered HTML to be an application of SGML. It was formally defined as such by the <a href=\"/wiki/Internet_Engineering_Task_Force\" title=\"Internet Engineering Task Force\">Internet Engineering Task Force</a> (IETF) with the mid-1993 publication of the first proposal for an HTML specification, the \"Hypertext Markup Language (HTML)\" Internet Draft by Berners-Lee and <a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Dan Connolly</a>, which included an SGML <a href=\"/wiki/Document_type_definition\" title=\"Document type definition\">Document type definition</a> to define the syntax.<sup id=\"cite_ref-11\" class=\"reference\"><a href=\"#cite_note-11\"><span class=\"cite-bracket\">&#91;</span>10<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-12\" class=\"reference\"><a href=\"#cite_note-12\"><span class=\"cite-bracket\">&#91;</span>11<span class=\"cite-bracket\">&#93;</span></a></sup> The draft expired after six months, but was notable for its acknowledgment of the <a href=\"/wiki/Mosaic_(web_browser)\" class=\"mw-redirect\" title=\"Mosaic (web browser)\">NCSA Mosaic</a> browser's custom tag for embedding in-line images, reflecting the IETF's philosophy of basing standards on successful prototypes. Similarly, <a href=\"/wiki/Dave_Raggett\" title=\"Dave Raggett\">Dave Raggett</a>'s competing Internet Draft, \"HTML+ (Hypertext Markup Format)\", from late 1993, suggested standardizing already-implemented features like tables and fill-out forms.<sup id=\"cite_ref-html+_13-0\" class=\"reference\"><a href=\"#cite_note-html+-13\"><span class=\"cite-bracket\">&#91;</span>12<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>After the HTML and HTML+ drafts expired in early 1994, the IETF created an HTML Working Group. In 1995, this working group completed \"HTML 2.0\", the first HTML specification intended to be treated as a standard against which future implementations should be based.<sup id=\"cite_ref-14\" class=\"reference\"><a href=\"#cite_note-14\"><span class=\"cite-bracket\">&#91;</span>13<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Further development under the auspices of the IETF was stalled by competing interests. Since 1996,<sup class=\"plainlinks noexcerpt noprint asof-tag update\" style=\"display:none;\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=HTML&amp;action=edit\">&#91;update&#93;</a></sup> the HTML specifications have been maintained, with input from commercial software vendors, by the <a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a> (W3C).<sup id=\"cite_ref-raggett_15-0\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup> In 2000, HTML became an international standard (<a href=\"/wiki/International_Organization_for_Standardization\" title=\"International Organization for Standardization\">ISO</a>/<a href=\"/wiki/International_Electrotechnical_Commission\" title=\"International Electrotechnical Commission\">IEC</a> 15445:2000). HTML 4.01 was published in late 1999, with further errata published through 2001. In 2004, development began on HTML5 in the <a href=\"/wiki/Web_Hypertext_Application_Technology_Working_Group\" class=\"mw-redirect\" title=\"Web Hypertext Application Technology Working Group\">Web Hypertext Application Technology Working Group</a> (WHATWG), which became a joint deliverable with the W3C in 2008, and was completed and standardized on 28 October 2014.<sup id=\"cite_ref-16\" class=\"reference\"><a href=\"#cite_note-16\"><span class=\"cite-bracket\">&#91;</span>15<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"HTML_version_timeline\">HTML version timeline</h3></div>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"HTML_2\">HTML 2</h4></div>\n<dl><dd><dl><dt>November 24, 1995</dt>\n<dd>HTML 2.0 was published as <style data-mw-deduplicate=\"TemplateStyles:r1238218222\">.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}.mw-parser-output .citation q{quotes:\"\\\"\"\"\\\"\"\"'\"\"'\"}.mw-parser-output .citation:target{background-color:rgba(0,127,255,0.133)}.mw-parser-output .id-lock-free.id-lock-free a{background:url(\"//upload.wikimedia.org/wikipedia/commons/6/65/Lock-green.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-limited.id-lock-limited a,.mw-parser-output .id-lock-registration.id-lock-registration a{background:url(\"//upload.wikimedia.org/wikipedia/commons/d/d6/Lock-gray-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-subscription.id-lock-subscription a{background:url(\"//upload.wikimedia.org/wikipedia/commons/a/aa/Lock-red-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .cs1-ws-icon a{background:url(\"//upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg\")right 0.1em center/12px no-repeat}body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-free a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-limited a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-registration a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-subscription a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .cs1-ws-icon a{background-size:contain;padding:0 1em 0 0}.mw-parser-output .cs1-code{color:inherit;background:inherit;border:none;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;color:var(--color-error,#d33)}.mw-parser-output .cs1-visible-error{color:var(--color-error,#d33)}.mw-parser-output .cs1-maint{display:none;color:#085;margin-left:0.3em}.mw-parser-output .cs1-kern-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right{padding-right:0.2em}.mw-parser-output .citation .mw-selflink{font-weight:inherit}@media screen{.mw-parser-output .cs1-format{font-size:95%}html.skin-theme-clientpref-night .mw-parser-output .cs1-maint{color:#18911f}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .cs1-maint{color:#18911f}}</style><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1866\">1866</a>. Supplemental <a href=\"/wiki/Request_for_Comments\" title=\"Request for Comments\">RFCs</a> added capabilities:\n<ul><li>November 25, 1995: <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1867\">1867</a> (form-based file upload)</li>\n<li>May 1996: <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1942\">1942</a> (tables)</li>\n<li>August 1996: <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1980\">1980</a> (client-side image maps)</li>\n<li>January 1997: <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc2070\">2070</a> (<a href=\"/wiki/Internationalization_and_localization\" title=\"Internationalization and localization\">internationalization</a>)</li></ul></dd></dl></dd></dl>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"HTML_3\">HTML 3</h4></div>\n<dl><dd><dl><dt>January 14, 1997</dt>\n<dd>HTML 3.2<sup id=\"cite_ref-17\" class=\"reference\"><a href=\"#cite_note-17\"><span class=\"cite-bracket\">&#91;</span>16<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a <a href=\"/wiki/W3C_Recommendation\" class=\"mw-redirect\" title=\"W3C Recommendation\">W3C Recommendation</a>. It was the first version developed and standardized exclusively by the W3C, as the IETF had closed its HTML Working Group on September 12, 1996.<sup id=\"cite_ref-18\" class=\"reference\"><a href=\"#cite_note-18\"><span class=\"cite-bracket\">&#91;</span>17<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dd>Initially code-named \"Wilbur\",<sup id=\"cite_ref-engelfriet_19-0\" class=\"reference\"><a href=\"#cite_note-engelfriet-19\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> HTML 3.2 dropped math formulas entirely, reconciled overlap among various proprietary extensions and adopted most of <a href=\"/wiki/Netscape\" title=\"Netscape\">Netscape</a>'s visual markup tags. Netscape's <a href=\"/wiki/Blink_element\" title=\"Blink element\">blink element</a> and <a href=\"/wiki/Microsoft\" title=\"Microsoft\">Microsoft</a>'s <a href=\"/wiki/Marquee_element\" title=\"Marquee element\">marquee element</a> were omitted due to a mutual agreement between the two companies.<sup id=\"cite_ref-raggett_15-1\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup> A markup for mathematical formulas similar to that of HTML was standardized 14 months later in <a href=\"/wiki/MathML\" title=\"MathML\">MathML</a>.</dd></dl></dd></dl>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"HTML_4\">HTML 4</h4></div>\n<dl><dd><dl><dt>December 18, 1997</dt>\n<dd>HTML 4.0<sup id=\"cite_ref-20\" class=\"reference\"><a href=\"#cite_note-20\"><span class=\"cite-bracket\">&#91;</span>19<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation. It offers three variations:</dd></dl>\n<dl><dt><ul><li>Strict, in which deprecated elements are forbidden</li>\n<li>Transitional, in which deprecated elements are allowed</li>\n<li>Frameset, in which mostly only <a href=\"/wiki/Framing_(World_Wide_Web)\" class=\"mw-redirect\" title=\"Framing (World Wide Web)\">frame</a> related elements are allowed.</li></ul></dt></dl></dd>\n<dd>Initially code-named \"Cougar\",<sup id=\"cite_ref-engelfriet_19-1\" class=\"reference\"><a href=\"#cite_note-engelfriet-19\"><span class=\"cite-bracket\">&#91;</span>18<span class=\"cite-bracket\">&#93;</span></a></sup> HTML 4.0 adopted many browser-specific element types and attributes, but also sought to phase out Netscape's visual markup features by marking them as <a href=\"/wiki/Deprecation\" title=\"Deprecation\">deprecated</a> in favor of style sheets. HTML 4 is an SGML application conforming to ISO 8879&#160;– SGML.<sup id=\"cite_ref-21\" class=\"reference\"><a href=\"#cite_note-21\"><span class=\"cite-bracket\">&#91;</span>20<span class=\"cite-bracket\">&#93;</span></a></sup>\n<dl><dt>April 24, 1998</dt></dl></dd>\n<dd>HTML 4.0<sup id=\"cite_ref-22\" class=\"reference\"><a href=\"#cite_note-22\"><span class=\"cite-bracket\">&#91;</span>21<span class=\"cite-bracket\">&#93;</span></a></sup> was reissued with minor edits without incrementing the version number.\n<dl><dt>December 24, 1999</dt>\n<dd>HTML 4.01<sup id=\"cite_ref-23\" class=\"reference\"><a href=\"#cite_note-23\"><span class=\"cite-bracket\">&#91;</span>22<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation. It offers the same three variations as HTML 4.0 and its last errata<sup id=\"cite_ref-24\" class=\"reference\"><a href=\"#cite_note-24\"><span class=\"cite-bracket\">&#91;</span>23<span class=\"cite-bracket\">&#93;</span></a></sup> were published on May 12, 2001.</dd>\n<dt>May 2000</dt>\n<dd>ISO/IEC 15445:2000<sup id=\"cite_ref-iso-html_25-0\" class=\"reference\"><a href=\"#cite_note-iso-html-25\"><span class=\"cite-bracket\">&#91;</span>24<span class=\"cite-bracket\">&#93;</span></a></sup> (\"<a href=\"/wiki/International_Organization_for_Standardization\" title=\"International Organization for Standardization\">ISO</a> HTML\", based on HTML 4.01 Strict) was published as an ISO/IEC international standard.<sup id=\"cite_ref-26\" class=\"reference\"><a href=\"#cite_note-26\"><span class=\"cite-bracket\">&#91;</span>25<span class=\"cite-bracket\">&#93;</span></a></sup> In the ISO, this standard is in the domain of the <a href=\"/wiki/ISO/IEC_JTC_1/SC_34\" title=\"ISO/IEC JTC 1/SC 34\">ISO/IEC JTC 1/SC 34</a> (ISO/IEC Joint Technical Committee 1, Subcommittee 34&#160;– Document description and processing languages).<sup id=\"cite_ref-iso-html_25-1\" class=\"reference\"><a href=\"#cite_note-iso-html-25\"><span class=\"cite-bracket\">&#91;</span>24<span class=\"cite-bracket\">&#93;</span></a></sup></dd></dl>\n<dl><dt><dl><dd>After HTML 4.01, there were no new versions of HTML for many years, as the development of the parallel, XML-based language XHTML occupied the W3C's HTML Working Group.</dd></dl></dt></dl></dd></dl>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"HTML_5\">HTML 5</h4></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a></div>\n<dl><dd><dl><dt>October 28, 2014</dt>\n<dd>HTML5<sup id=\"cite_ref-27\" class=\"reference\"><a href=\"#cite_note-27\"><span class=\"cite-bracket\">&#91;</span>26<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation.<sup id=\"cite_ref-28\" class=\"reference\"><a href=\"#cite_note-28\"><span class=\"cite-bracket\">&#91;</span>27<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>November 1, 2016</dt>\n<dd>HTML 5.1<sup id=\"cite_ref-29\" class=\"reference\"><a href=\"#cite_note-29\"><span class=\"cite-bracket\">&#91;</span>28<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation.<sup id=\"cite_ref-30\" class=\"reference\"><a href=\"#cite_note-30\"><span class=\"cite-bracket\">&#91;</span>29<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-31\" class=\"reference\"><a href=\"#cite_note-31\"><span class=\"cite-bracket\">&#91;</span>30<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>December 14, 2017</dt>\n<dd>HTML 5.2<sup id=\"cite_ref-32\" class=\"reference\"><a href=\"#cite_note-32\"><span class=\"cite-bracket\">&#91;</span>31<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation.<sup id=\"cite_ref-33\" class=\"reference\"><a href=\"#cite_note-33\"><span class=\"cite-bracket\">&#91;</span>32<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-34\" class=\"reference\"><a href=\"#cite_note-34\"><span class=\"cite-bracket\">&#91;</span>33<span class=\"cite-bracket\">&#93;</span></a></sup></dd></dl></dd></dl>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"HTML_draft_version_timeline\">HTML draft version timeline</h3></div>\n<dl><dt>October 1991</dt>\n<dd><i>HTML Tags</i>,<sup id=\"cite_ref-tagshtml_8-1\" class=\"reference\"><a href=\"#cite_note-tagshtml-8\"><span class=\"cite-bracket\">&#91;</span>7<span class=\"cite-bracket\">&#93;</span></a></sup> an informal CERN document listing 18 HTML tags, was first mentioned in public.</dd>\n<dt>June 1992</dt>\n<dd>First informal draft of the HTML DTD,<sup id=\"cite_ref-35\" class=\"reference\"><a href=\"#cite_note-35\"><span class=\"cite-bracket\">&#91;</span>34<span class=\"cite-bracket\">&#93;</span></a></sup> with seven subsequent revisions (July 15, August 6, August 18, November 17, November 19, November 20, November 22)<sup id=\"cite_ref-36\" class=\"reference\"><a href=\"#cite_note-36\"><span class=\"cite-bracket\">&#91;</span>35<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-37\" class=\"reference\"><a href=\"#cite_note-37\"><span class=\"cite-bracket\">&#91;</span>36<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-html11_38-0\" class=\"reference\"><a href=\"#cite_note-html11-38\"><span class=\"cite-bracket\">&#91;</span>37<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>November 1992</dt>\n<dd>HTML DTD 1.1 (the first with a version number, based on RCS revisions, which start with 1.1 rather than 1.0), an informal draft<sup id=\"cite_ref-html11_38-1\" class=\"reference\"><a href=\"#cite_note-html11-38\"><span class=\"cite-bracket\">&#91;</span>37<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>June 1993</dt>\n<dd>Hypertext Markup Language<sup id=\"cite_ref-39\" class=\"reference\"><a href=\"#cite_note-39\"><span class=\"cite-bracket\">&#91;</span>38<span class=\"cite-bracket\">&#93;</span></a></sup> was published by the <a href=\"/wiki/Internet_Engineering_Task_Force\" title=\"Internet Engineering Task Force\">IETF</a> IIIR Working Group as an Internet Draft (a rough proposal for a standard). It was replaced by a second version<sup id=\"cite_ref-ietfiiir_40-0\" class=\"reference\"><a href=\"#cite_note-ietfiiir-40\"><span class=\"cite-bracket\">&#91;</span>39<span class=\"cite-bracket\">&#93;</span></a></sup> one month later.</dd>\n<dt>November 1993</dt>\n<dd><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/HTMLPlus/htmlplus_1.html\">HTML+</a> was published by the IETF as an Internet Draft and was a competing proposal to the Hypertext Markup Language draft. It expired in July 1994.<sup id=\"cite_ref-41\" class=\"reference\"><a href=\"#cite_note-41\"><span class=\"cite-bracket\">&#91;</span>40<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>November 1994</dt>\n<dd>First draft (revision 00) of HTML 2.0 published by IETF itself<sup id=\"cite_ref-42\" class=\"reference\"><a href=\"#cite_note-42\"><span class=\"cite-bracket\">&#91;</span>41<span class=\"cite-bracket\">&#93;</span></a></sup> (called as \"HTML 2.0\" from revision 02<sup id=\"cite_ref-43\" class=\"reference\"><a href=\"#cite_note-43\"><span class=\"cite-bracket\">&#91;</span>42<span class=\"cite-bracket\">&#93;</span></a></sup>), that finally led to the publication of <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><a href=\"/wiki/RFC_(identifier)\" class=\"mw-redirect\" title=\"RFC (identifier)\">RFC</a>&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1866\">1866</a> in November 1995.<sup id=\"cite_ref-rfc1866_44-0\" class=\"reference\"><a href=\"#cite_note-rfc1866-44\"><span class=\"cite-bracket\">&#91;</span>43<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>April 1995 (authored March 1995)</dt>\n<dd>HTML 3.0<sup id=\"cite_ref-45\" class=\"reference\"><a href=\"#cite_note-45\"><span class=\"cite-bracket\">&#91;</span>44<span class=\"cite-bracket\">&#93;</span></a></sup> was proposed as a standard to the IETF, but the proposal expired five months later (28 September 1995)<sup id=\"cite_ref-html30cover_46-0\" class=\"reference\"><a href=\"#cite_note-html30cover-46\"><span class=\"cite-bracket\">&#91;</span>45<span class=\"cite-bracket\">&#93;</span></a></sup> without further action. It included many of the capabilities that were in Raggett's HTML+ proposal, such as support for tables, text flow around figures, and the display of complex mathematical formulas.<sup id=\"cite_ref-html30cover_46-1\" class=\"reference\"><a href=\"#cite_note-html30cover-46\"><span class=\"cite-bracket\">&#91;</span>45<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt></dt>\n<dd>W3C began development of its own <a href=\"/wiki/Arena_(web_browser)\" title=\"Arena (web browser)\">Arena browser</a> as a <a href=\"/wiki/Test_bed\" class=\"mw-redirect\" title=\"Test bed\">test bed</a> for HTML 3 and Cascading Style Sheets,<sup id=\"cite_ref-47\" class=\"reference\"><a href=\"#cite_note-47\"><span class=\"cite-bracket\">&#91;</span>46<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-48\" class=\"reference\"><a href=\"#cite_note-48\"><span class=\"cite-bracket\">&#91;</span>47<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-49\" class=\"reference\"><a href=\"#cite_note-49\"><span class=\"cite-bracket\">&#91;</span>48<span class=\"cite-bracket\">&#93;</span></a></sup> but HTML 3.0 did not succeed for several reasons. The draft was considered very large at 150 pages and the pace of browser development, as well as the number of interested parties, had outstripped the resources of the IETF.<sup id=\"cite_ref-raggett_15-2\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup> Browser vendors, including Microsoft and Netscape at the time, chose to implement different subsets of HTML 3's draft features as well as to introduce their own extensions to it.<sup id=\"cite_ref-raggett_15-3\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup> (See <a href=\"/wiki/Browser_wars\" title=\"Browser wars\">browser wars</a>.) These included extensions to control stylistic aspects of documents, contrary to the \"belief [of the academic engineering community] that such things as text color, background texture, font size, and font face were definitely outside the scope of a language when their only intent was to specify how a document would be organized.\"<sup id=\"cite_ref-raggett_15-4\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup> Dave Raggett, who has been a W3C Fellow for many years, has commented for example: \"To a certain extent, Microsoft built its business on the Web by extending HTML features.\"<sup id=\"cite_ref-raggett_15-5\" class=\"reference\"><a href=\"#cite_note-raggett-15\"><span class=\"cite-bracket\">&#91;</span>14<span class=\"cite-bracket\">&#93;</span></a></sup></dd></dl>\n<figure class=\"mw-default-size mw-halign-right\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:HTML5_logo_and_wordmark.svg\" class=\"mw-file-description\"><img alt=\"Official HTML5 logo\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/250px-HTML5_logo_and_wordmark.svg.png\" decoding=\"async\" width=\"190\" height=\"190\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/330px-HTML5_logo_and_wordmark.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/500px-HTML5_logo_and_wordmark.svg.png 2x\" data-file-width=\"512\" data-file-height=\"512\" /></a><figcaption>Logo of HTML5</figcaption></figure>\n<dl><dt>January 2008</dt>\n<dd><a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a> was published as a <a href=\"/wiki/World_Wide_Web_Consortium#Certification\" title=\"World Wide Web Consortium\">Working Draft</a> by the W3C.<sup id=\"cite_ref-50\" class=\"reference\"><a href=\"#cite_note-50\"><span class=\"cite-bracket\">&#91;</span>49<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt></dt>\n<dd>Although its syntax closely resembles that of <a href=\"/wiki/SGML\" class=\"mw-redirect\" title=\"SGML\">SGML</a>, <a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a> has abandoned any attempt to be an SGML application and has explicitly defined its own \"html\" serialization, in addition to an alternative XML-based XHTML5 serialization.<sup id=\"cite_ref-51\" class=\"reference\"><a href=\"#cite_note-51\"><span class=\"cite-bracket\">&#91;</span>50<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>2011&#160;HTML5 – Last Call</dt>\n<dd></dd>\n<dt></dt>\n<dd>On 14 February 2011, the W3C extended the charter of its HTML Working Group with clear milestones for HTML5. In May 2011, the working group advanced HTML5 to \"Last Call\", an invitation to communities inside and outside W3C to confirm the technical soundness of the specification. The W3C developed a comprehensive test suite to achieve broad interoperability for the full specification by 2014, which was the target date for recommendation.<sup id=\"cite_ref-w3c2014_52-0\" class=\"reference\"><a href=\"#cite_note-w3c2014-52\"><span class=\"cite-bracket\">&#91;</span>51<span class=\"cite-bracket\">&#93;</span></a></sup> In January 2011, the WHATWG renamed its \"HTML5\" living standard to \"HTML\". The W3C nevertheless continued its project to release HTML5.<sup id=\"cite_ref-53\" class=\"reference\"><a href=\"#cite_note-53\"><span class=\"cite-bracket\">&#91;</span>52<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>2012&#160;HTML5 – Candidate Recommendation</dt>\n<dd></dd>\n<dt></dt>\n<dd>In July 2012, WHATWG and <a href=\"/wiki/W3C\" class=\"mw-redirect\" title=\"W3C\">W3C</a> decided on a degree of separation. W3C will continue the HTML5 specification work, focusing on a single definitive standard, which is considered a \"snapshot\" by WHATWG. The WHATWG organization will continue its work with HTML5 as a \"Living Standard\". The concept of a living standard is that it is never complete and is always being updated and improved. New features can be added but functionality will not be removed.<sup id=\"cite_ref-54\" class=\"reference\"><a href=\"#cite_note-54\"><span class=\"cite-bracket\">&#91;</span>53<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt></dt>\n<dd>In December 2012, W3C designated HTML5 as a Candidate Recommendation.<sup id=\"cite_ref-55\" class=\"reference\"><a href=\"#cite_note-55\"><span class=\"cite-bracket\">&#91;</span>54<span class=\"cite-bracket\">&#93;</span></a></sup> The criterion for advancement to <a href=\"/wiki/W3C_recommendation#W3C_recommendation_(REC)\" class=\"mw-redirect\" title=\"W3C recommendation\">W3C Recommendation</a> is \"two 100% complete and fully interoperable implementations\".<sup id=\"cite_ref-W3Crec_56-0\" class=\"reference\"><a href=\"#cite_note-W3Crec-56\"><span class=\"cite-bracket\">&#91;</span>55<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>2014&#160;HTML5 – Proposed Recommendation and Recommendation</dt>\n<dd></dd>\n<dt></dt>\n<dd>In September 2014, W3C moved HTML5 to Proposed Recommendation.<sup id=\"cite_ref-57\" class=\"reference\"><a href=\"#cite_note-57\"><span class=\"cite-bracket\">&#91;</span>56<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt></dt>\n<dd>On 28 October 2014, HTML5 was released as a stable W3C Recommendation,<sup id=\"cite_ref-58\" class=\"reference\"><a href=\"#cite_note-58\"><span class=\"cite-bracket\">&#91;</span>57<span class=\"cite-bracket\">&#93;</span></a></sup> meaning the specification process is complete.<sup id=\"cite_ref-finalars_59-0\" class=\"reference\"><a href=\"#cite_note-finalars-59\"><span class=\"cite-bracket\">&#91;</span>58<span class=\"cite-bracket\">&#93;</span></a></sup></dd></dl>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"XHTML_versions\">XHTML versions</h4></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a></div>\n<p>XHTML is a separate language that began as a reformulation of HTML 4.01 using <a href=\"/wiki/XML\" title=\"XML\">XML</a> 1.0. It is now referred to as <i>the XML syntax for HTML</i> and is no longer being developed as a separate standard.<sup id=\"cite_ref-60\" class=\"reference\"><a href=\"#cite_note-60\"><span class=\"cite-bracket\">&#91;</span>59<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<ul><li>XHTML 1.0 was published as a W3C Recommendation on January 26, 2000,<sup id=\"cite_ref-61\" class=\"reference\"><a href=\"#cite_note-61\"><span class=\"cite-bracket\">&#91;</span>60<span class=\"cite-bracket\">&#93;</span></a></sup> and was later revised and republished on August 1, 2002. It offers the same three variations as HTML 4.0 and 4.01, reformulated in XML, with minor restrictions.</li>\n<li>XHTML 1.1<sup id=\"cite_ref-62\" class=\"reference\"><a href=\"#cite_note-62\"><span class=\"cite-bracket\">&#91;</span>61<span class=\"cite-bracket\">&#93;</span></a></sup> was published as a W3C Recommendation on May 31, 2001. It is based on XHTML 1.0 Strict, but includes minor changes, can be customized, and is reformulated using modules in the W3C recommendation \"Modularization of XHTML\", which was published on April 10, 2001.<sup id=\"cite_ref-63\" class=\"reference\"><a href=\"#cite_note-63\"><span class=\"cite-bracket\">&#91;</span>62<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>XHTML 2.0 was a working draft. Work on it was abandoned in 2009 in favor of work on <a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a> and <a href=\"/wiki/XHTML#XHTML5\" title=\"XHTML\">XHTML5</a>.<sup id=\"cite_ref-64\" class=\"reference\"><a href=\"#cite_note-64\"><span class=\"cite-bracket\">&#91;</span>63<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-65\" class=\"reference\"><a href=\"#cite_note-65\"><span class=\"cite-bracket\">&#91;</span>64<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-66\" class=\"reference\"><a href=\"#cite_note-66\"><span class=\"cite-bracket\">&#91;</span>65<span class=\"cite-bracket\">&#93;</span></a></sup> XHTML 2.0 was incompatible with XHTML 1.x and, therefore, would be more accurately characterized as an XHTML-inspired new language than an update to XHTML 1.x.</li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Transition_of_HTML_publication_to_WHATWG\">Transition of HTML publication to WHATWG</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/HTML5#W3C_and_WHATWG_conflict\" title=\"HTML5\">HTML5 §&#160;W3C and WHATWG conflict</a></div>\n<p>On 28 May 2019, the W3C announced that WHATWG would be the sole publisher of the HTML and DOM standards.<sup id=\"cite_ref-W3C_transfer_blog_67-0\" class=\"reference\"><a href=\"#cite_note-W3C_transfer_blog-67\"><span class=\"cite-bracket\">&#91;</span>66<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-W3C_transfer_HTML_68-0\" class=\"reference\"><a href=\"#cite_note-W3C_transfer_HTML-68\"><span class=\"cite-bracket\">&#91;</span>67<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-W3C_transfer_memo_69-0\" class=\"reference\"><a href=\"#cite_note-W3C_transfer_memo-69\"><span class=\"cite-bracket\">&#91;</span>68<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-W3C_transfer_ZDNet_70-0\" class=\"reference\"><a href=\"#cite_note-W3C_transfer_ZDNet-70\"><span class=\"cite-bracket\">&#91;</span>69<span class=\"cite-bracket\">&#93;</span></a></sup> The W3C and WHATWG had been publishing competing standards since 2012. While the W3C standard was identical to the WHATWG in 2007 the standards have since progressively diverged due to different design decisions.<sup id=\"cite_ref-W3C_forks_71-0\" class=\"reference\"><a href=\"#cite_note-W3C_forks-71\"><span class=\"cite-bracket\">&#91;</span>70<span class=\"cite-bracket\">&#93;</span></a></sup> The WHATWG \"Living Standard\" had been the <i>de facto</i> web standard for some time.<sup id=\"cite_ref-72\" class=\"reference\"><a href=\"#cite_note-72\"><span class=\"cite-bracket\">&#91;</span>71<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Markup\">Markup</h2></div>\n<p>HTML markup consists of several key components, including those called <i>tags</i> (and their <i>attributes</i>), character-based <i>data types</i>, <i>character references</i> and <i>entity references</i>. HTML tags most commonly come in pairs like <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">h1</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">h1</span><span class=\"p\">&gt;</span></code>, although some represent <i>empty elements</i> and so are unpaired, for example <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">img</span><span class=\"p\">&gt;</span></code>. The first tag in such a pair is the <i>start tag</i>, and the second is the <i>end tag</i> (they are also called <i>opening tags</i> and <i>closing tags</i>).\n</p><p>Another important component is the HTML <i><a href=\"/wiki/Document_type_declaration\" title=\"Document type declaration\">document type declaration</a></i>, which triggers <a href=\"/wiki/Standards_mode\" class=\"mw-redirect\" title=\"Standards mode\">standards mode</a> rendering.\n</p><p>The following is an example of the classic <a href=\"/wiki/%22Hello,_World!%22_program\" title=\"&quot;Hello, World!&quot; program\">\"Hello, World!\" program</a>:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"cp\">&lt;!DOCTYPE html&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">html</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span>\n    <span class=\"p\">&lt;</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span>This is a title<span class=\"p\">&lt;/</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;/</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;</span><span class=\"nt\">body</span><span class=\"p\">&gt;</span>\n    <span class=\"p\">&lt;</span><span class=\"nt\">div</span><span class=\"p\">&gt;</span>\n        <span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>Hello world!<span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>\n    <span class=\"p\">&lt;/</span><span class=\"nt\">div</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;/</span><span class=\"nt\">body</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;/</span><span class=\"nt\">html</span><span class=\"p\">&gt;</span>\n</pre></div>\n<p>The text between <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">html</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">html</span><span class=\"p\">&gt;</span></code> describes the web page, and the text between <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">body</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">body</span><span class=\"p\">&gt;</span></code> is the visible page content. The markup text <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span>This is a title<span class=\"p\">&lt;/</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span></code> defines the browser page title shown on <a href=\"/wiki/Browser_tab\" class=\"mw-redirect\" title=\"Browser tab\">browser tabs</a> and <a href=\"/wiki/Window_(computing)\" title=\"Window (computing)\">window</a> titles and the tag <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">div</span><span class=\"p\">&gt;</span></code> defines a division of the page used for easy styling. Between <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span></code>, a <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">meta</span><span class=\"p\">&gt;</span></code> element can be used to define webpage metadata.\n</p><p>The Document Type Declaration <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"cp\">&lt;!DOCTYPE html&gt;</span></code> is for HTML5. If a declaration is not included, various browsers will revert to \"<a href=\"/wiki/Quirks_mode\" title=\"Quirks mode\">quirks mode</a>\" for rendering.<sup id=\"cite_ref-hsivonen_73-0\" class=\"reference\"><a href=\"#cite_note-hsivonen-73\"><span class=\"cite-bracket\">&#91;</span>72<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Elements\">Elements</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/HTML_element\" title=\"HTML element\">HTML element</a></div>\n<figure class=\"mw-default-size\" typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:HTML_element_content_categories.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/10/HTML_element_content_categories.svg/250px-HTML_element_content_categories.svg.png\" decoding=\"async\" width=\"250\" height=\"177\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/10/HTML_element_content_categories.svg/500px-HTML_element_content_categories.svg.png 1.5x\" data-file-width=\"1123\" data-file-height=\"794\" /></a><figcaption>HTML element content categories</figcaption></figure>\n<p>HTML documents imply a structure of nested <a href=\"/wiki/HTML_element\" title=\"HTML element\">HTML elements</a>. These are indicated in the document by HTML <i>tags</i>, enclosed in angle brackets.<sup id=\"cite_ref-74\" class=\"reference\"><a href=\"#cite_note-74\"><span class=\"cite-bracket\">&#91;</span>73<span class=\"cite-bracket\">&#93;</span></a></sup><sup class=\"noprint Inline-Template noprint noexcerpt Template-Fact\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:NOTRS\" class=\"mw-redirect\" title=\"Wikipedia:NOTRS\"><span title=\"This claim needs references to better sources. (February 2019)\">better&#160;source&#160;needed</span></a></i>&#93;</sup>\n</p><p>In the simple, general case, the extent of an element is indicated by a pair of tags: a \"start tag\" <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code> and \"end tag\" <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code>. The text content of the element, if any, is placed between these tags.\n</p><p>Tags may also enclose further tag markup between the start and end, including a mixture of tags and text. This indicates further (nested) elements, as children of the parent element.\n</p><p>The start tag may also include the element's <i>attributes</i> within the tag. These indicate other information, such as identifiers for sections within the document, identifiers used to bind style information to the presentation of the document, and for some tags such as the <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">img</span><span class=\"p\">&gt;</span></code> used to embed images, the reference to the image resource in the format like this: <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">img</span> <span class=\"na\">src</span><span class=\"o\">=</span><span class=\"s\">&quot;example.com/example.jpg&quot;</span><span class=\"p\">&gt;</span></code>\n</p><p>Some elements, such as the <a href=\"/wiki/Line_breaking_character\" class=\"mw-redirect\" title=\"Line breaking character\">line break</a> <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> do not permit <i>any</i> embedded content, either text or further tags. These require only a single empty tag (akin to a start tag) and do not use an end tag.\n</p><p>Many tags, particularly the closing end tag for the very commonly used paragraph element <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code>, are optional. An HTML browser or other agent can infer the closure for the end of an element from the context and the structural rules defined by the HTML standard. These rules are complex and not widely understood by most HTML authors.\n</p><p>The general form of an HTML element is therefore: <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">tag</span> <span class=\"na\">attribute1</span><span class=\"o\">=</span><span class=\"s\">&quot;value1&quot;</span> <span class=\"na\">attribute2</span><span class=\"o\">=</span><span class=\"s\">&quot;value2&quot;</span><span class=\"p\">&gt;</span>&#39;&#39;content&#39;&#39;<span class=\"p\">&lt;/</span><span class=\"nt\">tag</span><span class=\"p\">&gt;</span></code>. Some HTML elements are defined as <i>empty elements</i> and take the form <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">tag</span> <span class=\"na\">attribute1</span><span class=\"o\">=</span><span class=\"s\">&quot;value1&quot;</span> <span class=\"na\">attribute2</span><span class=\"o\">=</span><span class=\"s\">&quot;value2&quot;</span><span class=\"p\">&gt;</span></code>. Empty elements may enclose no content, for instance, the <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> tag or the inline <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">img</span><span class=\"p\">&gt;</span></code> tag.\nThe name of an HTML element is the name used in the tags.\nThe end tag's name is preceded by a slash character <code>&#47;</code>. If a tag has no content, an end tag is not allowed. If attributes are not mentioned, default values are used in each case.\n</p>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Element_examples\">Element examples</h4></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/HTML_element\" title=\"HTML element\">HTML element</a></div>\n<p>Header of the HTML document: <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span>...<span class=\"p\">&lt;/</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span></code>. The title is included in the head, for example:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span>The Title<span class=\"p\">&lt;/</span><span class=\"nt\">title</span><span class=\"p\">&gt;</span>\n  <span class=\"p\">&lt;</span><span class=\"nt\">link</span> <span class=\"na\">rel</span><span class=\"o\">=</span><span class=\"s\">&quot;stylesheet&quot;</span> <span class=\"na\">href</span><span class=\"o\">=</span><span class=\"s\">&quot;stylebyjimbowales.css&quot;</span><span class=\"p\">&gt;</span> <span class=\"cm\">&lt;!-- Imports Stylesheets --&gt;</span>\n<span class=\"p\">&lt;/</span><span class=\"nt\">head</span><span class=\"p\">&gt;</span>\n</pre></div>\n<div class=\"mw-heading mw-heading5\"><h5 id=\"Headings\">Headings</h5></div>\n<p>HTML headings are defined with the <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">h1</span><span class=\"p\">&gt;</span></code> to <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">h6</span><span class=\"p\">&gt;</span></code> tags with H1 being the highest (or most important) level and H6 the least:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">h1</span><span class=\"p\">&gt;</span>Heading level 1<span class=\"p\">&lt;/</span><span class=\"nt\">h1</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">h2</span><span class=\"p\">&gt;</span>Heading level 2<span class=\"p\">&lt;/</span><span class=\"nt\">h2</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">h3</span><span class=\"p\">&gt;</span>Heading level 3<span class=\"p\">&lt;/</span><span class=\"nt\">h3</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">h4</span><span class=\"p\">&gt;</span>Heading level 4<span class=\"p\">&lt;/</span><span class=\"nt\">h4</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">h5</span><span class=\"p\">&gt;</span>Heading level 5<span class=\"p\">&lt;/</span><span class=\"nt\">h5</span><span class=\"p\">&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">h6</span><span class=\"p\">&gt;</span>Heading level 6<span class=\"p\">&lt;/</span><span class=\"nt\">h6</span><span class=\"p\">&gt;</span>\n</pre></div>\n<p>The effects are:\n</p>\n<blockquote>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 1.8em; font-family: Georgia,Times,serif; margin-top: 1em; margin-bottom: 0.25em; line-height: 1.3; padding: 0; border-bottom: 1px solid #AAAAAA;\">Heading Level 1</div>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 1.5em; font-family: Georgia,Times,serif; margin-top: 1em; margin-bottom: 0.25em; line-height: 1.3; padding: 0; border-bottom: 1px solid #AAAAAA;\">Heading Level 2</div>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 1.17em; font-weight: bold; margin-top: 0.3em; margin-bottom: 0; line-height: 1.6; padding-top: 0.5em; padding-bottom: 0;\">Heading Level 3</div>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 100%; font-weight: bold; margin-top: 0.3em; margin-bottom: 0; line-height: 1.6; padding-top: 0.5em; padding-bottom: 0;\">Heading Level 4</div>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 100%; font-weight: bold; margin-top: 0.3em; margin-bottom: 0; line-height: 1.6; padding-top: 0.5em; padding-bottom: 0;\">Heading Level 5</div>\n<div style=\"overflow: hidden; page-break-after: avoid; font-size: 100%; font-weight: bold; margin-top: 0.3em; margin-bottom: 0; line-height: 1.6; padding-top: 0.5em; padding-bottom: 0;\">Heading Level 6</div>\n</blockquote>\n<p>CSS can substantially change the rendering.\n</p><p>\nParagraphs:</p><div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>Paragraph 1<span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span> <span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>Paragraph 2<span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>\n</pre></div>\n<div class=\"mw-heading mw-heading5\"><h5 id=\"Line_breaks\">Line breaks</h5></div>\n<p><code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code>. The difference between <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code> is that <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> <a href=\"/wiki/Line_breaking_character\" class=\"mw-redirect\" title=\"Line breaking character\">breaks a line</a> without altering the semantic structure of the page, whereas <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span></code> sections the page into <a href=\"/wiki/Paragraph\" title=\"Paragraph\">paragraphs</a>. The element <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> is an <i>empty element</i> in that, although it may have attributes, it can take no content and it must not have an end tag.\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>This <span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span> is a paragraph <span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span> with <span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span> line breaks<span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>\n</pre></div>\n<div class=\"mw-heading mw-heading5\"><h5 id=\"Links\">Links</h5></div>\n<p>This is a link in HTML. To create a link the <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">a</span><span class=\"p\">&gt;</span></code> tag is used. The <code>href</code> attribute holds the <a href=\"/wiki/URL\" title=\"URL\">URL</a> address of the link.\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">a</span> <span class=\"na\">href</span><span class=\"o\">=</span><span class=\"s\">&quot;https://www.wikipedia.org/&quot;</span><span class=\"p\">&gt;</span>A link to Wikipedia!<span class=\"p\">&lt;/</span><span class=\"nt\">a</span><span class=\"p\">&gt;</span>\n</pre></div>\n<div class=\"mw-heading mw-heading5\"><h5 id=\"Inputs\">Inputs</h5></div><p>\nThere are many possible ways a user can give inputs like:</p><div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">input</span> <span class=\"na\">type</span><span class=\"o\">=</span><span class=\"s\">&quot;text&quot;</span><span class=\"p\">&gt;</span> <span class=\"cm\">&lt;!-- This is for text input --&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">input</span> <span class=\"na\">type</span><span class=\"o\">=</span><span class=\"s\">&quot;file&quot;</span><span class=\"p\">&gt;</span> <span class=\"cm\">&lt;!-- This is for uploading files --&gt;</span>\n<span class=\"p\">&lt;</span><span class=\"nt\">input</span> <span class=\"na\">type</span><span class=\"o\">=</span><span class=\"s\">&quot;checkbox&quot;</span><span class=\"p\">&gt;</span> <span class=\"cm\">&lt;!-- This is for checkboxes --&gt;</span>\n</pre></div>\n<p><span class=\"anchor\" id=\"Comments\"></span><span class=\"anchor\" id=\"comments\"></span><b>Comments:</b>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"cm\">&lt;!-- This is a comment --&gt;</span>\n</pre></div><p> Comments can help in the understanding of the markup and do not display in the webpage.\n</p><p>There are several types of markup elements used in HTML:\n</p>\n<dl><dt>Structural markup indicates the purpose of text</dt>\n<dd>For example, <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">h2</span><span class=\"p\">&gt;</span>Golf<span class=\"p\">&lt;/</span><span class=\"nt\">h2</span><span class=\"p\">&gt;</span></code> establishes \"Golf\" as a second-level <a href=\"/wiki/HTML_element#Basic_text\" title=\"HTML element\">heading</a>. Structural markup does not denote any specific rendering, but most web browsers have default styles for element formatting. Content may be further styled using <a href=\"/wiki/Cascading_Style_Sheets\" class=\"mw-redirect\" title=\"Cascading Style Sheets\">Cascading Style Sheets</a> (CSS).<sup id=\"cite_ref-75\" class=\"reference\"><a href=\"#cite_note-75\"><span class=\"cite-bracket\">&#91;</span>74<span class=\"cite-bracket\">&#93;</span></a></sup></dd>\n<dt>Presentational markup indicates the appearance of the text, regardless of its purpose</dt>\n<dd>For example, <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">b</span><span class=\"p\">&gt;</span>bold text<span class=\"p\">&lt;/</span><span class=\"nt\">b</span><span class=\"p\">&gt;</span></code> indicates that visual output devices should render \"boldface\" in bold text, but gives little indication what devices that are unable to do this (such as aural devices that read the text aloud) should do. In the case of both <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">b</span><span class=\"p\">&gt;</span>bold text<span class=\"p\">&lt;/</span><span class=\"nt\">b</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">i</span><span class=\"p\">&gt;</span>italic text<span class=\"p\">&lt;/</span><span class=\"nt\">i</span><span class=\"p\">&gt;</span></code>, there are other elements that may have equivalent visual renderings but that are more semantic in nature, such as <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">strong</span><span class=\"p\">&gt;</span>strong text<span class=\"p\">&lt;/</span><span class=\"nt\">strong</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">em</span><span class=\"p\">&gt;</span>emphasized text<span class=\"p\">&lt;/</span><span class=\"nt\">em</span><span class=\"p\">&gt;</span></code> respectively. It is easier to see how an aural user agent should interpret the latter two elements. However, they are not equivalent to their presentational counterparts: it would be undesirable for a screen reader to emphasize the name of a book, for instance, but on a screen, such a name would be italicized. Most presentational markup elements have become <a href=\"/wiki/Deprecation\" title=\"Deprecation\">deprecated</a> under the HTML 4.0 specification in favor of using <a href=\"/wiki/CSS\" title=\"CSS\">CSS</a> for styling.</dd>\n<dt>Hypertext markup makes parts of a document into links to other documents</dt>\n<dd>An anchor element creates a <a href=\"/wiki/Hyperlink\" title=\"Hyperlink\">hyperlink</a> in the document and its <code>href</code> attribute sets the link's target <a href=\"/wiki/URL\" title=\"URL\">URL</a>. For example, the HTML markup <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">a</span> <span class=\"na\">href</span><span class=\"o\">=</span><span class=\"s\">&quot;https://en.wikipedia.org/&quot;</span><span class=\"p\">&gt;</span>Wikipedia<span class=\"p\">&lt;/</span><span class=\"nt\">a</span><span class=\"p\">&gt;</span></code>, will render the word \"<span class=\"plainlinks\"><a class=\"external text\" href=\"https://en.wikipedia.org/\">Wikipedia</a></span>\" as a hyperlink. To render an image as a hyperlink, an <code>img</code> element is inserted as content into the <code>a</code> element. Like <code>br</code>, <code>img</code> is an empty element with attributes but no content or closing tag. <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">a</span> <span class=\"na\">href</span><span class=\"o\">=</span><span class=\"s\">&quot;https://example.org&quot;</span><span class=\"p\">&gt;&lt;</span><span class=\"nt\">img</span> <span class=\"na\">src</span><span class=\"o\">=</span><span class=\"s\">&quot;image.gif&quot;</span> <span class=\"na\">alt</span><span class=\"o\">=</span><span class=\"s\">&quot;descriptive text&quot;</span> <span class=\"na\">width</span><span class=\"o\">=</span><span class=\"s\">&quot;50&quot;</span> <span class=\"na\">height</span><span class=\"o\">=</span><span class=\"s\">&quot;50&quot;</span> <span class=\"na\">border</span><span class=\"o\">=</span><span class=\"s\">&quot;0&quot;</span><span class=\"p\">&gt;&lt;/</span><span class=\"nt\">a</span><span class=\"p\">&gt;</span></code>.</dd></dl>\n<div class=\"mw-heading mw-heading4\"><h4 id=\"Attributes\">Attributes</h4></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/HTML_attribute\" title=\"HTML attribute\">HTML attribute</a></div>\n<p>Most of the attributes of an element are <a href=\"/wiki/Name%E2%80%93value_pair\" title=\"Name–value pair\">name–value pairs</a>, separated by <code>&#61;</code> and written within the start tag of an element after the element's name. The value may be enclosed in single or double quotes, although values consisting of certain characters can be left unquoted in HTML (but not XHTML).<sup id=\"cite_ref-76\" class=\"reference\"><a href=\"#cite_note-76\"><span class=\"cite-bracket\">&#91;</span>75<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-77\" class=\"reference\"><a href=\"#cite_note-77\"><span class=\"cite-bracket\">&#91;</span>76<span class=\"cite-bracket\">&#93;</span></a></sup> Leaving attribute values unquoted is considered unsafe.<sup id=\"cite_ref-78\" class=\"reference\"><a href=\"#cite_note-78\"><span class=\"cite-bracket\">&#91;</span>77<span class=\"cite-bracket\">&#93;</span></a></sup> In contrast with name-value pair attributes, there are some attributes that affect the element simply by their presence in the start tag of the element,<sup id=\"cite_ref-tagshtml_8-2\" class=\"reference\"><a href=\"#cite_note-tagshtml-8\"><span class=\"cite-bracket\">&#91;</span>7<span class=\"cite-bracket\">&#93;</span></a></sup> like the <code>ismap</code> attribute for the <code>img</code> element.<sup id=\"cite_ref-79\" class=\"reference\"><a href=\"#cite_note-79\"><span class=\"cite-bracket\">&#91;</span>78<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>There are several common attributes that may appear in many elements&#160;:\n</p>\n<ul><li>The <code>id</code> attribute provides a document-wide unique identifier for an element. This is used to identify the element so that stylesheets can alter its presentational properties, and scripts may alter, animate or delete its contents or presentation. Appended to the URL of the page, it provides a globally unique identifier for the element, typically a sub-section of the page. For example, the ID \"Attributes\" in <code>https://en.wikipedia.org/wiki/HTML#Attributes</code>.</li>\n<li>The <code>class</code> attribute provides a way of classifying similar elements. This can be used for <a href=\"/wiki/Semantics\" title=\"Semantics\">semantic</a> or presentation purposes. For example, an HTML document might semantically use the designation <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">class</span><span class=\"err\">=&quot;</span><span class=\"na\">notation</span><span class=\"err\">&quot;</span><span class=\"p\">&gt;</span></code> to indicate that all elements with this class value are subordinate to the main text of the document. In presentation, such elements might be gathered together and presented as footnotes on a page instead of appearing in the place where they occur in the HTML source. Class attributes are used semantically in <a href=\"/wiki/Microformat\" title=\"Microformat\">microformats</a>. Multiple class values may be specified; for example <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">class</span><span class=\"err\">=&quot;</span><span class=\"na\">notation</span> <span class=\"na\">important</span><span class=\"err\">&quot;</span><span class=\"p\">&gt;</span></code> puts the element into both the <code>notation</code> and the <code>important</code> classes.</li>\n<li>An author may use the <code>style</code> attribute to assign presentational properties to a particular element. It is considered better practice to use an element's <code>id</code> or <code>class</code> attributes to select the element from within a <a href=\"/wiki/Cascading_Style_Sheets\" class=\"mw-redirect\" title=\"Cascading Style Sheets\">stylesheet</a>, though sometimes this can be too cumbersome for a simple, specific, or ad hoc styling.</li>\n<li>The <code>title</code> attribute is used to attach a subtextual explanation to an element. In most <a href=\"/wiki/Web_browser\" title=\"Web browser\">browsers</a> this attribute is displayed as a <a href=\"/wiki/Tooltip\" title=\"Tooltip\">tooltip</a>.</li>\n<li>The <code>lang</code> attribute identifies the natural language of the element's contents, which may be different from that of the rest of the document. For example, in an English-language document: <div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>Oh well, <span class=\"p\">&lt;</span><span class=\"nt\">span</span> <span class=\"na\">lang</span><span class=\"o\">=</span><span class=\"s\">&quot;fr&quot;</span><span class=\"p\">&gt;</span>c&#39;est la vie<span class=\"p\">&lt;/</span><span class=\"nt\">span</span><span class=\"p\">&gt;</span>, as they say in France.<span class=\"p\">&lt;/</span><span class=\"nt\">p</span><span class=\"p\">&gt;</span>\n</pre></div></li></ul>\n<p>The abbreviation element, <code>abbr</code>, can be used to demonstrate some of these attributes:\n</p>\n<div class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"p\">&lt;</span><span class=\"nt\">abbr</span> <span class=\"na\">id</span><span class=\"o\">=</span><span class=\"s\">&quot;anId&quot;</span> <span class=\"na\">class</span><span class=\"o\">=</span><span class=\"s\">&quot;jargon&quot;</span> <span class=\"na\">style</span><span class=\"o\">=</span><span class=\"s\">&quot;color:purple;&quot;</span> <span class=\"na\">title</span><span class=\"o\">=</span><span class=\"s\">&quot;Hypertext Markup Language&quot;</span><span class=\"p\">&gt;</span>HTML<span class=\"p\">&lt;/</span><span class=\"nt\">abbr</span><span class=\"p\">&gt;</span>\n</pre></div>\n<p>This example displays as <abbr id=\"anId\" class=\"jargon\" style=\"color:purple;\" title=\"Hypertext Markup Language\">HTML</abbr>; in most browsers, pointing the cursor at the abbreviation should display the title text \"Hypertext Markup Language.\"\n</p><p>Most elements take the language-related attribute <code>dir</code> to specify text direction, such as with \"rtl\" for right-to-left text in, for example, <a href=\"/wiki/Arabic_language\" class=\"mw-redirect\" title=\"Arabic language\">Arabic</a>, <a href=\"/wiki/Persian_language\" title=\"Persian language\">Persian</a> or <a href=\"/wiki/Hebrew_language\" title=\"Hebrew language\">Hebrew</a>.<sup id=\"cite_ref-80\" class=\"reference\"><a href=\"#cite_note-80\"><span class=\"cite-bracket\">&#91;</span>79<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Character_and_entity_references\">Character and entity references</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">See also: <a href=\"/wiki/List_of_XML_and_HTML_character_entity_references\" title=\"List of XML and HTML character entity references\">List of XML and HTML character entity references</a> and <a href=\"/wiki/Unicode_and_HTML\" title=\"Unicode and HTML\">Unicode and HTML</a></div>\n<p>As of version 4.0, HTML defines a set of 252 <a href=\"/wiki/Character_entity_reference\" class=\"mw-redirect\" title=\"Character entity reference\">character entity references</a> and a set of 1,114,050 <a href=\"/wiki/Numeric_character_reference\" title=\"Numeric character reference\">numeric character references</a>, both of which allow individual characters to be written via simple markup, rather than literally. A literal character and its markup counterpart are considered equivalent and are rendered identically.\n</p><p>The ability to \"<a href=\"/wiki/Escape_character\" title=\"Escape character\">escape</a>\" characters in this way allows for the characters <code>&lt;</code> and <code>&amp;</code> (when written as <code>&amp;lt;</code> and <code>&amp;amp;</code>, respectively) to be interpreted as character data, rather than markup. For example, a literal <code>&lt;</code> normally indicates the start of a tag, and <code>&amp;</code> normally indicates the start of a character entity reference or numeric character reference; writing it as <code>&amp;amp;</code> or <code>&amp;#x26;</code> or <code>&amp;#38;</code> allows <code>&amp;</code> to be included in the content of an element or in the value of an attribute. The double-quote character (<code>\"</code>), when not used to quote an attribute value, must also be escaped as <code>&amp;quot;</code> or <code>&amp;#x22;</code> or <code>&amp;#34;</code> when it appears within the attribute value itself. Equivalently, the single-quote character (<code>'</code>), when not used to quote an attribute value, must also be escaped as <code>&amp;#x27;</code> or <code>&amp;#39;</code> (or as <code>&amp;apos;</code> in HTML5 or XHTML documents<sup id=\"cite_ref-81\" class=\"reference\"><a href=\"#cite_note-81\"><span class=\"cite-bracket\">&#91;</span>80<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-aposhtml_82-0\" class=\"reference\"><a href=\"#cite_note-aposhtml-82\"><span class=\"cite-bracket\">&#91;</span>81<span class=\"cite-bracket\">&#93;</span></a></sup>) when it appears within the attribute value itself. If document authors overlook the need to escape such characters, some browsers can be very forgiving and try to use context to guess their intent. The result is still invalid markup, which makes the document less accessible to other browsers and to other <a href=\"/wiki/User_agent\" title=\"User agent\">user agents</a> that may try to parse the document for <a href=\"/wiki/Web_crawler\" title=\"Web crawler\">search and indexing</a> purposes for example.\n</p><p>Escaping also allows for characters that are not easily typed, or that are not available in the document's <a href=\"/wiki/Character_encoding\" title=\"Character encoding\">character encoding</a>, to be represented within the element and attribute content. For example, the acute-accented <code>e</code> (<code>é</code>), a character typically found only on Western European and South American keyboards, can be written in any HTML document as the entity reference <code>&amp;eacute;</code> or as the numeric references <code>&amp;#xE9;</code> or <code>&amp;#233;</code>, using characters that are available on all keyboards and are supported in all character encodings. <a href=\"/wiki/Unicode\" title=\"Unicode\">Unicode</a> character encodings such as <a href=\"/wiki/UTF-8\" title=\"UTF-8\">UTF-8</a> are compatible with all modern browsers and allow direct access to almost all the characters of the world's writing systems.<sup id=\"cite_ref-83\" class=\"reference\"><a href=\"#cite_note-83\"><span class=\"cite-bracket\">&#91;</span>82<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<table class=\"wikitable\">\n<caption>HTML escape sequence examples\n</caption>\n<tbody><tr>\n<th>Named\n</th>\n<th>Decimal\n</th>\n<th>Hexadecimal\n</th>\n<th>Result\n</th>\n<th>Description\n</th>\n<th>Notes\n</th></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;amp;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#38;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x26;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;</code>\n</td>\n<td><a href=\"/wiki/Ampersand\" title=\"Ampersand\">Ampersand</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;lt;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#60;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x3C;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&lt;</code>\n</td>\n<td><a href=\"/wiki/Less-than_sign\" title=\"Less-than sign\">Less Than</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;gt;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#62;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x3E;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&gt;</code>\n</td>\n<td><a href=\"/wiki/Greater-than_sign\" title=\"Greater-than sign\">Greater Than</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;quot;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#34;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x22;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&quot;</code>\n</td>\n<td><a href=\"/wiki/Double_quote\" class=\"mw-redirect\" title=\"Double quote\">Double Quote</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;apos;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#39;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x27;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&#39;</code>\n</td>\n<td><a href=\"/wiki/Single_quote\" class=\"mw-redirect\" title=\"Single quote\">Single Quote</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;nbsp;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#160;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#xA0;</code>\n</td>\n<td><code style=\"background-color: lightblue;\">&#160;</code>\n</td>\n<td><a href=\"/wiki/Non-breaking_space\" title=\"Non-breaking space\">Non-Breaking Space</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;copy;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#169;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#xA9;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">©</code>\n</td>\n<td><a href=\"/wiki/Copyright_symbol\" title=\"Copyright symbol\">Copyright</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;reg;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#174;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#xAE;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">®</code>\n</td>\n<td><a href=\"/wiki/Registered_trademark_symbol\" title=\"Registered trademark symbol\">Registered Trademark</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;dagger;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#8224;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x2020;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">†</code>\n</td>\n<td><a href=\"/wiki/Dagger_(mark)\" title=\"Dagger (mark)\">Dagger</a>\n</td>\n<td>\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;ddagger;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#8225;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x2021;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">‡</code>\n</td>\n<td><a href=\"/wiki/Dagger_(mark)\" title=\"Dagger (mark)\">Double dagger</a>\n</td>\n<td>Names are case-sensitive and may have synonyms.\n</td></tr>\n<tr>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;trade;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#8482;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">&amp;#x2122;</code>\n</td>\n<td><code class=\"mw-highlight mw-highlight-lang-text mw-content-ltr\" style=\"\" dir=\"ltr\">™</code>\n</td>\n<td><a href=\"/wiki/Trademark_symbol\" title=\"Trademark symbol\">Trademark</a>\n</td>\n<td>\n</td></tr></tbody></table>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Data_types\">Data types</h3></div>\n<p>HTML defines several <a href=\"/wiki/Data_type\" title=\"Data type\">data types</a> for element content, such as script data and stylesheet data, and a plethora of types for attribute values, including IDs, names, <a href=\"/wiki/Uniform_Resource_Identifier\" title=\"Uniform Resource Identifier\">URIs</a>, numbers, units of length, languages, media descriptors, colors, character encodings, dates and times, and so on. All of these data types are specializations of character data.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Document_type_declaration\">Document type declaration</h3></div>\n<p>HTML documents are required to start with a <a href=\"/wiki/Document_type_declaration\" title=\"Document type declaration\">document type declaration</a> (informally, a \"doctype\"). In browsers, the doctype helps to define the rendering mode—particularly whether to use <a href=\"/wiki/Quirks_mode\" title=\"Quirks mode\">quirks mode</a>.\n</p><p>The original purpose of the doctype was to enable the parsing and validation of HTML documents by SGML tools based on the <a href=\"/wiki/Document_type_definition\" title=\"Document type definition\">document type definition</a> (DTD). The DTD to which the DOCTYPE refers contains a machine-readable grammar specifying the permitted and prohibited content for a document conforming to such a DTD. Browsers, on the other hand, do not implement HTML as an application of SGML and as consequence do not read the DTD.\n</p><p><a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a> does not define a DTD; therefore, in HTML5 the doctype declaration is simpler and shorter:<sup id=\"cite_ref-84\" class=\"reference\"><a href=\"#cite_note-84\"><span class=\"cite-bracket\">&#91;</span>83<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-highlight mw-highlight-lang-dtd mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">&lt;!DOCTYPE</span> <span class=\"nt\">html</span><span class=\"k\">&gt;</span>\n</pre></div>\n<p>An example of an HTML 4 doctype\n</p>\n<div class=\"mw-highlight mw-highlight-lang-dtd mw-content-ltr\" dir=\"ltr\"><pre><span></span><span class=\"k\">&lt;!DOCTYPE</span> <span class=\"nt\">HTML</span> <span class=\"kc\">PUBLIC</span> <span class=\"s2\">&quot;-//W3C//DTD HTML 4.01//EN&quot;</span> <span class=\"s2\">&quot;https://www.w3.org/TR/html4/strict.dtd&quot;</span><span class=\"k\">&gt;</span>\n</pre></div>\n<p>This declaration references the DTD for the \"strict\" version of HTML 4.01. SGML-based validators read the DTD in order to properly parse the document and to perform validation. In modern browsers, a valid doctype activates standards mode as opposed to <a href=\"/wiki/Quirks_mode\" title=\"Quirks mode\">quirks mode</a>.\n</p><p>In addition, HTML 4.01 provides Transitional and Frameset DTDs, <a href=\"#Transitional_versus_strict\">as explained below</a>. The transitional type is the most inclusive, incorporating current tags as well as older or \"deprecated\" tags, with the Strict DTD excluding deprecated tags. The frameset has all tags necessary to make frames on a page along with the tags included in transitional type.<sup id=\"cite_ref-85\" class=\"reference\"><a href=\"#cite_note-85\"><span class=\"cite-bracket\">&#91;</span>84<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Semantic_HTML\">Semantic HTML</h2></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/Semantic_HTML\" title=\"Semantic HTML\">Semantic HTML</a></div>\n<p>Semantic HTML is a way of writing HTML that emphasizes the meaning of the encoded information over its presentation (look). HTML has included semantic markup from its inception,<sup id=\"cite_ref-86\" class=\"reference\"><a href=\"#cite_note-86\"><span class=\"cite-bracket\">&#91;</span>85<span class=\"cite-bracket\">&#93;</span></a></sup> but has also included presentational markup, such as <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">font</span><span class=\"p\">&gt;</span></code>, <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">i</span><span class=\"p\">&gt;</span></code> and <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">center</span><span class=\"p\">&gt;</span></code> tags. There are also the semantically neutral <a href=\"/wiki/Div_and_span\" title=\"Div and span\">div and span</a> tags. Since the late 1990s, when <a href=\"/wiki/Cascading_Style_Sheets\" class=\"mw-redirect\" title=\"Cascading Style Sheets\">Cascading Style Sheets</a> were beginning to work in most browsers, web authors have been encouraged to avoid the use of presentational HTML markup with a view to the <a href=\"/wiki/Separation_of_content_and_presentation\" title=\"Separation of content and presentation\">separation of content and presentation</a>.<sup id=\"cite_ref-87\" class=\"reference\"><a href=\"#cite_note-87\"><span class=\"cite-bracket\">&#91;</span>86<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>In a 2001 discussion of the <a href=\"/wiki/Semantic_Web\" title=\"Semantic Web\">Semantic Web</a>, <a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Tim Berners-Lee</a> and others gave examples of ways in which intelligent software \"agents\" may one day automatically crawl the web and find, filter, and correlate previously unrelated, published facts for the benefit of human users.<sup id=\"cite_ref-88\" class=\"reference\"><a href=\"#cite_note-88\"><span class=\"cite-bracket\">&#91;</span>87<span class=\"cite-bracket\">&#93;</span></a></sup> Such agents are not commonplace even now, but some of the ideas of <a href=\"/wiki/Web_2.0\" title=\"Web 2.0\">Web 2.0</a>, <a href=\"/wiki/Mashup_(web_application_hybrid)\" title=\"Mashup (web application hybrid)\">mashups</a> and <a href=\"/wiki/Price_comparison_service\" class=\"mw-redirect\" title=\"Price comparison service\">price comparison websites</a> may be coming close<sup class=\"noprint Inline-Template Template-Fact\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Citation_needed\" title=\"Wikipedia:Citation needed\"><span title=\"This claim needs references to reliable sources. (February 2025)\">citation needed</span></a></i>&#93;</sup>. The main difference between these web application hybrids and Berners-Lee's semantic agents lies in the fact that the current <a href=\"/wiki/Feed_aggregator\" class=\"mw-redirect\" title=\"Feed aggregator\">aggregation</a> and hybridization of information is usually designed by <a href=\"/wiki/Web_developer\" title=\"Web developer\">web developers</a>, who already know the web locations and the <a href=\"/wiki/Application_programming_interface\" class=\"mw-redirect\" title=\"Application programming interface\">API semantics</a> of the specific data they wish to mash, compare and combine.\n</p><p>An important type of web agent that does crawl and read web pages automatically, without prior knowledge of what it might find, is the <a href=\"/wiki/Web_crawler\" title=\"Web crawler\">web crawler</a> or search-engine spider. These software agents are dependent on the semantic clarity of web pages they find as they use various techniques and <a href=\"/wiki/Algorithm\" title=\"Algorithm\">algorithms</a> to read and index millions of web pages a day and provide web users with <a href=\"/wiki/Web_search_engine\" class=\"mw-redirect\" title=\"Web search engine\">search facilities</a> without which the World Wide Web's usefulness would be greatly reduced.\n</p><p>In order for search engine spiders to be able to rate the significance of pieces of text they find in HTML documents, and also for those creating mashups and other hybrids as well as for more automated agents as they are developed, the semantic structures that exist in HTML need to be widely and uniformly applied to bring out the meaning of the published text.<sup id=\"cite_ref-Semantic_Web_Revisted_89-0\" class=\"reference\"><a href=\"#cite_note-Semantic_Web_Revisted-89\"><span class=\"cite-bracket\">&#91;</span>88<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Presentational markup tags are <a href=\"/wiki/Deprecation\" title=\"Deprecation\">deprecated</a> in current HTML and <a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a> recommendations. The majority of presentational features from previous versions of HTML are no longer allowed as they lead to poorer accessibility, higher cost of site maintenance, and larger document sizes.<sup id=\"cite_ref-90\" class=\"reference\"><a href=\"#cite_note-90\"><span class=\"cite-bracket\">&#91;</span>89<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>Good semantic HTML also improves the <a href=\"/wiki/Accessibility\" title=\"Accessibility\">accessibility</a> of web documents (see also <a href=\"/wiki/Web_Content_Accessibility_Guidelines\" title=\"Web Content Accessibility Guidelines\">Web Content Accessibility Guidelines</a>). For example, when a screen reader or audio browser can correctly ascertain the structure of a document, it will not waste the visually impaired user's time by reading out repeated or irrelevant information when it has been marked up correctly.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Delivery\">Delivery</h2></div>\n<p>HTML documents can be delivered by the same means as any other computer file. However, they are most often delivered either by <a href=\"/wiki/Hypertext_Transfer_Protocol\" class=\"mw-redirect\" title=\"Hypertext Transfer Protocol\">HTTP</a> from a <a href=\"/wiki/Web_server\" title=\"Web server\">web server</a> or by <a href=\"/wiki/Email\" title=\"Email\">email</a>.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"HTTP\">HTTP</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/Hypertext_Transfer_Protocol\" class=\"mw-redirect\" title=\"Hypertext Transfer Protocol\">Hypertext Transfer Protocol</a></div>\n<p>The <a href=\"/wiki/World_Wide_Web\" title=\"World Wide Web\">World Wide Web</a> is composed primarily of HTML documents transmitted from web servers to web browsers using the <a href=\"/wiki/Hypertext_Transfer_Protocol\" class=\"mw-redirect\" title=\"Hypertext Transfer Protocol\">Hypertext Transfer Protocol</a> (HTTP). However, HTTP is used to serve images, sound, and other content, in addition to HTML. To allow the web browser to know how to handle each document it receives, other information is transmitted along with the document. This <a href=\"/wiki/Meta_data\" class=\"mw-redirect\" title=\"Meta data\">meta data</a> usually includes the <a href=\"/wiki/MIME_type\" class=\"mw-redirect\" title=\"MIME type\">MIME type</a> (e.g., <kbd>text/html</kbd> or <kbd>application/xhtml+xml</kbd>) and the character encoding (see <a href=\"/wiki/Character_encodings_in_HTML\" title=\"Character encodings in HTML\">Character encodings in HTML</a>).\n</p><p>In modern browsers, the MIME type that is sent with the HTML document may affect how the document is initially interpreted. A document sent with the XHTML MIME type is expected to be <a href=\"/wiki/Well-formed_document\" title=\"Well-formed document\">well-formed</a> XML; syntax errors may cause the browser to fail to render it. The same document sent with the HTML MIME type might be displayed successfully since some browsers are more lenient with HTML.\n</p><p>The W3C recommendations state that XHTML 1.0 documents that follow guidelines set forth in the recommendation's Appendix C may be labeled with either MIME Type.<sup id=\"cite_ref-91\" class=\"reference\"><a href=\"#cite_note-91\"><span class=\"cite-bracket\">&#91;</span>90<span class=\"cite-bracket\">&#93;</span></a></sup> XHTML 1.1 also states that XHTML 1.1 documents should<sup id=\"cite_ref-92\" class=\"reference\"><a href=\"#cite_note-92\"><span class=\"cite-bracket\">&#91;</span>91<span class=\"cite-bracket\">&#93;</span></a></sup> be labeled with either MIME type.<sup id=\"cite_ref-93\" class=\"reference\"><a href=\"#cite_note-93\"><span class=\"cite-bracket\">&#91;</span>92<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"HTML_e-mail\">HTML e-mail</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/HTML_email\" title=\"HTML email\">HTML email</a></div>\n<p>Most graphical email clients allow the use of a subset of HTML (often ill-defined) to provide formatting and <a href=\"/wiki/Semantic_web\" class=\"mw-redirect\" title=\"Semantic web\">semantic</a> markup not available with <a href=\"/wiki/Plain_text\" title=\"Plain text\">plain text</a>. This may include typographic information like colored headings, emphasized and quoted text, inline images and diagrams. Many such clients include both a <a href=\"/wiki/Graphical_user_interface\" title=\"Graphical user interface\">GUI</a> editor for composing HTML e-mail messages and a rendering engine for displaying them. Use of HTML in e-mail is criticized by some because of compatibility issues, because it can help disguise <a href=\"/wiki/Phishing\" title=\"Phishing\">phishing</a> attacks, because of accessibility issues for blind or visually impaired people, because it can confuse <a href=\"/wiki/Email_spam\" title=\"Email spam\">spam</a> filters and because the message size is larger than plain text.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Naming_conventions\">Naming conventions</h3></div>\n<p>The most common <a href=\"/wiki/Filename_extension\" title=\"Filename extension\">filename extension</a> for <a href=\"/wiki/Computer_file\" title=\"Computer file\">files</a> containing HTML is <kbd>.html</kbd>. A common abbreviation of this is <kbd>.htm</kbd>, which originated because some early operating systems and file systems, such as <a href=\"/wiki/DOS\" title=\"DOS\">DOS</a> and the limitations imposed by <a href=\"/wiki/File_Allocation_Table\" title=\"File Allocation Table\">FAT</a> data structure, limited file extensions to <a href=\"/wiki/8.3_filename\" title=\"8.3 filename\">three letters</a>.<sup id=\"cite_ref-94\" class=\"reference\"><a href=\"#cite_note-94\"><span class=\"cite-bracket\">&#91;</span>93<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"HTML_Application\">HTML Application</h3></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"/wiki/HTML_Application\" title=\"HTML Application\">HTML Application</a></div>\n<p>An HTML Application (HTA; file extension <kbd>.hta</kbd>) is a <a href=\"/wiki/Microsoft_Windows\" title=\"Microsoft Windows\">Microsoft Windows</a> application that uses HTML and Dynamic HTML in a <a href=\"/wiki/Web_browser\" title=\"Web browser\">browser</a> to provide the application's graphical interface. A regular HTML file is confined to the security model of the <a href=\"/wiki/Browser_security\" title=\"Browser security\">web browser's security</a>, communicating only to web servers and manipulating only web page objects and <a href=\"/wiki/HTTP_cookie\" title=\"HTTP cookie\">site cookies</a>. An HTA runs as a fully trusted application and therefore has more privileges, like creation/editing/removal of files and <a href=\"/wiki/Windows_Registry\" title=\"Windows Registry\">Windows Registry</a> entries. Because they operate outside the browser's security model, HTAs cannot be executed via HTTP, but must be downloaded (just like an <a href=\"/wiki/EXE\" class=\"mw-redirect\" title=\"EXE\">EXE</a> file) and executed from local file system.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"HTML4_variations\">HTML4 variations</h2></div>\n<p>Since its inception, HTML and its associated protocols gained acceptance relatively quickly. However, no clear standards existed in the early years of the language. Though its creators originally conceived of HTML as a semantic language devoid of presentation details,<sup id=\"cite_ref-95\" class=\"reference\"><a href=\"#cite_note-95\"><span class=\"cite-bracket\">&#91;</span>94<span class=\"cite-bracket\">&#93;</span></a></sup> practical uses pushed many presentational elements and attributes into the language, driven largely by the various browser vendors. The latest standards surrounding HTML reflect efforts to overcome the sometimes chaotic development of the language<sup id=\"cite_ref-96\" class=\"reference\"><a href=\"#cite_note-96\"><span class=\"cite-bracket\">&#91;</span>95<span class=\"cite-bracket\">&#93;</span></a></sup> and to create a rational foundation for building both meaningful and well-presented documents. To return HTML to its role as a semantic language, the <a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">W3C</a> has developed style languages such as <a href=\"/wiki/Cascading_Style_Sheets\" class=\"mw-redirect\" title=\"Cascading Style Sheets\">CSS</a> and <a href=\"/wiki/XSL\" title=\"XSL\">XSL</a> to shoulder the burden of presentation. In conjunction, the HTML specification has slowly reined in the presentational elements.\n</p><p>There are two axes differentiating various variations of HTML as currently specified: SGML-based HTML versus XML-based HTML (referred to as XHTML) on one axis, and strict versus transitional (loose) versus frameset on the other axis.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"SGML-based_versus_XML-based_HTML\">SGML-based versus XML-based HTML</h3></div>\n<p>One difference in the latest<sup class=\"noprint Inline-Template\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Manual_of_Style/Dates_and_numbers#Chronological_items\" title=\"Wikipedia:Manual of Style/Dates and numbers\"><span title=\"The time period mentioned near this tag is ambiguous. (March 2022)\">when?</span></a></i>&#93;</sup> HTML specifications lies in the distinction between the SGML-based specification and the XML-based specification. The XML-based specification is usually called <a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a> to distinguish it clearly from the more traditional definition. However, the root element name continues to be \"html\" even in the XHTML-specified HTML. The W3C intended XHTML 1.0 to be identical to HTML 4.01 except where limitations of XML over the more complex SGML require workarounds. Because XHTML and HTML are closely related, they are sometimes documented in parallel. In such circumstances, some authors <a href=\"/wiki/(X)HTML\" class=\"mw-redirect\" title=\"(X)HTML\">conflate the two names</a> as (X)HTML or X(HTML).\n</p><p>Like HTML 4.01, XHTML 1.0 has three sub-specifications: strict, transitional, and frameset.\n</p><p>Aside from the different opening declarations for a document, the differences between an HTML 4.01 and XHTML 1.0 document—in each of the corresponding DTDs—are largely syntactic. The underlying syntax of HTML allows many shortcuts that XHTML does not, such as elements with optional opening or closing tags, and even empty elements which must not have an end tag. By contrast, XHTML requires all elements to have an opening tag and a closing tag. XHTML, however, also introduces a new shortcut: an XHTML tag may be opened and closed within the same tag, by including a slash before the end of the tag like this: <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">/&gt;</span></code>. The introduction of this shorthand, which is not used in the SGML declaration for HTML 4.01, may confuse earlier software unfamiliar with this new convention. A fix for this is remove the slash preceding the closing angle bracket, as such: <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code>.<sup id=\"cite_ref-97\" class=\"reference\"><a href=\"#cite_note-97\"><span class=\"cite-bracket\">&#91;</span>96<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p><p>To understand the subtle differences between HTML and XHTML, consider the transformation of a valid and well-formed XHTML 1.0 document that adheres to Appendix C (see below) into a valid HTML 4.01 document. Making this translation requires the following steps:\n</p>\n<ol><li><b>The language for an element should be specified with a <code>lang</code> attribute rather than the XHTML <code>xml:lang</code> attribute.</b> XHTML uses XML's built-in language-defining functionality attribute.</li>\n<li><b>Remove the XML namespace (<code>xmlns=URI</code>).</b> HTML has no facilities for namespaces.</li>\n<li><b>Change the document type declaration</b> from XHTML 1.0 to HTML 4.01. (see <a href=\"#Document_type_declaration\">DTD section</a> for further explanation).</li>\n<li>If present, <b>remove the XML declaration.</b> (Typically this is: <code class=\"mw-highlight mw-highlight-lang-xml mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"cp\">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;</span></code>).</li>\n<li><b>Ensure that the document's MIME type is set to <code>text/html</code>.</b> For both HTML and XHTML, this comes from the HTTP <code>Content-Type</code> header sent by the server.</li>\n<li><b>Change the XML empty-element syntax to an HTML style empty element</b> (<code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">/&gt;</span></code> to <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code>).</li></ol>\n<p>Those are the main changes necessary to translate a document from XHTML 1.0 to HTML 4.01. To translate from HTML to XHTML would also require the addition of any omitted opening or closing tags. Whether coding in HTML or XHTML it may just be best to always include the optional tags within an HTML document rather than remembering which tags can be omitted.\n</p><p>A well-formed XHTML document adheres to all the syntax requirements of XML. A valid document adheres to the content specification for XHTML, which describes the document structure.\n</p><p>The W3C recommends several conventions to ensure an easy migration between HTML and XHTML (see <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml1/#guidelines\">HTML Compatibility Guidelines</a>). The following steps can be applied to XHTML 1.0 documents only:\n</p>\n<ul><li>Include both <code>xml:lang</code> and <code>lang</code> attributes on any elements assigning language.</li>\n<li>Use the empty-element syntax only for elements specified as empty in HTML.</li>\n<li>Remove the closing slash in empty-element tags: for example <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">&gt;</span></code> instead of <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">br</span><span class=\"p\">/&gt;</span></code>.</li>\n<li>Include explicit close tags for elements that permit content but are left empty (for example, <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"></code>, not <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">div</span> <span class=\"p\">/&gt;</span></code>).</li>\n<li>Omit the XML declaration.</li></ul>\n<p>By carefully following the W3C's compatibility guidelines, a user agent should be able to interpret the document equally as HTML or XHTML. For documents that are XHTML 1.0 and have been made compatible in this way, the W3C permits them to be served either as HTML (with a <code>text/html</code> <a href=\"/wiki/MIME_type\" class=\"mw-redirect\" title=\"MIME type\">MIME type</a>), or as XHTML (with an <code>application/xhtml+xml</code> or <code>application/xml</code> MIME type). When delivered as XHTML, browsers should use an XML parser, which adheres strictly to the XML specifications for parsing the document's contents.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Transitional_versus_strict\">Transitional versus strict</h3></div>\n<p>HTML 4 defined three different versions of the language: Strict, Transitional (once called Loose), and Frameset. The Strict version is intended for new documents and is considered best practice, while the Transitional and Frameset versions were developed to make it easier to transition documents that conformed to older HTML specifications or did not conform to any specification to a version of HTML 4. The Transitional and Frameset versions allow for presentational markup, which is omitted in the Strict version. Instead, <a href=\"/wiki/Cascading_style_sheets\" class=\"mw-redirect\" title=\"Cascading style sheets\">cascading style sheets</a> are encouraged to improve the presentation of HTML documents. Because XHTML 1 only defines an XML syntax for the language defined by HTML 4, the same differences apply to XHTML 1 as well.\n</p><p>The Transitional version allows the following parts of the vocabulary, which are not included in the Strict version:\n</p>\n<ul><li><b>A looser content model</b>\n<ul><li>Inline elements and plain text are allowed directly in: <code>body</code>, <code>blockquote</code>, <code>form</code>, <code>noscript</code> and <code>noframes</code></li></ul></li>\n<li><b>Presentation related elements</b>\n<ul><li>underline (<code>u</code>) (Deprecated. can confuse a visitor with a hyperlink.)</li>\n<li>strike-through (<code>s</code>)</li>\n<li><code>center</code> (Deprecated. use CSS instead.)</li>\n<li><code>font</code> (Deprecated. use CSS instead.)</li>\n<li><code>basefont</code> (Deprecated. use CSS instead.)</li></ul></li>\n<li><b>Presentation related attributes</b>\n<ul><li><code>background</code> (Deprecated. use CSS instead.) and <code>bgcolor</code> (Deprecated. use CSS instead.) attributes for <code>body</code> (required element according to the W3C.) element.</li>\n<li><code>align</code> (Deprecated. use CSS instead.) attribute on <code>div</code>, <code>form</code>, paragraph (<code>p</code>) and heading (<code>h1</code>...<code>h6</code>) elements</li>\n<li><code>align</code> (Deprecated. use CSS instead.), <code>noshade</code> (Deprecated. use CSS instead.), <code>size</code> (Deprecated. use CSS instead.) and <code>width</code> (Deprecated. use CSS instead.) attributes on <code>hr</code> element</li>\n<li><code>align</code> (Deprecated. use CSS instead.), <code>border</code>, <code>vspace</code> and <code>hspace</code> attributes on <code>img</code> and <code>object</code> (caution: the <code>object</code> element is only supported in Internet Explorer (from the major browsers)) elements</li>\n<li><code>align</code> (Deprecated. use CSS instead.) attribute on <code>legend</code> and <code>caption</code> elements</li>\n<li><code>align</code> (Deprecated. use CSS instead.) and <code>bgcolor</code> (Deprecated. use CSS instead.) on <code>table</code> element</li>\n<li><code>nowrap</code> (Obsolete), <code>bgcolor</code> (Deprecated. use CSS instead.), <code>width</code>, <code>height</code> on <code>td</code> and <code>th</code> elements</li>\n<li><code>bgcolor</code> (Deprecated. use CSS instead.) attribute on <code>tr</code> element</li>\n<li><code>clear</code> (Obsolete) attribute on <code>br</code> element</li>\n<li><code>compact</code> attribute on <code>dl</code>, <code>dir</code> and <code>menu</code> elements</li>\n<li><code>type</code> (Deprecated. use CSS instead.), <code>compact</code> (Deprecated. use CSS instead.) and <code>start</code> (Deprecated. use CSS instead.) attributes on <code>ol</code> and <code>ul</code> elements</li>\n<li><code>type</code> and <code>value</code> attributes on <code>li</code> element</li>\n<li><code>width</code> attribute on <code>pre</code> element</li></ul></li>\n<li><b>Additional elements in Transitional specification</b>\n<ul><li><code>menu</code> (Deprecated. use CSS instead.) list (no substitute, though the unordered list, is recommended)</li>\n<li><code>dir</code> (Deprecated. use CSS instead.) list (no substitute, though the unordered list is recommended)</li>\n<li><code>isindex</code> (Deprecated.) (element requires server-side support and is typically added to documents server-side, <code>form</code> and <code>input</code> elements can be used as a substitute)</li>\n<li><code>applet</code> (Deprecated. use the <code>object</code> element instead.)</li></ul></li>\n<li><b>The <code>language</code> (Obsolete) attribute on script element</b> (redundant with the <code>type</code> attribute).</li>\n<li><b>Frame related entities</b>\n<ul><li><code>iframe</code></li>\n<li><code>noframes</code></li>\n<li><code>target</code> (Deprecated in the <code>map</code>, <code>link</code> and <code>form</code> elements.) attribute on <code>a</code>, client-side image-map (<code>map</code>), <code>link</code>, <code>form</code> and <code>base</code> elements</li></ul></li></ul>\n<p>The Frameset version includes everything in the Transitional version, as well as the <code>frameset</code> element (used instead of <code>body</code>) and the <code>frame</code> element.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Frameset_versus_transitional\">Frameset versus transitional</h3></div>\n<p>In addition to the above transitional differences, the frameset specifications (whether XHTML 1.0 or HTML 4.01) specify a different content model, with <code>frameset</code> replacing <code>body</code>, that contains either <code>frame</code> elements, or optionally <code>noframes</code> with a <code>body</code>.\n</p>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Summary_of_specification_versions\">Summary of specification versions</h3></div>\n<p>As this list demonstrates, the loose versions of the specification are maintained for legacy support. However, contrary to popular misconceptions, the move to XHTML does not imply a removal of this legacy support. Rather the X in XML stands for extensible and the W3C is modularizing the entire specification and opens it up to independent extensions. The primary achievement in the move from XHTML 1.0 to XHTML 1.1 is the modularization of the entire specification. The strict version of HTML is deployed in XHTML 1.1 through a set of modular extensions to the base XHTML 1.1 specification. Likewise, someone looking for the loose (transitional) or frameset specifications will find similar extended XHTML 1.1 support (much of it is contained in the legacy or frame modules). Modularization also allows for separate features to develop on their own timetable. So for example, XHTML 1.1 will allow quicker migration to emerging XML standards such as <a href=\"/wiki/MathML\" title=\"MathML\">MathML</a> (a presentational and semantic math language based on XML) and <a href=\"/wiki/XForms\" title=\"XForms\">XForms</a>—a new highly advanced web-form technology to replace the existing HTML forms.\n</p><p>In summary, the HTML 4 specification primarily reined in all the various HTML implementations into a single clearly written specification based on SGML. XHTML 1.0, ported this specification, as is, to the new XML-defined specification. Next, XHTML 1.1 takes advantage of the extensible nature of XML and modularizes the whole specification. XHTML 2.0 was intended to be the first step in adding new features to the specification in a standards-body-based approach.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"WHATWG_HTML_versus_HTML5\">WHATWG HTML versus HTML5</h2></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236090951\" /><div role=\"note\" class=\"hatnote navigation-not-searchable\">Main article: <a href=\"#Transition_of_HTML_publication_to_WHATWG\">§&#160;Transition of HTML publication to WHATWG</a></div>\n<p>The HTML Living Standard, which is developed by WHATWG, is the official version, while W3C HTML5 is no longer separate from WHATWG.\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"WYSIWYG_editors\">WYSIWYG editors</h2></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1305433154\">.mw-parser-output .ambox{border:1px solid #a2a9b1;border-left:10px solid #36c;background-color:#fbfbfb;box-sizing:border-box}.mw-parser-output .ambox+link+.ambox,.mw-parser-output .ambox+link+style+.ambox,.mw-parser-output .ambox+link+link+.ambox,.mw-parser-output .ambox+.mw-empty-elt+link+.ambox,.mw-parser-output .ambox+.mw-empty-elt+link+style+.ambox,.mw-parser-output .ambox+.mw-empty-elt+link+link+.ambox{margin-top:-1px}html body.mediawiki .mw-parser-output .ambox.mbox-small-left{margin:4px 1em 4px 0;overflow:hidden;width:238px;border-collapse:collapse;font-size:88%;line-height:1.25em}.mw-parser-output .ambox-speedy{border-left:10px solid #b32424;background-color:#fee7e6}.mw-parser-output .ambox-delete{border-left:10px solid #b32424}.mw-parser-output .ambox-content{border-left:10px solid #f28500}.mw-parser-output .ambox-style{border-left:10px solid #fc3}.mw-parser-output .ambox-move{border-left:10px solid #9932cc}.mw-parser-output .ambox-protection{border-left:10px solid #a2a9b1}.mw-parser-output .ambox .mbox-text{border:none;padding:0.25em 0.5em;width:100%}.mw-parser-output .ambox .mbox-image{border:none;padding:2px 0 2px 0.5em;text-align:center}.mw-parser-output .ambox .mbox-imageright{border:none;padding:2px 0.5em 2px 0;text-align:center}.mw-parser-output .ambox .mbox-empty-cell{border:none;padding:0;width:1px}.mw-parser-output .ambox .mbox-image-div{width:52px}@media(min-width:720px){.mw-parser-output .ambox{margin:0 10%}}@media print{body.ns-0 .mw-parser-output .ambox{display:none!important}}</style><table class=\"box-Missing_information plainlinks metadata ambox ambox-content\" role=\"presentation\"><tbody><tr><td class=\"mbox-image\"><div class=\"mbox-image-div\"><span typeof=\"mw:File\"><a href=\"/wiki/File:Wiki_letter_w.svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/en/thumb/6/6c/Wiki_letter_w.svg/44px-Wiki_letter_w.svg.png\" decoding=\"async\" width=\"44\" height=\"44\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/6/6c/Wiki_letter_w.svg/66px-Wiki_letter_w.svg.png 1.5x, //upload.wikimedia.org/wikipedia/en/thumb/6/6c/Wiki_letter_w.svg/88px-Wiki_letter_w.svg.png 2x\" data-file-width=\"44\" data-file-height=\"44\" /></a></span></div></td><td class=\"mbox-text\"><div class=\"mbox-text-span\">This article <b>is missing information</b> about contenteditable.<span class=\"hide-when-compact\"> Please expand the article <span class=\"anonymous-show\"><span class=\"plainlinks\"><a class=\"external text\" href=\"https://en.wikipedia.org/w/index.php?title=Talk%3AHTML&amp;preload=Template%3ASubmit+an+edit+request%2Fpreload&amp;action=edit&amp;section=new&amp;editintro=Template%3AEdit+protected%2Feditintro&amp;preloadtitle=Protected+edit+request+on+1+October+2025&amp;preloadparams%5B%5D=edit+fully-protected&amp;preloadparams%5B%5D=HTML\">by making an edit request</a></span></span><span class=\"user-show\">to include this information </span>. Further details may exist on the <a href=\"/wiki/Talk:HTML\" title=\"Talk:HTML\">talk page</a>.</span>  <span class=\"date-container\"><i>(<span class=\"date\">January 2021</span>)</i></span></div></td></tr></tbody></table>\n<p>There are some <a href=\"/wiki/WYSIWYG\" title=\"WYSIWYG\">WYSIWYG</a> editors (<i>what you see is what you get</i>), in which the user lays out everything as it is to appear in the HTML document using a <a href=\"/wiki/Graphical_user_interface\" title=\"Graphical user interface\">graphical user interface</a> (GUI), often similar to <a href=\"/wiki/Word_processor\" title=\"Word processor\">word processors</a>. The editor renders the document rather than showing the code, so authors do not require extensive knowledge of HTML.\n</p><p>The WYSIWYG editing model has been criticized,<sup id=\"cite_ref-98\" class=\"reference\"><a href=\"#cite_note-98\"><span class=\"cite-bracket\">&#91;</span>97<span class=\"cite-bracket\">&#93;</span></a></sup><sup id=\"cite_ref-99\" class=\"reference\"><a href=\"#cite_note-99\"><span class=\"cite-bracket\">&#91;</span>98<span class=\"cite-bracket\">&#93;</span></a></sup> primarily because of the low quality of the generated code; there are voices<sup class=\"noprint Inline-Template\" style=\"white-space:nowrap;\">&#91;<i><a href=\"/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Unsupported_attributions\" title=\"Wikipedia:Manual of Style/Words to watch\"><span title=\"The material near this tag possibly uses too-vague attribution or weasel words. (June 2020)\">who?</span></a></i>&#93;</sup> advocating a change to the <a href=\"/wiki/WYSIWYM\" title=\"WYSIWYM\">WYSIWYM</a> model (<i>what you see is what you mean</i>).\n</p><p>WYSIWYG editors remain a controversial topic because of their perceived flaws such as:\n</p>\n<ul><li>Relying mainly on the layout as opposed to meaning, often using markup that does not convey the intended meaning but simply copies the layout.<sup id=\"cite_ref-100\" class=\"reference\"><a href=\"#cite_note-100\"><span class=\"cite-bracket\">&#91;</span>99<span class=\"cite-bracket\">&#93;</span></a></sup></li>\n<li>Often producing extremely verbose and redundant code that fails to make use of the cascading nature of HTML and <a href=\"/wiki/CSS\" title=\"CSS\">CSS</a>.</li>\n<li>Often producing ungrammatical markup, called <a href=\"/wiki/Tag_soup\" title=\"Tag soup\">tag soup</a> or semantically incorrect markup (such as <code class=\"mw-highlight mw-highlight-lang-html mw-content-ltr\" style=\"\" dir=\"ltr\"><span class=\"p\">&lt;</span><span class=\"nt\">em</span><span class=\"p\">&gt;</span></code> for italics).</li>\n<li>As a great deal of the information in HTML documents is not in the layout, the model has been criticized for its \"what you see is all you get\"-nature.<sup id=\"cite_ref-101\" class=\"reference\"><a href=\"#cite_note-101\"><span class=\"cite-bracket\">&#91;</span>100<span class=\"cite-bracket\">&#93;</span></a></sup></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"See_also\">See also</h2></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1184024115\">.mw-parser-output .div-col{margin-top:0.3em;column-width:30em}.mw-parser-output .div-col-small{font-size:90%}.mw-parser-output .div-col-rules{column-rule:1px solid #aaa}.mw-parser-output .div-col dl,.mw-parser-output .div-col ol,.mw-parser-output .div-col ul{margin-top:0}.mw-parser-output .div-col li,.mw-parser-output .div-col dd{page-break-inside:avoid;break-inside:avoid-column}</style><div class=\"div-col\" style=\"column-width: 20em;\">\n<ul><li><a href=\"/wiki/Breadcrumb_navigation\" title=\"Breadcrumb navigation\">Breadcrumb navigation</a></li>\n<li><a href=\"/wiki/Cellpadding\" title=\"Cellpadding\">Cellpadding</a></li>\n<li><a href=\"/wiki/Comparison_of_HTML_parsers\" title=\"Comparison of HTML parsers\">Comparison of HTML parsers</a></li>\n<li><a href=\"/wiki/Dynamic_web_page\" title=\"Dynamic web page\">Dynamic web page</a></li>\n<li><a href=\"/wiki/HTML_Application\" title=\"HTML Application\">HTML Application</a></li>\n<li><a href=\"/wiki/HTML_character_references\" class=\"mw-redirect\" title=\"HTML character references\">HTML character references</a></li>\n<li><a href=\"/wiki/List_of_document_markup_languages\" title=\"List of document markup languages\">List of document markup languages</a></li>\n<li><a href=\"/wiki/List_of_XML_and_HTML_character_entity_references\" title=\"List of XML and HTML character entity references\">List of XML and HTML character entity references</a></li>\n<li><a href=\"/wiki/Microdata_(HTML)\" title=\"Microdata (HTML)\">Microdata (HTML)</a></li>\n<li><a href=\"/wiki/Microformat\" title=\"Microformat\">Microformat</a></li>\n<li><a href=\"/wiki/Polyglot_markup\" class=\"mw-redirect\" title=\"Polyglot markup\">Polyglot markup</a></li>\n<li><a href=\"/wiki/Semantic_HTML\" title=\"Semantic HTML\">Semantic HTML</a></li>\n<li><a href=\"/wiki/W3C_Markup_Validation_Service\" title=\"W3C Markup Validation Service\">W3C (X)HTML Validator</a></li>\n<li><a href=\"/wiki/Web_colors\" title=\"Web colors\">Web colors</a></li></ul>\n</div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Notes\">Notes</h2></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1239543626\">.mw-parser-output .reflist{margin-bottom:0.5em;list-style-type:decimal}@media screen{.mw-parser-output .reflist{font-size:90%}}.mw-parser-output .reflist .references{font-size:100%;margin-bottom:0;list-style-type:inherit}.mw-parser-output .reflist-columns-2{column-width:30em}.mw-parser-output .reflist-columns-3{column-width:25em}.mw-parser-output .reflist-columns{margin-top:0.3em}.mw-parser-output .reflist-columns ol{margin-top:0}.mw-parser-output .reflist-columns li{page-break-inside:avoid;break-inside:avoid-column}.mw-parser-output .reflist-upper-alpha{list-style-type:upper-alpha}.mw-parser-output .reflist-upper-roman{list-style-type:upper-roman}.mw-parser-output .reflist-lower-alpha{list-style-type:lower-alpha}.mw-parser-output .reflist-lower-greek{list-style-type:lower-greek}.mw-parser-output .reflist-lower-roman{list-style-type:lower-roman}</style><div class=\"reflist reflist-lower-alpha\">\n<div class=\"mw-references-wrap\"><ol class=\"references\" data-mw-group=\"lower-alpha\">\n<li id=\"cite_note-3\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-3\">^</a></b></span> <span class=\"reference-text\">Even though HTML can be run in a browser, it is not viewed as a <a href=\"/wiki/Programming_language\" title=\"Programming language\">programming language</a> in programming language discourse.<sup id=\"cite_ref-2\" class=\"reference\"><a href=\"#cite_note-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup></span>\n</li>\n</ol></div></div>\n<p><br />\n</p>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"References\">References</h2></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239543626\" /><div class=\"reflist reflist-columns references-column-width\" style=\"column-width: 30em;\">\n<ol class=\"references\">\n<li id=\"cite_note-1\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-1\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/html/\">\"W3C Html\"</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=W3C+Html&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fhtml%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-2\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-2\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHermansSchlesinger2024\" class=\"citation book cs1\"><a href=\"/wiki/Felienne_Hermans\" title=\"Felienne Hermans\">Hermans, Felienne</a>; Schlesinger, Ari (2024-10-17). \"A Case for Feminism in Programming Language Design\". <i>Proceedings of the 2024 ACM SIGPLAN International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software</i>. ACM. pp.&#160;<span class=\"nowrap\">205–</span>222. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.1145%2F3689492.3689809\">10.1145/3689492.3689809</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/979-8-4007-1215-9\" title=\"Special:BookSources/979-8-4007-1215-9\"><bdi>979-8-4007-1215-9</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=A+Case+for+Feminism+in+Programming+Language+Design&amp;rft.btitle=Proceedings+of+the+2024+ACM+SIGPLAN+International+Symposium+on+New+Ideas%2C+New+Paradigms%2C+and+Reflections+on+Programming+and+Software&amp;rft.pages=205-222&amp;rft.pub=ACM&amp;rft.date=2024-10-17&amp;rft_id=info%3Adoi%2F10.1145%2F3689492.3689809&amp;rft.isbn=979-8-4007-1215-9&amp;rft.aulast=Hermans&amp;rft.aufirst=Felienne&amp;rft.au=Schlesinger%2C+Ari&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-deprecated-4\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-deprecated_4-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/REC-html40-971218/conform.html#deprecated\">\"HTML 4.0 Specification — W3C Recommendation — Conformance: requirements and recommendations\"</a>. World Wide Web Consortium. December 18, 1997. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20150705040855/http://www.w3.org/TR/REC-html40-971218/conform.html\">Archived</a> from the original on July 5, 2015<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">July 6,</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4.0+Specification+%E2%80%94+W3C+Recommendation+%E2%80%94+Conformance%3A+requirements+and+recommendations&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1997-12-18&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2FREC-html40-971218%2Fconform.html%23deprecated&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-5\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-5\">^</a></b></span> <span class=\"reference-text\">Tim Berners-Lee, \"<a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/History/1989/proposal.html\">Information Management: A Proposal</a>\". CERN (March 1989, May 1990). W3C.</span>\n</li>\n<li id=\"cite_note-6\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-6\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-Lee\" class=\"citation web cs1\">Berners-Lee, Tim. <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/DesignIssues/Uses.html\">\"Intended Uses\"</a>. <i>W3C</i>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C&amp;rft.atitle=Intended+Uses&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FDesignIssues%2FUses.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-7\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-7\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"http://info.cern.ch/hypertext/WWW/MarkUp/Tags.html\">\"Tags used in HTML\"</a>. <i>info.cern.ch</i>. October 1991<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2 March</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=info.cern.ch&amp;rft.atitle=Tags+used+in+HTML&amp;rft.date=1991-10&amp;rft_id=http%3A%2F%2Finfo.cern.ch%2Fhypertext%2FWWW%2FMarkUp%2FTags.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-tagshtml-8\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-tagshtml_8-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-tagshtml_8-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-tagshtml_8-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/History/19921103-hypertext/hypertext/WWW/MarkUp/Tags.html\">\"Tags used in HTML\"</a>. World Wide Web Consortium. November 3, 1992. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20100131184344/http://www.w3.org/History/19921103-hypertext/hypertext/WWW/MarkUp/Tags.html\">Archived</a> from the original on January 31, 2010<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Tags+used+in+HTML&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1992-11-03&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FHistory%2F19921103-hypertext%2Fhypertext%2FWWW%2FMarkUp%2FTags.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-9\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-9\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-Lee1991\" class=\"citation web cs1\">Berners-Lee, Tim (October 29, 1991). <a rel=\"nofollow\" class=\"external text\" href=\"http://lists.w3.org/Archives/Public/www-talk/1991SepOct/0003.html\">\"Re: status. Re: X11 BROWSER for WWW\"</a>. World Wide Web Consortium. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20070524045009/http://lists.w3.org/Archives/Public/www-talk/1991SepOct/0003.html\">Archived</a> from the original on May 24, 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">April 8,</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Re%3A+status.+Re%3A+X11+BROWSER+for+WWW&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1991-10-29&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft_id=http%3A%2F%2Flists.w3.org%2FArchives%2FPublic%2Fwww-talk%2F1991SepOct%2F0003.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-10\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-10\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/1999/REC-html401-19991224/index/elements\">\"Index of the HTML 4 elements\"</a>. World Wide Web Consortium. December 24, 1999. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20070505172415/https://www.w3.org/TR/1999/REC-html401-19991224/index/elements\">Archived</a> from the original on May 5, 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">April 8,</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Index+of+the+HTML+4+elements&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1999-12-24&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F1999%2FREC-html401-19991224%2Findex%2Felements&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-11\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-11\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-Lee1991\" class=\"citation web cs1\">Berners-Lee, Tim (December 9, 1991). <a rel=\"nofollow\" class=\"external text\" href=\"http://lists.w3.org/Archives/Public/www-talk/1991NovDec/0020.html\">\"Re: SGML/HTML docs, X Browser\"</a>. <i>w3</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20071222060359/http://lists.w3.org/Archives/Public/www-talk/1991NovDec/0020.html\">Archived</a> from the original on December 22, 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">June 16,</span> 2007</span>. <q>SGML is very general. HTML is a specific application of the SGML basic syntax applied to hypertext documents with simple structure.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=w3&amp;rft.atitle=Re%3A+SGML%2FHTML+docs%2C+X+Browser&amp;rft.date=1991-12-09&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft_id=http%3A%2F%2Flists.w3.org%2FArchives%2FPublic%2Fwww-talk%2F1991NovDec%2F0020.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-12\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-12\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1993\" class=\"citation web cs1\">Berners-Lee, Tim; Connolly, Daniel (June 1993). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt\">\"Hypertext Markup Language (HTML): A Representation of Textual Information and MetaInformation for Retrieval and Interchange\"</a>. <i>w3</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20170103041713/https://www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt\">Archived</a> from the original on January 3, 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">January 4,</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=w3&amp;rft.atitle=Hypertext+Markup+Language+%28HTML%29%3A+A+Representation+of+Textual+Information+and+MetaInformation+for+Retrieval+and+Interchange&amp;rft.date=1993-06&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2Fdraft-ietf-iiir-html-01.txt&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-html+-13\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-html+_13-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRaggett,_Dave\" class=\"citation web cs1\"><a href=\"/wiki/Dave_Raggett\" title=\"Dave Raggett\">Raggett, Dave</a>. <a rel=\"nofollow\" class=\"external text\" href=\"http://www.w3.org/MarkUp/htmlplus_paper/htmlplus.html\">\"A Review of the HTML+ Document Format\"</a>. <i>w3</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20000229205146/http://www.w3.org/MarkUp/htmlplus_paper/htmlplus.html\">Archived</a> from the original on February 29, 2000<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">May 22,</span> 2020</span>. <q>The hypertext markup language HTML was developed as a simple non-proprietary delivery format for global hypertext. HTML+ is a set of modular extensions to HTML and has been developed in response to a growing understanding of the needs of information providers. These extensions include text flow around floating figures, fill-out forms, tables, and mathematical equations.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=w3&amp;rft.atitle=A+Review+of+the+HTML%2B+Document+Format&amp;rft.au=Raggett%2C+Dave&amp;rft_id=http%3A%2F%2Fwww.w3.org%2FMarkUp%2Fhtmlplus_paper%2Fhtmlplus.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-14\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-14\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1995\" class=\"citation cs1\"><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Berners-Lee, Tim</a>; <a href=\"/wiki/Daniel_Connolly_(computer_scientist)\" class=\"mw-redirect\" title=\"Daniel Connolly (computer scientist)\">Connolly, Daniel W.</a> (November 1995). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1866\"><i>Hypertext Markup Language - 2.0</i></a>. Network Working Group. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.17487%2FRFC1866\">10.17487/RFC1866</a></span>. <a href=\"/wiki/Request_for_Comments\" title=\"Request for Comments\">RFC</a> <a rel=\"nofollow\" class=\"external text\" href=\"https://datatracker.ietf.org/doc/html/rfc1866\">1866</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Hypertext+Markup+Language+-+2.0&amp;rft.pub=Network+Working+Group&amp;rft.date=1995-11&amp;rft_id=info%3Adoi%2F10.17487%2F&#82;FC1866&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel+W.&amp;rft_id=https%3A%2F%2Fwww.rfc-editor.org%2Frfc%2Frfc1866&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> <i>Historic.</i>  Obsoleted by <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" />RFC&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc2854\">2854</a>. <q>This document thus defines an HTML 2.0 (to distinguish it from the previous informal specifications). Future (generally upwardly compatible) versions of HTML with new features will be released with higher version numbers.</q></span>\n</li>\n<li id=\"cite_note-raggett-15\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-raggett_15-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-raggett_15-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-raggett_15-2\"><sup><i><b>c</b></i></sup></a> <a href=\"#cite_ref-raggett_15-3\"><sup><i><b>d</b></i></sup></a> <a href=\"#cite_ref-raggett_15-4\"><sup><i><b>e</b></i></sup></a> <a href=\"#cite_ref-raggett_15-5\"><sup><i><b>f</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRaggett1998\" class=\"citation book cs1\">Raggett, Dave (1998). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20070809234115/https://www.w3.org/People/Raggett/book4/ch02.html\"><i>Raggett on HTML 4</i></a>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/People/Raggett/book4/ch02.html\">the original</a> on August 9, 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">July 9,</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Raggett+on+HTML+4&amp;rft.date=1998&amp;rft.aulast=Raggett&amp;rft.aufirst=Dave&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FPeople%2FRaggett%2Fbook4%2Fch02.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-16\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-16\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2014/10/html5-rec.html.en\">\"HTML5 – Hypertext Markup Language – 5.0\"</a>. Internet Engineering Task Force. 28 October 2014. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20141028233921/https://www.w3.org/2014/10/html5-rec.html.en\">Archived</a> from the original on October 28, 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 25,</span> 2014</span>. <q>This document recommends HTML 5.0 after completion.</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5+%E2%80%93+Hypertext+Markup+Language+%E2%80%93+5.0&amp;rft.pub=Internet+Engineering+Task+Force&amp;rft.date=2014-10-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2014%2F10%2Fhtml5-rec.html.en&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-17\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-17\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/REC-html32\">\"HTML 3.2 Reference Specification\"</a>. World Wide Web Consortium. January 14, 1997<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+3.2+Reference+Specification&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1997-01-14&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2FREC-html32&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-18\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-18\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/HTML-WG/\">\"IETF HTML WG\"</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">June 16,</span> 2007</span>. <q>Note: This working group is closed</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=IETF+HTML+WG&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2FHTML-WG%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-engelfriet-19\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-engelfriet_19-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-engelfriet_19-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFEngelfriet\" class=\"citation web cs1\"><a href=\"/w/index.php?title=Arnoud_Engelfriet&amp;action=edit&amp;redlink=1\" class=\"new\" title=\"Arnoud Engelfriet (page does not exist)\">Engelfriet, Arnoud</a>. <a rel=\"nofollow\" class=\"external text\" href=\"http://htmlhelp.com/reference/wilbur/intro.html\">\"Introduction to Wilbur\"</a>. <i>htmlhelp.com</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">June 16,</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=htmlhelp.com&amp;rft.atitle=Introduction+to+Wilbur&amp;rft.aulast=Engelfriet&amp;rft.aufirst=Arnoud&amp;rft_id=http%3A%2F%2Fhtmlhelp.com%2Freference%2Fwilbur%2Fintro.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-20\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-20\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/REC-html40-971218/\">\"HTML 4.0 Specification\"</a>. World Wide Web Consortium. December 18, 1997<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4.0+Specification&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1997-12-18&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2FREC-html40-971218%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-21\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-21\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html4/conform.html#h-4.2\">\"HTML 4 – 4 Conformance: requirements and recommendations\"</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">December 30,</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4+%E2%80%93+4+Conformance%3A+requirements+and+recommendations&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml4%2Fconform.html%23h-4.2&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-22\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-22\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/1998/REC-html40-19980424/\">\"HTML 4.0 Specification\"</a>. World Wide Web Consortium. April 24, 1998<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4.0+Specification&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1998-04-24&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F1998%2FREC-html40-19980424%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-23\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-23\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html401/\">\"HTML 4.01 Specification\"</a>. World Wide Web Consortium. December 24, 1999<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4.01+Specification&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1999-12-24&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml401%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-24\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-24\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/html4-updates/errata\">\"HTML 4 Errata\"</a>. W3C<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">March 2,</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+4+Errata&amp;rft.pub=W3C&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2Fhtml4-updates%2Ferrata&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-iso-html-25\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-iso-html_25-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-iso-html_25-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFISO2000\" class=\"citation web cs1\">ISO (2000). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.iso.org/standard/27688.html\">\"ISO/IEC 15445:2000 – Information technology – Document description and processing languages – HyperText Markup Language (HTML)\"</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">March 1,</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=ISO%2FIEC+15445%3A2000+%E2%80%93+Information+technology+%E2%80%93+Document+description+and+processing+languages+%E2%80%93+HyperText+Markup+Language+%28HTML%29&amp;rft.date=2000&amp;rft.au=ISO&amp;rft_id=https%3A%2F%2Fwww.iso.org%2Fstandard%2F27688.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-26\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-26\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.scss.tcd.ie/misc/15445/15445.HTML\">\"ISO/IEC 15445:2000(E) ISO-HTML\"</a>. <i>www.scss.tcd.ie</i>. Geneva, CH: ISO/IEC. May 15, 2000<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">March 1,</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=www.scss.tcd.ie&amp;rft.atitle=ISO%2FIEC+15445%3A2000%28E%29+ISO-HTML&amp;rft.date=2000-05-15&amp;rft_id=https%3A%2F%2Fwww.scss.tcd.ie%2Fmisc%2F15445%2F15445.HTML&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-27\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-27\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/2014/REC-html5-20141028/\">\"HTML5: A vocabulary and associated APIs for HTML and XHTML\"</a>. World Wide Web Consortium. 28 October 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 October</span> 2014</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5%3A+A+vocabulary+and+associated+APIs+for+HTML+and+XHTML&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2014-10-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F2014%2FREC-html5-20141028%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-28\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-28\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation pressrelease cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2014/10/html5-rec.html.en\">\"Open Web Platform Milestone Achieved with HTML5 Recommendation\"</a> (Press release). World Wide Web Consortium. 28 October 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">31 October</span> 2014</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Open+Web+Platform+Milestone+Achieved+with+HTML5+Recommendation&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2014-10-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2014%2F10%2Fhtml5-rec.html.en&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-29\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-29\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/2016/REC-html51-20161101/\">\"HTML 5.1\"</a>. World Wide Web Consortium. 1 November 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.1&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2016-11-01&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F2016%2FREC-html51-20161101%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-30\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-30\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/news/archives/5932\">\"HTML 5.1 is a W3C Recommendation\"</a>. World Wide Web Consortium. 1 November 2016<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.1+is+a+W3C+Recommendation&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2016-11-01&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2Fnews%2Farchives%2F5932&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-31\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-31\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFPhilippe_le_Hegaret2016\" class=\"citation web cs1\">Philippe le Hegaret (17 November 2016). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/2016/11/html-5-1-is-the-gold-standard/\">\"HTML 5.1 is the gold standard\"</a>. World Wide Web Consortium<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">6 January</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.1+is+the+gold+standard&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2016-11-17&amp;rft.au=Philippe+le+Hegaret&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2F2016%2F11%2Fhtml-5-1-is-the-gold-standard%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-32\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-32\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/2017/REC-html52-20171214/\">\"HTML 5.2\"</a>. World Wide Web Consortium. 14 December 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 December</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.2&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2017-12-14&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F2017%2FREC-html52-20171214%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-33\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-33\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/news/archives/6696\">\"HTML 5.2 is now a W3C Recommendation\"</a>. World Wide Web Consortium. 14 December 2017<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 December</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.2+is+now+a+W3C+Recommendation&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2017-12-14&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2Fnews%2Farchives%2F6696&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-34\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-34\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCharles_McCathie_Nevile2017\" class=\"citation web cs1\">Charles McCathie Nevile (14 December 2017). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/2017/12/html-5-2-is-done-html-5-3-is-coming/\">\"HTML 5.2 is done, HTML 5.3 is coming\"</a>. World Wide Web Consortium<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">15 December</span> 2017</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+5.2+is+done%2C+HTML+5.3+is+coming&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2017-12-14&amp;rft.au=Charles+McCathie+Nevile&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2F2017%2F12%2Fhtml-5-2-is-done-html-5-3-is-coming%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-35\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-35\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFConnolly1992\" class=\"citation web cs1\"><a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (6 June 1992). <a rel=\"nofollow\" class=\"external text\" href=\"http://lists.w3.org/Archives/Public/www-talk/1992MayJun/0020.html\">\"MIME as a hypertext architecture\"</a>. CERN<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 October</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=MIME+as+a+hypertext+architecture&amp;rft.pub=CERN&amp;rft.date=1992-06-06&amp;rft.aulast=Connolly&amp;rft.aufirst=Daniel&amp;rft_id=http%3A%2F%2Flists.w3.org%2FArchives%2FPublic%2Fwww-talk%2F1992MayJun%2F0020.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-36\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-36\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFConnolly1992\" class=\"citation web cs1\"><a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (15 July 1992). <a rel=\"nofollow\" class=\"external text\" href=\"http://lists.w3.org/Archives/Public/www-talk/1992JulAug/0020.html\">\"HTML DTD enclosed\"</a>. CERN<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 October</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+DTD+enclosed&amp;rft.pub=CERN&amp;rft.date=1992-07-15&amp;rft.aulast=Connolly&amp;rft.aufirst=Daniel&amp;rft_id=http%3A%2F%2Flists.w3.org%2FArchives%2FPublic%2Fwww-talk%2F1992JulAug%2F0020.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-37\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-37\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFConnolly1992\" class=\"citation web cs1\"><a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (18 August 1992). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120314055308/http://lost-contact.mit.edu/afs/cern.ch/w3.org/www/Frame/fminit2.0/html.dtd\">\"document type declaration subset for Hyper Text Markup Language as defined by the World Wide Web project\"</a>. CERN. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://lost-contact.mit.edu/afs/cern.ch/w3.org/www/Frame/fminit2.0/html.dtd\">the original</a> on 14 March 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 October</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=document+type+declaration+subset+for+Hyper+Text+Markup+Language+as+defined+by+the+World+Wide+Web+project&amp;rft.pub=CERN&amp;rft.date=1992-08-18&amp;rft.aulast=Connolly&amp;rft.aufirst=Daniel&amp;rft_id=http%3A%2F%2Flost-contact.mit.edu%2Fafs%2Fcern.ch%2Fw3.org%2Fwww%2FFrame%2Ffminit2.0%2Fhtml.dtd&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-html11-38\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-html11_38-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-html11_38-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFConnolly1992\" class=\"citation web cs1\"><a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (24 November 1992). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120118155040/http://lost-contact.mit.edu/afs/cern.ch/w3.org/www/MarkUp/Connolly/921125/archive.sh#html.dtd\">\"Document Type Definition for the Hyper Text Markup Language as used by the World Wide Web application\"</a>. CERN. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://lost-contact.mit.edu/afs/cern.ch/w3.org/www/MarkUp/Connolly/921125/archive.sh#html.dtd\">the original</a> on 18 January 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 October</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Document+Type+Definition+for+the+Hyper+Text+Markup+Language+as+used+by+the+World+Wide+Web+application&amp;rft.pub=CERN&amp;rft.date=1992-11-24&amp;rft.aulast=Connolly&amp;rft.aufirst=Daniel&amp;rft_id=http%3A%2F%2Flost-contact.mit.edu%2Fafs%2Fcern.ch%2Fw3.org%2Fwww%2FMarkUp%2FConnolly%2F921125%2Farchive.sh%23html.dtd&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> See section \"Revision History\"</span>\n</li>\n<li id=\"cite_note-39\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-39\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1993\" class=\"citation web cs1\"><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Berners-Lee, Tim</a>; <a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (June 1993). <a rel=\"nofollow\" class=\"external text\" href=\"http://tools.ietf.org/html/draft-ietf-iiir-html-00\">\"Hyper Text Markup Language (HTML) Internet-Draft version 1.1\"</a>. IETF IIIR Working Group<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 September</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Hyper+Text+Markup+Language+%28HTML%29+Internet-Draft+version+1.1&amp;rft.pub=IETF+IIIR+Working+Group&amp;rft.date=1993-06&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel&amp;rft_id=http%3A%2F%2Ftools.ietf.org%2Fhtml%2Fdraft-ietf-iiir-html-00&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-ietfiiir-40\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-ietfiiir_40-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1993\" class=\"citation web cs1\"><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Berners-Lee, Tim</a>; <a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (June 1993). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt\">\"Hypertext Markup Language (HTML) Internet-Draft version 1.2\"</a>. IETF IIIR Working Group<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 September</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Hypertext+Markup+Language+%28HTML%29+Internet-Draft+version+1.2&amp;rft.pub=IETF+IIIR+Working+Group&amp;rft.date=1993-06&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2Fdraft-ietf-iiir-html-01.txt&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-41\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-41\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRaggett1993\" class=\"citation journal cs1\">Raggett, Dave (1993-11-08). <a rel=\"nofollow\" class=\"external text\" href=\"https://datatracker.ietf.org/doc/draft-raggett-www-html/history/\">\"History for draft-raggett-www-html-00\"</a>. <i>IETF Datatracker</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-11-18</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=IETF+Datatracker&amp;rft.atitle=History+for+draft-raggett-www-html-00&amp;rft.date=1993-11-08&amp;rft.aulast=Raggett&amp;rft.aufirst=Dave&amp;rft_id=https%3A%2F%2Fdatatracker.ietf.org%2Fdoc%2Fdraft-raggett-www-html%2Fhistory%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-42\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-42\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1994\" class=\"citation journal cs1\"><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Berners-Lee, Tim</a>; <a href=\"/wiki/Dan_Connolly_(computer_scientist)\" title=\"Dan Connolly (computer scientist)\">Connolly, Daniel</a> (28 November 1994). <a rel=\"nofollow\" class=\"external text\" href=\"http://tools.ietf.org/html/draft-ietf-html-spec-00\">\"HyperText Markup Language Specification – 2.0 INTERNET DRAFT\"</a>. <i>Internet Engineering Task Force</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">24 October</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Internet+Engineering+Task+Force&amp;rft.atitle=HyperText+Markup+Language+Specification+%E2%80%93+2.0+INTERNET+DRAFT&amp;rft.date=1994-11-28&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel&amp;rft_id=http%3A%2F%2Ftools.ietf.org%2Fhtml%2Fdraft-ietf-html-spec-00&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-43\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-43\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFConnolly1995\" class=\"citation journal cs1\">Connolly, Daniel W. (1995-05-16). <a rel=\"nofollow\" class=\"external text\" href=\"https://tools.ietf.org/html/draft-ietf-html-spec-02#section-1.1\">\"Hypertext Markup Language – 2.0\"</a>. <i>tools.ietf.org</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2019-11-18</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=tools.ietf.org&amp;rft.atitle=Hypertext+Markup+Language+%E2%80%93+2.0&amp;rft.date=1995-05-16&amp;rft.aulast=Connolly&amp;rft.aufirst=Daniel+W.&amp;rft_id=https%3A%2F%2Ftools.ietf.org%2Fhtml%2Fdraft-ietf-html-spec-02%23section-1.1&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-rfc1866-44\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-rfc1866_44-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeConnolly1995\" class=\"citation cs1\"><a href=\"/wiki/Tim_Berners-Lee\" title=\"Tim Berners-Lee\">Berners-Lee, Tim</a>; <a href=\"/wiki/Daniel_Connolly_(computer_scientist)\" class=\"mw-redirect\" title=\"Daniel Connolly (computer scientist)\">Connolly, Daniel W.</a> (November 1995). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc1866\"><i>Hypertext Markup Language - 2.0</i></a>. Network Working Group. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.17487%2FRFC1866\">10.17487/RFC1866</a></span>. <a href=\"/wiki/Request_for_Comments\" title=\"Request for Comments\">RFC</a> <a rel=\"nofollow\" class=\"external text\" href=\"https://datatracker.ietf.org/doc/html/rfc1866\">1866</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Hypertext+Markup+Language+-+2.0&amp;rft.pub=Network+Working+Group&amp;rft.date=1995-11&amp;rft_id=info%3Adoi%2F10.17487%2F&#82;FC1866&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Connolly%2C+Daniel+W.&amp;rft_id=https%3A%2F%2Fwww.rfc-editor.org%2Frfc%2Frfc1866&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> <i>Historic.</i>  Obsoleted by <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" />RFC&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc2854\">2854</a>. </span>\n</li>\n<li id=\"cite_note-45\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-45\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/html3/\">\"HTML 3.0 Draft (Expired!) Materials\"</a>. World Wide Web Consortium. December 21, 1995<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+3.0+Draft+%28Expired%21%29+Materials&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1995-12-21&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2Fhtml3%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-html30cover-46\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-html30cover_46-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-html30cover_46-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/html3/CoverPage\">\"HyperText Markup Language Specification Version 3.0\"</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">June 16,</span> 2007</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HyperText+Markup+Language+Specification+Version+3.0&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2Fhtml3%2FCoverPage&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-47\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-47\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRaggett1995\" class=\"citation web cs1\">Raggett, Dave (28 March 1995). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/People/Raggett/html3/html3.txt\">\"HyperText Markup Language Specification Version 3.0\"</a>. <i>HTML 3.0 Internet Draft Expires in six months</i>. <a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">17 June</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=HTML+3.0+Internet+Draft+Expires+in+six+months&amp;rft.atitle=HyperText+Markup+Language+Specification+Version+3.0&amp;rft.date=1995-03-28&amp;rft.aulast=Raggett&amp;rft.aufirst=Dave&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FPeople%2FRaggett%2Fhtml3%2Fhtml3.txt&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-48\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-48\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBowers1998\" class=\"citation book cs1\">Bowers, N. (1998). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.usenix.org/publications/library/proceedings/usenix98/freenix/bowers.pdf\">\"Weblint: just another perl hack\"</a> <span class=\"cs1-format\">(PDF)</span>. <i>1998 USENIX Annual Technical Conference (USENIX ATC 98)</i>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=bookitem&amp;rft.atitle=Weblint%3A+just+another+perl+hack&amp;rft.btitle=1998+USENIX+Annual+Technical+Conference+%28USENIX+ATC+98%29&amp;rft.date=1998&amp;rft.aulast=Bowers&amp;rft.aufirst=N.&amp;rft_id=https%3A%2F%2Fwww.usenix.org%2Fpublications%2Flibrary%2Fproceedings%2Fusenix98%2Ffreenix%2Fbowers.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-49\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-49\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFLieBos1997\" class=\"citation book cs1\"><a href=\"/wiki/H%C3%A5kon_Wium_Lie\" title=\"Håkon Wium Lie\">Lie, Håkon Wium</a>; <a href=\"/wiki/Bert_Bos\" title=\"Bert Bos\">Bos, Bert</a> (April 1997). <span class=\"id-lock-registration\" title=\"Free registration required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/cascadingstylesh00lieh\"><i>Cascading style sheets: designing for the Web</i></a></span>. Addison Wesley Longman. p.&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/cascadingstylesh00lieh/page/263\">263</a>. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-201-41998-6\" title=\"Special:BookSources/978-0-201-41998-6\"><bdi>978-0-201-41998-6</bdi></a><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">9 June</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Cascading+style+sheets%3A+designing+for+the+Web&amp;rft.pages=263&amp;rft.pub=Addison+Wesley+Longman&amp;rft.date=1997-04&amp;rft.isbn=978-0-201-41998-6&amp;rft.aulast=Lie&amp;rft.aufirst=H%C3%A5kon+Wium&amp;rft.au=Bos%2C+Bert&amp;rft_id=https%3A%2F%2Farchive.org%2Fdetails%2Fcascadingstylesh00lieh&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-50\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-50\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html5/\">\"HTML5\"</a>. World Wide Web Consortium. June 10, 2008<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2008-06-10&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml5%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-51\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-51\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/2008/01/html5-is-html-and-xml/\">\"HTML5, one vocabulary, two serializations\"</a>. 15 January 2008<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">February 25,</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5%2C+one+vocabulary%2C+two+serializations&amp;rft.date=2008-01-15&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2F2008%2F01%2Fhtml5-is-html-and-xml%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-w3c2014-52\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-w3c2014_52-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2011/02/htmlwg-pr.html\">\"W3C Confirms May 2011 for HTML5 Last Call, Targets 2014 for HTML5 Standard\"</a>. <a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a>. 14 February 2011<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 February</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=W3C+Confirms+May+2011+for+HTML5+Last+Call%2C+Targets+2014+for+HTML5+Standard&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2011-02-14&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2011%2F02%2Fhtmlwg-pr.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-53\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-53\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFHickson,_Ian2011\" class=\"citation web cs1\">Hickson, Ian (January 19, 2011). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20191006023430/https://blog.whatwg.org/html-is-the-new-html5\">\"HTML Is the New HTML5\"</a>. <i>The WHATWG Blog</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://blog.whatwg.org/html-is-the-new-html5\">the original</a> on 6 October 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">21 January</span> 2011</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=The+WHATWG+Blog&amp;rft.atitle=HTML+Is+the+New+HTML5&amp;rft.date=2011-01-19&amp;rft.au=Hickson%2C+Ian&amp;rft_id=http%3A%2F%2Fblog.whatwg.org%2Fhtml-is-the-new-html5&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-54\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-54\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFGrannell2012\" class=\"citation web cs1\">Grannell, Craig (July 23, 2012). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20120725214739/http://www.netmagazine.com/news/html5-gets-splits-122102\">\"HTML5 gets the splits\"</a>. Net magazine. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://www.netmagazine.com/news/html5-gets-splits-122102\">the original</a> on Jul 25, 2012<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">23 July</span> 2012</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5+gets+the+splits&amp;rft.pub=Net+magazine&amp;rft.date=2012-07-23&amp;rft.aulast=Grannell&amp;rft.aufirst=Craig&amp;rft_id=http%3A%2F%2Fwww.netmagazine.com%2Fnews%2Fhtml5-gets-splits-122102&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-55\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-55\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/2012/CR-html5-20121217/\">\"HTML5\"</a>. W3C. 2012-12-17<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2013-06-15</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML5&amp;rft.pub=W3C&amp;rft.date=2012-12-17&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F2012%2FCR-html5-20121217%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3Crec-56\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3Crec_56-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.whatwg.org/wiki/FAQ#What.27s_this_I_hear_about_2022.3F\">\"When Will HTML5 Be Finished?\"</a>. <i>FAQ</i>. WHAT Working Group<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 November</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=FAQ&amp;rft.atitle=When+Will+HTML5+Be+Finished%3F&amp;rft_id=http%3A%2F%2Fwiki.whatwg.org%2Fwiki%2FFAQ%23What.27s_this_I_hear_about_2022.3F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-57\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-57\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/news/archives/4074\">\"Call for Review: HTML5 Proposed Recommendation Published W3C News\"</a>. W3C. 2014-09-16<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2014-09-27</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Call+for+Review%3A+HTML5+Proposed+Recommendation+Published+W3C+News&amp;rft.pub=W3C&amp;rft.date=2014-09-16&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2Fnews%2Farchives%2F4074&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-58\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-58\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2014/10/html5-rec.html.en\">\"Open Web Platform Milestone Achieved with HTML5 Recommendation\"</a>. W3C. 28 October 2014<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 October</span> 2014</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Open+Web+Platform+Milestone+Achieved+with+HTML5+Recommendation&amp;rft.pub=W3C&amp;rft.date=2014-10-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2014%2F10%2Fhtml5-rec.html.en&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-finalars-59\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-finalars_59-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://arstechnica.com/information-technology/2014/10/html5-specification-finalized-squabbling-over-who-writes-the-specs-continues/\">\"HTML5 specification finalized, squabbling over specs continues\"</a>. <i>Ars Technica</i>. 2014-10-29<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2014-10-29</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Ars+Technica&amp;rft.atitle=HTML5+specification+finalized%2C+squabbling+over+specs+continues&amp;rft.date=2014-10-29&amp;rft_id=https%3A%2F%2Farstechnica.com%2Finformation-technology%2F2014%2F10%2Fhtml5-specification-finalized-squabbling-over-who-writes-the-specs-continues%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-60\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-60\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/multipage/introduction.html#html-vs-xhtml\">\"HTML vs XML syntax\"</a>. WHATWG<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">22 March</span> 2023</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+vs+XML+syntax&amp;rft.pub=WHATWG&amp;rft_id=https%3A%2F%2Fhtml.spec.whatwg.org%2Fmultipage%2Fintroduction.html%23html-vs-xhtml&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-61\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-61\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml1/\">\"XHTML 1.0: The Extensible HyperText Markup Language (Second Edition)\"</a>. World Wide Web Consortium. January 26, 2000<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+1.0%3A+The+Extensible+HyperText+Markup+Language+%28Second+Edition%29&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2000-01-26&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml1%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-62\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-62\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml11/\">\"XHTML 1.1 – Module-based XHTML&#160;— Second Edition\"</a>. World Wide Web Consortium. February 16, 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+1.1+%E2%80%93+Module-based+XHTML+%E2%80%94+Second+Edition&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2007-02-16&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml11%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-63\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-63\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/2001/REC-xhtml-modularization-20010410/\">\"Modularization of XHTML\"</a>. <i>W3C</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2017-01-04</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C&amp;rft.atitle=Modularization+of+XHTML&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F2001%2FREC-xhtml-modularization-20010410%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-64\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-64\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml2/\">\"XHTM 2.0\"</a>. World Wide Web Consortium. July 26, 2006<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTM+2.0&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2006-07-26&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml2%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-65\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-65\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/News/2009#item119\">\"XHTML 2 Working Group Expected to Stop Work End of 2009, W3C to Increase Resources on HTML5\"</a>. World Wide Web Consortium. July 17, 2009<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+2+Working+Group+Expected+to+Stop+Work+End+of+2009%2C+W3C+to+Increase+Resources+on+HTML5&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2009-07-17&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FNews%2F2009%23item119&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-66\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-66\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2009/06/xhtml-faq.html\">\"W3C XHTML FAQ\"</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=W3C+XHTML+FAQ&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2009%2F06%2Fxhtml-faq.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3C_transfer_blog-67\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3C_transfer_blog_67-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFJaffe2019\" class=\"citation web cs1\">Jaffe, Jeff (28 May 2019). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/blog/2019/05/w3c-and-whatwg-to-work-together-to-advance-the-open-web-platform/\">\"W3C and WHATWG to Work Together to Advance the Open Web Platform\"</a>. <i>W3C Blog</i>. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190529021122/https://www.w3.org/blog/2019/05/w3c-and-whatwg-to-work-together-to-advance-the-open-web-platform/\">Archived</a> from the original on 29 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C+Blog&amp;rft.atitle=W3C+and+WHATWG+to+Work+Together+to+Advance+the+Open+Web+Platform&amp;rft.date=2019-05-28&amp;rft.aulast=Jaffe&amp;rft.aufirst=Jeff&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fblog%2F2019%2F05%2Fw3c-and-whatwg-to-work-together-to-advance-the-open-web-platform%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3C_transfer_HTML-68\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3C_transfer_HTML_68-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/html/\">\"W3C and the WHATWG Signed an Agreement to Collaborate on a Single Version of HTML and DOM\"</a>. <i>W3C</i>. 28 May 2019. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190529012655/https://www.w3.org/html/\">Archived</a> from the original on 29 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C&amp;rft.atitle=W3C+and+the+WHATWG+Signed+an+Agreement+to+Collaborate+on+a+Single+Version+of+HTML+and+DOM&amp;rft.date=2019-05-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2Fhtml%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3C_transfer_memo-69\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3C_transfer_memo_69-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/2019/04/WHATWG-W3C-MOU.html\">\"Memorandum of Understanding Between W3C and WHATWG\"</a>. <i>W3C</i>. 28 May 2019. <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190529012854/https://www.w3.org/2019/04/WHATWG-W3C-MOU.html\">Archived</a> from the original on 29 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C&amp;rft.atitle=Memorandum+of+Understanding+Between+W3C+and+WHATWG&amp;rft.date=2019-05-28&amp;rft_id=https%3A%2F%2Fwww.w3.org%2F2019%2F04%2FWHATWG-W3C-MOU.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3C_transfer_ZDNet-70\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3C_transfer_ZDNet_70-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFCimpanu2019\" class=\"citation news cs1\">Cimpanu, Catalin (29 May 2019). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190529021959/https://www.zdnet.com/article/browser-vendors-win-war-with-w3c-over-html-and-dom-standards/\">\"Browser vendors Win War with W3C over HTML and DOM standards\"</a>. <i>ZDNet</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://www.zdnet.com/article/browser-vendors-win-war-with-w3c-over-html-and-dom-standards/\">the original</a> on 29 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=ZDNet&amp;rft.atitle=Browser+vendors+Win+War+with+W3C+over+HTML+and+DOM+standards&amp;rft.date=2019-05-29&amp;rft.aulast=Cimpanu&amp;rft.aufirst=Catalin&amp;rft_id=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fbrowser-vendors-win-war-with-w3c-over-html-and-dom-standards%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-W3C_forks-71\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-W3C_forks_71-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20190529013834/https://wiki.whatwg.org/wiki/W3C\">\"W3C – WHATWG Wiki\"</a>. <i>WHATWG Wiki</i>. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"https://wiki.whatwg.org/wiki/W3C\">the original</a> on 29 May 2019<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">29 May</span> 2019</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=WHATWG+Wiki&amp;rft.atitle=W3C+%E2%80%93+WHATWG+Wiki&amp;rft_id=https%3A%2F%2Fwiki.whatwg.org%2Fwiki%2FW3C&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-72\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-72\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFShankland2009\" class=\"citation web cs1\">Shankland, Stephen (July 9, 2009). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.cnet.com/news/an-epitaph-for-the-web-standard-xhtml-2/\">\"An epitaph for the Web standard, XHTML 2\"</a>. <i>CNET</i>. CBS INTERACTIVE INC.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=CNET&amp;rft.atitle=An+epitaph+for+the+Web+standard%2C+XHTML+2&amp;rft.date=2009-07-09&amp;rft.aulast=Shankland&amp;rft.aufirst=Stephen&amp;rft_id=https%3A%2F%2Fwww.cnet.com%2Fnews%2Fan-epitaph-for-the-web-standard-xhtml-2%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-hsivonen-73\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-hsivonen_73-0\">^</a></b></span> <span class=\"reference-text\"><a rel=\"nofollow\" class=\"external text\" href=\"https://hsivonen.iki.fi/doctype/\">Activating Browser Modes with Doctype</a>. Hsivonen.iki.fi. Retrieved on 2012-02-16.</span>\n</li>\n<li id=\"cite_note-74\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-74\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3schools.com/html/html_elements.asp\">\"HTML Elements\"</a>. w3schools<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">16 March</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=HTML+Elements&amp;rft.pub=w3schools&amp;rft_id=https%3A%2F%2Fwww.w3schools.com%2Fhtml%2Fhtml_elements.asp&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-75\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-75\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3schools.com/css/css_intro.asp\">\"CSS Introduction\"</a>. W3schools<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">16 March</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=CSS+Introduction&amp;rft.pub=W3schools&amp;rft_id=https%3A%2F%2Fwww.w3schools.com%2Fcss%2Fcss_intro.asp&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-76\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-76\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.2\">\"On SGML and HTML\"</a>. World Wide Web Consortium<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=On+SGML+and+HTML&amp;rft.pub=World+Wide+Web+Consortium&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml401%2Fintro%2Fsgmltut.html%23h-3.2.2&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-77\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-77\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml1/diffs.html#h-4.4\">\"XHTML 1.0 – Differences with HTML&#160;4\"</a>. World Wide Web Consortium<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+1.0+%E2%80%93+Differences+with+HTML+4&amp;rft.pub=World+Wide+Web+Consortium&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml1%2Fdiffs.html%23h-4.4&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-78\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-78\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFKorpela1998\" class=\"citation web cs1\">Korpela, Jukka (July 6, 1998). <a rel=\"nofollow\" class=\"external text\" href=\"https://jkorpela.fi/qattr.html\">\"Why attribute values should always be quoted in HTML\"</a>. Cs.tut.fi<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Why+attribute+values+should+always+be+quoted+in+HTML&amp;rft.pub=Cs.tut.fi&amp;rft.date=1998-07-06&amp;rft.aulast=Korpela&amp;rft.aufirst=Jukka&amp;rft_id=https%3A%2F%2Fjkorpela.fi%2Fqattr.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-79\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-79\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/1999/REC-html401-19991224/struct/objects.html#adef-ismap\">\"Objects, Images, and Applets in HTML documents\"</a>. World Wide Web Consortium. December 24, 1999<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">November 16,</span> 2008</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Objects%2C+Images%2C+and+Applets+in+HTML+documents&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=1999-12-24&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2F1999%2FREC-html401-19991224%2Fstruct%2Fobjects.html%23adef-ismap&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-80\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-80\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/WCAG-TECHS/H56.html\">\"H56: Using the dir attribute on an inline element to resolve problems with nested directional runs\"</a>. <i>Techniques for WCAG 2.0</i>. W3C<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">18 September</span> 2010</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Techniques+for+WCAG+2.0&amp;rft.atitle=H56%3A+Using+the+dir+attribute+on+an+inline+element+to+resolve+problems+with+nested+directional+runs&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2FWCAG-TECHS%2FH56.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-81\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-81\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://dev.w3.org/html5/html-author/charref\">\"Character Entity Reference Chart\"</a>. World Wide Web Consortium. October 24, 2012.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Character+Entity+Reference+Chart&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2012-10-24&amp;rft_id=https%3A%2F%2Fdev.w3.org%2Fhtml5%2Fhtml-author%2Fcharref&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-aposhtml-82\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-aposhtml_82-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml1/#C_16\">\"The Named Character Reference '<span class=\"cs1-kern-right\"></span>\"</a>. World Wide Web Consortium. January 26, 2000.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Named+Character+Reference+%27&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2000-01-26&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml1%2F%23C_16&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-83\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-83\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.unicode.org/standard/principles.html\">\"<i>The Unicode Standard</i>: A Technical Introduction\"</a>. Unicode<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2010-03-16</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Unicode+Standard%3A+A+Technical+Introduction&amp;rft.pub=Unicode&amp;rft_id=https%3A%2F%2Fwww.unicode.org%2Fstandard%2Fprinciples.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-84\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-84\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html/syntax.html#doctype-syntax\">\"The HTML syntax\"</a>. <i>HTML Standard</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2013-08-19</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=HTML+Standard&amp;rft.atitle=The+HTML+syntax&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml%2Fsyntax.html%23doctype-syntax&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-85\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-85\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/html401/sgml/framesetdtd.html\">\"HTML 4 Frameset Document Type Definition\"</a>. <i>W3C</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2021-12-25</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=W3C&amp;rft.atitle=HTML+4+Frameset+Document+Type+Definition&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fhtml401%2Fsgml%2Fframesetdtd.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-86\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-86\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeFischetti2000\" class=\"citation book cs1\">Berners-Lee, Tim; Fischetti, Mark (2000). <span class=\"id-lock-registration\" title=\"Free registration required\"><a rel=\"nofollow\" class=\"external text\" href=\"https://archive.org/details/weavingweborigin00bern_0\"><i>Weaving the Web: The Original Design and Ultimate Destiny of the World Wide Web by Its Inventor</i></a></span>. San Francisco: Harper. <a href=\"/wiki/ISBN_(identifier)\" class=\"mw-redirect\" title=\"ISBN (identifier)\">ISBN</a>&#160;<a href=\"/wiki/Special:BookSources/978-0-06-251587-2\" title=\"Special:BookSources/978-0-06-251587-2\"><bdi>978-0-06-251587-2</bdi></a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Weaving+the+Web%3A+The+Original+Design+and+Ultimate+Destiny+of+the+World+Wide+Web+by+Its+Inventor&amp;rft.place=San+Francisco&amp;rft.pub=Harper&amp;rft.date=2000&amp;rft.isbn=978-0-06-251587-2&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Fischetti%2C+Mark&amp;rft_id=https%3A%2F%2Farchive.org%2Fdetails%2Fweavingweborigin00bern_0&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-87\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-87\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFRaggett2002\" class=\"citation web cs1\">Raggett, Dave (2002). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/Guide/Style.html\">\"Adding a touch of style\"</a>. W3C<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">October 2,</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Adding+a+touch+of+style&amp;rft.pub=W3C&amp;rft.date=2002&amp;rft.aulast=Raggett&amp;rft.aufirst=Dave&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FMarkUp%2FGuide%2FStyle.html&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> This article notes that presentational HTML markup may be useful when targeting browsers \"before Netscape 4.0 and Internet Explorer 4.0\". See the <a href=\"/wiki/List_of_web_browsers\" title=\"List of web browsers\">list of web browsers</a> to confirm that these were both released in 1997.</span>\n</li>\n<li id=\"cite_note-88\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-88\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFBerners-LeeHendlerLassila2001\" class=\"citation magazine cs1\">Berners-Lee, Tim; Hendler, James; Lassila, Ora (May 1, 2001). <a rel=\"nofollow\" class=\"external text\" href=\"http://www.scientificamerican.com/article.cfm?id=the-semantic-web\">\"The Semantic Web\"</a>. <i>Scientific American</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">October 2,</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=article&amp;rft.jtitle=Scientific+American&amp;rft.atitle=The+Semantic+Web&amp;rft.date=2001-05-01&amp;rft.aulast=Berners-Lee&amp;rft.aufirst=Tim&amp;rft.au=Hendler%2C+James&amp;rft.au=Lassila%2C+Ora&amp;rft_id=http%3A%2F%2Fwww.scientificamerican.com%2Farticle.cfm%3Fid%3Dthe-semantic-web&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-Semantic_Web_Revisted-89\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-Semantic_Web_Revisted_89-0\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFNigel_Shadbolt,_Wendy_Hall_and_Tim_Berners-Lee2006\" class=\"citation web cs1\">Nigel Shadbolt, Wendy Hall and Tim Berners-Lee (2006). <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20130320130521/http://eprints.soton.ac.uk/262614/1/Semantic_Web_Revisted.pdf\">\"The Semantic Web Revisited\"</a> <span class=\"cs1-format\">(PDF)</span>. IEEE Intelligent Systems. Archived from <a rel=\"nofollow\" class=\"external text\" href=\"http://eprints.ecs.soton.ac.uk/12614/1/Semantic_Web_Revisted.pdf\">the original</a> <span class=\"cs1-format\">(PDF)</span> on March 20, 2013<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">October 2,</span> 2009</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=The+Semantic+Web+Revisited&amp;rft.pub=IEEE+Intelligent+Systems&amp;rft.date=2006&amp;rft.au=Nigel+Shadbolt%2C+Wendy+Hall+and+Tim+Berners-Lee&amp;rft_id=http%3A%2F%2Feprints.ecs.soton.ac.uk%2F12614%2F1%2FSemantic_Web_Revisted.pdf&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-90\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-90\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/dev/introduction.html#restrictions-on-content-models-and-on-attribute-values\">\"HTML: The Living Standard\"</a>. <i>WHATWG</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">27 September</span> 2018</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=WHATWG&amp;rft.atitle=HTML%3A+The+Living+Standard&amp;rft_id=https%3A%2F%2Fhtml.spec.whatwg.org%2Fdev%2Fintroduction.html%23restrictions-on-content-models-and-on-attribute-values&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-91\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-91\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml1/#media\">\"XHTML 1.0 The Extensible HyperText Markup Language (Second Edition)\"</a>. World Wide Web Consortium. 2002 [2000]<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">December 7,</span> 2008</span>. <q>XHTML Documents which follow the guidelines set forth in Appendix C, \"HTML Compatibility Guidelines\" may be labeled with the Internet Media Type \"text/html\" [RFC2854], as they are compatible with most HTML browsers. Those documents, and any other document conforming to this specification, may also be labeled with the Internet Media Type \"application/xhtml+xml\" as defined in [RFC3236].</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+1.0+The+Extensible+HyperText+Markup+Language+%28Second+Edition%29&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2002&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml1%2F%23media&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-92\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-92\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFS._Bradner1997\" class=\"citation cs1\"><a href=\"/wiki/Scott_Bradner\" title=\"Scott Bradner\">S. Bradner</a> (March 1997). <a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc2119\"><i>Key words for use in RFCs to Indicate Requirement Levels</i></a>. <a href=\"/wiki/Internet_Engineering_Task_Force\" title=\"Internet Engineering Task Force\">IETF</a> Network Working Group. <a href=\"/wiki/Doi_(identifier)\" class=\"mw-redirect\" title=\"Doi (identifier)\">doi</a>:<span class=\"id-lock-free\" title=\"Freely accessible\"><a rel=\"nofollow\" class=\"external text\" href=\"https://doi.org/10.17487%2FRFC2119\">10.17487/RFC2119</a></span>. BCP 14.&#32;<a href=\"/wiki/Request_for_Comments\" title=\"Request for Comments\">RFC</a> <a rel=\"nofollow\" class=\"external text\" href=\"https://datatracker.ietf.org/doc/html/rfc2119\">2119</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=Key+words+for+use+in+&#82;FCs+to+Indicate+Requirement+Levels&amp;rft.pub=IETF+Network+Working+Group&amp;rft.date=1997-03&amp;rft_id=info%3Adoi%2F10.17487%2F&#82;FC2119&amp;rft.au=S.+Bradner&amp;rft_id=https%3A%2F%2Fwww.rfc-editor.org%2Frfc%2Frfc2119&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> <i>Best Current Practice 14.</i>  Updated by <link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" />RFC&#160;<a rel=\"nofollow\" class=\"external text\" href=\"https://www.rfc-editor.org/rfc/rfc8174\">8174</a>. <q>3. SHOULD This word, or the adjective \"RECOMMENDED\", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.</q></span>\n</li>\n<li id=\"cite_note-93\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-93\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/TR/xhtml11/conformance.html#strict\">\"XHTML 1.1 – Module-based XHTML&#160;— Second Edition\"</a>. World Wide Web Consortium. 2007<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">December 7,</span> 2008</span>. <q>XHTML 1.1 documents SHOULD be labeled with the Internet Media Type text/html as defined in [RFC2854] or application/xhtml+xml as defined in [RFC3236].</q></cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=XHTML+1.1+%E2%80%93+Module-based+XHTML+%E2%80%94+Second+Edition&amp;rft.pub=World+Wide+Web+Consortium&amp;rft.date=2007&amp;rft_id=https%3A%2F%2Fwww.w3.org%2FTR%2Fxhtml11%2Fconformance.html%23strict&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-94\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-94\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx?f=255&amp;MSPPError=-2147217396\">\"Naming Files, Paths, and Namespaces\"</a>. Microsoft<span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">16 March</span> 2015</span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Naming+Files%2C+Paths%2C+and+Namespaces&amp;rft.pub=Microsoft&amp;rft_id=https%3A%2F%2Fmsdn.microsoft.com%2Fen-us%2Flibrary%2Fwindows%2Fdesktop%2Faa365247%2528v%3Dvs.85%2529.aspx%3Ff%3D255%26MSPPError%3D-2147217396&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-95\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-95\">^</a></b></span> <span class=\"reference-text\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/History/19921103-hypertext/hypertext/WWW/MarkUp/HTMLConstraints.html\">HTML Design Constraints</a>, W3C Archives</span>\n</li>\n<li id=\"cite_note-96\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-96\">^</a></b></span> <span class=\"reference-text\"><a rel=\"nofollow\" class=\"external text\" href=\"http://ei.cs.vt.edu/~wwwbtb/book/chap13/who.html\">WWW: BTB – HTML</a>, Pris Sears</span>\n</li>\n<li id=\"cite_note-97\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-97\">^</a></b></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation cs2\"><a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-br-element\"><i>HTML Standard - The br Element</i></a>, WHATWG</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=book&amp;rft.btitle=HTML+Standard+-+The+br+Element&amp;rft.pub=WHATWG&amp;rft_id=https%3A%2F%2Fhtml.spec.whatwg.org%2Fmultipage%2Ftext-level-semantics.html%23the-br-element&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-98\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-98\">^</a></b></span> <span class=\"reference-text\">Sauer, C.: WYSIWIKI&#160;– Questioning WYSIWYG in the Internet Age. In: Wikimania (2006)</span>\n</li>\n<li id=\"cite_note-99\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-99\">^</a></b></span> <span class=\"reference-text\">Spiesser, J., Kitchen, L.: Optimization of HTML automatically generated by WYSIWYG programs. In: 13th International Conference on World Wide Web, pp. 355–364. WWW '04. ACM, New York, NY (New York, NY, U.S., May 17–20, 2004)</span>\n</li>\n<li id=\"cite_note-100\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-100\">^</a></b></span> <span class=\"reference-text\"><a rel=\"nofollow\" class=\"external text\" href=\"http://xhtml.com/en/xhtml/reference/blockquote/\">XHTML Reference: blockquote</a> <a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20100325160356/http://xhtml.com/en/xhtml/reference/blockquote/\">Archived</a> 2010-03-25 at the <a href=\"/wiki/Wayback_Machine\" title=\"Wayback Machine\">Wayback Machine</a>. Xhtml.com. Retrieved on 2012-02-16.</span>\n</li>\n<li id=\"cite_note-101\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-101\">^</a></b></span> <span class=\"reference-text\"><a rel=\"nofollow\" class=\"external text\" href=\"http://www.invisiblerevolution.net/\">Doug Engelbart's INVISIBLE REVOLUTION</a>. Invisiblerevolution.net. Retrieved on 2012-02-16.</span>\n</li>\n</ol></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links\">External links</h2></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1308029216\">.mw-parser-output .side-box{margin:4px 0;box-sizing:border-box;border:1px solid #aaa;font-size:88%;line-height:1.25em;background-color:var(--background-color-interactive-subtle,#f8f9fa);display:flow-root}.mw-parser-output .infobox .side-box{font-size:100%}.mw-parser-output .side-box-abovebelow,.mw-parser-output .side-box-text{padding:0.25em 0.9em}.mw-parser-output .side-box-image{padding:2px 0 2px 0.9em;text-align:center}.mw-parser-output .side-box-imageright{padding:2px 0.9em 2px 0;text-align:center}@media(min-width:500px){.mw-parser-output .side-box-flex{display:flex;align-items:center}.mw-parser-output .side-box-text{flex:1;min-width:0}}@media(min-width:640px){.mw-parser-output .side-box{width:238px}.mw-parser-output .side-box-right{clear:right;float:right;margin-left:1em}.mw-parser-output .side-box-left{margin-right:1em}}</style><style data-mw-deduplicate=\"TemplateStyles:r1311551236\">@media print{body.ns-0 .mw-parser-output .sistersitebox{display:none!important}}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-en-v2.svg\"]{filter:invert(1)brightness(55%)contrast(250%)hue-rotate(180deg)}}</style><div class=\"side-box side-box-right plainlinks sistersitebox\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" />\n<div class=\"side-box-flex\">\n<div class=\"side-box-image\"><span class=\"noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo-en-noslogan.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/40px-Wikibooks-logo-en-noslogan.svg.png\" decoding=\"async\" width=\"40\" height=\"40\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/60px-Wikibooks-logo-en-noslogan.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikibooks-logo-en-noslogan.svg/120px-Wikibooks-logo-en-noslogan.svg.png 2x\" data-file-width=\"400\" data-file-height=\"400\" /></a></span></div>\n<div class=\"side-box-text plainlist\">Wikibooks has more on the topic of: <i><b><a href=\"https://en.wikibooks.org/wiki/Special:Search/HTML\" class=\"extiw\" title=\"wikibooks:Special:Search/HTML\">HTML</a></b></i></div></div>\n</div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1308029216\" /><style data-mw-deduplicate=\"TemplateStyles:r1307723979\">.mw-parser-output .sister-box .side-box-abovebelow{padding:0.75em 0;text-align:center}.mw-parser-output .sister-box .side-box-abovebelow>b{display:block}.mw-parser-output .sister-box .side-box-text>ul{border-top:1px solid #aaa;padding:0.75em 0;width:220px;margin:0 auto}.mw-parser-output .sister-box .side-box-text>ul>li{min-height:31px}.mw-parser-output .sister-logo{display:inline-block;width:31px;line-height:31px;vertical-align:middle;text-align:center}.mw-parser-output .sister-link{display:inline-block;margin-left:7px;width:182px;vertical-align:middle}@media print{body.ns-0 .mw-parser-output .sistersitebox{display:none!important}}@media screen{html.skin-theme-clientpref-night .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .sistersitebox img[src*=\"Wiktionary-logo-v2.svg\"]{background-color:white}}</style><div role=\"navigation\" aria-labelledby=\"sister-projects\" class=\"side-box metadata side-box-right sister-box sistersitebox plainlinks\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1126788409\" />\n<div class=\"side-box-abovebelow\">\n<b>HTML</b>  at Wikipedia's <a href=\"/wiki/Wikipedia:Wikimedia_sister_projects\" title=\"Wikipedia:Wikimedia sister projects\"><span id=\"sister-projects\">sister projects</span></a></div>\n<div class=\"side-box-flex\">\n<div class=\"side-box-text plainlist\"><ul><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer skin-invert-image\" typeof=\"mw:File\"><a href=\"/wiki/File:Wiktionary-logo-v2.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/0/06/Wiktionary-logo-v2.svg/40px-Wiktionary-logo-v2.svg.png\" decoding=\"async\" width=\"27\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/0/06/Wiktionary-logo-v2.svg/60px-Wiktionary-logo-v2.svg.png 1.5x\" data-file-width=\"391\" data-file-height=\"391\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wiktionary.org/wiki/HTML\" class=\"extiw\" title=\"wikt:HTML\">Definitions</a> from Wiktionary</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Commons-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/20px-Commons-logo.svg.png\" decoding=\"async\" width=\"20\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/40px-Commons-logo.svg.png 1.5x\" data-file-width=\"1024\" data-file-height=\"1376\" /></a></span></span><span class=\"sister-link\"><a href=\"https://commons.wikimedia.org/wiki/category:HTML\" class=\"extiw\" title=\"c:category:HTML\">Media</a> from Commons</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikibooks-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikibooks-logo.svg/40px-Wikibooks-logo.svg.png\" decoding=\"async\" width=\"27\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikibooks-logo.svg/60px-Wikibooks-logo.svg.png 1.5x\" data-file-width=\"300\" data-file-height=\"300\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikibooks.org/wiki/HyperText_Markup_Language\" class=\"extiw\" title=\"b:HyperText Markup Language\">Textbooks</a> from Wikibooks</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikiversity_logo_2017.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/40px-Wikiversity_logo_2017.svg.png\" decoding=\"async\" width=\"27\" height=\"22\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/60px-Wikiversity_logo_2017.svg.png 1.5x\" data-file-width=\"626\" data-file-height=\"512\" /></a></span></span><span class=\"sister-link\"><a href=\"https://en.wikiversity.org/wiki/HTML\" class=\"extiw\" title=\"v:HTML\">Resources</a> from Wikiversity</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Wikidata-logo.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/40px-Wikidata-logo.svg.png\" decoding=\"async\" width=\"27\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/60px-Wikidata-logo.svg.png 1.5x\" data-file-width=\"1050\" data-file-height=\"590\" /></a></span></span><span class=\"sister-link\"><a href=\"https://www.wikidata.org/wiki/Q8811\" class=\"extiw\" title=\"d:Q8811\">Data</a> from Wikidata</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/75/Wikimedia_Community_Logo.svg/40px-Wikimedia_Community_Logo.svg.png\" decoding=\"async\" width=\"27\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/75/Wikimedia_Community_Logo.svg/60px-Wikimedia_Community_Logo.svg.png 1.5x\" data-file-width=\"900\" data-file-height=\"900\" /></span></span></span><span class=\"sister-link\"><a href=\"https://meta.wikimedia.org/wiki/Help:HTML_in_wikitext\" class=\"extiw\" title=\"m:Help:HTML in wikitext\">Discussions</a> from Meta-Wiki</span></li><li><span class=\"sister-logo\"><span class=\"mw-valign-middle noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:MediaWiki-2020-icon.svg\" class=\"mw-file-description\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/40px-MediaWiki-2020-icon.svg.png\" decoding=\"async\" width=\"27\" height=\"27\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a6/MediaWiki-2020-icon.svg/60px-MediaWiki-2020-icon.svg.png 1.5x\" data-file-width=\"100\" data-file-height=\"100\" /></a></span></span><span class=\"sister-link\"><a href=\"https://www.mediawiki.org/wiki/HTML_restriction\" class=\"extiw\" title=\"mw:HTML restriction\">Documentation</a> from MediaWiki</span></li></ul></div></div>\n</div>\n<ul><li><a href=\"/wiki/WHATWG\" title=\"WHATWG\">WHATWG</a>'s <a rel=\"nofollow\" class=\"external text\" href=\"https://html.spec.whatwg.org/multipage/\">HTML Living Standard</a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3.org/MarkUp/Guide/\">Dave Raggett's Introduction to HTML</a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20110412130543/http://computemagazine.com/man-who-invented-world-wide-web-gives-new-definition\">Tim Berners-Lee Gives the Web a New Definition</a> (archived 12 April 2011)</li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://meiert.com/en/indices/html-elements/\">List of all HTML elements from all major versions</a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://www.w3schools.com/html/html_entities.asp\">HTML Entities</a></li>\n<li><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite id=\"CITEREFSean_B._Palmer\" class=\"citation web cs1\">Sean B. Palmer. <a rel=\"nofollow\" class=\"external text\" href=\"http://infomesh.net/html/history/early/\">\"Early History of HTML – 1990 to 1992\"</a>. <i>Infomesh</i><span class=\"reference-accessdate\">. Retrieved <span class=\"nowrap\">2022-04-13</span></span>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Ajournal&amp;rft.genre=unknown&amp;rft.jtitle=Infomesh&amp;rft.atitle=Early+History+of+HTML+%E2%80%93+1990+to+1992&amp;rft.au=Sean+B.+Palmer&amp;rft_id=http%3A%2F%2Finfomesh.net%2Fhtml%2Fhistory%2Fearly%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AHTML\" class=\"Z3988\"></span> (Timeframe: 1980–1995)</li></ul>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1236075235\">.mw-parser-output .navbox{box-sizing:border-box;border:1px solid #a2a9b1;width:100%;clear:both;font-size:88%;text-align:center;padding:1px;margin:1em auto 0}.mw-parser-output .navbox .navbox{margin-top:0}.mw-parser-output .navbox+.navbox,.mw-parser-output .navbox+.navbox-styles+.navbox{margin-top:-1px}.mw-parser-output .navbox-inner,.mw-parser-output .navbox-subgroup{width:100%}.mw-parser-output .navbox-group,.mw-parser-output .navbox-title,.mw-parser-output .navbox-abovebelow{padding:0.25em 1em;line-height:1.5em;text-align:center}.mw-parser-output .navbox-group{white-space:nowrap;text-align:right}.mw-parser-output .navbox,.mw-parser-output .navbox-subgroup{background-color:#fdfdfd}.mw-parser-output .navbox-list{line-height:1.5em;border-color:#fdfdfd}.mw-parser-output .navbox-list-with-group{text-align:left;border-left-width:2px;border-left-style:solid}.mw-parser-output tr+tr>.navbox-abovebelow,.mw-parser-output tr+tr>.navbox-group,.mw-parser-output tr+tr>.navbox-image,.mw-parser-output tr+tr>.navbox-list{border-top:2px solid #fdfdfd}.mw-parser-output .navbox-title{background-color:#ccf}.mw-parser-output .navbox-abovebelow,.mw-parser-output .navbox-group,.mw-parser-output .navbox-subgroup .navbox-title{background-color:#ddf}.mw-parser-output .navbox-subgroup .navbox-group,.mw-parser-output .navbox-subgroup .navbox-abovebelow{background-color:#e6e6ff}.mw-parser-output .navbox-even{background-color:#f7f7f7}.mw-parser-output .navbox-odd{background-color:transparent}.mw-parser-output .navbox .hlist td dl,.mw-parser-output .navbox .hlist td ol,.mw-parser-output .navbox .hlist td ul,.mw-parser-output .navbox td.hlist dl,.mw-parser-output .navbox td.hlist ol,.mw-parser-output .navbox td.hlist ul{padding:0.125em 0}.mw-parser-output .navbox .navbar{display:block;font-size:100%}.mw-parser-output .navbox-title .navbar{float:left;text-align:left;margin-right:0.5em}body.skin--responsive .mw-parser-output .navbox-image img{max-width:none!important}@media print{body.ns-0 .mw-parser-output .navbox{display:none!important}}</style></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Web_browsers9587\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Web_browsers\" title=\"Template:Web browsers\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Web_browsers\" title=\"Template talk:Web browsers\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Web_browsers\" title=\"Special:EditPage/Template:Web browsers\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Web_browsers9587\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Web_browser\" title=\"Web browser\">Web browsers</a></div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible uncollapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Features,_standards_&amp;amp;_protocols1266\" style=\"font-size:114%;margin:0 4em\">Features, standards &amp; protocols</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Features</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Bookmark_(digital)\" class=\"mw-redirect\" title=\"Bookmark (digital)\">Bookmarks</a></li>\n<li><a href=\"/wiki/Browser_extension\" title=\"Browser extension\">Extensions</a></li>\n<li><a href=\"/wiki/Private_browsing\" title=\"Private browsing\">Privacy mode</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Web_standards\" title=\"Web standards\">Web standards</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a class=\"mw-selflink selflink\">HTML</a>\n<ul><li><a href=\"/wiki/HTML5\" title=\"HTML5\">v5</a></li></ul></li>\n<li><a href=\"/wiki/CSS\" title=\"CSS\">CSS</a></li>\n<li><a href=\"/wiki/Document_Object_Model\" title=\"Document Object Model\">DOM</a></li>\n<li><a href=\"/wiki/JavaScript\" title=\"JavaScript\">JavaScript</a>\n<ul><li><a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a></li>\n<li><a href=\"/wiki/Web_storage\" title=\"Web storage\">Web storage</a></li>\n<li><a href=\"/wiki/IndexedDB\" title=\"IndexedDB\">IndexedDB</a></li>\n<li><a href=\"/wiki/WebGL\" title=\"WebGL\">WebGL</a></li>\n<li><a href=\"/wiki/WebGPU\" title=\"WebGPU\">WebGPU</a></li></ul></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Protocols</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/HTTP\" title=\"HTTP\">HTTP</a>\n<ul><li><a href=\"/wiki/HTTPS\" title=\"HTTPS\">Encryption</a></li>\n<li><a href=\"/wiki/HTTP_cookie\" title=\"HTTP cookie\">Cookies</a>\n<ul><li><a href=\"/wiki/Third-party_cookies\" title=\"Third-party cookies\">third-party</a></li></ul></li></ul></li>\n<li><a href=\"/wiki/Online_Certificate_Status_Protocol\" title=\"Online Certificate Status Protocol\">OCSP</a></li>\n<li><a href=\"/wiki/WebRTC\" title=\"WebRTC\">WebRTC</a></li>\n<li><a href=\"/wiki/WebSocket\" title=\"WebSocket\">WebSocket</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr></tbody></table><div></div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Active3468\" style=\"font-size:114%;margin:0 4em\">Active</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Blink_(browser_engine)\" title=\"Blink (browser engine)\">Blink</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Proprietary_software\" title=\"Proprietary software\">Proprietary</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Google_Chrome\" title=\"Google Chrome\">Google Chrome</a></li>\n<li><a href=\"/wiki/Arc_(web_browser)\" title=\"Arc (web browser)\">Arc</a></li>\n<li><a href=\"/wiki/Avast_Secure_Browser\" title=\"Avast Secure Browser\">Avast</a></li>\n<li><a href=\"/wiki/C%E1%BB%91c_C%E1%BB%91c\" title=\"Cốc Cốc\">Cốc Cốc</a></li>\n<li><a href=\"/wiki/Perplexity_AI#Comet\" title=\"Perplexity AI\">Comet</a></li>\n<li><a href=\"/wiki/Comodo_Dragon\" title=\"Comodo Dragon\">Comodo</a></li>\n<li><a href=\"/wiki/Ecosia#Ecosia_Browser\" title=\"Ecosia\">Ecosia</a></li>\n<li><a href=\"/wiki/Epic_(web_browser)\" title=\"Epic (web browser)\">Epic</a></li>\n<li><a href=\"/wiki/Huawei_Mobile_Services\" title=\"Huawei Mobile Services\">Huawei</a></li>\n<li><a href=\"/wiki/Maxthon\" title=\"Maxthon\">Maxthon</a></li>\n<li><a href=\"/wiki/Microsoft_Edge\" title=\"Microsoft Edge\">Microsoft Edge</a></li>\n<li><a href=\"/wiki/Opera_(web_browser)\" title=\"Opera (web browser)\">Opera</a> (<a href=\"/wiki/Opera_Mobile\" title=\"Opera Mobile\">Mobile</a>)</li>\n<li><a href=\"/wiki/Puffin_Browser\" title=\"Puffin Browser\">Puffin </a></li>\n<li><a href=\"/wiki/QQ_Browser\" title=\"QQ Browser\">QQ</a></li>\n<li><a href=\"/wiki/Samsung_Internet\" title=\"Samsung Internet\">Samsung</a></li>\n<li><a href=\"/wiki/Amazon_Silk\" title=\"Amazon Silk\">Silk</a></li>\n<li><a href=\"/wiki/Sleipnir_(web_browser)\" title=\"Sleipnir (web browser)\">Sleipnir</a></li>\n<li><a href=\"/wiki/SRWare_Iron\" title=\"SRWare Iron\">SRWare</a></li>\n<li><a href=\"/wiki/UC_Browser\" title=\"UC Browser\">UC</a></li>\n<li><a href=\"/wiki/Vivaldi_(web_browser)\" title=\"Vivaldi (web browser)\">Vivaldi</a></li>\n<li><a href=\"/wiki/Naver_Whale\" title=\"Naver Whale\">Whale</a></li>\n<li><a href=\"/wiki/Yandex_Browser\" title=\"Yandex Browser\">Yandex</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Free_and_open-source_software\" title=\"Free and open-source software\">FOSS</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Chromium_(web_browser)\" title=\"Chromium (web browser)\">Chromium</a></li>\n<li><a href=\"/wiki/Brave_(web_browser)\" title=\"Brave (web browser)\">Brave</a></li>\n<li><a href=\"/wiki/Dooble\" title=\"Dooble\">Dooble</a></li>\n<li><a href=\"/wiki/Falkon\" title=\"Falkon\">Falkon</a></li>\n<li><a href=\"/wiki/Otter_Browser\" title=\"Otter Browser\">Otter</a></li>\n<li><a href=\"/wiki/Supermium\" title=\"Supermium\">Supermium</a></li>\n<li><a href=\"/wiki/Ungoogled-chromium\" title=\"Ungoogled-chromium\">ungoogled</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Firefox\" title=\"Firefox\">Firefox</a></li>\n<li><a href=\"/wiki/Floorp\" title=\"Floorp\">Floorp</a></li>\n<li><a href=\"/wiki/GNU_IceCat\" title=\"GNU IceCat\">GNU IceCat</a></li>\n<li><a href=\"/wiki/LibreWolf\" title=\"LibreWolf\">LibreWolf</a></li>\n<li><a href=\"/wiki/Midori_(web_browser)\" title=\"Midori (web browser)\">Midori</a></li>\n<li><a href=\"/wiki/Mullvad_Browser\" class=\"mw-redirect\" title=\"Mullvad Browser\">Mullvad</a></li>\n<li><a href=\"/wiki/SlimBrowser\" title=\"SlimBrowser\">SlimBrowser</a></li>\n<li><a href=\"/wiki/Tor_Browser\" class=\"mw-redirect\" title=\"Tor Browser\">Tor</a></li>\n<li><a href=\"/wiki/Zen_Browser\" title=\"Zen Browser\">Zen</a></li>\n<li>Gecko <a href=\"/wiki/Fork_(software_development)\" title=\"Fork (software development)\">forks</a>\n<ul><li><a href=\"/wiki/Basilisk_(web_browser)\" title=\"Basilisk (web browser)\">Basilisk</a></li>\n<li><a href=\"/wiki/K-Meleon\" title=\"K-Meleon\">K-Meleon</a></li>\n<li><a href=\"/wiki/Pale_Moon\" title=\"Pale Moon\">Pale Moon</a></li>\n<li><a href=\"/wiki/SeaMonkey\" title=\"SeaMonkey\">SeaMonkey</a></li>\n<li><a href=\"/wiki/Waterfox\" title=\"Waterfox\">Waterfox</a></li></ul></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/WebKit\" title=\"WebKit\">WebKit</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Safari_(web_browser)\" title=\"Safari (web browser)\">Safari</a></li>\n<li><a href=\"/wiki/GNOME_Web\" title=\"GNOME Web\">GNOME Web</a></li>\n<li><a href=\"/wiki/ICab\" title=\"ICab\">iCab</a></li>\n<li><a href=\"/wiki/Kagi_(search_engine)#Orion_Browser\" title=\"Kagi (search engine)\">Orion</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Multi-<a href=\"/wiki/Browser_engine\" title=\"Browser engine\">engine</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/360_Secure_Browser\" title=\"360 Secure Browser\">360</a></li>\n<li><a href=\"/wiki/DuckDuckGo_Private_Browser\" title=\"DuckDuckGo Private Browser\">DuckDuckGo</a></li>\n<li><a href=\"/wiki/Konqueror\" title=\"Konqueror\">Konqueror</a></li>\n<li><a href=\"/wiki/Lunascape\" title=\"Lunascape\">Lunascape</a></li>\n<li><a href=\"/wiki/NetFront\" title=\"NetFront\">NetFront</a></li>\n<li><a href=\"/wiki/Qutebrowser\" title=\"Qutebrowser\">qutebrowser</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Dillo\" title=\"Dillo\">Dillo</a></li>\n<li><a href=\"/wiki/Eww_(web_browser)\" title=\"Eww (web browser)\">eww</a></li>\n<li><a href=\"/wiki/Flow_(web_browser)\" title=\"Flow (web browser)\">Flow</a></li>\n<li><a href=\"/wiki/Ladybird_(web_browser)\" title=\"Ladybird (web browser)\">Ladybird</a></li>\n<li><a href=\"/wiki/Links_(web_browser)\" title=\"Links (web browser)\">Links</a></li>\n<li><a href=\"/wiki/Lynx_(web_browser)\" title=\"Lynx (web browser)\">Lynx</a></li>\n<li><a href=\"/wiki/NetSurf\" title=\"NetSurf\">NetSurf</a></li>\n<li><a href=\"/wiki/Opera_Mini\" title=\"Opera Mini\">Opera Mini</a></li>\n<li><a href=\"/wiki/W3m\" title=\"W3m\">w3m</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr></tbody></table><div></div></td></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks mw-collapsible mw-collapsed navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Discontinued3534\" style=\"font-size:114%;margin:0 4em\">Discontinued</div></th></tr><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Blink_(browser_engine)\" title=\"Blink (browser engine)\">Blink</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Beaker_(web_browser)\" title=\"Beaker (web browser)\">Beaker</a></li>\n<li><a href=\"/wiki/Citrio\" title=\"Citrio\">Citrio</a></li>\n<li><a href=\"/wiki/Flock_(web_browser)\" title=\"Flock (web browser)\">Flock</a></li>\n<li><a href=\"/wiki/Redcore\" title=\"Redcore\">Redcore</a></li>\n<li><a href=\"/wiki/Rockmelt\" title=\"Rockmelt\">Rockmelt</a></li>\n<li><a href=\"/wiki/SalamWeb\" title=\"SalamWeb\">SalamWeb</a></li>\n<li><a href=\"/wiki/Sputnik_(search_engine)#Browser\" title=\"Sputnik (search engine)\">Sputnik</a></li>\n<li><a href=\"/wiki/Torch_(web_browser)\" title=\"Torch (web browser)\">Torch</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Gecko_(software)\" title=\"Gecko (software)\">Gecko</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Beonex_Communicator\" title=\"Beonex Communicator\">Beonex</a></li>\n<li><a href=\"/wiki/Camino_(web_browser)\" title=\"Camino (web browser)\">Camino</a></li>\n<li><a href=\"/wiki/Classilla\" title=\"Classilla\">Classilla</a></li>\n<li><a href=\"/wiki/Conkeror\" title=\"Conkeror\">Conkeror</a></li>\n<li><a href=\"/wiki/Firefox_Lite\" class=\"mw-redirect\" title=\"Firefox Lite\">Firefox Lite</a></li>\n<li><a href=\"/wiki/Galeon\" title=\"Galeon\">Galeon</a></li>\n<li><a href=\"/wiki/Ghostzilla\" title=\"Ghostzilla\">Ghostzilla</a></li>\n<li><a href=\"/wiki/Comodo_IceDragon\" title=\"Comodo IceDragon\">IceDragon</a></li>\n<li><a href=\"/wiki/Kazehakase\" title=\"Kazehakase\">Kazehakase</a></li>\n<li><a href=\"/wiki/Kylo_(web_browser)\" title=\"Kylo (web browser)\">Kylo</a></li>\n<li><a href=\"/wiki/IBM_Lotus_Symphony\" title=\"IBM Lotus Symphony\">Lotus</a></li>\n<li><a href=\"/wiki/MicroB\" title=\"MicroB\">MicroB</a></li>\n<li><a href=\"/wiki/Minimo\" title=\"Minimo\">Minimo</a></li>\n<li><a href=\"/wiki/Mozilla_Application_Suite\" title=\"Mozilla Application Suite\">Mozilla suite</a></li>\n<li><a href=\"/wiki/PirateBrowser\" title=\"PirateBrowser\">PirateBrowser</a></li>\n<li><a href=\"/wiki/AT%26T_Pogo\" title=\"AT&amp;T Pogo\">Pogo</a></li>\n<li><a href=\"/wiki/Kirix_Strata\" title=\"Kirix Strata\">Strata</a></li>\n<li><a href=\"/wiki/Swiftfox\" title=\"Swiftfox\">Swiftfox</a></li>\n<li><a href=\"/wiki/Swiftweasel\" title=\"Swiftweasel\">Swiftweasel</a></li>\n<li><a href=\"/wiki/TenFourFox\" class=\"mw-redirect\" title=\"TenFourFox\">TenFourFox</a></li>\n<li><a href=\"/wiki/Timberwolf_(web_browser)\" title=\"Timberwolf (web browser)\">Timberwolf</a></li>\n<li><a href=\"/wiki/XB_Browser\" title=\"XB Browser\">xB</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Trident_(software)\" title=\"Trident (software)\">MSHTML</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Internet_Explorer\" title=\"Internet Explorer\">Internet Explorer</a></li>\n<li><a href=\"/wiki/AOL_Explorer\" title=\"AOL Explorer\">AOL</a></li>\n<li><a href=\"/wiki/Deepnet_Explorer\" title=\"Deepnet Explorer\">Deepnet</a></li>\n<li><a href=\"/wiki/GreenBrowser\" title=\"GreenBrowser\">GreenBrowser</a></li>\n<li><a href=\"/wiki/MediaBrowser\" title=\"MediaBrowser\">MediaBrowser</a></li>\n<li><a href=\"/wiki/MSN_Dial-Up_Internet_Access#MSN_Explorer\" title=\"MSN Dial-Up Internet Access\">MSN Explorer</a></li>\n<li><a href=\"/wiki/MSN_Dial-Up_Internet_Access#MSN_2.0\" title=\"MSN Dial-Up Internet Access\">MSN Program Viewer</a></li>\n<li><a href=\"/wiki/NeoPlanet\" title=\"NeoPlanet\">NeoPlanet</a></li>\n<li><a href=\"/wiki/NetCaptor\" title=\"NetCaptor\">NetCaptor</a></li>\n<li><a href=\"/wiki/SpaceTime_(software)\" title=\"SpaceTime (software)\">SpaceTime</a></li>\n<li><a href=\"/wiki/ZAC_Browser\" title=\"ZAC Browser\">ZAC</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/WebKit\" title=\"WebKit\">WebKit</a>-based</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Arora_(web_browser)\" title=\"Arora (web browser)\">Arora</a></li>\n<li><a href=\"/wiki/Bolt_(web_browser)\" title=\"Bolt (web browser)\">BOLT</a></li>\n<li><a href=\"/wiki/Dolphin_Browser\" title=\"Dolphin Browser\">Dolphin</a></li>\n<li><a href=\"/wiki/Fluid_(web_browser)\" title=\"Fluid (web browser)\">Fluid</a></li>\n<li><a href=\"/wiki/Google_TV_(smart_TV_platform)\" class=\"mw-redirect\" title=\"Google TV (smart TV platform)\">Google TV</a></li>\n<li><a href=\"/wiki/Iris_Browser\" title=\"Iris Browser\">Iris</a></li>\n<li><a href=\"/wiki/Mercury_Browser\" title=\"Mercury Browser\">Mercury</a></li>\n<li><a href=\"/wiki/Nokia_Browser_for_Symbian\" title=\"Nokia Browser for Symbian\">Nokia Symbian</a></li>\n<li><a href=\"/wiki/OmniWeb\" title=\"OmniWeb\">OmniWeb</a></li>\n<li><a href=\"/wiki/Opera_Coast\" title=\"Opera Coast\">Opera Coast</a></li>\n<li><a href=\"/wiki/Origyn_Web_Browser\" title=\"Origyn Web Browser\">Origyn</a></li>\n<li><a href=\"/wiki/QtWeb\" title=\"QtWeb\">QtWeb</a></li>\n<li><a href=\"/wiki/Shiira\" title=\"Shiira\">Shiira</a></li>\n<li><a href=\"/wiki/Steel_(web_browser)\" title=\"Steel (web browser)\">Steel</a></li>\n<li><a href=\"/wiki/Surf_(web_browser)\" title=\"Surf (web browser)\">surf</a></li>\n<li><a href=\"/wiki/Uzbl\" title=\"Uzbl\">Uzbl</a></li>\n<li><a href=\"/wiki/WebPositive\" class=\"mw-redirect\" title=\"WebPositive\">WebPositive</a></li>\n<li><a href=\"/wiki/Xombrero\" title=\"Xombrero\">xombrero</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Abaco_(web_browser)\" title=\"Abaco (web browser)\">abaco</a></li>\n<li><a href=\"/wiki/Amaya_(web_editor)\" title=\"Amaya (web editor)\">Amaya</a></li>\n<li><a href=\"/wiki/Arachne_(web_browser)\" title=\"Arachne (web browser)\">Arachne</a></li>\n<li><a href=\"/wiki/Arena_(web_browser)\" title=\"Arena (web browser)\">Arena</a></li>\n<li><a href=\"/wiki/Blazer_(web_browser)\" title=\"Blazer (web browser)\">Blazer</a></li>\n<li><a href=\"/wiki/Cake_Browser\" title=\"Cake Browser\">Cake</a></li>\n<li><a href=\"/wiki/CM_Browser\" title=\"CM Browser\">CM</a></li>\n<li><a href=\"/wiki/Microsoft_Live_Labs_Deepfish\" title=\"Microsoft Live Labs Deepfish\">Deepfish</a></li>\n<li><a href=\"/wiki/Microsoft_Edge_Legacy\" title=\"Microsoft Edge Legacy\">Edge Legacy</a></li>\n<li><a href=\"/wiki/ELinks\" title=\"ELinks\">ELinks</a></li>\n<li><a href=\"/wiki/Gazelle_(web_browser)\" title=\"Gazelle (web browser)\">Gazelle</a></li>\n<li><a href=\"/wiki/HotJava\" title=\"HotJava\">HotJava</a></li>\n<li><a href=\"/wiki/IBM_Home_Page_Reader\" title=\"IBM Home Page Reader\">IBM Home Page Reader</a></li>\n<li><a href=\"/wiki/IBM_WebExplorer\" title=\"IBM WebExplorer\">IBM WebExplorer</a></li>\n<li><a href=\"/wiki/IBrowse\" title=\"IBrowse\">IBrowse</a></li>\n<li><a href=\"/wiki/Internet_Explorer_for_Mac\" title=\"Internet Explorer for Mac\">Internet Explorer for Mac</a></li>\n<li><a href=\"/wiki/KidZui\" title=\"KidZui\">KidZui</a></li>\n<li><a href=\"/wiki/Line_Mode_Browser\" title=\"Line Mode Browser\">Line Mode</a></li>\n<li><a href=\"/wiki/Mosaic_(web_browser)\" class=\"mw-redirect\" title=\"Mosaic (web browser)\">Mosaic</a></li>\n<li><a href=\"/wiki/MSN_TV\" title=\"MSN TV\">MSN TV</a></li>\n<li><a href=\"/wiki/NetPositive\" class=\"mw-redirect\" title=\"NetPositive\">NetPositive</a></li>\n<li><a href=\"/wiki/Netscape_(web_browser)\" title=\"Netscape (web browser)\">Netscape</a></li>\n<li><a href=\"/wiki/Skweezer\" title=\"Skweezer\">Skweezer</a></li>\n<li><a href=\"/wiki/Skyfire_(company)\" title=\"Skyfire (company)\">Skyfire</a></li>\n<li><a href=\"/wiki/ThunderHawk\" title=\"ThunderHawk\">ThunderHawk</a></li>\n<li><a href=\"/wiki/Vision_Mobile_Browser\" title=\"Vision Mobile Browser\">Vision</a></li>\n<li><a href=\"/wiki/WinWAP\" title=\"WinWAP\">WinWAP</a></li>\n<li><a href=\"/wiki/WorldWideWeb\" title=\"WorldWideWeb\">WorldWideWeb</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr></tbody></table><div></div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><a href=\"/wiki/List_of_web_browsers\" title=\"List of web browsers\">List</a></li>\n<li><a href=\"/wiki/Comparison_of_web_browsers\" title=\"Comparison of web browsers\">Comparison</a></li>\n<li><a href=\"/wiki/Category:Web_browsers\" title=\"Category:Web browsers\">Category</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"World_Wide_Web_Consortium_(W3C)7139\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:W3C_standards\" title=\"Template:W3C standards\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:W3C_standards\" title=\"Template talk:W3C standards\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:W3C_standards\" title=\"Special:EditPage/Template:W3C standards\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"World_Wide_Web_Consortium_(W3C)7139\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/World_Wide_Web_Consortium\" title=\"World Wide Web Consortium\">World Wide Web Consortium</a> (W3C)</div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Products,<br />standards</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/World_Wide_Web_Consortium#W3C_recommendation_(REC)\" title=\"World Wide Web Consortium\">Recommendations</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ActivityPub\" title=\"ActivityPub\">ActivityPub</a></li>\n<li><a href=\"/wiki/Activity_Streams_(format)\" title=\"Activity Streams (format)\">Activity Streams</a></li>\n<li><a href=\"/wiki/WAI-ARIA\" title=\"WAI-ARIA\">ARIA</a></li>\n<li><a href=\"/wiki/Canonical_XML\" title=\"Canonical XML\">Canonical XML</a></li>\n<li><a href=\"/wiki/Compound_Document_Format\" title=\"Compound Document Format\">CDF</a></li>\n<li><a href=\"/wiki/CSS\" title=\"CSS\">CSS</a>\n<ul><li><a href=\"/wiki/CSS_animations\" title=\"CSS animations\">Animations</a></li>\n<li><a href=\"/wiki/Flexbox\" title=\"Flexbox\">Flexbox</a></li>\n<li><a href=\"/wiki/CSS_grid_layout\" title=\"CSS grid layout\">Grid</a></li></ul></li>\n<li><a href=\"/wiki/Document_Object_Model\" title=\"Document Object Model\">DOM</a></li>\n<li><a href=\"/wiki/Efficient_XML_Interchange\" title=\"Efficient XML Interchange\">EXI</a></li>\n<li><a href=\"/wiki/Emotion_Markup_Language\" title=\"Emotion Markup Language\">EmotionML</a></li>\n<li><a href=\"/wiki/W3C_Geolocation_API\" title=\"W3C Geolocation API\">Geolocation API</a></li>\n<li><a class=\"mw-selflink selflink\">HTML</a>\n<ul><li><a href=\"/wiki/HTML5\" title=\"HTML5\">HTML5</a></li></ul></li>\n<li><a href=\"/wiki/IndexedDB\" title=\"IndexedDB\">IndexedDB</a></li>\n<li><a href=\"/wiki/Internationalization_Tag_Set\" title=\"Internationalization Tag Set\">ITS</a></li>\n<li><a href=\"/wiki/JSON-LD\" title=\"JSON-LD\">JSON-LD</a></li>\n<li><a href=\"/wiki/Linked_Data_Notifications\" title=\"Linked Data Notifications\">Linked Data Notifications</a></li>\n<li><a href=\"/wiki/MathML\" title=\"MathML\">MathML</a></li>\n<li><a href=\"/wiki/Micropub_(protocol)\" title=\"Micropub (protocol)\">Micropub</a></li>\n<li><a href=\"/wiki/Web_Ontology_Language\" title=\"Web Ontology Language\">OWL</a></li>\n<li><a href=\"/wiki/Pronunciation_Lexicon_Specification\" title=\"Pronunciation Lexicon Specification\">PLS</a></li>\n<li><a href=\"/wiki/Resource_Description_Framework\" title=\"Resource Description Framework\">RDF</a>\n<ul><li><a href=\"/wiki/RDF_Schema\" title=\"RDF Schema\">Schema</a></li>\n<li><a href=\"/wiki/RDFa\" title=\"RDFa\">RDFa</a></li></ul></li>\n<li><a href=\"/wiki/Semantic_Interpretation_for_Speech_Recognition\" title=\"Semantic Interpretation for Speech Recognition\">SISR</a></li>\n<li><a href=\"/wiki/Simple_Knowledge_Organization_System\" title=\"Simple Knowledge Organization System\">SKOS</a></li>\n<li><a href=\"/wiki/Synchronized_Multimedia_Integration_Language\" title=\"Synchronized Multimedia Integration Language\">SMIL</a></li>\n<li><a href=\"/wiki/SOAP\" title=\"SOAP\">SOAP</a></li>\n<li><a href=\"/wiki/Speech_Recognition_Grammar_Specification\" title=\"Speech Recognition Grammar Specification\">SRGS</a></li>\n<li><a href=\"/wiki/Subresource_Integrity\" title=\"Subresource Integrity\">SRI</a></li>\n<li><a href=\"/wiki/Speech_Synthesis_Markup_Language\" title=\"Speech Synthesis Markup Language\">SSML</a></li>\n<li><a href=\"/wiki/SVG\" title=\"SVG\">SVG</a>\n<ul><li><a href=\"/wiki/SVG_filter_effects\" title=\"SVG filter effects\">Filter Effects</a></li></ul></li>\n<li><a href=\"/wiki/SCXML\" title=\"SCXML\">SCXML</a></li>\n<li><a href=\"/wiki/SHACL\" title=\"SHACL\">SHACL</a></li>\n<li><a href=\"/wiki/SPARQL\" title=\"SPARQL\">SPARQL</a></li>\n<li><a href=\"/wiki/Timed_text\" title=\"Timed text\">Timed text</a></li>\n<li><a href=\"/wiki/VoiceXML\" title=\"VoiceXML\">VoiceXML</a></li>\n<li><a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a></li>\n<li><a href=\"/wiki/Web_of_Things\" title=\"Web of Things\">WoT</a>\n<ul><li><a href=\"/wiki/Thing_Description\" title=\"Thing Description\">TD</a></li></ul></li>\n<li><a href=\"/wiki/Web_storage\" title=\"Web storage\">Web storage</a></li>\n<li><a href=\"/wiki/Web_Services_Description_Language\" title=\"Web Services Description Language\">WSDL</a></li>\n<li><a href=\"/wiki/Webmention\" title=\"Webmention\">Webmention</a></li>\n<li><a href=\"/wiki/WebSub\" title=\"WebSub\">WebSub</a></li>\n<li><a href=\"/wiki/WebVTT\" title=\"WebVTT\">WebVTT</a></li>\n<li><a href=\"/wiki/Web_Open_Font_Format\" title=\"Web Open Font Format\">WOFF</a></li>\n<li><a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a>\n<ul><li><a href=\"/wiki/XHTML%2BRDFa\" title=\"XHTML+RDFa\">+RDFa</a></li></ul></li>\n<li><a href=\"/wiki/XML\" title=\"XML\">XML</a>\n<ul><li><a href=\"/wiki/XML_Base\" title=\"XML Base\">Base</a></li>\n<li><a href=\"/wiki/XML_Encryption\" title=\"XML Encryption\">Encryption</a></li>\n<li><a href=\"/wiki/XML_Events\" title=\"XML Events\">Events</a></li>\n<li><a href=\"/wiki/XML_Information_Set\" title=\"XML Information Set\">Information Set</a></li>\n<li><a href=\"/wiki/XML_namespace\" title=\"XML namespace\">Namespace</a></li>\n<li><a href=\"/wiki/XML_Schema_(W3C)\" title=\"XML Schema (W3C)\">Schema</a></li>\n<li><a href=\"/wiki/XML_Signature\" title=\"XML Signature\">Signature</a></li>\n<li><a href=\"/wiki/XForms\" title=\"XForms\">XForms</a></li>\n<li><a href=\"/wiki/XInclude\" title=\"XInclude\">XInclude</a></li>\n<li><a href=\"/wiki/XLink\" title=\"XLink\">XLink</a></li>\n<li><a href=\"/wiki/XML-binary_Optimized_Packaging\" title=\"XML-binary Optimized Packaging\">XOP</a></li>\n<li><a href=\"/wiki/XPath\" title=\"XPath\">XPath</a>\n<ul><li><a href=\"/wiki/XPath_2.0\" title=\"XPath 2.0\">2.0</a></li>\n<li><a href=\"/wiki/XPath_3\" title=\"XPath 3\">3.x</a></li></ul></li>\n<li><a href=\"/wiki/XPointer\" title=\"XPointer\">XPointer</a></li>\n<li><a href=\"/wiki/XProc\" title=\"XProc\">XProc</a></li>\n<li><a href=\"/wiki/XQuery\" title=\"XQuery\">XQuery</a></li>\n<li><a href=\"/wiki/XSL\" title=\"XSL\">XSL</a></li>\n<li><a href=\"/wiki/XSL_Formatting_Objects\" title=\"XSL Formatting Objects\">XSL-FO</a></li>\n<li><a href=\"/wiki/XSLT\" title=\"XSLT\">XSLT</a>\n<ul><li><a href=\"/wiki/XSLT_elements\" title=\"XSLT elements\">elements</a></li></ul></li></ul></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Notes</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/IndieAuth\" title=\"IndieAuth\">IndieAuth</a></li>\n<li><a href=\"/wiki/XAdES\" title=\"XAdES\">XAdES</a></li>\n<li><a href=\"/wiki/XBL\" title=\"XBL\">XBL</a></li>\n<li><a href=\"/wiki/XHTML%2BSMIL\" title=\"XHTML+SMIL\">XHTML+SMIL</a></li>\n<li><a href=\"/wiki/Extensible_User_Interface_Protocol\" title=\"Extensible User Interface Protocol\">XUP</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/World_Wide_Web_Consortium#Working_draft_(WD)\" title=\"World Wide Web Consortium\">Working drafts</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Call_Control_eXtensible_Markup_Language\" title=\"Call Control eXtensible Markup Language\">CCXML</a></li>\n<li><a href=\"/wiki/CURIE\" title=\"CURIE\">CURIE</a></li>\n<li><a href=\"/wiki/Encrypted_Media_Extensions\" title=\"Encrypted Media Extensions\">EME</a></li>\n<li><a href=\"/wiki/InkML\" title=\"InkML\">InkML</a></li>\n<li><a href=\"/wiki/Media_Source_Extensions\" title=\"Media Source Extensions\">MSE</a></li>\n<li><a href=\"/wiki/Rule_Interchange_Format\" title=\"Rule Interchange Format\">RIF</a></li>\n<li><a href=\"/wiki/SMIL_Timesheets\" title=\"SMIL Timesheets\">SMIL Timesheets</a></li>\n<li><a href=\"/wiki/SXBL\" title=\"SXBL\">sXBL</a></li>\n<li><a href=\"/wiki/WebGPU\" title=\"WebGPU\">WebGPU</a></li>\n<li><a href=\"/wiki/WebXR\" title=\"WebXR\">WebXR</a></li>\n<li><a href=\"/wiki/Extensible_Forms_Description_Language\" title=\"Extensible Forms Description Language\">XFDL</a></li>\n<li><a href=\"/wiki/XFrames\" title=\"XFrames\">XFrames</a></li>\n<li><a href=\"/wiki/XMLHttpRequest\" title=\"XMLHttpRequest\">XMLHttpRequest</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Guidelines</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Web_Content_Accessibility_Guidelines\" title=\"Web Content Accessibility Guidelines\">Web Content Accessibility Guidelines</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Initiative</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/W3C_Markup_Validation_Service\" title=\"W3C Markup Validation Service\">Markup Validation Service</a></li>\n<li><a href=\"/wiki/Web_Accessibility_Initiative\" title=\"Web Accessibility Initiative\">Web Accessibility Initiative</a></li>\n<li><a href=\"/wiki/Web_Components\" title=\"Web Components\">Web Components</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Deprecated</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/C-HTML\" class=\"mw-redirect\" title=\"C-HTML\">C-HTML</a></li>\n<li><a href=\"/wiki/Handheld_Device_Markup_Language\" title=\"Handheld Device Markup Language\">HDML</a></li>\n<li><a href=\"/wiki/JavaScript_Style_Sheets\" title=\"JavaScript Style Sheets\">JSSS</a></li>\n<li><a href=\"/wiki/Precision_Graphics_Markup_Language\" title=\"Precision Graphics Markup Language\">PGML</a></li>\n<li><a href=\"/wiki/Vector_Markup_Language\" title=\"Vector Markup Language\">VML</a></li>\n<li><a href=\"/wiki/WebPlatform.org\" title=\"WebPlatform.org\">WebPlatform</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Obsoleted</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/P3P\" title=\"P3P\">P3P</a></li>\n<li><a href=\"/wiki/XHTML%2BMathML%2BSVG\" title=\"XHTML+MathML+SVG\">XHTML+MathML+SVG</a></li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Groups,<br />organizations</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><td colspan=\"2\" class=\"navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/WHATWG\" title=\"WHATWG\">WHATWG</a></li>\n<li>Defunct: <a href=\"/wiki/World_Wide_Web_Foundation\" title=\"World Wide Web Foundation\">World Wide Web Foundation</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Elected</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/w/index.php?title=W3C_Advisory_Board&amp;action=edit&amp;redlink=1\" class=\"new\" title=\"W3C Advisory Board (page does not exist)\">AB</a></li>\n<li><a href=\"/w/index.php?title=W3C_Board&amp;action=edit&amp;redlink=1\" class=\"new\" title=\"W3C Board (page does not exist)\">Board</a></li>\n<li><a href=\"/wiki/Technical_Architecture_Group\" title=\"Technical Architecture Group\">TAG</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Working</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/CSS_Working_Group\" title=\"CSS Working Group\">CSS</a></li>\n<li><a href=\"/wiki/SVG_Working_Group\" title=\"SVG Working Group\">SVG</a></li>\n<li><a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a></li>\n<li><a href=\"/wiki/Web_Authentication_Working_Group\" title=\"Web Authentication Working Group\">WebAuthn</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Community, business</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Improving_Web_Advertising_Business_Group\" title=\"Improving Web Advertising Business Group\">Web Advertising BG</a></li>\n<li><a href=\"/wiki/WebAssembly\" title=\"WebAssembly\">WebAssembly</a> CG</li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Closed</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/W3C_Device_Description_Working_Group\" title=\"W3C Device Description Working Group\">Device Description</a> (DDWG)</li>\n<li><a href=\"/wiki/HTML_Working_Group\" title=\"HTML Working Group\">HTML</a></li>\n<li><a href=\"/wiki/W3C_MMI\" title=\"W3C MMI\">Multimodal Interaction Activity</a> (MMI)</li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Software</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"></div><table class=\"nowraplinks navbox-subgroup\" style=\"border-spacing:0\"><tbody><tr><td colspan=\"2\" class=\"navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/CERN_httpd\" title=\"CERN httpd\">CERN httpd</a></li>\n<li><a href=\"/wiki/Libwww\" title=\"Libwww\">Libwww</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Web_browser\" title=\"Web browser\">Browsers</a></th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Line_Mode_Browser\" title=\"Line Mode Browser\">Line Mode</a> (1990–)</li>\n<li><a href=\"/wiki/Arena_(web_browser)\" title=\"Arena (web browser)\">Arena</a> (1993–98)</li>\n<li><a href=\"/wiki/Agora_(web_browser)\" title=\"Agora (web browser)\">Agora</a> (1994–97)</li>\n<li><a href=\"/wiki/Argo_(web_browser)\" title=\"Argo (web browser)\">Argo</a> (1994–97)</li>\n<li><a href=\"/wiki/Amaya_(web_editor)\" title=\"Amaya (web editor)\">Amaya</a> (browser/editor, 1996–2012)</li></ul>\n</div></td></tr></tbody></table><div></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Conferences</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/The_Web_Conference\" title=\"The Web Conference\">International World Wide Web Conference</a> (IW3C)\n<ul><li><a href=\"/wiki/International_World_Wide_Web_Conference_Committee\" title=\"International World Wide Web Conference Committee\">Steering Committee</a> (IW3C2)</li>\n<li><a href=\"/wiki/First_International_Conference_on_the_World-Wide_Web\" title=\"First International Conference on the World-Wide Web\">First conference</a> (\"WWW1\", 1994)</li></ul></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Document_markup_languages1386\" style=\"padding:3px\"><table class=\"nowraplinks mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Document_markup_languages\" title=\"Template:Document markup languages\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Document_markup_languages\" title=\"Template talk:Document markup languages\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Document_markup_languages\" title=\"Special:EditPage/Template:Document markup languages\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Document_markup_languages1386\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Markup_language\" title=\"Markup language\">Document markup languages</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Office_suite\" class=\"mw-redirect\" title=\"Office suite\">Office suite</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Compound_Document_Format\" title=\"Compound Document Format\">Compound Document Format</a></li>\n<li><a href=\"/wiki/Office_Open_XML\" title=\"Office Open XML\">OOXML</a>\n<ul><li><a href=\"/wiki/SpreadsheetML\" title=\"SpreadsheetML\">SpreadsheetML</a></li>\n<li><a href=\"/wiki/PresentationML\" class=\"mw-redirect\" title=\"PresentationML\">PresentationML</a></li>\n<li><a href=\"/wiki/WordprocessingML\" class=\"mw-redirect\" title=\"WordprocessingML\">WordprocessingML</a></li></ul></li>\n<li><a href=\"/wiki/OpenDocument\" title=\"OpenDocument\">ODF</a></li>\n<li><a href=\"/wiki/Uniform_Office_Format\" title=\"Uniform Office Format\">UOF</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Well-known</th><td class=\"navbox-list-with-group navbox-list navbox-even hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a class=\"mw-selflink selflink\">HTML</a></li>\n<li><a href=\"/wiki/XHTML\" title=\"XHTML\">XHTML</a></li>\n<li><a href=\"/wiki/MathML\" title=\"MathML\">MathML</a></li>\n<li><a href=\"/wiki/Rich_Text_Format\" title=\"Rich Text Format\">RTF</a></li>\n<li><a href=\"/wiki/TeX\" title=\"TeX\">TeX</a></li>\n<li><a href=\"/wiki/LaTeX\" title=\"LaTeX\">LaTeX</a></li>\n<li><a href=\"/wiki/Markdown\" title=\"Markdown\">Markdown</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Lesser-known</th><td class=\"navbox-list-with-group navbox-list navbox-odd hlist\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/AmigaGuide\" title=\"AmigaGuide\">AmigaGuide</a></li>\n<li><a href=\"/wiki/AsciiDoc\" title=\"AsciiDoc\">AsciiDoc</a></li>\n<li><a href=\"/wiki/BBCode\" title=\"BBCode\">BBCode</a></li>\n<li><a href=\"/wiki/Chemical_Markup_Language\" title=\"Chemical Markup Language\">CML</a></li>\n<li><a href=\"/wiki/C-HTML\" class=\"mw-redirect\" title=\"C-HTML\">C-HTML</a></li>\n<li><a href=\"/wiki/ConTeXt\" title=\"ConTeXt\">ConTeXt</a></li>\n<li><a href=\"/wiki/CrossMark\" class=\"mw-redirect\" title=\"CrossMark\">CrossMark</a></li>\n<li><a href=\"/wiki/Darwin_Information_Typing_Architecture\" title=\"Darwin Information Typing Architecture\">DITA</a></li>\n<li><a href=\"/wiki/DocBook\" title=\"DocBook\">DocBook</a></li>\n<li><a href=\"/wiki/Encoded_Archival_Description\" title=\"Encoded Archival Description\">EAD</a></li>\n<li><a href=\"/wiki/Enriched_text\" title=\"Enriched text\">Enriched text</a></li>\n<li><a href=\"/wiki/FHTML\" title=\"FHTML\">FHTML</a></li>\n<li><a href=\"/wiki/List_of_document_markup_languages#GML_Disambiguation\" title=\"List of document markup languages\">GML</a></li>\n<li><a href=\"/wiki/GuideML\" class=\"mw-redirect\" title=\"GuideML\">GuideML</a></li>\n<li><a href=\"/wiki/Handheld_Device_Markup_Language\" title=\"Handheld Device Markup Language\">HDML</a></li>\n<li><a href=\"/wiki/HyTime\" title=\"HyTime\">HyTime</a></li>\n<li><a href=\"/wiki/Information_Presentation_Facility\" title=\"Information Presentation Facility\">IPF</a></li>\n<li><a href=\"/wiki/LilyPond\" title=\"LilyPond\">LilyPond</a></li>\n<li><a href=\"/wiki/LinuxDoc\" title=\"LinuxDoc\">LinuxDoc</a></li>\n<li>Lout</li>\n<li><a href=\"/wiki/Maker_Interchange_Format\" class=\"mw-redirect\" title=\"Maker Interchange Format\">MIF</a></li>\n<li><a href=\"/wiki/Microsoft_Assistance_Markup_Language\" title=\"Microsoft Assistance Markup Language\">MAML</a></li>\n<li><a href=\"/wiki/Music_Encoding_Initiative\" title=\"Music Encoding Initiative\">MEI</a></li>\n<li><a href=\"/wiki/MusicXML\" title=\"MusicXML\">MusicXML</a></li>\n<li><a href=\"/wiki/OMDoc\" title=\"OMDoc\">OMDoc</a></li>\n<li><a href=\"/wiki/OpenMath\" title=\"OpenMath\">OpenMath</a></li>\n<li><a href=\"/wiki/Org-mode\" title=\"Org-mode\">Org-mode</a></li>\n<li><a href=\"/wiki/Plain_Old_Documentation\" title=\"Plain Old Documentation\">POD</a></li>\n<li><a href=\"/wiki/ReStructuredText\" title=\"ReStructuredText\">ReStructuredText</a></li>\n<li><a href=\"/wiki/RTML\" title=\"RTML\">RTML</a></li>\n<li><a href=\"/wiki/Revisable-Form_Text\" class=\"mw-redirect\" title=\"Revisable-Form Text\">RFT</a></li>\n<li><a href=\"/wiki/S1000D\" title=\"S1000D\">S1000D</a></li>\n<li><a href=\"/wiki/Setext\" title=\"Setext\">Setext</a></li>\n<li><a href=\"/wiki/Text_Encoding_Initiative\" title=\"Text Encoding Initiative\">TEI</a></li>\n<li><a href=\"/wiki/Texinfo\" title=\"Texinfo\">Texinfo</a></li>\n<li><a href=\"/wiki/Troff\" title=\"Troff\">troff</a></li>\n<li><a href=\"/wiki/Wiki#Editing\" title=\"Wiki\">Wikitext</a></li>\n<li><a href=\"/wiki/Wireless_Markup_Language\" title=\"Wireless Markup Language\">WML</a></li>\n<li><a href=\"/wiki/WapTV\" title=\"WapTV\">WapTV</a></li>\n<li><a href=\"/wiki/Extensible_Application_Markup_Language\" title=\"Extensible Application Markup Language\">XAML</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div><a href=\"/wiki/List_of_document_markup_languages\" title=\"List of document markup languages\">List of document markup languages</a></div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"International_Organization_for_Standardization_(ISO)_standards11486\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible mw-collapsed navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:ISO_standards\" title=\"Template:ISO standards\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:ISO_standards\" title=\"Template talk:ISO standards\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:ISO_standards\" title=\"Special:EditPage/Template:ISO standards\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"International_Organization_for_Standardization_(ISO)_standards11486\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/International_Organization_for_Standardization\" title=\"International Organization for Standardization\">International Organization for Standardization</a> (ISO) standards</div></th></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>List of <a href=\"/wiki/List_of_ISO_standards\" title=\"List of ISO standards\">ISO standards</a> – <a href=\"/wiki/List_of_ISO_romanizations\" title=\"List of ISO romanizations\">ISO romanizations</a> – <a href=\"/wiki/List_of_IEC_standards\" title=\"List of IEC standards\">IEC standards</a></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">1–9999</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ISO_1\" title=\"ISO 1\">1</a></li>\n<li><a href=\"/wiki/ISO_2\" title=\"ISO 2\">2</a></li>\n<li><a href=\"/wiki/Renard_series\" title=\"Renard series\">3</a></li>\n<li><a href=\"/wiki/ISO_4\" title=\"ISO 4\">4</a></li>\n<li><a href=\"/wiki/Film_speed\" title=\"Film speed\">6</a></li>\n<li><a href=\"/wiki/British_Standard_Pipe\" title=\"British Standard Pipe\">7</a></li>\n<li><a href=\"/wiki/ISO_9\" title=\"ISO 9\">9</a></li>\n<li><a href=\"/wiki/A440_(pitch_standard)\" title=\"A440 (pitch standard)\">16</a></li>\n<li><a href=\"/wiki/Renard_series\" title=\"Renard series\">17</a></li>\n<li><a href=\"/wiki/ISO_31\" title=\"ISO 31\">31</a>\n<ul><li><a href=\"/wiki/ISO_31-0\" title=\"ISO 31-0\">-0</a></li>\n<li><a href=\"/wiki/ISO_31-1\" title=\"ISO 31-1\">-1</a></li>\n<li><a href=\"/wiki/ISO_31-3\" title=\"ISO 31-3\">-3</a></li>\n<li><a href=\"/wiki/ISO_31-4\" title=\"ISO 31-4\">-4</a></li>\n<li><a href=\"/wiki/ISO_31-5\" title=\"ISO 31-5\">-5</a></li>\n<li><a href=\"/wiki/ISO_31-6\" title=\"ISO 31-6\">-6</a></li>\n<li><a href=\"/wiki/ISO_31-7\" title=\"ISO 31-7\">-7</a></li>\n<li><a href=\"/wiki/ISO_31-8\" title=\"ISO 31-8\">-8</a></li>\n<li><a href=\"/wiki/ISO_31-9\" class=\"mw-redirect\" title=\"ISO 31-9\">-9</a></li>\n<li><a href=\"/wiki/ISO_31-10\" title=\"ISO 31-10\">-10</a></li>\n<li><a href=\"/wiki/ISO_31-11\" title=\"ISO 31-11\">-11</a></li>\n<li><a href=\"/wiki/ISO_31-12\" class=\"mw-redirect\" title=\"ISO 31-12\">-12</a></li>\n<li><a href=\"/wiki/ISO_31-13\" class=\"mw-redirect\" title=\"ISO 31-13\">-13</a></li></ul></li>\n<li><a href=\"/wiki/ISO_metric_screw_thread\" title=\"ISO metric screw thread\">68-1</a></li>\n<li><a href=\"/wiki/ISO_128\" title=\"ISO 128\">128</a></li>\n<li><a href=\"/wiki/ISO_216\" title=\"ISO 216\">216</a></li>\n<li><a href=\"/wiki/ISO_217\" title=\"ISO 217\">217</a></li>\n<li><a href=\"/wiki/Equal-loudness_contour\" title=\"Equal-loudness contour\">226</a></li>\n<li><a href=\"/wiki/British_Standard_Pipe\" title=\"British Standard Pipe\">228</a></li>\n<li><a href=\"/wiki/ISO_233\" title=\"ISO 233\">233</a></li>\n<li><a href=\"/wiki/ISO_259\" title=\"ISO 259\">259</a></li>\n<li><a href=\"/wiki/ISO_metric_screw_thread\" title=\"ISO metric screw thread\">261</a></li>\n<li><a href=\"/wiki/ISO_metric_screw_thread\" title=\"ISO metric screw thread\">262</a></li>\n<li><a href=\"/wiki/Kappa_number\" title=\"Kappa number\">302</a></li>\n<li><a href=\"/wiki/Vicat_softening_point\" title=\"Vicat softening point\">306</a></li>\n<li><a href=\"/wiki/Hazard_symbol#Ionizing_radiation_symbol\" title=\"Hazard symbol\">361</a></li>\n<li><a href=\"/wiki/Power_take-off\" title=\"Power take-off\">500</a></li>\n<li><a href=\"/wiki/Hot_shoe\" title=\"Hot shoe\">518</a></li>\n<li><a href=\"/wiki/Prontor-Compur\" title=\"Prontor-Compur\">519</a></li>\n<li><a href=\"/wiki/ISO_639\" title=\"ISO 639\">639</a>\n<ul><li><a href=\"/wiki/ISO_639-1\" title=\"ISO 639-1\">-1</a></li>\n<li><a href=\"/wiki/ISO_639-2\" title=\"ISO 639-2\">-2</a></li>\n<li><a href=\"/wiki/ISO_639-3\" title=\"ISO 639-3\">-3</a></li>\n<li><a href=\"/wiki/ISO_639-5\" title=\"ISO 639-5\">-5</a></li>\n<li><a href=\"/wiki/ISO_639-6\" title=\"ISO 639-6\">-6</a></li></ul></li>\n<li><a href=\"/wiki/ISO/IEC_646\" title=\"ISO/IEC 646\">646</a></li>\n<li><a href=\"/wiki/ISO_657\" title=\"ISO 657\">657</a></li>\n<li><a href=\"/wiki/ISO_668\" title=\"ISO 668\">668</a></li>\n<li><a href=\"/wiki/ISO_690\" title=\"ISO 690\">690</a></li>\n<li><a href=\"/wiki/ISO_704\" title=\"ISO 704\">704</a></li>\n<li><a href=\"/wiki/ISO_732\" title=\"ISO 732\">732</a></li>\n<li><a href=\"/wiki/Antimagnetic_watch\" title=\"Antimagnetic watch\">764</a></li>\n<li><a href=\"/wiki/Hole_punch\" title=\"Hole punch\">838</a></li>\n<li><a href=\"/wiki/ISO_843\" title=\"ISO 843\">843</a></li>\n<li><a href=\"/wiki/ISO_860\" title=\"ISO 860\">860</a></li>\n<li><a href=\"/wiki/ISO_898\" title=\"ISO 898\">898</a></li>\n<li><a href=\"/wiki/ISO_965\" title=\"ISO 965\">965</a></li>\n<li><a href=\"/wiki/ISO_999\" title=\"ISO 999\">999</a></li>\n<li><a href=\"/wiki/ISO_1000\" title=\"ISO 1000\">1000</a></li>\n<li><a href=\"/wiki/Magnetic_ink_character_recognition\" title=\"Magnetic ink character recognition\">1004</a></li>\n<li><a href=\"/wiki/135_film\" title=\"135 film\">1007</a></li>\n<li><a href=\"/wiki/OCR-A\" title=\"OCR-A\">1073-1</a></li>\n<li><a href=\"/wiki/OCR-B\" title=\"OCR-B\">1073-2</a></li>\n<li><a href=\"/wiki/Longitudinal_redundancy_check\" title=\"Longitudinal redundancy check\">1155</a></li>\n<li><a href=\"/wiki/Shock-resistant_watch#ISO_1413_shock-resistant_standard\" title=\"Shock-resistant watch\">1413</a></li>\n<li><a href=\"/wiki/ALGOL_60\" title=\"ALGOL 60\">1538</a></li>\n<li><a href=\"/wiki/ISO_1629\" title=\"ISO 1629\">1629</a></li>\n<li><a href=\"/wiki/ISO_1745\" title=\"ISO 1745\">1745</a></li>\n<li><a href=\"/wiki/COBOL\" title=\"COBOL\">1989</a></li>\n<li><a href=\"/wiki/ISO_2014\" title=\"ISO 2014\">2014</a></li>\n<li><a href=\"/wiki/ISO_2015\" title=\"ISO 2015\">2015</a></li>\n<li><a href=\"/wiki/ISO/IEC_2022\" title=\"ISO/IEC 2022\">2022</a></li>\n<li><a href=\"/wiki/ISO_2033\" title=\"ISO 2033\">2033</a></li>\n<li><a href=\"/wiki/ISO_2047\" title=\"ISO 2047\">2047</a></li>\n<li><a href=\"/wiki/ISBN\" title=\"ISBN\">2108</a></li>\n<li><a href=\"/wiki/ISO_2145\" title=\"ISO 2145\">2145</a></li>\n<li><a href=\"/wiki/ISO_2146\" title=\"ISO 2146\">2146</a></li>\n<li><a href=\"/wiki/Film_speed\" title=\"Film speed\">2240</a></li>\n<li><a href=\"/wiki/Water_Resistant_mark\" title=\"Water Resistant mark\">2281</a></li>\n<li><a href=\"/wiki/International_Standard_Atmosphere\" title=\"International Standard Atmosphere\">2533</a></li>\n<li><a href=\"/wiki/ISO_2709\" title=\"ISO 2709\">2709</a></li>\n<li><a href=\"/wiki/ISO_2711\" title=\"ISO 2711\">2711</a></li>\n<li><a href=\"/wiki/Film_speed\" title=\"Film speed\">2720</a></li>\n<li><a href=\"/wiki/ISO_2788\" title=\"ISO 2788\">2788</a></li>\n<li><a href=\"/wiki/ISO_2848\" title=\"ISO 2848\">2848</a></li>\n<li><a href=\"/wiki/ISO_2852\" title=\"ISO 2852\">2852</a></li>\n<li><a href=\"/wiki/ISO_2921\" title=\"ISO 2921\">2921</a></li>\n<li><a href=\"/wiki/126_film\" title=\"126 film\">3029</a></li>\n<li><a href=\"/wiki/ISO_3103\" title=\"ISO 3103\">3103</a></li>\n<li><a href=\"/wiki/ISO_3166\" title=\"ISO 3166\">3166</a>\n<ul><li><a href=\"/wiki/ISO_3166-1\" title=\"ISO 3166-1\">-1</a></li>\n<li><a href=\"/wiki/ISO_3166-2\" title=\"ISO 3166-2\">-2</a></li>\n<li><a href=\"/wiki/ISO_3166-3\" title=\"ISO 3166-3\">-3</a></li></ul></li>\n<li><a href=\"/wiki/ISSN\" title=\"ISSN\">3297</a></li>\n<li><a href=\"/wiki/ISO_3307\" title=\"ISO 3307\">3307</a></li>\n<li><a href=\"/wiki/O-ring\" title=\"O-ring\">3601</a></li>\n<li><a href=\"/wiki/Kunrei-shiki\" title=\"Kunrei-shiki\">3602</a></li>\n<li><a href=\"/wiki/ISO_3864\" title=\"ISO 3864\">3864</a></li>\n<li><a href=\"/wiki/International_Standard_Recording_Code\" title=\"International Standard Recording Code\">3901</a></li>\n<li><a href=\"/wiki/FDI_World_Dental_Federation_notation\" title=\"FDI World Dental Federation notation\">3950</a></li>\n<li><a href=\"/wiki/ISO_3977\" title=\"ISO 3977\">3977</a></li>\n<li><a href=\"/wiki/ISO_4031\" title=\"ISO 4031\">4031</a></li>\n<li><a href=\"/wiki/ISO_4157\" class=\"mw-redirect\" title=\"ISO 4157\">4157</a></li>\n<li><a href=\"/wiki/ISO_4165\" title=\"ISO 4165\">4165</a></li>\n<li><a href=\"/wiki/ISO_4217\" title=\"ISO 4217\">4217</a></li>\n<li><a href=\"/wiki/ISO/IEC_4909\" title=\"ISO/IEC 4909\">4909</a></li>\n<li><a href=\"/wiki/ISO/IEC_5218\" title=\"ISO/IEC 5218\">5218</a></li>\n<li><a href=\"/wiki/ISO_5426\" title=\"ISO 5426\">5426</a></li>\n<li><a href=\"/wiki/ISO_5427\" title=\"ISO 5427\">5427</a></li>\n<li><a href=\"/wiki/ISO_5428\" title=\"ISO 5428\">5428</a></li>\n<li><a href=\"/wiki/Accuracy_and_precision\" title=\"Accuracy and precision\">5725</a></li>\n<li><a href=\"/wiki/ISO_5775\" title=\"ISO 5775\">5775</a></li>\n<li><a href=\"/wiki/ISO_5776\" title=\"ISO 5776\">5776</a></li>\n<li><a href=\"/wiki/Film_speed\" title=\"Film speed\">5800</a></li>\n<li><a href=\"/wiki/Flowchart\" title=\"Flowchart\">5807</a></li>\n<li><a href=\"/wiki/ISO_5964\" title=\"ISO 5964\">5964</a></li>\n<li><a href=\"/wiki/International_Securities_Identification_Number\" title=\"International Securities Identification Number\">6166</a></li>\n<li><a href=\"/wiki/ISO_6344\" title=\"ISO 6344\">6344</a></li>\n<li><a href=\"/wiki/ISO_6346\" title=\"ISO 6346\">6346</a></li>\n<li><a href=\"/wiki/Minimal_BASIC\" title=\"Minimal BASIC\">6373</a></li>\n<li><a href=\"/wiki/ISO_6385\" title=\"ISO 6385\">6385</a></li>\n<li><a href=\"/wiki/Water_Resistant_mark\" title=\"Water Resistant mark\">6425</a></li>\n<li><a href=\"/wiki/ANSI_escape_code\" title=\"ANSI escape code\">6429</a></li>\n<li><a href=\"/wiki/ISO_6438\" title=\"ISO 6438\">6438</a></li>\n<li><a href=\"/wiki/ISO/IEC_6523\" title=\"ISO/IEC 6523\">6523</a></li>\n<li><a href=\"/wiki/ISO_6709\" title=\"ISO 6709\">6709</a></li>\n<li><a href=\"/wiki/ISO_6943\" title=\"ISO 6943\">6943</a></li>\n<li><a href=\"/wiki/ISO_7001\" title=\"ISO 7001\">7001</a></li>\n<li><a href=\"/wiki/ISO_7002\" title=\"ISO 7002\">7002</a></li>\n<li><a href=\"/wiki/ISO_7010\" title=\"ISO 7010\">7010</a></li>\n<li><a href=\"/wiki/ISO_7027\" title=\"ISO 7027\">7027</a></li>\n<li><a href=\"/wiki/ISO/IEC_7064\" title=\"ISO/IEC 7064\">7064</a></li>\n<li><a href=\"/wiki/Pinyin\" title=\"Pinyin\">7098</a></li>\n<li><a href=\"/wiki/Pascal_(programming_language)\" title=\"Pascal (programming language)\">7185</a></li>\n<li><a href=\"/wiki/ISO_7200\" title=\"ISO 7200\">7200</a></li>\n<li><a href=\"/wiki/OSI_model\" title=\"OSI model\">7498</a>\n<ul><li><a href=\"/wiki/OSI_model\" title=\"OSI model\">-1</a></li></ul></li>\n<li><a href=\"/wiki/ISO_7637\" title=\"ISO 7637\">7637</a></li>\n<li><a href=\"/wiki/ISO_7736\" title=\"ISO 7736\">7736</a></li>\n<li><a href=\"/wiki/ISO/IEC_7810\" title=\"ISO/IEC 7810\">7810</a></li>\n<li><a href=\"/wiki/ISO/IEC_7811\" title=\"ISO/IEC 7811\">7811</a></li>\n<li><a href=\"/wiki/ISO/IEC_7812\" title=\"ISO/IEC 7812\">7812</a></li>\n<li><a href=\"/wiki/ISO/IEC_7813\" title=\"ISO/IEC 7813\">7813</a></li>\n<li><a href=\"/wiki/ISO/IEC_7816\" title=\"ISO/IEC 7816\">7816</a></li>\n<li><a href=\"/wiki/Graphical_Kernel_System\" title=\"Graphical Kernel System\">7942</a></li>\n<li><a href=\"/wiki/ISO_8000\" title=\"ISO 8000\">8000</a></li>\n<li><a href=\"/wiki/On-board_diagnostics\" title=\"On-board diagnostics\">8093</a></li>\n<li><a href=\"/wiki/ISO_8178\" title=\"ISO 8178\">8178</a></li>\n<li><a href=\"/wiki/Fuel_oil\" title=\"Fuel oil\">8217</a></li>\n<li><a href=\"/wiki/ISO_8373\" class=\"mw-redirect\" title=\"ISO 8373\">8373</a></li>\n<li><a href=\"/wiki/ISO_8501-1\" title=\"ISO 8501-1\">8501-1</a></li>\n<li><a href=\"/wiki/FTAM\" title=\"FTAM\">8571</a></li>\n<li><a href=\"/wiki/ISO_8583\" title=\"ISO 8583\">8583</a></li>\n<li><a href=\"/wiki/ISO_8601\" title=\"ISO 8601\">8601</a></li>\n<li><a href=\"/wiki/Open_Document_Architecture\" title=\"Open Document Architecture\">8613</a></li>\n<li><a href=\"/wiki/Computer_Graphics_Metafile\" title=\"Computer Graphics Metafile\">8632</a></li>\n<li><a href=\"/wiki/Graphical_Kernel_System\" title=\"Graphical Kernel System\">8651</a></li>\n<li><a href=\"/wiki/ISO/IEC_8652\" title=\"ISO/IEC 8652\">8652</a></li>\n<li><a href=\"/wiki/ISO_8691\" title=\"ISO 8691\">8691</a></li>\n<li><a href=\"/wiki/Graphical_Kernel_System\" title=\"Graphical Kernel System\">8805/8806</a></li>\n<li><a href=\"/wiki/Language_of_Temporal_Ordering_Specification\" title=\"Language of Temporal Ordering Specification\">8807</a></li>\n<li><a href=\"/wiki/Automotive_fuse\" title=\"Automotive fuse\">8820-5</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859\" title=\"ISO/IEC 8859\">8859</a>\n<ul><li><a href=\"/wiki/ISO/IEC_8859-1\" title=\"ISO/IEC 8859-1\">-1</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-2\" title=\"ISO/IEC 8859-2\">-2</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-3\" title=\"ISO/IEC 8859-3\">-3</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-4\" title=\"ISO/IEC 8859-4\">-4</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-5\" title=\"ISO/IEC 8859-5\">-5</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-6\" title=\"ISO/IEC 8859-6\">-6</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-7\" title=\"ISO/IEC 8859-7\">-7</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-8\" title=\"ISO/IEC 8859-8\">-8</a></li>\n<li><a href=\"/wiki/ISO-8859-8-I\" title=\"ISO-8859-8-I\">-8-I</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-9\" title=\"ISO/IEC 8859-9\">-9</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-10\" title=\"ISO/IEC 8859-10\">-10</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-11\" title=\"ISO/IEC 8859-11\">-11</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-12\" class=\"mw-redirect\" title=\"ISO/IEC 8859-12\">-12</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-13\" title=\"ISO/IEC 8859-13\">-13</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-14\" title=\"ISO/IEC 8859-14\">-14</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-15\" title=\"ISO/IEC 8859-15\">-15</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859-16\" title=\"ISO/IEC 8859-16\">-16</a></li></ul></li>\n<li><a href=\"/wiki/Standard_Generalized_Markup_Language\" title=\"Standard Generalized Markup Language\">8879</a></li>\n<li><a href=\"/wiki/ISO_9000_family\" title=\"ISO 9000 family\">9000/9001</a></li>\n<li><a href=\"/wiki/ASMO_449\" title=\"ASMO 449\">9036</a></li>\n<li><a href=\"/wiki/SQL\" title=\"SQL\">9075</a></li>\n<li><a href=\"/wiki/ISO/IEC_9126\" title=\"ISO/IEC 9126\">9126</a></li>\n<li><a href=\"/wiki/On-board_diagnostics\" title=\"On-board diagnostics\">9141</a></li>\n<li><a href=\"/wiki/Salt_spray_test\" title=\"Salt spray test\">9227</a></li>\n<li><a href=\"/wiki/ISO_9241\" title=\"ISO 9241\">9241</a></li>\n<li><a href=\"/wiki/File_Allocation_Table\" title=\"File Allocation Table\">9293</a></li>\n<li><a href=\"/wiki/Fiber_Distributed_Data_Interface\" title=\"Fiber Distributed Data Interface\">9314</a></li>\n<li><a href=\"/wiki/ISO_9362\" title=\"ISO 9362\">9362</a></li>\n<li><a href=\"/wiki/Shoe_size\" title=\"Shoe size\">9407</a></li>\n<li><a href=\"/wiki/CHILL\" title=\"CHILL\">9496</a></li>\n<li><a href=\"/wiki/Manufacturing_Message_Specification\" title=\"Manufacturing Message Specification\">9506</a></li>\n<li><a href=\"/wiki/ISO/IEC_9529\" title=\"ISO/IEC 9529\">9529</a></li>\n<li><a href=\"/wiki/ISO_9564\" title=\"ISO 9564\">9564</a></li>\n<li><a href=\"/wiki/PHIGS\" title=\"PHIGS\">9592/9593</a></li>\n<li><a href=\"/wiki/X.500\" title=\"X.500\">9594</a></li>\n<li><a href=\"/wiki/ISO_9660\" title=\"ISO 9660\">9660</a></li>\n<li><a href=\"/wiki/ISO/IEC_9797-1\" title=\"ISO/IEC 9797-1\">9797-1</a></li>\n<li><a href=\"/wiki/ISO_9897\" title=\"ISO 9897\">9897</a></li>\n<li><a href=\"/wiki/ANSI_C\" title=\"ANSI C\">9899</a></li>\n<li><a href=\"/wiki/POSIX\" title=\"POSIX\">9945</a></li>\n<li><a href=\"/wiki/Romanization_of_Georgian\" title=\"Romanization of Georgian\">9984</a></li>\n<li><a href=\"/wiki/Romanization_of_Armenian\" title=\"Romanization of Armenian\">9985</a></li>\n<li><a href=\"/wiki/ISO/IEC_9995\" title=\"ISO/IEC 9995\">9995</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">10000–19999</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ISO_10006\" title=\"ISO 10006\">10006</a></li>\n<li><a href=\"/wiki/ISO_10007\" title=\"ISO 10007\">10007</a></li>\n<li><a href=\"/wiki/ISO/IEC_10116\" title=\"ISO/IEC 10116\">10116</a></li>\n<li><a href=\"/wiki/Whirlpool_(hash_function)\" title=\"Whirlpool (hash function)\">10118-3</a></li>\n<li><a href=\"/wiki/ISO_10160\" title=\"ISO 10160\">10160</a></li>\n<li><a href=\"/wiki/ISO_10161\" title=\"ISO 10161\">10161</a></li>\n<li><a href=\"/wiki/Guidelines_for_the_Definition_of_Managed_Objects\" title=\"Guidelines for the Definition of Managed Objects\">10165</a></li>\n<li><a href=\"/wiki/Document_Style_Semantics_and_Specification_Language\" title=\"Document Style Semantics and Specification Language\">10179</a></li>\n<li><a href=\"/wiki/Pascal_(programming_language)#ISO/IEC_10206:1990_Extended_Pascal\" title=\"Pascal (programming language)\">10206</a></li>\n<li><a href=\"/wiki/ISO_10218\" title=\"ISO 10218\">10218</a></li>\n<li><a href=\"/wiki/Full_BASIC\" title=\"Full BASIC\">10279</a></li>\n<li><a href=\"/wiki/ISO_10303\" title=\"ISO 10303\">10303</a>\n<ul><li><a href=\"/wiki/EXPRESS_(data_modeling_language)\" title=\"EXPRESS (data modeling language)\">-11</a></li>\n<li><a href=\"/wiki/ISO_10303-21\" title=\"ISO 10303-21\">-21</a></li>\n<li><a href=\"/wiki/ISO_10303-22\" title=\"ISO 10303-22\">-22</a></li>\n<li><a href=\"/wiki/ISO_10303-28\" title=\"ISO 10303-28\">-28</a></li>\n<li><a href=\"/wiki/STEP-NC\" title=\"STEP-NC\">-238</a></li></ul></li>\n<li><a href=\"/wiki/Market_Identifier_Code\" title=\"Market Identifier Code\">10383</a></li>\n<li><a href=\"/wiki/ArmSCII\" title=\"ArmSCII\">10585</a></li>\n<li><a href=\"/wiki/IS-IS\" title=\"IS-IS\">10589</a></li>\n<li><a href=\"/wiki/ISO_10628\" title=\"ISO 10628\">10628</a></li>\n<li><a href=\"/wiki/Universal_Coded_Character_Set\" title=\"Universal Coded Character Set\">10646</a></li>\n<li><a href=\"/wiki/Torx\" title=\"Torx\">10664</a></li>\n<li><a href=\"/wiki/RM-ODP\" title=\"RM-ODP\">10746</a></li>\n<li><a href=\"/wiki/Multibus\" title=\"Multibus\">10861</a></li>\n<li><a href=\"/wiki/International_Standard_Music_Number\" title=\"International Standard Music Number\">10957</a></li>\n<li><a href=\"/wiki/ISO_10962\" title=\"ISO 10962\">10962</a></li>\n<li><a href=\"/wiki/ISO/IEC_10967\" title=\"ISO/IEC 10967\">10967</a></li>\n<li><a href=\"/wiki/ISO/IEEE_11073\" title=\"ISO/IEEE 11073\">11073</a></li>\n<li><a href=\"/wiki/ISO_11170\" title=\"ISO 11170\">11170</a></li>\n<li><a href=\"/wiki/MPEG-1\" title=\"MPEG-1\">11172</a></li>\n<li><a href=\"/wiki/ISO/IEC_11179\" title=\"ISO/IEC 11179\">11179</a></li>\n<li><a href=\"/wiki/ISO/IEC_11404\" title=\"ISO/IEC 11404\">11404</a></li>\n<li><a href=\"/wiki/JBIG\" title=\"JBIG\">11544</a></li>\n<li><a href=\"/wiki/ISO_11783\" title=\"ISO 11783\">11783</a></li>\n<li><a href=\"/wiki/ISO_11784_and_ISO_11785\" title=\"ISO 11784 and ISO 11785\">11784</a></li>\n<li><a href=\"/wiki/ISO_11784_and_ISO_11785\" title=\"ISO 11784 and ISO 11785\">11785</a></li>\n<li><a href=\"/wiki/ISO/IEC_11801\" title=\"ISO/IEC 11801\">11801</a></li>\n<li><a href=\"/wiki/Trusted_Platform_Module\" title=\"Trusted Platform Module\">11889</a></li>\n<li><a href=\"/wiki/CAN_bus#CAN_lower-layer_standards\" title=\"CAN bus\">11898</a></li>\n<li><a href=\"/wiki/ISO_11940\" title=\"ISO 11940\">11940</a> (<a href=\"/wiki/ISO_11940-2\" title=\"ISO 11940-2\">-2</a>)</li>\n<li><a href=\"/wiki/ISO/TR_11941\" title=\"ISO/TR 11941\">11941</a></li>\n<li><a href=\"/wiki/ISO/TR_11941\" title=\"ISO/TR 11941\">11941 (TR)</a></li>\n<li><a href=\"/wiki/ISO_11992\" title=\"ISO 11992\">11992</a></li>\n<li><a href=\"/wiki/ISO_12006\" title=\"ISO 12006\">12006</a></li>\n<li><a href=\"/wiki/DICOM\" title=\"DICOM\">12052</a></li>\n<li><a href=\"/wiki/ISO/IEC_TR_12182\" title=\"ISO/IEC TR 12182\">12182</a></li>\n<li><a href=\"/wiki/ISO/IEC_12207\" title=\"ISO/IEC 12207\">12207</a></li>\n<li><a href=\"/wiki/TIFF/EP\" title=\"TIFF/EP\">12234-2</a></li>\n<li><a href=\"/wiki/Linguistic_categories#ISO_12620_(ISO_TC37_Data_Category_Registry,_ISOcat)\" title=\"Linguistic categories\">12620</a></li>\n<li><a href=\"/wiki/Prolog\" title=\"Prolog\">13211</a>\n<ul><li><a href=\"/wiki/Prolog\" title=\"Prolog\">-1</a></li>\n<li><a href=\"/wiki/Prolog\" title=\"Prolog\">-2</a></li></ul></li>\n<li><a href=\"/wiki/Isofix\" title=\"Isofix\">13216</a></li>\n<li><a href=\"/wiki/Topic_map\" title=\"Topic map\">13250</a></li>\n<li><a href=\"/wiki/ISO_13399\" title=\"ISO 13399\">13399</a></li>\n<li><a href=\"/wiki/ISO_13406-2\" title=\"ISO 13406-2\">13406-2</a></li>\n<li><a href=\"/wiki/110_film\" title=\"110 film\">13450</a></li>\n<li><a href=\"/wiki/ISO_13485\" title=\"ISO 13485\">13485</a></li>\n<li><a href=\"/wiki/ISO_13490\" title=\"ISO 13490\">13490</a></li>\n<li><a href=\"/wiki/ISO_13567\" title=\"ISO 13567\">13567</a></li>\n<li><a href=\"/wiki/Z_notation\" title=\"Z notation\">13568</a></li>\n<li><a href=\"/wiki/ISO_13584\" title=\"ISO 13584\">13584</a></li>\n<li><a href=\"/wiki/International_Bank_Account_Number\" title=\"International Bank Account Number\">13616</a></li>\n<li><a href=\"/wiki/ISLISP\" title=\"ISLISP\">13816</a></li>\n<li><a href=\"/wiki/MPEG-2\" title=\"MPEG-2\">13818</a></li>\n<li><a href=\"/wiki/ISO_14000_family\" title=\"ISO 14000 family\">14000</a></li>\n<li><a href=\"/wiki/ISO_14031\" title=\"ISO 14031\">14031</a></li>\n<li><a href=\"/wiki/ISO_14224\" title=\"ISO 14224\">14224</a></li>\n<li><a href=\"/wiki/PDF/UA\" title=\"PDF/UA\">14289</a></li>\n<li><a href=\"/wiki/Horsepower\" title=\"Horsepower\">14396</a></li>\n<li><a href=\"/wiki/ISO/IEC_14443\" title=\"ISO/IEC 14443\">14443</a></li>\n<li><a href=\"/wiki/MPEG-4\" title=\"MPEG-4\">14496</a>\n<ul><li><a href=\"/wiki/MPEG-4_Part_2\" title=\"MPEG-4 Part 2\">-2</a></li>\n<li><a href=\"/wiki/MPEG-4_Part_3\" title=\"MPEG-4 Part 3\">-3</a></li>\n<li><a href=\"/wiki/Delivery_Multimedia_Integration_Framework\" title=\"Delivery Multimedia Integration Framework\">-6</a></li>\n<li><a href=\"/wiki/Advanced_Video_Coding\" title=\"Advanced Video Coding\">-10</a></li>\n<li><a href=\"/wiki/MPEG-4_Part_11\" title=\"MPEG-4 Part 11\">-11</a></li>\n<li><a href=\"/wiki/ISO_base_media_file_format\" title=\"ISO base media file format\">-12</a></li>\n<li><a href=\"/wiki/MP4_file_format\" title=\"MP4 file format\">-14</a></li>\n<li><a href=\"/wiki/MP4_file_format\" title=\"MP4 file format\">-17</a></li>\n<li><a href=\"/wiki/MP4_file_format\" title=\"MP4 file format\">-20</a></li></ul></li>\n<li><a href=\"/wiki/ISO_14617\" title=\"ISO 14617\">14617</a></li>\n<li><a href=\"/wiki/ISO_14644\" title=\"ISO 14644\">14644</a></li>\n<li><a href=\"/wiki/STEP-NC\" title=\"STEP-NC\">14649</a></li>\n<li><a href=\"/wiki/ISO/IEC_14651\" title=\"ISO/IEC 14651\">14651</a></li>\n<li><a href=\"/wiki/ISO_14698\" title=\"ISO 14698\">14698</a></li>\n<li><a href=\"/wiki/Software_maintenance\" title=\"Software maintenance\">14764</a></li>\n<li><a href=\"/wiki/C%2B%2B\" title=\"C++\">14882</a></li>\n<li><a href=\"/wiki/ISO_14971\" title=\"ISO 14971\">14971</a></li>\n<li><a href=\"/wiki/ISO_15022\" title=\"ISO 15022\">15022</a></li>\n<li><a href=\"/wiki/ISO_15189\" title=\"ISO 15189\">15189</a></li>\n<li><a href=\"/wiki/ISO/IEC_15288\" title=\"ISO/IEC 15288\">15288</a></li>\n<li><a href=\"/wiki/Ada_Semantic_Interface_Specification\" title=\"Ada Semantic Interface Specification\">15291</a></li>\n<li><a href=\"/wiki/ISO_15398\" title=\"ISO 15398\">15398</a></li>\n<li><a href=\"/wiki/Common_Criteria\" title=\"Common Criteria\">15408</a></li>\n<li><a href=\"/wiki/JPEG_2000\" title=\"JPEG 2000\">15444</a>\n<ul><li><a href=\"/wiki/Motion_JPEG_2000\" title=\"Motion JPEG 2000\">-3</a></li>\n<li><a href=\"/wiki/JPIP\" title=\"JPIP\">-9</a></li></ul></li>\n<li><a class=\"mw-selflink selflink\">15445</a></li>\n<li><a href=\"/wiki/PDF417\" title=\"PDF417\">15438</a></li>\n<li><a href=\"/wiki/ISO/IEC_15504\" title=\"ISO/IEC 15504\">15504</a></li>\n<li><a href=\"/wiki/International_Standard_Identifier_for_Libraries_and_Related_Organizations\" title=\"International Standard Identifier for Libraries and Related Organizations\">15511</a></li>\n<li><a href=\"/wiki/ISO_15686\" title=\"ISO 15686\">15686</a></li>\n<li><a href=\"/wiki/ISO/IEC_15693\" title=\"ISO/IEC 15693\">15693</a></li>\n<li><a href=\"/wiki/International_Standard_Audiovisual_Number\" title=\"International Standard Audiovisual Number\">15706</a>\n<ul><li><a href=\"/wiki/International_Standard_Audiovisual_Number\" title=\"International Standard Audiovisual Number\">-2</a></li></ul></li>\n<li><a href=\"/wiki/International_Standard_Musical_Work_Code\" title=\"International Standard Musical Work Code\">15707</a></li>\n<li><a href=\"/wiki/ISO/IEC_15897\" title=\"ISO/IEC 15897\">15897</a></li>\n<li><a href=\"/wiki/ISO_15919\" title=\"ISO 15919\">15919</a></li>\n<li><a href=\"/wiki/ISO_15924\" title=\"ISO 15924\">15924</a></li>\n<li><a href=\"/wiki/ISO_15926\" title=\"ISO 15926\">15926</a></li>\n<li><a href=\"/wiki/ISO_15926_WIP\" title=\"ISO 15926 WIP\">15926 WIP</a></li>\n<li><a href=\"/wiki/PDF/X\" title=\"PDF/X\">15930</a></li>\n<li><a href=\"/wiki/MPEG-7\" title=\"MPEG-7\">15938</a></li>\n<li><a href=\"/wiki/MaxiCode\" title=\"MaxiCode\">16023</a></li>\n<li><a href=\"/wiki/ECMAScript\" title=\"ECMAScript\">16262</a></li>\n<li><a href=\"/wiki/Quality_function_deployment\" title=\"Quality function deployment\">16355-1</a></li>\n<li><a href=\"/wiki/Mixed_raster_content\" title=\"Mixed raster content\">16485</a></li>\n<li><a href=\"/wiki/PDF/VT\" title=\"PDF/VT\">16612-2</a></li>\n<li><a href=\"/wiki/ISO_16750\" title=\"ISO 16750\">16750</a></li>\n<li><a href=\"/wiki/IATF_16949\" title=\"IATF 16949\">16949 (TS)</a></li>\n<li><a href=\"/wiki/ISO/IEC_17024\" title=\"ISO/IEC 17024\">17024</a></li>\n<li><a href=\"/wiki/ISO/IEC_17025\" title=\"ISO/IEC 17025\">17025</a></li>\n<li><a href=\"/wiki/ISO_17100\" title=\"ISO 17100\">17100</a></li>\n<li><a href=\"/wiki/Open_Virtualization_Format\" title=\"Open Virtualization Format\">17203</a></li>\n<li><a href=\"/wiki/SDMX\" title=\"SDMX\">17369</a></li>\n<li><a href=\"/wiki/Legal_Entity_Identifier\" title=\"Legal Entity Identifier\">17442</a></li>\n<li><a href=\"/wiki/COLLADA\" title=\"COLLADA\">17506</a></li>\n<li><a href=\"/wiki/ISO/IEC_27002\" title=\"ISO/IEC 27002\">17799</a></li>\n<li><a href=\"/wiki/QR_code\" title=\"QR code\">18004</a></li>\n<li><a href=\"/wiki/ISO/IEC_18014\" title=\"ISO/IEC 18014\">18014</a></li>\n<li><a href=\"/wiki/JPEG_XL\" title=\"JPEG XL\">18181</a></li>\n<li><a href=\"/wiki/ISO_18245\" title=\"ISO 18245\">18245</a></li>\n<li><a href=\"/wiki/Process_Specification_Language\" title=\"Process Specification Language\">18629</a></li>\n<li><a href=\"/wiki/SoftWare_Hash_IDentifier\" title=\"SoftWare Hash IDentifier\">18760</a></li>\n<li><a href=\"/wiki/Photographic_Activity_Test\" title=\"Photographic Activity Test\">18916</a></li>\n<li><a href=\"/wiki/PDF/A\" title=\"PDF/A\">19005</a></li>\n<li><a href=\"/wiki/ISO_19011\" title=\"ISO 19011\">19011</a></li>\n<li><a href=\"/wiki/ISO_19092\" title=\"ISO 19092\">19092</a>\n<ul><li><a href=\"/wiki/ISO_19092-1\" class=\"mw-redirect\" title=\"ISO 19092-1\">-1</a></li>\n<li><a href=\"/wiki/ISO_19092-2\" class=\"mw-redirect\" title=\"ISO 19092-2\">-2</a></li></ul></li>\n<li><a href=\"/wiki/ISO_19114\" title=\"ISO 19114\">19114</a></li>\n<li><a href=\"/wiki/Geospatial_metadata#ISO_19115:_Geographic_information_–_Metadata\" title=\"Geospatial metadata\">19115</a></li>\n<li><a href=\"/wiki/Simple_Features\" title=\"Simple Features\">19125</a></li>\n<li><a href=\"/wiki/Geography_Markup_Language#ISO_19136\" title=\"Geography Markup Language\">19136</a></li>\n<li><a href=\"/wiki/Shoe_size\" title=\"Shoe size\">19407</a></li>\n<li><a href=\"/wiki/ISO_19439\" title=\"ISO 19439\">19439</a></li>\n<li><a href=\"/wiki/Common_Object_Request_Broker_Architecture\" title=\"Common Object Request Broker Architecture\">19500</a></li>\n<li><a href=\"/wiki/Unified_Modeling_Language\" title=\"Unified Modeling Language\">19501</a></li>\n<li><a href=\"/wiki/Meta-Object_Facility\" title=\"Meta-Object Facility\">19502</a></li>\n<li><a href=\"/wiki/XML_Metadata_Interchange\" title=\"XML Metadata Interchange\">19503</a></li>\n<li><a href=\"/wiki/Unified_Modeling_Language\" title=\"Unified Modeling Language\">19505</a></li>\n<li><a href=\"/wiki/Knowledge_Discovery_Metamodel\" title=\"Knowledge Discovery Metamodel\">19506</a></li>\n<li><a href=\"/wiki/Object_Constraint_Language\" title=\"Object Constraint Language\">19507</a></li>\n<li><a href=\"/wiki/Meta-Object_Facility\" title=\"Meta-Object Facility\">19508</a></li>\n<li><a href=\"/wiki/XML_Metadata_Interchange\" title=\"XML Metadata Interchange\">19509</a></li>\n<li><a href=\"/wiki/Business_Process_Model_and_Notation\" title=\"Business Process Model and Notation\">19510</a></li>\n<li><a href=\"/wiki/ISO_19600\" title=\"ISO 19600\">19600</a></li>\n<li><a href=\"/wiki/ISO/IEC_19752\" title=\"ISO/IEC 19752\">19752</a></li>\n<li><a href=\"/wiki/RELAX_NG\" title=\"RELAX NG\">19757</a></li>\n<li><a href=\"/wiki/ISO/IEC_19770\" title=\"ISO/IEC 19770\">19770</a></li>\n<li><a href=\"/wiki/X3D\" title=\"X3D\">19775-1</a></li>\n<li><a href=\"/wiki/ISO/IEC_19794-5\" title=\"ISO/IEC 19794-5\">19794-5</a></li>\n<li><a href=\"/wiki/Cloud_Infrastructure_Management_Interface\" title=\"Cloud Infrastructure Management Interface\">19831</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">20000–29999</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ISO/IEC_20000\" title=\"ISO/IEC 20000\">20000</a></li>\n<li><a href=\"/wiki/ISO_20022\" title=\"ISO 20022\">20022</a></li>\n<li><a href=\"/wiki/ISO_20121\" title=\"ISO 20121\">20121</a></li>\n<li><a href=\"/wiki/ISO_20400\" title=\"ISO 20400\">20400</a></li>\n<li><a href=\"/wiki/Open_Data_Protocol\" title=\"Open Data Protocol\">20802</a></li>\n<li><a href=\"/wiki/Han_Xin_code\" title=\"Han Xin code\">20830</a></li>\n<li><a href=\"/wiki/MPEG-21\" title=\"MPEG-21\">21000</a></li>\n<li><a href=\"/wiki/ISO_21001\" title=\"ISO 21001\">21001</a></li>\n<li><a href=\"/wiki/International_Standard_Text_Code\" title=\"International Standard Text Code\">21047</a></li>\n<li><a href=\"/wiki/JPEG_XS\" title=\"JPEG XS\">21122</a></li>\n<li><a href=\"/wiki/ISO_21500\" title=\"ISO 21500\">21500</a></li>\n<li><a href=\"/wiki/JSON\" title=\"JSON\">21778</a></li>\n<li><a href=\"/wiki/ISO/IEC_21827\" title=\"ISO/IEC 21827\">21827</a></li>\n<li><a href=\"/wiki/ISO_22000\" title=\"ISO 22000\">22000</a></li>\n<li><a href=\"/wiki/ECMAScript\" title=\"ECMAScript\">22275</a></li>\n<li><a href=\"/wiki/ISO_22300\" title=\"ISO 22300\">22300</a></li>\n<li><a href=\"/wiki/ISO_22301\" title=\"ISO 22301\">22301</a></li>\n<li><a href=\"/wiki/ISO_22395\" title=\"ISO 22395\">22395</a></li>\n<li><a href=\"/wiki/ECMAScript_for_XML\" title=\"ECMAScript for XML\">22537</a></li>\n<li><a href=\"/wiki/MPEG-A\" title=\"MPEG-A\">23000</a></li>\n<li><a href=\"/wiki/MPEG-D\" title=\"MPEG-D\">23003</a></li>\n<li><a href=\"/wiki/MPEG-H\" title=\"MPEG-H\">23008</a></li>\n<li><a href=\"/wiki/Dynamic_Adaptive_Streaming_over_HTTP\" title=\"Dynamic Adaptive Streaming over HTTP\">23009</a></li>\n<li><a href=\"/wiki/Versatile_Video_Coding\" title=\"Versatile Video Coding\">23090-3</a></li>\n<li><a href=\"/wiki/MPEG-G\" title=\"MPEG-G\">23092</a></li>\n<li><a href=\"/wiki/Essential_Video_Coding\" title=\"Essential Video Coding\">23094-1</a></li>\n<li><a href=\"/wiki/LCEVC\" title=\"LCEVC\">23094-2</a></li>\n<li><a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">23270</a></li>\n<li><a href=\"/wiki/Common_Language_Infrastructure\" title=\"Common Language Infrastructure\">23271</a></li>\n<li><a href=\"/wiki/Linux_Standard_Base\" title=\"Linux Standard Base\">23360</a></li>\n<li><a href=\"/wiki/Rectangular_Micro_QR_Code\" title=\"Rectangular Micro QR Code\">23941</a></li>\n<li><a href=\"/wiki/PDF/E\" title=\"PDF/E\">24517</a></li>\n<li><a href=\"/wiki/Lexical_Markup_Framework\" title=\"Lexical Markup Framework\">24613</a></li>\n<li><a href=\"/wiki/ISO-TimeML\" title=\"ISO-TimeML\">24617</a></li>\n<li><a href=\"/wiki/Common_Logic\" title=\"Common Logic\">24707</a></li>\n<li><a href=\"/wiki/MicroPDF417\" title=\"MicroPDF417\">24728</a></li>\n<li><a href=\"/wiki/ISO_25178\" title=\"ISO 25178\">25178</a></li>\n<li><a href=\"/wiki/ISO_25964\" title=\"ISO 25964\">25964</a></li>\n<li><a href=\"/wiki/ISO_26000\" title=\"ISO 26000\">26000</a></li>\n<li><a href=\"/wiki/ISO_26262\" title=\"ISO 26262\">26262</a></li>\n<li><a href=\"/wiki/OpenDocument\" title=\"OpenDocument\">26300</a></li>\n<li><a href=\"/wiki/Digital_object_identifier\" title=\"Digital object identifier\">26324</a></li>\n<li><a href=\"/wiki/ISO/IEC_27000_family\" title=\"ISO/IEC 27000 family\">27000 series</a></li>\n<li><a href=\"/wiki/ISO/IEC_27000\" title=\"ISO/IEC 27000\">27000</a></li>\n<li><a href=\"/wiki/ISO/IEC_27001\" title=\"ISO/IEC 27001\">27001</a></li>\n<li><a href=\"/wiki/ISO/IEC_27002\" title=\"ISO/IEC 27002\">27002</a></li>\n<li><a href=\"/wiki/ISO/IEC_27005\" title=\"ISO/IEC 27005\">27005</a></li>\n<li><a href=\"/wiki/ISO/IEC_27006\" title=\"ISO/IEC 27006\">27006</a></li>\n<li><a href=\"/wiki/International_Standard_Name_Identifier\" title=\"International Standard Name Identifier\">27729</a></li>\n<li><a href=\"/wiki/ISO_28000\" title=\"ISO 28000\">28000</a></li>\n<li>29110</li>\n<li><a href=\"/wiki/Requirements_engineering\" title=\"Requirements engineering\">29148</a></li>\n<li><a href=\"/wiki/JPEG_XR\" title=\"JPEG XR\">29199-2</a></li>\n<li><a href=\"/wiki/Office_Open_XML\" title=\"Office Open XML\">29500</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">30000+</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Ruby_(programming_language)\" title=\"Ruby (programming language)\">30170</a></li>\n<li><a href=\"/wiki/ISO_31000\" title=\"ISO 31000\">31000</a></li>\n<li><a href=\"/wiki/PDF\" title=\"PDF\">32000</a></li>\n<li><a href=\"/wiki/ISO_37001\" title=\"ISO 37001\">37001</a></li>\n<li><a href=\"/wiki/ISO/IEC_38500\" title=\"ISO/IEC 38500\">38500</a></li>\n<li><a href=\"/wiki/Graph_Query_Language\" title=\"Graph Query Language\">39075</a></li>\n<li><a href=\"/wiki/MathML\" title=\"MathML\">40314</a></li>\n<li><a href=\"/wiki/Web_Content_Accessibility_Guidelines\" title=\"Web Content Accessibility Guidelines\">40500</a></li>\n<li><a href=\"/wiki/ISO/IEC_42010\" title=\"ISO/IEC 42010\">42010</a></li>\n<li><a href=\"/wiki/ISO_45001\" title=\"ISO 45001\">45001</a></li>\n<li><a href=\"/wiki/ISO_50001\" title=\"ISO 50001\">50001</a></li>\n<li><a href=\"/wiki/ISO_55000\" title=\"ISO 55000\">55000</a></li>\n<li><a href=\"/wiki/ISO_56000\" title=\"ISO 56000\">56000</a></li>\n<li><a href=\"/wiki/ISO/IEC_80000\" title=\"ISO/IEC 80000\">80000</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div>\n<ul><li><span class=\"noviewer\" typeof=\"mw:File\"><span title=\"Category\"><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/20px-Symbol_category_class.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/96/Symbol_category_class.svg/40px-Symbol_category_class.svg.png 1.5x\" data-file-width=\"180\" data-file-height=\"185\" /></span></span> <a href=\"/wiki/Category:ISO_standards\" title=\"Category:ISO standards\">Category</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"IEC_standards4276\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239400231\" /><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:List_of_IEC_standards\" title=\"Template:List of IEC standards\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:List_of_IEC_standards\" title=\"Template talk:List of IEC standards\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:List_of_IEC_standards\" title=\"Special:EditPage/Template:List of IEC standards\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"IEC_standards4276\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/List_of_IEC_standards\" title=\"List of IEC standards\">IEC standards</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">IEC</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/IEC_60027\" title=\"IEC 60027\">60027</a></li>\n<li><a href=\"/wiki/IEC_60034\" title=\"IEC 60034\">60034</a></li>\n<li><a href=\"/wiki/IEC_60038\" title=\"IEC 60038\">60038</a></li>\n<li><a href=\"/wiki/RKM_code\" title=\"RKM code\">60062</a></li>\n<li><a href=\"/wiki/E_series_of_preferred_numbers\" title=\"E series of preferred numbers\">60063</a></li>\n<li><a href=\"/wiki/IEC_60068\" title=\"IEC 60068\">60068</a></li>\n<li><a href=\"/wiki/Comparative_Tracking_Index\" title=\"Comparative Tracking Index\">60112</a></li>\n<li><a href=\"/wiki/IEC_60228\" title=\"IEC 60228\">60228</a></li>\n<li><a href=\"/wiki/IEC_60269\" title=\"IEC 60269\">60269</a></li>\n<li><a href=\"/wiki/19-inch_rack\" title=\"19-inch rack\">60297</a></li>\n<li><a href=\"/wiki/IEC_60309\" title=\"IEC 60309\">60309</a></li>\n<li><a href=\"/wiki/IEC_60320\" title=\"IEC 60320\">60320</a></li>\n<li><a href=\"/wiki/IEC_60364\" title=\"IEC 60364\">60364</a></li>\n<li><a href=\"/wiki/IEC_60446\" title=\"IEC 60446\">60446</a></li>\n<li><a href=\"/wiki/IEEE_754\" title=\"IEEE 754\">60559</a></li>\n<li><a href=\"/wiki/IEC_60601\" title=\"IEC 60601\">60601</a></li>\n<li><a href=\"/wiki/IEC_60870\" title=\"IEC 60870\">60870</a>\n<ul><li><a href=\"/wiki/IEC_60870-5\" title=\"IEC 60870-5\">60870-5</a></li>\n<li><a href=\"/wiki/IEC_60870-6\" title=\"IEC 60870-6\">60870-6</a></li></ul></li>\n<li><a href=\"/wiki/IEC_60906-1\" title=\"IEC 60906-1\">60906-1</a></li>\n<li><a href=\"/wiki/Compact_Disc_Digital_Audio\" title=\"Compact Disc Digital Audio\">60908</a></li>\n<li><a href=\"/wiki/IEC_60929\" title=\"IEC 60929\">60929</a></li>\n<li><a href=\"/wiki/AES3\" title=\"AES3\">60958</a></li>\n<li><a href=\"/w/index.php?title=IEC_60980-344&amp;action=edit&amp;redlink=1\" class=\"new\" title=\"IEC 60980-344 (page does not exist)\">60980-344</a></li>\n<li><a href=\"/wiki/IEC_61030\" title=\"IEC 61030\">61030</a></li>\n<li><a href=\"/wiki/IEC_61131\" title=\"IEC 61131\">61131</a>\n<ul><li><a href=\"/wiki/IEC_61131-3\" title=\"IEC 61131-3\">61131-3</a></li>\n<li><a href=\"/wiki/IO-Link\" title=\"IO-Link\">61131-9</a></li></ul></li>\n<li><a href=\"/wiki/Fieldbus\" title=\"Fieldbus\">61158</a></li>\n<li><a href=\"/wiki/IEC_61162\" title=\"IEC 61162\">61162</a></li>\n<li><a href=\"/wiki/IEC_61334\" title=\"IEC 61334\">61334</a></li>\n<li><a href=\"/wiki/IEC_61355\" title=\"IEC 61355\">61355</a></li>\n<li><a href=\"/wiki/IEC_61360\" title=\"IEC 61360\">61360</a></li>\n<li><a href=\"/wiki/IEC_61400\" title=\"IEC 61400\">61400</a></li>\n<li><a href=\"/wiki/IEC_61499\" title=\"IEC 61499\">61499</a></li>\n<li><a href=\"/wiki/IEC_61508\" title=\"IEC 61508\">61508</a></li>\n<li><a href=\"/wiki/IEC_61511\" title=\"IEC 61511\">61511</a></li>\n<li><a href=\"/wiki/Fieldbus\" title=\"Fieldbus\">61784</a></li>\n<li><a href=\"/wiki/IEC_61850\" title=\"IEC 61850\">61850</a></li>\n<li><a href=\"/wiki/IEC_61851\" title=\"IEC 61851\">61851</a></li>\n<li><a href=\"/wiki/IEC_61883\" title=\"IEC 61883\">61883</a></li>\n<li><a href=\"/wiki/Battery_nomenclature\" title=\"Battery nomenclature\">61960</a></li>\n<li><a href=\"/wiki/IEC_61968\" title=\"IEC 61968\">61968</a></li>\n<li><a href=\"/wiki/IEC_61970\" title=\"IEC 61970\">61970</a></li>\n<li><a href=\"/wiki/IP-XACT\" title=\"IP-XACT\">62014-4</a></li>\n<li><a href=\"/wiki/Fieldbus\" title=\"Fieldbus\">62026</a></li>\n<li><a href=\"/wiki/IEC_62056\" title=\"IEC 62056\">62056</a></li>\n<li><a href=\"/wiki/IEC_62061\" title=\"IEC 62061\">62061</a></li>\n<li><a href=\"/wiki/IEC_62196\" title=\"IEC 62196\">62196</a></li>\n<li><a href=\"/wiki/EN_62262\" title=\"EN 62262\">62262</a></li>\n<li><a href=\"/wiki/IEC_62264\" title=\"IEC 62264\">62264</a></li>\n<li><a href=\"/wiki/IEC_62304\" title=\"IEC 62304\">62304</a></li>\n<li><a href=\"/wiki/IEC_62325\" title=\"IEC 62325\">62325</a></li>\n<li><a href=\"/wiki/IEC_62351\" title=\"IEC 62351\">62351</a></li>\n<li><a href=\"/wiki/AES47\" title=\"AES47\">62365</a></li>\n<li><a href=\"/wiki/IEC_62366\" title=\"IEC 62366\">62366</a></li>\n<li><a href=\"/wiki/IEC_62379\" title=\"IEC 62379\">62379</a></li>\n<li><a href=\"/wiki/Digital_Addressable_Lighting_Interface\" title=\"Digital Addressable Lighting Interface\">62386</a></li>\n<li><a href=\"/wiki/IEC_62455\" title=\"IEC 62455\">62455</a></li>\n<li><a href=\"/wiki/USB\" title=\"USB\">62680</a></li>\n<li><a href=\"/wiki/IEC_62682\" title=\"IEC 62682\">62682</a></li>\n<li><a href=\"/wiki/IEC_62700\" title=\"IEC 62700\">62700</a></li>\n<li><a href=\"/wiki/IEC_63110\" title=\"IEC 63110\">63110</a></li>\n<li><a href=\"/wiki/IEC_63119\" title=\"IEC 63119\">63119</a></li>\n<li><a href=\"/wiki/IEC_63382\" title=\"IEC 63382\">63382</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">ISO/IEC</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/ISO/IEC_646\" title=\"ISO/IEC 646\">646</a></li>\n<li><a href=\"/wiki/COBOL\" title=\"COBOL\">1989</a></li>\n<li><a href=\"/wiki/ISO/IEC_2022\" title=\"ISO/IEC 2022\">2022</a></li>\n<li><a href=\"/wiki/ISO/IEC_4909\" title=\"ISO/IEC 4909\">4909</a></li>\n<li><a href=\"/wiki/ISO/IEC_5218\" title=\"ISO/IEC 5218\">5218</a></li>\n<li><a href=\"/wiki/ANSI_escape_code\" title=\"ANSI escape code\">6429</a></li>\n<li><a href=\"/wiki/ISO/IEC_6523\" title=\"ISO/IEC 6523\">6523</a></li>\n<li><a href=\"/wiki/ISO/IEC_7810\" title=\"ISO/IEC 7810\">7810</a></li>\n<li><a href=\"/wiki/ISO/IEC_7811\" title=\"ISO/IEC 7811\">7811</a></li>\n<li><a href=\"/wiki/ISO/IEC_7812\" title=\"ISO/IEC 7812\">7812</a></li>\n<li><a href=\"/wiki/ISO/IEC_7813\" title=\"ISO/IEC 7813\">7813</a></li>\n<li><a href=\"/wiki/ISO/IEC_7816\" title=\"ISO/IEC 7816\">7816</a></li>\n<li><a href=\"/wiki/Graphical_Kernel_System\" title=\"Graphical Kernel System\">7942</a></li>\n<li><a href=\"/wiki/Open_Document_Architecture\" title=\"Open Document Architecture\">8613</a></li>\n<li><a href=\"/wiki/Computer_Graphics_Metafile\" title=\"Computer Graphics Metafile\">8632</a></li>\n<li><a href=\"/wiki/ISO/IEC_8652\" title=\"ISO/IEC 8652\">8652</a></li>\n<li><a href=\"/wiki/ISO/IEC_8859\" title=\"ISO/IEC 8859\">8859</a></li>\n<li><a href=\"/wiki/ISO/IEC_9126\" title=\"ISO/IEC 9126\">9126</a></li>\n<li><a href=\"/wiki/File_Allocation_Table\" title=\"File Allocation Table\">9293</a></li>\n<li><a href=\"/wiki/CHILL\" title=\"CHILL\">9496</a></li>\n<li><a href=\"/wiki/ISO/IEC_9529\" title=\"ISO/IEC 9529\">9529</a></li>\n<li><a href=\"/wiki/PHIGS\" title=\"PHIGS\">9592</a></li>\n<li><a href=\"/wiki/PHIGS\" title=\"PHIGS\">9593</a></li>\n<li><a href=\"/wiki/ANSI_C\" title=\"ANSI C\">9899</a></li>\n<li><a href=\"/wiki/POSIX\" title=\"POSIX\">9945</a></li>\n<li><a href=\"/wiki/ISO/IEC_9995\" title=\"ISO/IEC 9995\">9995</a></li>\n<li><a href=\"/wiki/ISO/IEC_10021\" title=\"ISO/IEC 10021\">10021</a></li>\n<li><a href=\"/wiki/ISO/IEC_10116\" title=\"ISO/IEC 10116\">10116</a></li>\n<li><a href=\"/wiki/Guidelines_for_the_Definition_of_Managed_Objects\" title=\"Guidelines for the Definition of Managed Objects\">10165</a></li>\n<li><a href=\"/wiki/Document_Style_Semantics_and_Specification_Language\" title=\"Document Style Semantics and Specification Language\">10179</a></li>\n<li><a href=\"/wiki/Full_BASIC\" title=\"Full BASIC\">10279</a></li>\n<li><a href=\"/wiki/Universal_Coded_Character_Set\" title=\"Universal Coded Character Set\">10646</a></li>\n<li><a href=\"/wiki/ISO/IEC_10967\" title=\"ISO/IEC 10967\">10967</a></li>\n<li><a href=\"/wiki/MPEG-1\" title=\"MPEG-1\">11172</a></li>\n<li><a href=\"/wiki/ISO/IEC_11179\" title=\"ISO/IEC 11179\">11179</a></li>\n<li><a href=\"/wiki/ISO/IEC_11404\" title=\"ISO/IEC 11404\">11404</a></li>\n<li><a href=\"/wiki/JBIG\" title=\"JBIG\">11544</a></li>\n<li><a href=\"/wiki/ISO/IEC_11801\" title=\"ISO/IEC 11801\">11801</a></li>\n<li><a href=\"/wiki/ISO/IEC_12207\" title=\"ISO/IEC 12207\">12207</a></li>\n<li><a href=\"/wiki/Topic_map\" title=\"Topic map\">13250</a></li>\n<li><a href=\"/wiki/Universal_Disk_Format\" title=\"Universal Disk Format\">13346</a></li>\n<li><a href=\"/wiki/MHEG-5\" title=\"MHEG-5\">13522-5</a></li>\n<li><a href=\"/wiki/Z_notation\" title=\"Z notation\">13568</a></li>\n<li><a href=\"/wiki/ISLISP\" title=\"ISLISP\">13816</a></li>\n<li><a href=\"/wiki/MPEG-2\" title=\"MPEG-2\">13818</a></li>\n<li><a href=\"/wiki/ISO/IEC_14443\" title=\"ISO/IEC 14443\">14443</a></li>\n<li><a href=\"/wiki/MPEG-4\" title=\"MPEG-4\">14496</a></li>\n<li><a href=\"/wiki/ISO/IEC_14651\" title=\"ISO/IEC 14651\">14651</a></li>\n<li><a href=\"/wiki/C%2B%2B\" title=\"C++\">14882</a></li>\n<li><a href=\"/wiki/ISO/IEC_15288\" title=\"ISO/IEC 15288\">15288</a></li>\n<li><a href=\"/wiki/Ada_Semantic_Interface_Specification\" title=\"Ada Semantic Interface Specification\">15291</a></li>\n<li><a href=\"/wiki/Common_Criteria\" title=\"Common Criteria\">15408</a></li>\n<li><a href=\"/wiki/JPEG_2000\" title=\"JPEG 2000\">15444</a></li>\n<li><a class=\"mw-selflink selflink\">15445</a></li>\n<li><a href=\"/wiki/ISO/IEC_15504\" title=\"ISO/IEC 15504\">15504</a></li>\n<li><a href=\"/wiki/International_Standard_Identifier_for_Libraries_and_Related_Organizations\" title=\"International Standard Identifier for Libraries and Related Organizations\">15511</a></li>\n<li><a href=\"/wiki/ISO/IEC_15693\" title=\"ISO/IEC 15693\">15693</a></li>\n<li><a href=\"/wiki/ISO/IEC_15897\" title=\"ISO/IEC 15897\">15897</a></li>\n<li><a href=\"/wiki/MPEG-7\" title=\"MPEG-7\">15938</a></li>\n<li><a href=\"/wiki/ECMAScript\" title=\"ECMAScript\">16262</a></li>\n<li><a href=\"/wiki/Mixed_raster_content\" title=\"Mixed raster content\">16485</a></li>\n<li><a href=\"/wiki/ISO/IEC_17024\" title=\"ISO/IEC 17024\">17024</a></li>\n<li><a href=\"/wiki/ISO/IEC_17025\" title=\"ISO/IEC 17025\">17025</a></li>\n<li><a href=\"/wiki/QR_code\" title=\"QR code\">18004</a></li>\n<li><a href=\"/wiki/ISO/IEC_18014\" title=\"ISO/IEC 18014\">18014</a></li>\n<li><a href=\"/wiki/JPEG_XL\" title=\"JPEG XL\">18181</a></li>\n<li><a href=\"/wiki/ISO/IEC_19752\" title=\"ISO/IEC 19752\">19752</a></li>\n<li><a href=\"/wiki/RELAX_NG\" title=\"RELAX NG\">19757</a></li>\n<li><a href=\"/wiki/ISO/IEC_19770\" title=\"ISO/IEC 19770\">19770</a></li>\n<li><a href=\"/wiki/ISO/IEC_19788\" title=\"ISO/IEC 19788\">19788</a></li>\n<li><a href=\"/wiki/ISO/IEC_20000\" title=\"ISO/IEC 20000\">20000</a></li>\n<li><a href=\"/wiki/Open_Data_Protocol\" title=\"Open Data Protocol\">20802</a></li>\n<li><a href=\"/wiki/MPEG-21\" title=\"MPEG-21\">21000</a></li>\n<li><a href=\"/wiki/ISO/IEC_21827\" title=\"ISO/IEC 21827\">21827</a></li>\n<li><a href=\"/wiki/ECMAScript\" title=\"ECMAScript\">22275</a></li>\n<li><a href=\"/wiki/ECMAScript_for_XML\" title=\"ECMAScript for XML\">22537</a></li>\n<li><a href=\"/wiki/MPEG-A\" title=\"MPEG-A\">23000</a></li>\n<li><a href=\"/wiki/MPEG-D\" title=\"MPEG-D\">23003</a></li>\n<li><a href=\"/wiki/MPEG-H\" title=\"MPEG-H\">23008</a></li>\n<li><a href=\"/wiki/C_Sharp_(programming_language)\" title=\"C Sharp (programming language)\">23270</a></li>\n<li><a href=\"/wiki/Linux_Standard_Base\" title=\"Linux Standard Base\">23360</a></li>\n<li><a href=\"/wiki/Common_Logic\" title=\"Common Logic\">24707</a></li>\n<li><a href=\"/wiki/ISO/IEC_24727\" title=\"ISO/IEC 24727\">24727</a></li>\n<li><a href=\"/wiki/ISO/IEC_24744\" title=\"ISO/IEC 24744\">24744</a></li>\n<li><a href=\"/wiki/Universal_Remote_Console\" title=\"Universal Remote Console\">24752</a></li>\n<li><a href=\"/wiki/OpenDocument\" title=\"OpenDocument\">26300</a></li>\n<li><a href=\"/wiki/ISO/IEC_27000\" title=\"ISO/IEC 27000\">27000</a></li>\n<li><a href=\"/wiki/ISO/IEC_27000_family\" title=\"ISO/IEC 27000 family\">27000 family</a></li>\n<li><a href=\"/wiki/ISO/IEC_27002\" title=\"ISO/IEC 27002\">27002</a></li>\n<li><a href=\"/wiki/ISO/IEC_27040\" title=\"ISO/IEC 27040\">27040</a></li>\n<li>29110</li>\n<li><a href=\"/wiki/ISO/IEC_29119\" title=\"ISO/IEC 29119\">29119</a></li>\n<li><a href=\"/wiki/ISO/IEC_33001\" title=\"ISO/IEC 33001\">33001</a></li>\n<li><a href=\"/wiki/ISO/IEC_38500\" title=\"ISO/IEC 38500\">38500</a></li>\n<li><a href=\"/wiki/Graph_Query_Language\" title=\"Graph Query Language\">39075</a></li>\n<li><a href=\"/wiki/ISO/IEC_42010\" title=\"ISO/IEC 42010\">42010</a></li>\n<li><a href=\"/wiki/ISO/IEC_80000\" title=\"ISO/IEC 80000\">80000</a></li>\n<li><a href=\"/wiki/IEC_81346\" title=\"IEC 81346\">81346</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Related</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/International_Electrotechnical_Commission\" title=\"International Electrotechnical Commission\">International Electrotechnical Commission</a></li></ul>\n</div></td></tr></tbody></table></div>\n<div class=\"navbox-styles\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1236075235\" /><style data-mw-deduplicate=\"TemplateStyles:r1038841319\">.mw-parser-output .tooltip-dotted{border-bottom:1px dotted;cursor:help}</style><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1038841319\" /></div><div role=\"navigation\" class=\"navbox authority-control\" aria-labelledby=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q8811#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1496\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><div id=\"Authority_control_databases_frameless&amp;#124;text-top&amp;#124;10px&amp;#124;alt=Edit_this_at_Wikidata&amp;#124;link=https&amp;#58;//www.wikidata.org/wiki/Q8811#identifiers&amp;#124;class=noprint&amp;#124;Edit_this_at_Wikidata1496\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Help:Authority_control\" title=\"Help:Authority control\">Authority control databases</a> <span class=\"mw-valign-text-top noprint\" typeof=\"mw:File/Frameless\"><a href=\"https://www.wikidata.org/wiki/Q8811#identifiers\" title=\"Edit this at Wikidata\"><img alt=\"Edit this at Wikidata\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png\" decoding=\"async\" width=\"10\" height=\"10\" class=\"mw-file-element\" data-file-width=\"20\" data-file-height=\"20\" /></a></span></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">International</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://d-nb.info/gnd/4373477-7\">GND</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">National</th><td class=\"navbox-list-with-group navbox-list navbox-even\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><span class=\"rt-commentedText tooltip tooltip-dotted\" title=\"HTML (Document markup language)\"><a rel=\"nofollow\" class=\"external text\" href=\"https://id.loc.gov/authorities/sh95002791\">United States</a></span></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://catalogue.bnf.fr/ark:/12148/cb12493600c\">France</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://data.bnf.fr/ark:/12148/cb12493600c\">BnF data</a></span></li><li><span class=\"uid\"><span class=\"rt-commentedText tooltip tooltip-dotted\" title=\"HTML (značkovací jazyk)\"><a rel=\"nofollow\" class=\"external text\" href=\"https://aleph.nkp.cz/F/?func=find-c&amp;local_base=aut&amp;ccl_term=ica=ph117008&amp;CON_LNG=ENG\">Czech Republic</a></span></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://datos.bne.es/resource/XX539726\">Spain</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://www.nli.org.il/en/authorities/987007541929605171\">Israel</a></span></li></ul></div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other</th><td class=\"navbox-list-with-group navbox-list navbox-odd\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\"><ul><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://elmcip.net/node/8854\">ELMCIP</a></span></li><li><span class=\"uid\"><a rel=\"nofollow\" class=\"external text\" href=\"https://lux.collections.yale.edu/view/concept/7112a1f6-e5ef-467d-a704-8b1c5de1e30c\">Yale LUX</a></span></li></ul></div></td></tr></tbody></table></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1130092004\">.mw-parser-output .portal-bar{font-size:88%;font-weight:bold;display:flex;justify-content:center;align-items:baseline}.mw-parser-output .portal-bar-bordered{padding:0 2em;background-color:#fdfdfd;border:1px solid #a2a9b1;clear:both;margin:1em auto 0}.mw-parser-output .portal-bar-related{font-size:100%;justify-content:flex-start}.mw-parser-output .portal-bar-unbordered{padding:0 1.7em;margin-left:0}.mw-parser-output .portal-bar-header{margin:0 1em 0 0.5em;flex:0 0 auto;min-height:24px}.mw-parser-output .portal-bar-content{display:flex;flex-flow:row wrap;flex:0 1 auto;padding:0.15em 0;column-gap:1em;align-items:baseline;margin:0;list-style:none}.mw-parser-output .portal-bar-content-related{margin:0;list-style:none}.mw-parser-output .portal-bar-item{display:inline-block;margin:0.15em 0.2em;min-height:24px;line-height:24px}@media screen and (max-width:768px){.mw-parser-output .portal-bar{font-size:88%;font-weight:bold;display:flex;flex-flow:column wrap;align-items:baseline}.mw-parser-output .portal-bar-header{text-align:center;flex:0;padding-left:0.5em;margin:0 auto}.mw-parser-output .portal-bar-related{font-size:100%;align-items:flex-start}.mw-parser-output .portal-bar-content{display:flex;flex-flow:row wrap;align-items:center;flex:0;column-gap:1em;border-top:1px solid #a2a9b1;margin:0 auto;list-style:none}.mw-parser-output .portal-bar-content-related{border-top:none;margin:0;list-style:none}}.mw-parser-output .navbox+link+.portal-bar,.mw-parser-output .navbox+style+.portal-bar,.mw-parser-output .navbox+link+.portal-bar-bordered,.mw-parser-output .navbox+style+.portal-bar-bordered,.mw-parser-output .sister-bar+link+.portal-bar,.mw-parser-output .sister-bar+style+.portal-bar,.mw-parser-output .portal-bar+.navbox-styles+.navbox,.mw-parser-output .portal-bar+.navbox-styles+.sister-bar{margin-top:-1px}</style><div class=\"portal-bar noprint metadata noviewer portal-bar-bordered\" role=\"navigation\" aria-label=\"Portals\"><span class=\"portal-bar-header\"><a href=\"/wiki/Wikipedia:Contents/Portals\" title=\"Wikipedia:Contents/Portals\">Portal</a>:</span><ul class=\"portal-bar-content\"><li class=\"portal-bar-item\"><span class=\"nowrap\"><span class=\"skin-invert-image noviewer\" typeof=\"mw:File\"><a href=\"/wiki/File:Octicons-terminal.svg\" class=\"mw-file-description\"><img alt=\"icon\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/20px-Octicons-terminal.svg.png\" decoding=\"async\" width=\"17\" height=\"19\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Octicons-terminal.svg/40px-Octicons-terminal.svg.png 1.5x\" data-file-width=\"896\" data-file-height=\"1024\" /></a></span> </span><a href=\"/wiki/Portal:Computer_programming\" title=\"Portal:Computer programming\">Computer programming</a></li></ul></div>\n<!--\nNewPP limit report\nParsed by mw‐web.codfw.main‐9794dfc48‐cmszl\nCached time: 20251001133543\nCache expiry: 2592000\nReduced expiry: false\nComplications: [vary‐revision‐sha1, show‐toc]\nCPU time usage: 1.608 seconds\nReal time usage: 2.121 seconds\nPreprocessor visited node count: 11029/1000000\nRevision size: 85683/2097152 bytes\nPost‐expand include size: 344657/2097152 bytes\nTemplate argument size: 8880/2097152 bytes\nHighest expansion depth: 16/100\nExpensive parser function count: 143/500\nUnstrip recursion depth: 1/20\nUnstrip post‐expand size: 434834/5000000 bytes\nLua time usage: 0.884/10.000 seconds\nLua memory usage: 8994706/52428800 bytes\nNumber of Wikibase entities loaded: 1/500\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 1706.360      1 -total\n 31.86%  543.591      2 Template:Reflist\n 21.16%  361.030     78 Template:Cite_web\n  5.41%   92.274      1 Template:Short_description\n  4.97%   84.807      1 Template:HTML\n  4.83%   82.340      1 Template:Sidebar\n  4.77%   81.384      1 Template:Infobox_file_format\n  4.56%   77.824    104 Template:Code\n  4.44%   75.769      1 Template:Infobox\n  4.03%   68.695      1 Template:Sister_project_links\n-->\n\n<!-- Saved in parser cache with key enwiki:pcache:13191:|#|:idhash:canonical and timestamp 20251001133543 and revision id 1314044013. Rendering was triggered because: page_view\n -->\n</div><noscript><img src=\"https://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1&amp;usesul3=1\" alt=\"\" width=\"1\" height=\"1\" style=\"border: none; position: absolute;\"></noscript>\n<div class=\"printfooter\" data-nosnippet=\"\">Retrieved from \"<a dir=\"ltr\" href=\"https://en.wikipedia.org/w/index.php?title=HTML&amp;oldid=1314044013\">https://en.wikipedia.org/w/index.php?title=HTML&amp;oldid=1314044013</a>\"</div></div>\n\t\t\t\t\t<div id=\"catlinks\" class=\"catlinks\" data-mw=\"interface\"><div id=\"mw-normal-catlinks\" class=\"mw-normal-catlinks\"><a href=\"/wiki/Help:Category\" title=\"Help:Category\">Categories</a>: <ul><li><a href=\"/wiki/Category:HTML\" title=\"Category:HTML\">HTML</a></li><li><a href=\"/wiki/Category:Computer-related_introductions_in_1990\" title=\"Category:Computer-related introductions in 1990\">Computer-related introductions in 1990</a></li><li><a href=\"/wiki/Category:Markup_languages\" title=\"Category:Markup languages\">Markup languages</a></li><li><a href=\"/wiki/Category:Open_formats\" title=\"Category:Open formats\">Open formats</a></li><li><a href=\"/wiki/Category:Technical_communication\" title=\"Category:Technical communication\">Technical communication</a></li><li><a href=\"/wiki/Category:World_Wide_Web_Consortium_standards\" title=\"Category:World Wide Web Consortium standards\">World Wide Web Consortium standards</a></li><li><a href=\"/wiki/Category:SGML\" title=\"Category:SGML\">SGML</a></li></ul></div><div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks mw-hidden-cats-hidden\">Hidden categories: <ul><li><a href=\"/wiki/Category:Webarchive_template_wayback_links\" title=\"Category:Webarchive template wayback links\">Webarchive template wayback links</a></li><li><a href=\"/wiki/Category:Articles_with_short_description\" title=\"Category:Articles with short description\">Articles with short description</a></li><li><a href=\"/wiki/Category:Short_description_is_different_from_Wikidata\" title=\"Category:Short description is different from Wikidata\">Short description is different from Wikidata</a></li><li><a href=\"/wiki/Category:Wikipedia_pages_semi-protected_against_vandalism\" title=\"Category:Wikipedia pages semi-protected against vandalism\">Wikipedia pages semi-protected against vandalism</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_1997\" title=\"Category:Articles containing potentially dated statements from 1997\">Articles containing potentially dated statements from 1997</a></li><li><a href=\"/wiki/Category:All_articles_containing_potentially_dated_statements\" title=\"Category:All articles containing potentially dated statements\">All articles containing potentially dated statements</a></li><li><a href=\"/wiki/Category:Articles_containing_potentially_dated_statements_from_1996\" title=\"Category:Articles containing potentially dated statements from 1996\">Articles containing potentially dated statements from 1996</a></li><li><a href=\"/wiki/Category:All_articles_lacking_reliable_references\" title=\"Category:All articles lacking reliable references\">All articles lacking reliable references</a></li><li><a href=\"/wiki/Category:Articles_lacking_reliable_references_from_February_2019\" title=\"Category:Articles lacking reliable references from February 2019\">Articles lacking reliable references from February 2019</a></li><li><a href=\"/wiki/Category:All_articles_with_unsourced_statements\" title=\"Category:All articles with unsourced statements\">All articles with unsourced statements</a></li><li><a href=\"/wiki/Category:Articles_with_unsourced_statements_from_February_2025\" title=\"Category:Articles with unsourced statements from February 2025\">Articles with unsourced statements from February 2025</a></li><li><a href=\"/wiki/Category:All_articles_with_vague_or_ambiguous_time\" title=\"Category:All articles with vague or ambiguous time\">All articles with vague or ambiguous time</a></li><li><a href=\"/wiki/Category:Vague_or_ambiguous_time_from_March_2022\" title=\"Category:Vague or ambiguous time from March 2022\">Vague or ambiguous time from March 2022</a></li><li><a href=\"/wiki/Category:Articles_to_be_expanded_from_January_2021\" title=\"Category:Articles to be expanded from January 2021\">Articles to be expanded from January 2021</a></li><li><a href=\"/wiki/Category:All_articles_with_specifically_marked_weasel-worded_phrases\" title=\"Category:All articles with specifically marked weasel-worded phrases\">All articles with specifically marked weasel-worded phrases</a></li><li><a href=\"/wiki/Category:Articles_with_specifically_marked_weasel-worded_phrases_from_June_2020\" title=\"Category:Articles with specifically marked weasel-worded phrases from June 2020\">Articles with specifically marked weasel-worded phrases from June 2020</a></li><li><a href=\"/wiki/Category:Pages_using_Sister_project_links_with_wikidata_namespace_mismatch\" title=\"Category:Pages using Sister project links with wikidata namespace mismatch\">Pages using Sister project links with wikidata namespace mismatch</a></li><li><a href=\"/wiki/Category:Pages_using_Sister_project_links_with_hidden_wikidata\" title=\"Category:Pages using Sister project links with hidden wikidata\">Pages using Sister project links with hidden wikidata</a></li><li><a href=\"/wiki/Category:Articles_with_example_code\" title=\"Category:Articles with example code\">Articles with example code</a></li></ul></div></div>\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t</div>\n\t\t<div class=\"mw-footer-container\">\n\n<footer id=\"footer\" class=\"mw-footer\" >\n\t<ul id=\"footer-info\">\n\t<li id=\"footer-info-lastmod\"> This page was last edited on 29 September 2025, at 12:13<span class=\"anonymous-show\">&#160;(UTC)</span>.</li>\n\t<li id=\"footer-info-copyright\">Text is available under the <a href=\"/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License\" title=\"Wikipedia:Text of the Creative Commons Attribution-ShareAlike 4.0 International License\">Creative Commons Attribution-ShareAlike 4.0 License</a>;\nadditional terms may apply. By using this site, you agree to the <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Terms of Use\">Terms of Use</a> and <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Privacy policy\">Privacy Policy</a>. Wikipedia® is a registered trademark of the <a rel=\"nofollow\" class=\"external text\" href=\"https://wikimediafoundation.org/\">Wikimedia Foundation, Inc.</a>, a non-profit organization.</li>\n</ul>\n\n\t<ul id=\"footer-places\">\n\t<li id=\"footer-places-privacy\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\">Privacy policy</a></li>\n\t<li id=\"footer-places-about\"><a href=\"/wiki/Wikipedia:About\">About Wikipedia</a></li>\n\t<li id=\"footer-places-disclaimers\"><a href=\"/wiki/Wikipedia:General_disclaimer\">Disclaimers</a></li>\n\t<li id=\"footer-places-contact\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\">Contact Wikipedia</a></li>\n\t<li id=\"footer-places-wm-codeofconduct\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Universal_Code_of_Conduct\">Code of Conduct</a></li>\n\t<li id=\"footer-places-developers\"><a href=\"https://developer.wikimedia.org\">Developers</a></li>\n\t<li id=\"footer-places-statslink\"><a href=\"https://stats.wikimedia.org/#/en.wikipedia.org\">Statistics</a></li>\n\t<li id=\"footer-places-cookiestatement\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Cookie_statement\">Cookie statement</a></li>\n\t<li id=\"footer-places-mobileview\"><a href=\"//en.m.wikipedia.org/w/index.php?title=HTML&amp;mobileaction=toggle_view_mobile\" class=\"noprint stopMobileRedirectToggle\">Mobile view</a></li>\n</ul>\n\n\t<ul id=\"footer-icons\" class=\"noprint\">\n\t<li id=\"footer-copyrightico\"><a href=\"https://www.wikimedia.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/static/images/footer/wikimedia-button.svg\" width=\"84\" height=\"29\"><img src=\"/static/images/footer/wikimedia.svg\" width=\"25\" height=\"25\" alt=\"Wikimedia Foundation\" lang=\"en\" loading=\"lazy\"></picture></a></li>\n\t<li id=\"footer-poweredbyico\"><a href=\"https://www.mediawiki.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/w/resources/assets/poweredby_mediawiki.svg\" width=\"88\" height=\"31\"><img src=\"/w/resources/assets/mediawiki_compact.svg\" alt=\"Powered by MediaWiki\" lang=\"en\" width=\"25\" height=\"25\" loading=\"lazy\"></picture></a></li>\n</ul>\n\n</footer>\n\n\t\t</div>\n\t</div>\n</div>\n<div class=\"vector-header-container vector-sticky-header-container no-font-mode-scale\">\n\t<div id=\"vector-sticky-header\" class=\"vector-sticky-header\">\n\t\t<div class=\"vector-sticky-header-start\">\n\t\t\t<div class=\"vector-sticky-header-icon-start vector-button-flush-left vector-button-flush-right\" aria-hidden=\"true\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-sticky-header-search-toggle\" tabindex=\"-1\" data-event-name=\"ui.vector-sticky-search-form.icon\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div role=\"search\" class=\"vector-search-box-vue  vector-search-box-show-thumbnail vector-search-box\">\n\t\t\t<div class=\"vector-typeahead-search-container\">\n\t\t\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail\">\n\t\t\t\t\t<form action=\"/w/index.php\" id=\"vector-sticky-search-form\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t\t\t<div  class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\n\t\t\t\t\t\t\t\t\ttype=\"search\" name=\"search\" placeholder=\"Search Wikipedia\">\n\t\t\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-context-bar\">\n\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n\t\t\t\t\t<div id=\"vector-sticky-header-toc\" class=\"vector-dropdown mw-portlet mw-portlet-sticky-header-toc vector-sticky-header-toc vector-button-flush-left\"  >\n\t\t\t\t\t\t<input type=\"checkbox\" id=\"vector-sticky-header-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-sticky-header-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t\t\t\t\t\t<label id=\"vector-sticky-header-toc-label\" for=\"vector-sticky-header-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<div class=\"vector-dropdown-content\">\n\n\t\t\t\t\t\t<div id=\"vector-sticky-header-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t</nav>\n\t\t\t\t<div class=\"vector-sticky-header-context-bar-primary\" aria-hidden=\"true\" ><span class=\"mw-page-title-main\">HTML</span></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-end\" aria-hidden=\"true\">\n\t\t\t<div class=\"vector-sticky-header-icons\">\n\t\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-talk-sticky-header\" tabindex=\"-1\" data-event-name=\"talk-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbles mw-ui-icon-wikimedia-speechBubbles\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-subject-sticky-header\" tabindex=\"-1\" data-event-name=\"subject-sticky-header\"><span class=\"vector-icon mw-ui-icon-article mw-ui-icon-wikimedia-article\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-history-sticky-header\" tabindex=\"-1\" data-event-name=\"history-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-history mw-ui-icon-wikimedia-wikimedia-history\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only mw-watchlink\" id=\"ca-watchstar-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-star mw-ui-icon-wikimedia-wikimedia-star\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only reading-lists-bookmark\" id=\"ca-bookmark-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-bookmark\"><span class=\"vector-icon mw-ui-icon-wikimedia-bookmarkOutline mw-ui-icon-wikimedia-wikimedia-bookmarkOutline\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"wikitext-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-wikiText mw-ui-icon-wikimedia-wikimedia-wikiText\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-ve-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-edit mw-ui-icon-wikimedia-wikimedia-edit\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-viewsource-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-protected-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-editLock mw-ui-icon-wikimedia-wikimedia-editLock\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-buttons\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet mw-interlanguage-selector\" id=\"p-lang-btn-sticky-header\" tabindex=\"-1\" data-event-name=\"ui.dropdown-p-lang-btn-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-language mw-ui-icon-wikimedia-wikimedia-language\"></span>\n\n<span>139 languages</span>\n\t\t\t</button>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive\" id=\"ca-addsection-sticky-header\" tabindex=\"-1\" data-event-name=\"addsection-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbleAdd-progressive mw-ui-icon-wikimedia-speechBubbleAdd-progressive\"></span>\n\n<span>Add topic</span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-icon-end\">\n\t\t\t\t<div class=\"vector-user-links\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"mw-portlet mw-portlet-dock-bottom emptyPortlet\" id=\"p-dock-bottom\">\n\t<ul>\n\n\t</ul>\n</div>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({\"wgHostname\":\"mw-web.codfw.main-9794dfc48-hwpzl\",\"wgBackendResponseTime\":206,\"wgPageParseReport\":{\"limitreport\":{\"cputime\":\"1.608\",\"walltime\":\"2.121\",\"ppvisitednodes\":{\"value\":11029,\"limit\":1000000},\"revisionsize\":{\"value\":85683,\"limit\":2097152},\"postexpandincludesize\":{\"value\":344657,\"limit\":2097152},\"templateargumentsize\":{\"value\":8880,\"limit\":2097152},\"expansiondepth\":{\"value\":16,\"limit\":100},\"expensivefunctioncount\":{\"value\":143,\"limit\":500},\"unstrip-depth\":{\"value\":1,\"limit\":20},\"unstrip-size\":{\"value\":434834,\"limit\":5000000},\"entityaccesscount\":{\"value\":1,\"limit\":500},\"timingprofile\":[\"100.00% 1706.360      1 -total\",\" 31.86%  543.591      2 Template:Reflist\",\" 21.16%  361.030     78 Template:Cite_web\",\"  5.41%   92.274      1 Template:Short_description\",\"  4.97%   84.807      1 Template:HTML\",\"  4.83%   82.340      1 Template:Sidebar\",\"  4.77%   81.384      1 Template:Infobox_file_format\",\"  4.56%   77.824    104 Template:Code\",\"  4.44%   75.769      1 Template:Infobox\",\"  4.03%   68.695      1 Template:Sister_project_links\"]},\"scribunto\":{\"limitreport-timeusage\":{\"value\":\"0.884\",\"limit\":\"10.000\"},\"limitreport-memusage\":{\"value\":8994706,\"limit\":52428800}},\"cachereport\":{\"origin\":\"mw-web.codfw.main-9794dfc48-cmszl\",\"timestamp\":\"20251001133543\",\"ttl\":2592000,\"transientcontent\":false}}});});</script>\n<script type=\"application/ld+json\">{\"@context\":\"https:\\/\\/schema.org\",\"@type\":\"Article\",\"name\":\"HTML\",\"url\":\"https:\\/\\/en.wikipedia.org\\/wiki\\/HTML\",\"sameAs\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q8811\",\"mainEntity\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q8811\",\"author\":{\"@type\":\"Organization\",\"name\":\"Contributors to Wikimedia projects\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Wikimedia Foundation, Inc.\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/www.wikimedia.org\\/static\\/images\\/wmf-hor-googpub.png\"}},\"datePublished\":\"2001-07-19T17:08:26Z\",\"dateModified\":\"2025-09-29T12:13:16Z\",\"image\":\"https:\\/\\/upload.wikimedia.org\\/wikipedia\\/commons\\/6\\/61\\/HTML5_logo_and_wordmark.svg\",\"headline\":\"family of markup languages for displaying content in a web browser\"}</script>\n</body>\n</html>\n"
  },
  {
    "path": "test_documents/html/wikipedia/tables_countries.html",
    "content": "<!DOCTYPE html>\n<html class=\"client-nojs vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\" lang=\"en\" dir=\"ltr\">\n<head>\n<meta charset=\"UTF-8\">\n<title>List of countries and dependencies by population (United Nations) - Wikipedia</title>\n<script>(function(){var className=\"client-js vector-feature-language-in-header-enabled vector-feature-language-in-main-page-header-disabled vector-feature-page-tools-pinned-disabled vector-feature-toc-pinned-clientpref-1 vector-feature-main-menu-pinned-disabled vector-feature-limited-width-clientpref-1 vector-feature-limited-width-content-enabled vector-feature-custom-font-size-clientpref-1 vector-feature-appearance-pinned-clientpref-1 vector-feature-night-mode-enabled skin-theme-clientpref-day vector-sticky-header-enabled vector-toc-available\";var cookie=document.cookie.match(/(?:^|; )enwikimwclientpreferences=([^;]+)/);if(cookie){cookie[1].split('%2C').forEach(function(pref){className=className.replace(new RegExp('(^| )'+pref.replace(/-clientpref-\\w+$|[^\\w-]+/g,'')+'-clientpref-\\\\w+( |$)'),'$1'+pref+'$2');});}document.documentElement.className=className;}());RLCONF={\"wgBreakFrames\":false,\"wgSeparatorTransformTable\":[\"\",\"\"],\"wgDigitTransformTable\":[\"\",\"\"],\"wgDefaultDateFormat\":\"dmy\",\"wgMonthNames\":[\"\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"wgRequestId\":\"b02db67d-ea98-495e-b07a-f7b1a7bb9cf6\",\"wgCanonicalNamespace\":\"\",\"wgCanonicalSpecialPageName\":false,\"wgNamespaceNumber\":0,\"wgPageName\":\"List_of_countries_and_dependencies_by_population_(United_Nations)\",\"wgTitle\":\"List of countries and dependencies by population (United Nations)\",\"wgCurRevisionId\":1311341016,\"wgRevisionId\":1311341016,\"wgArticleId\":39707994,\"wgIsArticle\":true,\"wgIsRedirect\":false,\"wgAction\":\"view\",\"wgUserName\":null,\"wgUserGroups\":[\"*\"],\"wgCategories\":[\"Articles with short description\",\"Short description is different from Wikidata\",\"Pages where node count is exceeded\",\"Lists of countries by past and future population (United Nations)\",\"Lists of countries by continent\"],\"wgPageViewLanguage\":\"en\",\"wgPageContentLanguage\":\"en\",\"wgPageContentModel\":\"wikitext\",\"wgRelevantPageName\":\"List_of_countries_and_dependencies_by_population_(United_Nations)\",\"wgRelevantArticleId\":39707994,\"wgIsProbablyEditable\":true,\"wgRelevantPageIsProbablyEditable\":true,\"wgRestrictionEdit\":[],\"wgRestrictionMove\":[],\"wgRedirectedFrom\":\"List_of_countries_by_population_(United_Nations)\",\"wgNoticeProject\":\"wikipedia\",\"wgFlaggedRevsParams\":{\"tags\":{\"status\":{\"levels\":1}}},\"wgMediaViewerOnClick\":true,\"wgMediaViewerEnabledByDefault\":true,\"wgPopupsFlags\":0,\"wgVisualEditor\":{\"pageLanguageCode\":\"en\",\"pageLanguageDir\":\"ltr\",\"pageVariantFallbacks\":\"en\"},\"wgMFDisplayWikibaseDescriptions\":{\"search\":true,\"watchlist\":true,\"tagline\":false,\"nearby\":true},\"wgWMESchemaEditAttemptStepOversample\":false,\"wgWMEPageLength\":80000,\"wgMetricsPlatformUserExperiments\":{\"active_experiments\":[],\"overrides\":[],\"enrolled\":[],\"assigned\":[],\"subject_ids\":[],\"sampling_units\":[]},\"wgInternalRedirectTargetUrl\":\"/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\",\"wgEditSubmitButtonLabelPublish\":true,\"wgULSPosition\":\"interlanguage\",\"wgULSisCompactLinksEnabled\":false,\"wgVector2022LanguageInHeader\":true,\"wgULSisLanguageSelectorEmpty\":false,\"wgWikibaseItemId\":\"Q14940491\",\"wgCheckUserClientHintsHeadersJsApi\":[\"brands\",\"architecture\",\"bitness\",\"fullVersionList\",\"mobile\",\"model\",\"platform\",\"platformVersion\"],\"GEHomepageSuggestedEditsEnableTopics\":true,\"wgGESuggestedEditsTaskTypes\":{\"taskTypes\":[\"copyedit\",\"link-recommendation\"],\"unavailableTaskTypes\":[]},\"wgGETopicsMatchModeEnabled\":false,\"wgGELevelingUpEnabledForUser\":false};\nRLSTATE={\"ext.globalCssJs.user.styles\":\"ready\",\"site.styles\":\"ready\",\"user.styles\":\"ready\",\"ext.globalCssJs.user\":\"ready\",\"user\":\"ready\",\"user.options\":\"loading\",\"ext.cite.styles\":\"ready\",\"ext.wikimediamessages.styles\":\"ready\",\"skins.vector.search.codex.styles\":\"ready\",\"skins.vector.styles\":\"ready\",\"skins.vector.icons\":\"ready\",\"jquery.tablesorter.styles\":\"ready\",\"jquery.makeCollapsible.styles\":\"ready\",\"ext.visualEditor.desktopArticleTarget.noscript\":\"ready\",\"ext.uls.interlanguage\":\"ready\",\"wikibase.client.init\":\"ready\"};RLPAGEMODULES=[\"ext.xLab\",\"mediawiki.action.view.redirect\",\"ext.cite.ux-enhancements\",\"mediawiki.page.media\",\"site\",\"mediawiki.page.ready\",\"jquery.tablesorter\",\"jquery.makeCollapsible\",\"mediawiki.toc\",\"skins.vector.js\",\"ext.centralNotice.geoIP\",\"ext.centralNotice.startUp\",\"ext.gadget.ReferenceTooltips\",\"ext.gadget.switcher\",\"ext.urlShortener.toolbar\",\"ext.centralauth.centralautologin\",\"mmv.bootstrap\",\"ext.popups\",\"ext.visualEditor.desktopArticleTarget.init\",\"ext.visualEditor.targetLoader\",\"ext.echo.centralauth\",\"ext.eventLogging\",\"ext.wikimediaEvents\",\"ext.navigationTiming\",\"ext.uls.interface\",\"ext.cx.eventlogging.campaigns\",\"ext.cx.uls.quick.actions\",\"wikibase.client.vector-2022\",\"ext.checkUser.clientHints\",\"ext.quicksurveys.init\",\"ext.growthExperiments.SuggestedEditSession\"];</script>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return[\"user.options@12s5i\",function($,jQuery,require,module){mw.user.tokens.set({\"patrolToken\":\"+\\\\\",\"watchToken\":\"+\\\\\",\"csrfToken\":\"+\\\\\"});\n}];});});</script>\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=ext.cite.styles%7Cext.uls.interlanguage%7Cext.visualEditor.desktopArticleTarget.noscript%7Cext.wikimediamessages.styles%7Cjquery.makeCollapsible.styles%7Cjquery.tablesorter.styles%7Cskins.vector.icons%2Cstyles%7Cskins.vector.search.codex.styles%7Cwikibase.client.init&amp;only=styles&amp;skin=vector-2022\">\n<script async=\"\" src=\"/w/load.php?lang=en&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;skin=vector-2022\"></script>\n<meta name=\"ResourceLoaderDynamicStyles\" content=\"\">\n<link rel=\"stylesheet\" href=\"/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=vector-2022\">\n<meta name=\"generator\" content=\"MediaWiki 1.45.0-wmf.19\">\n<meta name=\"referrer\" content=\"origin\">\n<meta name=\"referrer\" content=\"origin-when-cross-origin\">\n<meta name=\"robots\" content=\"max-image-preview:standard\">\n<meta name=\"format-detection\" content=\"telephone=no\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/United_Nations_geographical_subregions.png/1200px-United_Nations_geographical_subregions.png\">\n<meta property=\"og:image:width\" content=\"1200\">\n<meta property=\"og:image:height\" content=\"555\">\n<meta name=\"viewport\" content=\"width=1120\">\n<meta property=\"og:title\" content=\"List of countries and dependencies by population (United Nations) - Wikipedia\">\n<meta property=\"og:type\" content=\"website\">\n<link rel=\"preconnect\" href=\"//upload.wikimedia.org\">\n<link rel=\"alternate\" media=\"only screen and (max-width: 640px)\" href=\"//en.m.wikipedia.org/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\">\n<link rel=\"alternate\" type=\"application/x-wiki\" title=\"Edit this page\" href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit\">\n<link rel=\"apple-touch-icon\" href=\"/static/apple-touch/wikipedia.png\">\n<link rel=\"icon\" href=\"/static/favicon/wikipedia.ico\">\n<link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"/w/rest.php/v1/search\" title=\"Wikipedia (en)\">\n<link rel=\"EditURI\" type=\"application/rsd+xml\" href=\"//en.wikipedia.org/w/api.php?action=rsd\">\n<link rel=\"canonical\" href=\"https://en.wikipedia.org/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\">\n<link rel=\"license\" href=\"https://creativecommons.org/licenses/by-sa/4.0/deed.en\">\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Wikipedia Atom feed\" href=\"/w/index.php?title=Special:RecentChanges&amp;feed=atom\">\n<link rel=\"dns-prefetch\" href=\"//meta.wikimedia.org\" />\n<link rel=\"dns-prefetch\" href=\"auth.wikimedia.org\">\n</head>\n<body class=\"skin--responsive skin-vector skin-vector-search-vue mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject mw-editable page-List_of_countries_and_dependencies_by_population_United_Nations rootpage-List_of_countries_and_dependencies_by_population_United_Nations skin-vector-2022 action-view\"><a class=\"mw-jump-link\" href=\"#bodyContent\">Jump to content</a>\n<div class=\"vector-header-container\">\n\t<header class=\"vector-header mw-header no-font-mode-scale\">\n\t\t<div class=\"vector-header-start\">\n\t\t\t<nav class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\n<div id=\"vector-main-menu-dropdown\" class=\"vector-dropdown vector-main-menu-dropdown vector-button-flush-left vector-button-flush-right\"  title=\"Main menu\" >\n\t<input type=\"checkbox\" id=\"vector-main-menu-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-main-menu-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Main menu\"  >\n\t<label id=\"vector-main-menu-dropdown-label\" for=\"vector-main-menu-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu\"></span>\n\n<span class=\"vector-dropdown-label-text\">Main menu</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t<div id=\"vector-main-menu-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-main-menu\" class=\"vector-main-menu vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-main-menu-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"main-menu-pinned\"\n\tdata-pinnable-element-id=\"vector-main-menu\"\n\tdata-pinned-container-id=\"vector-main-menu-pinned-container\"\n\tdata-unpinned-container-id=\"vector-main-menu-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Main menu</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-main-menu.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-main-menu.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-navigation\" class=\"vector-menu mw-portlet mw-portlet-navigation\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tNavigation\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-mainpage-description\" class=\"mw-list-item\"><a href=\"/wiki/Main_Page\" title=\"Visit the main page [z]\" accesskey=\"z\"><span>Main page</span></a></li><li id=\"n-contents\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Contents\" title=\"Guides to browsing Wikipedia\"><span>Contents</span></a></li><li id=\"n-currentevents\" class=\"mw-list-item\"><a href=\"/wiki/Portal:Current_events\" title=\"Articles related to current events\"><span>Current events</span></a></li><li id=\"n-randompage\" class=\"mw-list-item\"><a href=\"/wiki/Special:Random\" title=\"Visit a randomly selected article [x]\" accesskey=\"x\"><span>Random article</span></a></li><li id=\"n-aboutsite\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:About\" title=\"Learn about Wikipedia and how it works\"><span>About Wikipedia</span></a></li><li id=\"n-contactpage\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\" title=\"How to contact Wikipedia\"><span>Contact us</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\n<div id=\"p-interaction\" class=\"vector-menu mw-portlet mw-portlet-interaction\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tContribute\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"n-help\" class=\"mw-list-item\"><a href=\"/wiki/Help:Contents\" title=\"Guidance on how to use and edit Wikipedia\"><span>Help</span></a></li><li id=\"n-introduction\" class=\"mw-list-item\"><a href=\"/wiki/Help:Introduction\" title=\"Learn how to edit Wikipedia\"><span>Learn to edit</span></a></li><li id=\"n-portal\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:Community_portal\" title=\"The hub for editors\"><span>Community portal</span></a></li><li id=\"n-recentchanges\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChanges\" title=\"A list of recent changes to Wikipedia [r]\" accesskey=\"r\"><span>Recent changes</span></a></li><li id=\"n-upload\" class=\"mw-list-item\"><a href=\"/wiki/Wikipedia:File_upload_wizard\" title=\"Add images or other media for use on Wikipedia\"><span>Upload file</span></a></li><li id=\"n-specialpages\" class=\"mw-list-item\"><a href=\"/wiki/Special:SpecialPages\"><span>Special pages</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t</nav>\n\n<a href=\"/wiki/Main_Page\" class=\"mw-logo\">\n\t<img class=\"mw-logo-icon\" src=\"/static/images/icons/wikipedia.png\" alt=\"\" aria-hidden=\"true\" height=\"50\" width=\"50\">\n\t<span class=\"mw-logo-container skin-invert\">\n\t\t<img class=\"mw-logo-wordmark\" alt=\"Wikipedia\" src=\"/static/images/mobile/copyright/wikipedia-wordmark-en.svg\" style=\"width: 7.5em; height: 1.125em;\">\n\t\t<img class=\"mw-logo-tagline\" alt=\"The Free Encyclopedia\" src=\"/static/images/mobile/copyright/wikipedia-tagline-en.svg\" width=\"117\" height=\"13\" style=\"width: 7.3125em; height: 0.8125em;\">\n\t</span>\n</a>\n\n\t\t</div>\n\t\t<div class=\"vector-header-end\">\n\n<div id=\"p-search\" role=\"search\" class=\"vector-search-box-vue  vector-search-box-collapses vector-search-box-show-thumbnail vector-search-box-auto-expand-width vector-search-box\">\n\t<a href=\"/wiki/Special:Search\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only search-toggle\" title=\"Search Wikipedia [f]\" accesskey=\"f\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t</a>\n\t<div class=\"vector-typeahead-search-container\">\n\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail cdx-typeahead-search--auto-expand-width\">\n\t\t\t<form action=\"/w/index.php\" id=\"searchform\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t<div id=\"simpleSearch\" class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\t\t\t\t\t\t\t type=\"search\" name=\"search\" placeholder=\"Search Wikipedia\" aria-label=\"Search Wikipedia\" autocapitalize=\"sentences\" spellcheck=\"false\" title=\"Search Wikipedia [f]\" accesskey=\"f\" id=\"searchInput\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t</div>\n\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n\t\t\t<nav class=\"vector-user-links vector-user-links-wide\" aria-label=\"Personal tools\">\n\t<div class=\"vector-user-links-main\">\n\n<div id=\"p-vector-user-menu-preferences\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-userpage\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\n<div id=\"vector-appearance-dropdown\" class=\"vector-dropdown \"  title=\"Change the appearance of the page&#039;s font size, width, and color\" >\n\t<input type=\"checkbox\" id=\"vector-appearance-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-appearance-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Appearance\"  >\n\t<label id=\"vector-appearance-dropdown-label\" for=\"vector-appearance-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-appearance mw-ui-icon-wikimedia-appearance\"></span>\n\n<span class=\"vector-dropdown-label-text\">Appearance</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t<div id=\"vector-appearance-unpinned-container\" class=\"vector-unpinned-container\">\n\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t</nav>\n\n<div id=\"p-vector-user-menu-notifications\" class=\"vector-menu mw-portlet emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"p-vector-user-menu-overflow\" class=\"vector-menu mw-portlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\t\t\t<li id=\"pt-sitesupport-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\" class=\"\"><span>Donate</span></a>\n</li>\n<li id=\"pt-createaccount-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=List+of+countries+and+dependencies+by+population+%28United+Nations%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\" class=\"\"><span>Create account</span></a>\n</li>\n<li id=\"pt-login-2\" class=\"user-links-collapsible-item mw-list-item user-links-collapsible-item\"><a data-mw=\"interface\" href=\"/w/index.php?title=Special:UserLogin&amp;returnto=List+of+countries+and+dependencies+by+population+%28United+Nations%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\" class=\"\"><span>Log in</span></a>\n</li>\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t</div>\n\n<div id=\"vector-user-links-dropdown\" class=\"vector-dropdown vector-user-menu vector-button-flush-right vector-user-menu-logged-out\"  title=\"Log in and more options\" >\n\t<input type=\"checkbox\" id=\"vector-user-links-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-user-links-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Personal tools\"  >\n\t<label id=\"vector-user-links-dropdown-label\" for=\"vector-user-links-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-ellipsis mw-ui-icon-wikimedia-ellipsis\"></span>\n\n<span class=\"vector-dropdown-label-text\">Personal tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-personal\" class=\"vector-menu mw-portlet mw-portlet-personal user-links-collapsible-item\"  title=\"User menu\" >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-sitesupport\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"https://donate.wikimedia.org/?wmf_source=donate&amp;wmf_medium=sidebar&amp;wmf_campaign=en.wikipedia.org&amp;uselang=en\"><span>Donate</span></a></li><li id=\"pt-createaccount\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:CreateAccount&amp;returnto=List+of+countries+and+dependencies+by+population+%28United+Nations%29\" title=\"You are encouraged to create an account and log in; however, it is not mandatory\"><span class=\"vector-icon mw-ui-icon-userAdd mw-ui-icon-wikimedia-userAdd\"></span> <span>Create account</span></a></li><li id=\"pt-login\" class=\"user-links-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=Special:UserLogin&amp;returnto=List+of+countries+and+dependencies+by+population+%28United+Nations%29\" title=\"You&#039;re encouraged to log in; however, it&#039;s not mandatory. [o]\" accesskey=\"o\"><span class=\"vector-icon mw-ui-icon-logIn mw-ui-icon-wikimedia-logIn\"></span> <span>Log in</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-user-menu-anon-editor\" class=\"vector-menu mw-portlet mw-portlet-user-menu-anon-editor\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPages for logged out editors <a href=\"/wiki/Help:Introduction\" aria-label=\"Learn more about editing\"><span>learn more</span></a>\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"pt-anoncontribs\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyContributions\" title=\"A list of edits made from this IP address [y]\" accesskey=\"y\"><span>Contributions</span></a></li><li id=\"pt-anontalk\" class=\"mw-list-item\"><a href=\"/wiki/Special:MyTalk\" title=\"Discussion about edits from this IP address [n]\" accesskey=\"n\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n</nav>\n\n\t\t</div>\n\t</header>\n</div>\n<div class=\"mw-page-container\">\n\t<div class=\"mw-page-container-inner\">\n\t\t<div class=\"vector-sitenotice-container\">\n\t\t\t<div id=\"siteNotice\"><!-- CentralNotice --></div>\n\t\t</div>\n\t\t<div class=\"vector-column-start\">\n\t\t\t<div class=\"vector-main-menu-container\">\n\t\t<div id=\"mw-navigation\">\n\t\t\t<nav id=\"mw-panel\" class=\"vector-main-menu-landmark\" aria-label=\"Site\">\n\t\t\t\t<div id=\"vector-main-menu-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t</div>\n\t\t</nav>\n\t\t</div>\n\t</div>\n\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t<nav id=\"mw-panel-toc\" aria-label=\"Contents\" data-event-name=\"ui.sidebar-toc\" class=\"mw-table-of-contents-container vector-toc-landmark\">\n\t\t\t\t\t<div id=\"vector-toc-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t\t<div id=\"vector-toc\" class=\"vector-toc vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"toc-pinned\"\n\tdata-pinnable-element-id=\"vector-toc\"\n\n\n>\n\t<h2 class=\"vector-pinnable-header-label\">Contents</h2>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-toc.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-toc.unpin\">hide</button>\n</div>\n\n\n\t<ul class=\"vector-toc-contents\" id=\"mw-panel-toc-list\">\n\t\t<li id=\"toc-mw-content-text\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-1\">\n\t\t\t<a href=\"#\" class=\"vector-toc-link\">\n\t\t\t\t<div class=\"vector-toc-text\">(Top)</div>\n\t\t\t</a>\n\t\t</li>\n\t\t<li id=\"toc-List\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#List\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">1</span>\n\t\t\t\t<span>List</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-List-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-See_also\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#See_also\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">2</span>\n\t\t\t\t<span>See also</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t\t<button aria-controls=\"toc-See_also-sublist\" class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle\">\n\t\t\t\t<span class=\"vector-icon mw-ui-icon-wikimedia-expand\"></span>\n\t\t\t\t<span>Toggle See also subsection</span>\n\t\t\t</button>\n\n\t\t<ul id=\"toc-See_also-sublist\" class=\"vector-toc-list\">\n\t\t\t<li id=\"toc-World\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#World\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.1</span>\n\t\t\t\t\t<span>World</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-World-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Continental\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Continental\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.2</span>\n\t\t\t\t\t<span>Continental</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Continental-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Transcontinental\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Transcontinental\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.3</span>\n\t\t\t\t\t<span>Transcontinental</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Transcontinental-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Subregional\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Subregional\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.4</span>\n\t\t\t\t\t<span>Subregional</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Subregional-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t\t<li id=\"toc-Others\"\n\t\t\tclass=\"vector-toc-list-item vector-toc-level-2\">\n\t\t\t<a class=\"vector-toc-link\" href=\"#Others\">\n\t\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t\t<span class=\"vector-toc-numb\">2.5</span>\n\t\t\t\t\t<span>Others</span>\n\t\t\t\t</div>\n\t\t\t</a>\n\n\t\t\t<ul id=\"toc-Others-sublist\" class=\"vector-toc-list\">\n\t\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>\n\t<li id=\"toc-Explanatory_notes\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#Explanatory_notes\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">3</span>\n\t\t\t\t<span>Explanatory notes</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-Explanatory_notes-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-References\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#References\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">4</span>\n\t\t\t\t<span>References</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-References-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n\t<li id=\"toc-External_links\"\n\t\tclass=\"vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded\">\n\t\t<a class=\"vector-toc-link\" href=\"#External_links\">\n\t\t\t<div class=\"vector-toc-text\">\n\t\t\t\t<span class=\"vector-toc-numb\">5</span>\n\t\t\t\t<span>External links</span>\n\t\t\t</div>\n\t\t</a>\n\n\t\t<ul id=\"toc-External_links-sublist\" class=\"vector-toc-list\">\n\t\t</ul>\n\t</li>\n</ul>\n</div>\n\n\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mw-content-container\">\n\t\t\t<main id=\"content\" class=\"mw-body\">\n\t\t\t\t<header class=\"mw-body-header vector-page-titlebar no-font-mode-scale\">\n\t\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n<div id=\"vector-page-titlebar-toc\" class=\"vector-dropdown vector-page-titlebar-toc vector-button-flush-left\"  title=\"Table of Contents\" >\n\t<input type=\"checkbox\" id=\"vector-page-titlebar-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-titlebar-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t<label id=\"vector-page-titlebar-toc-label\" for=\"vector-page-titlebar-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t<div id=\"vector-page-titlebar-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t</nav>\n\t\t\t\t\t<h1 id=\"firstHeading\" class=\"firstHeading mw-first-heading\"><span class=\"mw-page-title-main\">List of countries and dependencies by population (United Nations)</span></h1>\n\n<div id=\"p-lang-btn\" class=\"vector-dropdown mw-portlet mw-portlet-lang\"  >\n\t<input type=\"checkbox\" id=\"p-lang-btn-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-p-lang-btn\" class=\"vector-dropdown-checkbox mw-interlanguage-selector\" aria-label=\"Go to an article in another language. Available in 13 languages\"   >\n\t<label id=\"p-lang-btn-label\" for=\"p-lang-btn-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive mw-portlet-lang-heading-13\" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-language-progressive mw-ui-icon-wikimedia-language-progressive\"></span>\n\n<span class=\"vector-dropdown-label-text\">13 languages</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\t\t<div class=\"vector-menu-content\">\n\n\t\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t\t<li class=\"interlanguage-link interwiki-ar mw-list-item\"><a href=\"https://ar.wikipedia.org/wiki/%D9%82%D8%A7%D8%A6%D9%85%D8%A9_%D8%A7%D9%84%D8%AF%D9%88%D9%84_%D8%AD%D8%B3%D8%A8_%D8%AA%D8%B9%D8%AF%D8%A7%D8%AF_%D8%A7%D9%84%D8%B3%D9%83%D8%A7%D9%86_(%D8%A7%D9%84%D8%A3%D9%85%D9%85_%D8%A7%D9%84%D9%85%D8%AA%D8%AD%D8%AF%D8%A9)\" title=\"قائمة الدول حسب تعداد السكان (الأمم المتحدة) – Arabic\" lang=\"ar\" hreflang=\"ar\" data-title=\"قائمة الدول حسب تعداد السكان (الأمم المتحدة)\" data-language-autonym=\"العربية\" data-language-local-name=\"Arabic\" class=\"interlanguage-link-target\"><span>العربية</span></a></li><li class=\"interlanguage-link interwiki-ban mw-list-item\"><a href=\"https://ban.wikipedia.org/wiki/Lis_negara_manut_populasi_(PBB)\" title=\"Lis negara manut populasi (PBB) – Balinese\" lang=\"ban\" hreflang=\"ban\" data-title=\"Lis negara manut populasi (PBB)\" data-language-autonym=\"Basa Bali\" data-language-local-name=\"Balinese\" class=\"interlanguage-link-target\"><span>Basa Bali</span></a></li><li class=\"interlanguage-link interwiki-hif mw-list-item\"><a href=\"https://hif.wikipedia.org/wiki/Duniyaa_ke_des_ke_aabadi_ke_suchi\" title=\"Duniyaa ke des ke aabadi ke suchi – Fiji Hindi\" lang=\"hif\" hreflang=\"hif\" data-title=\"Duniyaa ke des ke aabadi ke suchi\" data-language-autonym=\"Fiji Hindi\" data-language-local-name=\"Fiji Hindi\" class=\"interlanguage-link-target\"><span>Fiji Hindi</span></a></li><li class=\"interlanguage-link interwiki-hy mw-list-item\"><a href=\"https://hy.wikipedia.org/wiki/%D4%B5%D6%80%D5%AF%D6%80%D5%B6%D5%A5%D6%80%D5%AB_%D6%81%D5%B8%D6%82%D6%81%D5%A1%D5%AF_%D5%A8%D5%BD%D5%BF_%D5%A2%D5%B6%D5%A1%D5%AF%D5%B9%D5%B8%D6%82%D5%A9%D5%B5%D5%A1%D5%B6_(%D5%84%D4%B1%D4%BF)\" title=\"Երկրների ցուցակ ըստ բնակչության (ՄԱԿ) – Armenian\" lang=\"hy\" hreflang=\"hy\" data-title=\"Երկրների ցուցակ ըստ բնակչության (ՄԱԿ)\" data-language-autonym=\"Հայերեն\" data-language-local-name=\"Armenian\" class=\"interlanguage-link-target\"><span>Հայերեն</span></a></li><li class=\"interlanguage-link interwiki-pnb mw-list-item\"><a href=\"https://pnb.wikipedia.org/wiki/%D8%AF%DB%8C%D8%B3_%D9%84%D8%B3%D9%B9_%D8%A8%D9%84%D8%AD%D8%A7%D8%B8_%D8%A2%D8%A8%D8%A7%D8%AF_(%D8%A7%D9%82%D9%88%D8%A7%D9%85_%D9%85%D8%AA%D8%AD%D8%AF%DB%81)\" title=\"دیس لسٹ بلحاظ آباد (اقوام متحدہ) – Western Punjabi\" lang=\"pnb\" hreflang=\"pnb\" data-title=\"دیس لسٹ بلحاظ آباد (اقوام متحدہ)\" data-language-autonym=\"پنجابی\" data-language-local-name=\"Western Punjabi\" class=\"interlanguage-link-target\"><span>پنجابی</span></a></li><li class=\"interlanguage-link interwiki-simple mw-list-item\"><a href=\"https://simple.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)\" title=\"List of countries by population (United Nations) – Simple English\" lang=\"en-simple\" hreflang=\"en-simple\" data-title=\"List of countries by population (United Nations)\" data-language-autonym=\"Simple English\" data-language-local-name=\"Simple English\" class=\"interlanguage-link-target\"><span>Simple English</span></a></li><li class=\"interlanguage-link interwiki-ta mw-list-item\"><a href=\"https://ta.wikipedia.org/wiki/%E0%AE%AE%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D_%E0%AE%A4%E0%AF%8A%E0%AE%95%E0%AF%88_%E0%AE%85%E0%AE%9F%E0%AE%BF%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AE%9F%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%B2%E0%AF%8D_%E0%AE%A8%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%AA%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AE%BF%E0%AE%AF%E0%AE%B2%E0%AF%8D_(%E0%AE%90%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%AF_%E0%AE%A8%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AF%8D)\" title=\"மக்கள் தொகை அடிப்படையில் நாடுகளின் பட்டியல் (ஐக்கிய நாடுகள்) – Tamil\" lang=\"ta\" hreflang=\"ta\" data-title=\"மக்கள் தொகை அடிப்படையில் நாடுகளின் பட்டியல் (ஐக்கிய நாடுகள்)\" data-language-autonym=\"தமிழ்\" data-language-local-name=\"Tamil\" class=\"interlanguage-link-target\"><span>தமிழ்</span></a></li><li class=\"interlanguage-link interwiki-th mw-list-item\"><a href=\"https://th.wikipedia.org/wiki/%E0%B8%A3%E0%B8%B2%E0%B8%A2%E0%B8%8A%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%9B%E0%B8%A3%E0%B8%B0%E0%B9%80%E0%B8%97%E0%B8%A8%E0%B8%95%E0%B8%B2%E0%B8%A1%E0%B8%88%E0%B8%B3%E0%B8%99%E0%B8%A7%E0%B8%99%E0%B8%9B%E0%B8%A3%E0%B8%B0%E0%B8%8A%E0%B8%B2%E0%B8%81%E0%B8%A3_(%E0%B8%AA%E0%B8%AB%E0%B8%9B%E0%B8%A3%E0%B8%B0%E0%B8%8A%E0%B8%B2%E0%B8%8A%E0%B8%B2%E0%B8%95%E0%B8%B4)\" title=\"รายชื่อประเทศตามจำนวนประชากร (สหประชาชาติ) – Thai\" lang=\"th\" hreflang=\"th\" data-title=\"รายชื่อประเทศตามจำนวนประชากร (สหประชาชาติ)\" data-language-autonym=\"ไทย\" data-language-local-name=\"Thai\" class=\"interlanguage-link-target\"><span>ไทย</span></a></li><li class=\"interlanguage-link interwiki-tr mw-list-item\"><a href=\"https://tr.wikipedia.org/wiki/N%C3%BCfuslar%C4%B1na_g%C3%B6re_%C3%BClkeler_listesi_(Birle%C5%9Fmi%C5%9F_Milletler)\" title=\"Nüfuslarına göre ülkeler listesi (Birleşmiş Milletler) – Turkish\" lang=\"tr\" hreflang=\"tr\" data-title=\"Nüfuslarına göre ülkeler listesi (Birleşmiş Milletler)\" data-language-autonym=\"Türkçe\" data-language-local-name=\"Turkish\" class=\"interlanguage-link-target\"><span>Türkçe</span></a></li><li class=\"interlanguage-link interwiki-uk mw-list-item\"><a href=\"https://uk.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D1%80%D0%B0%D1%97%D0%BD_%D0%B7%D0%B0_%D0%BD%D0%B0%D1%81%D0%B5%D0%BB%D0%B5%D0%BD%D0%BD%D1%8F%D0%BC_(%D0%9E%D1%80%D0%B3%D0%B0%D0%BD%D1%96%D0%B7%D0%B0%D1%86%D1%96%D1%8F_%D0%9E%D0%B1%27%D1%94%D0%B4%D0%BD%D0%B0%D0%BD%D0%B8%D1%85_%D0%9D%D0%B0%D1%86%D1%96%D0%B9)\" title=\"Список країн за населенням (Організація Об&#039;єднаних Націй) – Ukrainian\" lang=\"uk\" hreflang=\"uk\" data-title=\"Список країн за населенням (Організація Об&#039;єднаних Націй)\" data-language-autonym=\"Українська\" data-language-local-name=\"Ukrainian\" class=\"interlanguage-link-target\"><span>Українська</span></a></li><li class=\"interlanguage-link interwiki-ur mw-list-item\"><a href=\"https://ur.wikipedia.org/wiki/%D9%81%DB%81%D8%B1%D8%B3%D8%AA_%D9%85%D9%85%D8%A7%D9%84%DA%A9_%D8%A8%D9%84%D8%AD%D8%A7%D8%B8_%D8%A2%D8%A8%D8%A7%D8%AF_(%D8%A7%D9%82%D9%88%D8%A7%D9%85_%D9%85%D8%AA%D8%AD%D8%AF%DB%81)\" title=\"فہرست ممالک بلحاظ آباد (اقوام متحدہ) – Urdu\" lang=\"ur\" hreflang=\"ur\" data-title=\"فہرست ممالک بلحاظ آباد (اقوام متحدہ)\" data-language-autonym=\"اردو\" data-language-local-name=\"Urdu\" class=\"interlanguage-link-target\"><span>اردو</span></a></li><li class=\"interlanguage-link interwiki-ug mw-list-item\"><a href=\"https://ug.wikipedia.org/wiki/%D8%AF%DB%86%D9%84%DB%95%D8%AA%D9%84%DB%95%D8%B1%D9%86%D9%89%DA%AD_%D9%86%D9%88%D9%BE%DB%87%D8%B3_%D8%AA%D9%89%D8%B2%D9%89%D9%85%D9%84%D9%89%D9%83%D9%89_(%D8%A8_%D8%AF_%D8%AA)\" title=\"دۆلەتلەرنىڭ نوپۇس تىزىملىكى (ب د ت) – Uyghur\" lang=\"ug\" hreflang=\"ug\" data-title=\"دۆلەتلەرنىڭ نوپۇس تىزىملىكى (ب د ت)\" data-language-autonym=\"ئۇيغۇرچە / Uyghurche\" data-language-local-name=\"Uyghur\" class=\"interlanguage-link-target\"><span>ئۇيغۇرچە / Uyghurche</span></a></li><li class=\"interlanguage-link interwiki-zh mw-list-item\"><a href=\"https://zh.wikipedia.org/wiki/%E4%B8%96%E7%95%8C%E5%9B%BD%E5%AE%B6%E5%92%8C%E5%9C%B0%E5%8C%BA%E4%BA%BA%E5%8F%A3%E6%8E%92%E5%90%8D%E5%88%97%E8%A1%A8\" title=\"世界国家和地区人口排名列表 – Chinese\" lang=\"zh\" hreflang=\"zh\" data-title=\"世界国家和地区人口排名列表\" data-language-autonym=\"中文\" data-language-local-name=\"Chinese\" class=\"interlanguage-link-target\"><span>中文</span></a></li>\n\t\t\t</ul>\n\t\t\t<div class=\"after-portlet after-portlet-lang\"><span class=\"wb-langlinks-edit wb-langlinks-link\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q14940491#sitelinks-wikipedia\" title=\"Edit interlanguage links\" class=\"wbc-editpage\">Edit links</a></span></div>\n\t\t</div>\n\n\t</div>\n</div>\n</header>\n\t\t\t\t<div class=\"vector-page-toolbar vector-feature-custom-font-size-clientpref--excluded\">\n\t\t\t\t\t<div class=\"vector-page-toolbar-container\">\n\t\t\t\t\t\t<div id=\"left-navigation\">\n\t\t\t\t\t\t\t<nav aria-label=\"Namespaces\">\n\n<div id=\"p-associated-pages\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-associated-pages\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-nstab-main\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\" title=\"View the content page [c]\" accesskey=\"c\"><span>Article</span></a></li><li id=\"ca-talk\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/wiki/Talk:List_of_countries_and_dependencies_by_population_(United_Nations)\" rel=\"discussion\" title=\"Discuss improvements to the content page [t]\" accesskey=\"t\"><span>Talk</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n<div id=\"vector-variants-dropdown\" class=\"vector-dropdown emptyPortlet\"  >\n\t<input type=\"checkbox\" id=\"vector-variants-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-variants-dropdown\" class=\"vector-dropdown-checkbox \" aria-label=\"Change language variant\"   >\n\t<label id=\"vector-variants-dropdown-label\" for=\"vector-variants-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">English</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\n<div id=\"p-variants\" class=\"vector-menu mw-portlet mw-portlet-variants emptyPortlet\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\n\t\t</ul>\n\n\t</div>\n</div>\n\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div id=\"right-navigation\" class=\"vector-collapsible\">\n\t\t\t\t\t\t\t<nav aria-label=\"Views\">\n\n<div id=\"p-views\" class=\"vector-menu vector-menu-tabs mw-portlet mw-portlet-views\"  >\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-view\" class=\"selected vector-tab-noicon mw-list-item\"><a href=\"/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\"><span>Read</span></a></li><li id=\"ca-edit\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-history\" class=\"vector-tab-noicon mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=history\" title=\"Past revisions of this page [h]\" accesskey=\"h\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\n<div id=\"vector-page-tools-dropdown\" class=\"vector-dropdown vector-page-tools-dropdown\"  >\n\t<input type=\"checkbox\" id=\"vector-page-tools-dropdown-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-page-tools-dropdown\" class=\"vector-dropdown-checkbox \"  aria-label=\"Tools\"  >\n\t<label id=\"vector-page-tools-dropdown-label\" for=\"vector-page-tools-dropdown-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet\" aria-hidden=\"true\"  ><span class=\"vector-dropdown-label-text\">Tools</span>\n\t</label>\n\t<div class=\"vector-dropdown-content\">\n\n\n\t\t\t\t\t\t\t\t\t<div id=\"vector-page-tools-unpinned-container\" class=\"vector-unpinned-container\">\n\n<div id=\"vector-page-tools\" class=\"vector-page-tools vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-page-tools-pinnable-header vector-pinnable-header-unpinned\"\n\tdata-feature-name=\"page-tools-pinned\"\n\tdata-pinnable-element-id=\"vector-page-tools\"\n\tdata-pinned-container-id=\"vector-page-tools-pinned-container\"\n\tdata-unpinned-container-id=\"vector-page-tools-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Tools</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-page-tools.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-page-tools.unpin\">hide</button>\n</div>\n\n\n<div id=\"p-cactions\" class=\"vector-menu mw-portlet mw-portlet-cactions emptyPortlet vector-has-collapsible-items\"  title=\"More options\" >\n\t<div class=\"vector-menu-heading\">\n\t\tActions\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"ca-more-view\" class=\"selected vector-more-collapsible-item mw-list-item\"><a href=\"/wiki/List_of_countries_and_dependencies_by_population_(United_Nations)\"><span>Read</span></a></li><li id=\"ca-more-edit\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit\" title=\"Edit this page [e]\" accesskey=\"e\"><span>Edit</span></a></li><li id=\"ca-more-history\" class=\"vector-more-collapsible-item mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=history\"><span>View history</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-tb\" class=\"vector-menu mw-portlet mw-portlet-tb\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tGeneral\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-whatlinkshere\" class=\"mw-list-item\"><a href=\"/wiki/Special:WhatLinksHere/List_of_countries_and_dependencies_by_population_(United_Nations)\" title=\"List of all English Wikipedia pages containing links to this page [j]\" accesskey=\"j\"><span>What links here</span></a></li><li id=\"t-recentchangeslinked\" class=\"mw-list-item\"><a href=\"/wiki/Special:RecentChangesLinked/List_of_countries_and_dependencies_by_population_(United_Nations)\" rel=\"nofollow\" title=\"Recent changes in pages linked from this page [k]\" accesskey=\"k\"><span>Related changes</span></a></li><li id=\"t-upload\" class=\"mw-list-item\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:File_Upload_Wizard\" title=\"Upload files [u]\" accesskey=\"u\"><span>Upload file</span></a></li><li id=\"t-permalink\" class=\"mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;oldid=1311341016\" title=\"Permanent link to this revision of this page\"><span>Permanent link</span></a></li><li id=\"t-info\" class=\"mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=info\" title=\"More information about this page\"><span>Page information</span></a></li><li id=\"t-cite\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:CiteThisPage&amp;page=List_of_countries_and_dependencies_by_population_%28United_Nations%29&amp;id=1311341016&amp;wpFormIdentifier=titleform\" title=\"Information on how to cite this page\"><span>Cite this page</span></a></li><li id=\"t-urlshortener\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:UrlShortener&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FList_of_countries_and_dependencies_by_population_%28United_Nations%29\"><span>Get shortened URL</span></a></li><li id=\"t-urlshortener-qrcode\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:QrCode&amp;url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FList_of_countries_and_dependencies_by_population_%28United_Nations%29\"><span>Download QR code</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-coll-print_export\" class=\"vector-menu mw-portlet mw-portlet-coll-print_export\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tPrint/export\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"coll-download-as-rl\" class=\"mw-list-item\"><a href=\"/w/index.php?title=Special:DownloadAsPdf&amp;page=List_of_countries_and_dependencies_by_population_%28United_Nations%29&amp;action=show-download-screen\" title=\"Download this page as a PDF file\"><span>Download as PDF</span></a></li><li id=\"t-print\" class=\"mw-list-item\"><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;printable=yes\" title=\"Printable version of this page [p]\" accesskey=\"p\"><span>Printable version</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n<div id=\"p-wikibase-otherprojects\" class=\"vector-menu mw-portlet mw-portlet-wikibase-otherprojects\"  >\n\t<div class=\"vector-menu-heading\">\n\t\tIn other projects\n\t</div>\n\t<div class=\"vector-menu-content\">\n\n\t\t<ul class=\"vector-menu-content-list\">\n\n\t\t\t<li id=\"t-wikibase\" class=\"wb-otherproject-link wb-otherproject-wikibase-dataitem mw-list-item\"><a href=\"https://www.wikidata.org/wiki/Special:EntityPage/Q14940491\" title=\"Structured data on this page hosted by Wikidata [g]\" accesskey=\"g\"><span>Wikidata item</span></a></li>\n\t\t</ul>\n\n\t</div>\n</div>\n\n</div>\n\n\t\t\t\t\t\t\t\t\t</div>\n\n\t</div>\n</div>\n\n\t\t\t\t\t\t\t</nav>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"vector-column-end no-font-mode-scale\">\n\t\t\t\t\t<div class=\"vector-sticky-pinned-container\">\n\t\t\t\t\t\t<nav class=\"vector-page-tools-landmark\" aria-label=\"Page tools\">\n\t\t\t\t\t\t\t<div id=\"vector-page-tools-pinned-container\" class=\"vector-pinned-container\">\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t\t<nav class=\"vector-appearance-landmark\" aria-label=\"Appearance\">\n\t\t\t\t\t\t\t<div id=\"vector-appearance-pinned-container\" class=\"vector-pinned-container\">\n\t\t\t\t<div id=\"vector-appearance\" class=\"vector-appearance vector-pinnable-element\">\n\t<div\n\tclass=\"vector-pinnable-header vector-appearance-pinnable-header vector-pinnable-header-pinned\"\n\tdata-feature-name=\"appearance-pinned\"\n\tdata-pinnable-element-id=\"vector-appearance\"\n\tdata-pinned-container-id=\"vector-appearance-pinned-container\"\n\tdata-unpinned-container-id=\"vector-appearance-unpinned-container\"\n>\n\t<div class=\"vector-pinnable-header-label\">Appearance</div>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-pin-button\" data-event-name=\"pinnable-header.vector-appearance.pin\">move to sidebar</button>\n\t<button class=\"vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button\" data-event-name=\"pinnable-header.vector-appearance.unpin\">hide</button>\n</div>\n\n\n</div>\n\n\t\t\t\t\t\t\t</div>\n\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"bodyContent\" class=\"vector-body\" aria-labelledby=\"firstHeading\" data-mw-ve-target-container>\n\t\t\t\t\t<div class=\"vector-body-before-content\">\n\t\t\t\t\t\t\t<div class=\"mw-indicators\">\n\t\t</div>\n\n\t\t\t\t\t\t<div id=\"siteSub\" class=\"noprint\">From Wikipedia, the free encyclopedia</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div id=\"contentSub\"><div id=\"mw-content-subtitle\"><span class=\"mw-redirectedfrom\">(Redirected from <a href=\"/w/index.php?title=List_of_countries_by_population_(United_Nations)&amp;redirect=no\" class=\"mw-redirect\" title=\"List of countries by population (United Nations)\">List of countries by population (United Nations)</a>)</span></div></div>\n\n\n\t\t\t\t\t<div id=\"mw-content-text\" class=\"mw-body-content\"><div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\"><div class=\"shortdescription nomobile noexcerpt noprint searchaux\" style=\"display:none\">UNSD list of countries and territories ries by population</div>\n<figure typeof=\"mw:File/Thumb\"><a href=\"/wiki/File:United_Nations_geographical_subregions.png\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/United_Nations_geographical_subregions.png/500px-United_Nations_geographical_subregions.png\" decoding=\"async\" width=\"350\" height=\"162\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/United_Nations_geographical_subregions.png/960px-United_Nations_geographical_subregions.png 1.5x\" data-file-width=\"1357\" data-file-height=\"628\" /></a><figcaption>Statistical subregions as <a href=\"/wiki/United_Nations_geoscheme\" title=\"United Nations geoscheme\">defined by</a> the <a href=\"/wiki/United_Nations_Statistics_Division\" title=\"United Nations Statistics Division\">United Nations Statistics Division</a><sup id=\"cite_ref-UNregions_1-0\" class=\"reference\"><a href=\"#cite_note-UNregions-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup></figcaption></figure>\n<p>This is the list of <a href=\"/wiki/List_of_sovereign_states\" title=\"List of sovereign states\">countries</a> and other <a href=\"/wiki/Dependent_territory\" title=\"Dependent territory\">inhabited territories</a> of the world by estimated total population. It is based on estimates published by the <a href=\"/wiki/United_Nations\" title=\"United Nations\">United Nations</a> in the 2024 revision of <i><a href=\"/wiki/World_Population_Prospects\" title=\"World Population Prospects\">World Population Prospects</a></i>. It presents population estimates from 1950 to the present.<sup id=\"cite_ref-UN_2-0\" class=\"reference\"><a href=\"#cite_note-UN-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup>\n</p>\n<meta property=\"mw:PageProp/toc\" />\n<div class=\"mw-heading mw-heading2\"><h2 id=\"List\">List</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=1\" title=\"Edit section: List\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<p>Data are mid-year estimates from the United Nations and are for 2022 and 2023.<sup id=\"cite_ref-UN_2-1\" class=\"reference\"><a href=\"#cite_note-UN-2\"><span class=\"cite-bracket\">&#91;</span>2<span class=\"cite-bracket\">&#93;</span></a></sup>\n<style data-mw-deduplicate=\"TemplateStyles:r1304002105\">.mw-parser-output .hover-highlight tr:hover,.mw-parser-output .mw-datatable tr:hover{background-color:var(--background-color-progressive-subtle,#f1f4fd);color:var(--color-base,#202122)}.mw-parser-output .mw-datatable{background-color:var(--background-color-base,#fff);color:var(--color-base,#202122)}</style><style data-mw-deduplicate=\"TemplateStyles:r1305414688\">@media screen and (min-width:640px){.mw-parser-output .sticky-header>thead>tr:first-child,.mw-parser-output .sticky-header>caption+tbody>tr:first-child,.mw-parser-output .sticky-header>tbody:first-child>tr:first-child,.mw-parser-output .sticky-header-multi>thead{position:sticky;top:0;z-index:10}body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header>thead>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header>caption+tbody>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header>tbody:first-child>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header-multi>thead{position:static}.mw-parser-output .sticky-header:not(.wikitable),.mw-parser-output .sticky-header-multi:not(.wikitable){background-color:var(--color-inverted,#fff)}.mw-parser-output .sticky-header:not(.wikitable)>*,.mw-parser-output .sticky-header:not(.wikitable)>thead>tr:first-child,.mw-parser-output .sticky-header:not(.wikitable)>caption+tbody>tr:first-child,.mw-parser-output .sticky-header:not(.wikitable)>tbody:first-child>tr:first-child,.mw-parser-output .sticky-header-multi:not(.wikitable)>thead,.mw-parser-output .sticky-header-multi>thead{background-color:inherit}.mw-parser-output .sticky-header.wikitable,.mw-parser-output .sticky-header-multi.wikitable{border-collapse:separate;border-spacing:0;border-width:0 1px 1px 0}.mw-parser-output .sticky-header.wikitable td,.mw-parser-output .sticky-header.wikitable th,.mw-parser-output .sticky-header-multi.wikitable td,.mw-parser-output .sticky-header-multi.wikitable th{border-width:1px 0 0 1px}body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header.wikitable,body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header-multi.wikitable{border-bottom-width:0.2em;padding:0}.mw-parser-output .sticky-header.static-row-numbers.wikitable tr::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable tr::before{border-left-width:1px}.mw-parser-output .sticky-header.static-row-numbers.wikitable>thead>tr:first-child::before,.mw-parser-output .sticky-header.static-row-numbers.wikitable>caption+tbody>tr:first-child::before,.mw-parser-output .sticky-header.static-row-numbers.wikitable>tbody:first-child>tr:first-child::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable>thead>tr:first-child::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable>caption+tbody>tr:first-child::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable>tbody:first-child>tr:first-child::before,.mw-parser-output .sticky-header.static-row-numbers.wikitable .sortbottom::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable .sortbottom::before{border-top-width:1px}.mw-parser-output .sticky-header.static-row-numbers.wikitable .sortbottom~.sortbottom::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable .sortbottom~.sortbottom::before{border-top-width:0}.mw-parser-output .sticky-header.static-row-numbers.wikitable>tbody:first-of-type>tr:not(.static-row-header)::before,.mw-parser-output .sticky-header-multi.static-row-numbers.wikitable>tbody:first-of-type>tr:not(.static-row-header)::before{border-bottom-width:0;border-right-width:0}body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header.wikitable,body.skin-timeless .mw-parser-output .content-table-wrapper.overflowed .sticky-header-multi.wikitable{border-collapse:collapse;border-width:1px}}@media screen and (min-width:1120px){body.vector-sticky-header-visible .mw-parser-output .sticky-header>thead>tr:first-child,body.vector-sticky-header-visible .mw-parser-output .sticky-header>caption+tbody>tr:first-child,body.vector-sticky-header-visible .mw-parser-output .sticky-header>tbody:first-child>tr:first-child,body.vector-sticky-header-visible .mw-parser-output .sticky-header-multi>thead{top:3.125rem}}@media screen and (min-width:851px){body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header>thead>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header>caption+tbody>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header>tbody:first-child>tr:first-child,body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header-multi>thead{top:3.51em}}@media screen{.mw-parser-output .sticky-header.jquery-tablesorter>thead,.mw-parser-output .sticky-header.mw-sticky-header>thead{position:static;top:auto;z-index:auto}.mw-parser-output .sticky-header.jquery-tablesorter>tfoot,.mw-parser-output .sticky-header.mw-sticky-header>tfoot,.mw-parser-output .sticky-header-multi.jquery-tablesorter>tfoot,.mw-parser-output .sticky-header-multi.mw-sticky-header>tfoot{position:static;bottom:auto;z-index:auto}}@media screen and (min-width:1120px){body.skin-vector-2022.vector-sticky-header-visible .mw-parser-output .sticky-header.jquery-tablesorter>thead{top:auto}html.client-js.vector-sticky-header-enabled .mw-parser-output .sticky-header .mw-sticky-header-element{top:auto!important}}@media screen and (min-width:851px){body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header.jquery-tablesorter>thead,body.skin-timeless .mw-parser-output .content-table-wrapper:not(.overflowed) .sticky-header.mw-sticky-header>thead{top:auto}}</style><style data-mw-deduplicate=\"TemplateStyles:r1311145907\">.mw-parser-output .static-row-numbers{counter-reset:rowNumber}.mw-parser-output .static-row-numbers tr::before{content:\"\";display:table-cell;padding-right:0.5em;padding-left:0.5em;text-align:right;vertical-align:inherit}.mw-parser-output .static-row-numbers.static-row-numbers-center tr::before{text-align:center}.mw-parser-output .static-row-numbers.static-row-numbers-left tr::before{text-align:left}.mw-parser-output .static-row-numbers.wikitable tr::before{background-color:var(--background-color-neutral,#eaecf0)}.mw-parser-output .static-row-numbers thead+tbody tr:first-child:not(.static-row-header):not(.static-row-numbers-norank)::before,.mw-parser-output .static-row-numbers tbody tr:not(:first-child):not(.static-row-header):not(.static-row-numbers-norank)::before{counter-increment:rowNumber;content:counter(rowNumber)}.mw-parser-output .static-row-numbers.sortable[data-srn-limit=\"10\"] tbody tr:nth-child(n/**/+11)::before{content:\"\"}.mw-parser-output .static-row-header-text.static-row-numbers thead tr:first-child::before,.mw-parser-output .static-row-header-text.static-row-numbers caption+tbody tr:first-child::before,.mw-parser-output .static-row-header-text.static-row-numbers tbody:first-child tr:first-child::before{content:\"No.\";font-weight:bold}.mw-parser-output .static-row-header-hash.static-row-numbers thead tr:first-child::before,.mw-parser-output .static-row-header-hash.static-row-numbers caption+tbody tr:first-child::before,.mw-parser-output .static-row-header-hash.static-row-numbers tbody:first-child tr:first-child::before{content:\"#\";font-weight:bold}.mw-parser-output .static-row-numbers.wikitable tr::before{border:0 solid var(--border-color-base,#a2a9b1)}.mw-parser-output .static-row-numbers.wikitable thead+tbody tr:first-child:not(.static-row-header)::before,.mw-parser-output .static-row-numbers.wikitable tbody tr:not(:first-child):not(.static-row-header)::before{border-width:1px}body.skin-monobook .mw-parser-output .static-row-numbers.wikitable tr::before{border-color:#aaa}body.skin-timeless .mw-parser-output .static-row-numbers.wikitable tr::before{border-color:#c8ccd1}.mw-parser-output table[border].static-row-numbers:not(.wikitable) tr::before{border:0 inset var(--color-base,#202122)}.mw-parser-output table[border].static-row-numbers:not(.wikitable) thead+tbody tr:first-child:not(.static-row-header)::before,.mw-parser-output table[border].static-row-numbers:not(.wikitable) tbody tr:not(:first-child):not(.static-row-header)::before{border-width:1px}html.skin-theme-clientpref-night .mw-parser-output table[border].static-row-numbers:not(.wikitable),html.skin-theme-clientpref-night .mw-parser-output table[border].static-row-numbers:not(.wikitable) tr::before,html.skin-theme-clientpref-night .mw-parser-output table[border].static-row-numbers:not(.wikitable) th,html.skin-theme-clientpref-night .mw-parser-output table[border].static-row-numbers:not(.wikitable) td{border-color:gray}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output table[border].static-row-numbers:not(.wikitable),html.skin-theme-clientpref-os .mw-parser-output table[border].static-row-numbers:not(.wikitable) tr::before,html.skin-theme-clientpref-os .mw-parser-output table[border].static-row-numbers:not(.wikitable) th,html.skin-theme-clientpref-os .mw-parser-output table[border].static-row-numbers:not(.wikitable) td{border-color:gray}}body.skin-timeless .mw-parser-output .static-row-numbers.mw-datatable:not(.wikitable) tr::before{border:0 solid #c8ccd1}body.skin-timeless .mw-parser-output .static-row-numbers.mw-datatable:not(.wikitable) thead+tbody tr:first-child:not(.static-row-header)::before,body.skin-timeless .mw-parser-output .static-row-numbers.mw-datatable:not(.wikitable) tbody tr:not(:first-child):not(.static-row-header)::before{border-width:1px}</style><style data-mw-deduplicate=\"TemplateStyles:r1303987615\">@media screen{.mw-parser-output .sort-under.sortable.wikitable th.headerSort,.mw-parser-output .sort-under-center.sortable.wikitable th.headerSort{padding-right:0.4em}.mw-parser-output .sort-under.sortable:not(.wikitable) th.headerSort,.mw-parser-output .sort-under-center.sortable:not(.wikitable) th.headerSort{padding-right:1px}body.skin-minerva .mw-parser-output .sort-under.sortable.wikitable th.headerSort,body.skin-minerva .mw-parser-output .sort-under-center.sortable.wikitable th.headerSort{padding-right:0.2em}body.skin-timeless .mw-parser-output .sort-under.sortable.wikitable th.headerSort,body.skin-timeless .mw-parser-output .sort-under-center.sortable.wikitable th.headerSort{padding-right:0.5em}html.client-js .mw-parser-output .sort-under.sortable th.headerSort{background-position:right bottom 0.2em}html.client-js .mw-parser-output .sort-under-center.sortable th.headerSort{background-position:center bottom 0.2em}.mw-parser-output .sort-under.sortable th.headerSort,.mw-parser-output .sort-under.sortable th.unsortable,.mw-parser-output .sort-under-center.sortable th.headerSort,.mw-parser-output .sort-under-center.sortable th.unsortable{padding-bottom:1em}body.skin-timeless .mw-parser-output .sort-under.sortable.wikitable th.headerSort,body.skin-timeless .mw-parser-output .sort-under.sortable.wikitable th.unsortable,body.skin-timeless .mw-parser-output .sort-under-center.sortable.wikitable th.headerSort,body.skin-timeless .mw-parser-output .sort-under-center.sortable.wikitable th.unsortable{padding-bottom:1.2em}body.skin-timeless .mw-parser-output .sort-under.sortable:not(.wikitable) th.headerSort,body.skin-timeless .mw-parser-output .sort-under.sortable:not(.wikitable) th.unsortable,body.skin-timeless .mw-parser-output .sort-under-center.sortable:not(.wikitable) th.headerSort,body.skin-timeless .mw-parser-output .sort-under-center.sortable:not(.wikitable) th.unsortable,body.skin-minerva .mw-parser-output .sort-under.sortable:not(.wikitable) th.headerSort,body.skin-minerva .mw-parser-output .sort-under.sortable:not(.wikitable) th.unsortable,body.skin-minerva .mw-parser-output .sort-under-center.sortable:not(.wikitable) th.headerSort,body.skin-minerva .mw-parser-output .sort-under-center.sortable:not(.wikitable) th.unsortable{padding-bottom:0.8em}.mw-parser-output .static-row-numbers.sort-under.sortable thead tr:only-child::before,.mw-parser-output .static-row-numbers.sort-under-center.sortable thead tr:only-child::before{padding-bottom:0.9em}body.skin-timeless .mw-parser-output .static-row-numbers.sort-under.sortable thead tr:only-child::before,body.skin-timeless .mw-parser-output .static-row-numbers.sort-under-center.sortable thead tr:only-child::before,body.skin-minerva .mw-parser-output .static-row-numbers.sort-under.sortable thead tr:only-child::before,body.skin-minerva .mw-parser-output .static-row-numbers.sort-under-center.sortable thead tr:only-child::before{padding-bottom:0.8em}.mw-parser-output .sort-under.sortable th.ts-vertical-header.headerSort,.mw-parser-output .sort-under-center.sortable th.ts-vertical-header.headerSort{padding-top:0.4em;padding-right:0.4em}}@media screen and (pointer:coarse){html.client-js .mw-parser-output .sort-under.sortable.wikitable th.headerSort{background-position:right bottom 0.5em}html.client-js .mw-parser-output .sort-under-center.sortable.wikitable th.headerSort{background-position:center bottom 0.5em}.mw-parser-output .sort-under.sortable.wikitable th.headerSort,.mw-parser-output .sort-under.sortable.wikitable th.unsortable,.mw-parser-output .sort-under-center.sortable.wikitable th.headerSort,.mw-parser-output .sort-under-center.sortable.wikitable th.unsortable{padding-bottom:1.6em}body.skin-timeless .mw-parser-output .sort-under.sortable.wikitable th.headerSort,body.skin-timeless .mw-parser-output .sort-under.sortable.wikitable th.unsortable,body.skin-timeless .mw-parser-output .sort-under-center.sortable.wikitable th.headerSort,body.skin-timeless .mw-parser-output .sort-under-center.sortable.wikitable th.unsortable{padding-bottom:1.8em}.mw-parser-output .static-row-numbers.sort-under.sortable.wikitable thead tr:only-child::before,.mw-parser-output .static-row-numbers.sort-under-center.sortable.wikitable thead tr:only-child::before{padding-bottom:1.5em}body.skin-timeless .mw-parser-output .static-row-numbers.sort-under.sortable.wikitable thead tr:only-child::before,body.skin-timeless .mw-parser-output .static-row-numbers.sort-under-center.sortable.wikitable thead tr:only-child::before,body.skin-minerva .mw-parser-output .static-row-numbers.sort-under.sortable.wikitable thead tr:only-child::before,body.skin-minerva .mw-parser-output .static-row-numbers.sort-under-center.sortable.wikitable thead tr:only-child::before{padding-bottom:1.4em}}</style><style data-mw-deduplicate=\"TemplateStyles:r1305644437\">.mw-parser-output .defaultleft{text-align:left}.mw-parser-output .defaultcenter{text-align:center}.mw-parser-output .defaultright{text-align:right}.mw-parser-output .col1left td:nth-child(1),.mw-parser-output .col2left td:nth-child(2),.mw-parser-output .col3left td:nth-child(3),.mw-parser-output .col4left td:nth-child(4),.mw-parser-output .col5left td:nth-child(5),.mw-parser-output .col6left td:nth-child(6),.mw-parser-output .col7left td:nth-child(7),.mw-parser-output .col8left td:nth-child(8),.mw-parser-output .col9left td:nth-child(9),.mw-parser-output .col10left td:nth-child(10),.mw-parser-output .col11left td:nth-child(11),.mw-parser-output .col12left td:nth-child(12),.mw-parser-output .col13left td:nth-child(13),.mw-parser-output .col14left td:nth-child(14),.mw-parser-output .col15left td:nth-child(15),.mw-parser-output .col16left td:nth-child(16),.mw-parser-output .col17left td:nth-child(17),.mw-parser-output .col18left td:nth-child(18),.mw-parser-output .col19left td:nth-child(19),.mw-parser-output .col20left td:nth-child(20),.mw-parser-output .col21left td:nth-child(21),.mw-parser-output .col22left td:nth-child(22),.mw-parser-output .col23left td:nth-child(23),.mw-parser-output .col24left td:nth-child(24),.mw-parser-output .col25left td:nth-child(25),.mw-parser-output .col26left td:nth-child(26),.mw-parser-output .col27left td:nth-child(27),.mw-parser-output .col28left td:nth-child(28),.mw-parser-output .col29left td:nth-child(29),.mw-parser-output .col-1left td:nth-last-child(1),.mw-parser-output .col-2left td:nth-last-child(2),.mw-parser-output .col-3left td:nth-last-child(3),.mw-parser-output .col-4left td:nth-last-child(4),.mw-parser-output .col-5left td:nth-last-child(5),.mw-parser-output .col-6left td:nth-last-child(6),.mw-parser-output .col-7left td:nth-last-child(7),.mw-parser-output .col-8left td:nth-last-child(8),.mw-parser-output .col-9left td:nth-last-child(9){text-align:left}.mw-parser-output .col1center td:nth-child(1),.mw-parser-output .col2center td:nth-child(2),.mw-parser-output .col3center td:nth-child(3),.mw-parser-output .col4center td:nth-child(4),.mw-parser-output .col5center td:nth-child(5),.mw-parser-output .col6center td:nth-child(6),.mw-parser-output .col7center td:nth-child(7),.mw-parser-output .col8center td:nth-child(8),.mw-parser-output .col9center td:nth-child(9),.mw-parser-output .col10center td:nth-child(10),.mw-parser-output .col11center td:nth-child(11),.mw-parser-output .col12center td:nth-child(12),.mw-parser-output .col13center td:nth-child(13),.mw-parser-output .col14center td:nth-child(14),.mw-parser-output .col15center td:nth-child(15),.mw-parser-output .col16center td:nth-child(16),.mw-parser-output .col17center td:nth-child(17),.mw-parser-output .col18center td:nth-child(18),.mw-parser-output .col19center td:nth-child(19),.mw-parser-output .col20center td:nth-child(20),.mw-parser-output .col21center td:nth-child(21),.mw-parser-output .col22center td:nth-child(22),.mw-parser-output .col23center td:nth-child(23),.mw-parser-output .col24center td:nth-child(24),.mw-parser-output .col25center td:nth-child(25),.mw-parser-output .col26center td:nth-child(26),.mw-parser-output .col27center td:nth-child(27),.mw-parser-output .col28center td:nth-child(28),.mw-parser-output .col29center td:nth-child(29),.mw-parser-output .col-1center td:nth-last-child(1),.mw-parser-output .col-2center td:nth-last-child(2),.mw-parser-output .col-3center td:nth-last-child(3),.mw-parser-output .col-4center td:nth-last-child(4),.mw-parser-output .col-5center td:nth-last-child(5),.mw-parser-output .col-6center td:nth-last-child(6),.mw-parser-output .col-7center td:nth-last-child(7),.mw-parser-output .col-8center td:nth-last-child(8),.mw-parser-output .col-9center td:nth-last-child(9){text-align:center}.mw-parser-output .col1right td:nth-child(1),.mw-parser-output .col2right td:nth-child(2),.mw-parser-output .col3right td:nth-child(3),.mw-parser-output .col4right td:nth-child(4),.mw-parser-output .col5right td:nth-child(5),.mw-parser-output .col6right td:nth-child(6),.mw-parser-output .col7right td:nth-child(7),.mw-parser-output .col8right td:nth-child(8),.mw-parser-output .col9right td:nth-child(9),.mw-parser-output .col10right td:nth-child(10),.mw-parser-output .col11right td:nth-child(11),.mw-parser-output .col12right td:nth-child(12),.mw-parser-output .col13right td:nth-child(13),.mw-parser-output .col14right td:nth-child(14),.mw-parser-output .col15right td:nth-child(15),.mw-parser-output .col16right td:nth-child(16),.mw-parser-output .col17right td:nth-child(17),.mw-parser-output .col18right td:nth-child(18),.mw-parser-output .col19right td:nth-child(19),.mw-parser-output .col20right td:nth-child(20),.mw-parser-output .col21right td:nth-child(21),.mw-parser-output .col22right td:nth-child(22),.mw-parser-output .col23right td:nth-child(23),.mw-parser-output .col24right td:nth-child(24),.mw-parser-output .col25right td:nth-child(25),.mw-parser-output .col26right td:nth-child(26),.mw-parser-output .col27right td:nth-child(27),.mw-parser-output .col28right td:nth-child(28),.mw-parser-output .col29right td:nth-child(29),.mw-parser-output .col-1right td:nth-last-child(1),.mw-parser-output .col-2right td:nth-last-child(2),.mw-parser-output .col-3right td:nth-last-child(3),.mw-parser-output .col-4right td:nth-last-child(4),.mw-parser-output .col-5right td:nth-last-child(5),.mw-parser-output .col-6right td:nth-last-child(6),.mw-parser-output .col-7right td:nth-last-child(7),.mw-parser-output .col-8right td:nth-last-child(8),.mw-parser-output .col-9right td:nth-last-child(9){text-align:right}</style>\n</p>\n<table class=\"wikitable sortable mw-datatable sticky-header static-row-numbers sort-under col1left col5left col6left\" style=\"text-align:right\">\n<caption>List of countries and inhabited territories by total population. United Nations\n</caption>\n<tbody><tr>\n<th>Country or territory\n</th>\n<th>Population<br />(1 July 2022)\n</th>\n<th>Population<br />(1 July 2023)\n</th>\n<th>Change<br />(%)\n</th>\n<th style=\"max-width:9em;\"><a href=\"/wiki/United_Nations_geoscheme\" title=\"United Nations geoscheme\">UN continental<br />region</a><sup id=\"cite_ref-UNregions_1-1\" class=\"reference\"><a href=\"#cite_note-UNregions-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup>\n</th>\n<th style=\"max-width:8em;\"><a href=\"/wiki/List_of_countries_and_territories_by_the_United_Nations_geoscheme\" title=\"List of countries and territories by the United Nations geoscheme\">UN statistical<br />subregion</a><sup id=\"cite_ref-UNregions_1-2\" class=\"reference\"><a href=\"#cite_note-UNregions-1\"><span class=\"cite-bracket\">&#91;</span>1<span class=\"cite-bracket\">&#93;</span></a></sup>\n</th></tr>\n\n<tr class=\"static-row-numbers-norank\">\n<td><b><span class=\"flagicon\" style=\"padding-left:25px;\">&#160;</span><a href=\"/wiki/World_population\" title=\"World population\">World</a></b></td>\n<td>8,021,407,192</td>\n<td>8,091,734,930</td>\n<td><span style=\"display:none\" data-sort-value=\"6999880000000000000♠\"></span><span style=\"color:green\">+0.88%</span></td>\n<td>–</td>\n<td>–\n</td></tr>\n<tr>\n<td><span data-sort-value=\"India\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/4/41/Flag_of_India.svg/40px-Flag_of_India.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/4/41/Flag_of_India.svg/60px-Flag_of_India.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/India\" title=\"India\">India</a></span></td>\n<td>1,425,423,212</td>\n<td>1,438,069,596</td>\n<td><span style=\"display:none\" data-sort-value=\"6999890000000000000♠\"></span><span style=\"color:green\">+0.89%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"China\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_the_People%27s_Republic_of_China.svg/40px-Flag_of_the_People%27s_Republic_of_China.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_the_People%27s_Republic_of_China.svg/60px-Flag_of_the_People%27s_Republic_of_China.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/China\" title=\"China\">China</a></span><sup id=\"cite_ref-3\" class=\"reference\"><a href=\"#cite_note-3\"><span class=\"cite-bracket\">&#91;</span>a<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>1,425,179,569</td>\n<td>1,422,584,933</td>\n<td><span style=\"display:none\" data-sort-value=\"3000820000000000000♠\"></span><span style=\"color:red\">−0.18%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"United States\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/40px-Flag_of_the_United_States.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/60px-Flag_of_the_United_States.svg.png 2x\" data-file-width=\"1235\" data-file-height=\"650\" /></span></span>&#160;</span><a href=\"/wiki/United_States\" title=\"United States\">United States</a></span></td>\n<td>341,534,046</td>\n<td>343,477,335</td>\n<td><span style=\"display:none\" data-sort-value=\"6999570000000000000♠\"></span><span style=\"color:green\">+0.57%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Northern_America\" title=\"Northern America\">Northern America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Indonesia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_Indonesia.svg/40px-Flag_of_Indonesia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_Indonesia.svg/60px-Flag_of_Indonesia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Indonesia\" title=\"Indonesia\">Indonesia</a></span></td>\n<td>278,830,529</td>\n<td>281,190,067</td>\n<td><span style=\"display:none\" data-sort-value=\"6999850000000000000♠\"></span><span style=\"color:green\">+0.85%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Pakistan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/32/Flag_of_Pakistan.svg/40px-Flag_of_Pakistan.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/32/Flag_of_Pakistan.svg/60px-Flag_of_Pakistan.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Pakistan\" title=\"Pakistan\">Pakistan</a></span></td>\n<td>243,700,667</td>\n<td>247,504,495</td>\n<td><span style=\"display:none\" data-sort-value=\"7000156000000000000♠\"></span><span style=\"color:green\">+1.56%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Nigeria\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/79/Flag_of_Nigeria.svg/40px-Flag_of_Nigeria.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/79/Flag_of_Nigeria.svg/60px-Flag_of_Nigeria.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Nigeria\" title=\"Nigeria\">Nigeria</a></span></td>\n<td>223,150,896</td>\n<td>227,882,945</td>\n<td><span style=\"display:none\" data-sort-value=\"7000212000000000000♠\"></span><span style=\"color:green\">+2.12%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Brazil\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/0/05/Flag_of_Brazil.svg/40px-Flag_of_Brazil.svg.png\" decoding=\"async\" width=\"22\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/0/05/Flag_of_Brazil.svg/60px-Flag_of_Brazil.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"700\" /></span></span>&#160;</span><a href=\"/wiki/Brazil\" title=\"Brazil\">Brazil</a></span></td>\n<td>210,306,415</td>\n<td>211,140,729</td>\n<td><span style=\"display:none\" data-sort-value=\"6999400000000000000♠\"></span><span style=\"color:green\">+0.40%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bangladesh\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Flag_of_Bangladesh.svg/40px-Flag_of_Bangladesh.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Flag_of_Bangladesh.svg/60px-Flag_of_Bangladesh.svg.png 2x\" data-file-width=\"512\" data-file-height=\"307\" /></span></span>&#160;</span><a href=\"/wiki/Bangladesh\" title=\"Bangladesh\">Bangladesh</a></span></td>\n<td>169,384,897</td>\n<td>171,466,990</td>\n<td><span style=\"display:none\" data-sort-value=\"7000123000000000000♠\"></span><span style=\"color:green\">+1.23%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Russia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/f/f3/Flag_of_Russia.svg/40px-Flag_of_Russia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/f/f3/Flag_of_Russia.svg/60px-Flag_of_Russia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Russia\" title=\"Russia\">Russia</a></span></td>\n<td>145,579,899</td>\n<td>145,440,500</td>\n<td><span style=\"display:none\" data-sort-value=\"3000900000000000000♠\"></span><span style=\"color:red\">−0.10%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mexico\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Mexico.svg/40px-Flag_of_Mexico.svg.png\" decoding=\"async\" width=\"23\" height=\"13\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Mexico.svg/60px-Flag_of_Mexico.svg.png 2x\" data-file-width=\"980\" data-file-height=\"560\" /></span></span>&#160;</span><a href=\"/wiki/Mexico\" title=\"Mexico\">Mexico</a></span></td>\n<td>128,613,117</td>\n<td>129,739,759</td>\n<td><span style=\"display:none\" data-sort-value=\"6999880000000000000♠\"></span><span style=\"color:green\">+0.88%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ethiopia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/71/Flag_of_Ethiopia.svg/40px-Flag_of_Ethiopia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/71/Flag_of_Ethiopia.svg/60px-Flag_of_Ethiopia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Ethiopia\" title=\"Ethiopia\">Ethiopia</a></span></td>\n<td>125,384,287</td>\n<td>128,691,692</td>\n<td><span style=\"display:none\" data-sort-value=\"7000264000000000000♠\"></span><span style=\"color:green\">+2.64%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Japan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/9e/Flag_of_Japan.svg/40px-Flag_of_Japan.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/9e/Flag_of_Japan.svg/60px-Flag_of_Japan.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Japan\" title=\"Japan\">Japan</a></span></td>\n<td>124,997,578</td>\n<td>124,370,947</td>\n<td><span style=\"display:none\" data-sort-value=\"3000500000000000000♠\"></span><span style=\"color:red\">−0.50%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Philippines\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/99/Flag_of_the_Philippines.svg/40px-Flag_of_the_Philippines.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/99/Flag_of_the_Philippines.svg/60px-Flag_of_the_Philippines.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Philippines\" title=\"Philippines\">Philippines</a></span></td>\n<td>113,964,338</td>\n<td>114,891,199</td>\n<td><span style=\"display:none\" data-sort-value=\"6999810000000000000♠\"></span><span style=\"color:green\">+0.81%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Egypt\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Egypt.svg/40px-Flag_of_Egypt.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Egypt.svg/60px-Flag_of_Egypt.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Egypt\" title=\"Egypt\">Egypt</a></span></td>\n<td>112,618,250</td>\n<td>114,535,772</td>\n<td><span style=\"display:none\" data-sort-value=\"7000170000000000000♠\"></span><span style=\"color:green\">+1.70%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"DR Congo\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Flag_of_the_Democratic_Republic_of_the_Congo.svg/20px-Flag_of_the_Democratic_Republic_of_the_Congo.svg.png\" decoding=\"async\" width=\"20\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Flag_of_the_Democratic_Republic_of_the_Congo.svg/40px-Flag_of_the_Democratic_Republic_of_the_Congo.svg.png 1.5x\" data-file-width=\"800\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Democratic_Republic_of_the_Congo\" title=\"Democratic Republic of the Congo\">DR Congo</a></span></td>\n<td>102,396,968</td>\n<td>105,789,731</td>\n<td><span style=\"display:none\" data-sort-value=\"7000331000000000000♠\"></span><span style=\"color:green\">+3.31%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Vietnam\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Flag_of_Vietnam.svg/40px-Flag_of_Vietnam.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Flag_of_Vietnam.svg/60px-Flag_of_Vietnam.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Vietnam\" title=\"Vietnam\">Vietnam</a></span></td>\n<td>99,680,655</td>\n<td>100,352,192</td>\n<td><span style=\"display:none\" data-sort-value=\"6999670000000000000♠\"></span><span style=\"color:green\">+0.67%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Iran\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Flag_of_Iran.svg/40px-Flag_of_Iran.svg.png\" decoding=\"async\" width=\"23\" height=\"13\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Flag_of_Iran.svg/60px-Flag_of_Iran.svg.png 2x\" data-file-width=\"630\" data-file-height=\"360\" /></span></span>&#160;</span><a href=\"/wiki/Iran\" title=\"Iran\">Iran</a></span></td>\n<td>89,524,246</td>\n<td>90,608,707</td>\n<td><span style=\"display:none\" data-sort-value=\"7000121000000000000♠\"></span><span style=\"color:green\">+1.21%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Turkey\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Turkey.svg/40px-Flag_of_Turkey.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Turkey.svg/60px-Flag_of_Turkey.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Turkey\" title=\"Turkey\">Turkey</a></span></td>\n<td>87,058,473</td>\n<td>87,270,501</td>\n<td><span style=\"display:none\" data-sort-value=\"6999240000000000000♠\"></span><span style=\"color:green\">+0.24%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Germany\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/b/ba/Flag_of_Germany.svg/40px-Flag_of_Germany.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/b/ba/Flag_of_Germany.svg/60px-Flag_of_Germany.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Germany\" title=\"Germany\">Germany</a></span></td>\n<td>84,086,227</td>\n<td>84,548,231</td>\n<td><span style=\"display:none\" data-sort-value=\"6999550000000000000♠\"></span><span style=\"color:green\">+0.55%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Thailand\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Flag_of_Thailand.svg/40px-Flag_of_Thailand.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Flag_of_Thailand.svg/60px-Flag_of_Thailand.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Thailand\" title=\"Thailand\">Thailand</a></span></td>\n<td>71,735,329</td>\n<td>71,702,435</td>\n<td><span style=\"display:none\" data-sort-value=\"3001500000000000000♠\"></span><span style=\"color:red\">−0.05%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span class=\"flagicon nowrap\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/a/ae/Flag_of_the_United_Kingdom.svg/40px-Flag_of_the_United_Kingdom.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/a/ae/Flag_of_the_United_Kingdom.svg/60px-Flag_of_the_United_Kingdom.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span> </span><a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a></td>\n<td>68,179,315</td>\n<td>68,682,962</td>\n<td><span style=\"display:none\" data-sort-value=\"6999740000000000000♠\"></span><span style=\"color:green\">+0.74%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Tanzania\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Tanzania.svg/40px-Flag_of_Tanzania.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Tanzania.svg/60px-Flag_of_Tanzania.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Tanzania\" title=\"Tanzania\">Tanzania</a></span><sup id=\"cite_ref-4\" class=\"reference\"><a href=\"#cite_note-4\"><span class=\"cite-bracket\">&#91;</span>b<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>64,711,821</td>\n<td>66,617,606</td>\n<td><span style=\"display:none\" data-sort-value=\"7000295000000000000♠\"></span><span style=\"color:green\">+2.95%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"France\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/c/c3/Flag_of_France.svg/40px-Flag_of_France.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/c/c3/Flag_of_France.svg/60px-Flag_of_France.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/France\" title=\"France\">France</a></span><sup id=\"cite_ref-5\" class=\"reference\"><a href=\"#cite_note-5\"><span class=\"cite-bracket\">&#91;</span>c<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>66,277,409</td>\n<td>66,438,822</td>\n<td><span style=\"display:none\" data-sort-value=\"6999240000000000000♠\"></span><span style=\"color:green\">+0.24%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"South Africa\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/af/Flag_of_South_Africa.svg/40px-Flag_of_South_Africa.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/af/Flag_of_South_Africa.svg/60px-Flag_of_South_Africa.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/South_Africa\" title=\"South Africa\">South Africa</a></span></td>\n<td>62,378,410</td>\n<td>63,212,384</td>\n<td><span style=\"display:none\" data-sort-value=\"7000134000000000000♠\"></span><span style=\"color:green\">+1.34%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Southern_Africa\" title=\"Southern Africa\">Southern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Italy\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/0/03/Flag_of_Italy.svg/40px-Flag_of_Italy.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/0/03/Flag_of_Italy.svg/60px-Flag_of_Italy.svg.png 2x\" data-file-width=\"1500\" data-file-height=\"1000\" /></span></span>&#160;</span><a href=\"/wiki/Italy\" title=\"Italy\">Italy</a></span></td>\n<td>59,619,115</td>\n<td>59,499,453</td>\n<td><span style=\"display:none\" data-sort-value=\"3000800000000000000♠\"></span><span style=\"color:red\">−0.20%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Kenya\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Kenya.svg/40px-Flag_of_Kenya.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Kenya.svg/60px-Flag_of_Kenya.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Kenya\" title=\"Kenya\">Kenya</a></span></td>\n<td>54,252,461</td>\n<td>55,339,003</td>\n<td><span style=\"display:none\" data-sort-value=\"7000200000000000000♠\"></span><span style=\"color:green\">+2.00%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Myanmar\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Flag_of_Myanmar.svg/40px-Flag_of_Myanmar.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Flag_of_Myanmar.svg/60px-Flag_of_Myanmar.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Myanmar\" title=\"Myanmar\">Myanmar</a></span></td>\n<td>53,756,787</td>\n<td>54,133,798</td>\n<td><span style=\"display:none\" data-sort-value=\"6999700000000000000♠\"></span><span style=\"color:green\">+0.70%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Colombia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Flag_of_Colombia.svg/40px-Flag_of_Colombia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/21/Flag_of_Colombia.svg/60px-Flag_of_Colombia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Colombia\" title=\"Colombia\">Colombia</a></span></td>\n<td>51,737,944</td>\n<td>52,321,152</td>\n<td><span style=\"display:none\" data-sort-value=\"7000112999999999999♠\"></span><span style=\"color:green\">+1.13%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"South Korea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/09/Flag_of_South_Korea.svg/40px-Flag_of_South_Korea.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/09/Flag_of_South_Korea.svg/60px-Flag_of_South_Korea.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/South_Korea\" title=\"South Korea\">South Korea</a></span></td>\n<td>51,782,512</td>\n<td>51,748,739</td>\n<td><span style=\"display:none\" data-sort-value=\"3001300000000000000♠\"></span><span style=\"color:red\">−0.07%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Sudan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Sudan.svg/40px-Flag_of_Sudan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Sudan.svg/60px-Flag_of_Sudan.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Sudan\" title=\"Sudan\">Sudan</a></span></td>\n<td>49,383,346</td>\n<td>50,042,791</td>\n<td><span style=\"display:none\" data-sort-value=\"7000134000000000000♠\"></span><span style=\"color:green\">+1.34%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Uganda\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Flag_of_Uganda.svg/40px-Flag_of_Uganda.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Flag_of_Uganda.svg/60px-Flag_of_Uganda.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Uganda\" title=\"Uganda\">Uganda</a></span></td>\n<td>47,312,719</td>\n<td>48,656,601</td>\n<td><span style=\"display:none\" data-sort-value=\"7000284000000000000♠\"></span><span style=\"color:green\">+2.84%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Spain\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/9/9a/Flag_of_Spain.svg/40px-Flag_of_Spain.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/9/9a/Flag_of_Spain.svg/60px-Flag_of_Spain.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Spain\" title=\"Spain\">Spain</a></span><sup id=\"cite_ref-6\" class=\"reference\"><a href=\"#cite_note-6\"><span class=\"cite-bracket\">&#91;</span>d<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>47,828,382</td>\n<td>47,911,579</td>\n<td><span style=\"display:none\" data-sort-value=\"6999170000000000000♠\"></span><span style=\"color:green\">+0.17%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Algeria\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_Algeria.svg/40px-Flag_of_Algeria.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_Algeria.svg/60px-Flag_of_Algeria.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Algeria\" title=\"Algeria\">Algeria</a></span></td>\n<td>45,477,389</td>\n<td>46,164,219</td>\n<td><span style=\"display:none\" data-sort-value=\"7000151000000000000♠\"></span><span style=\"color:green\">+1.51%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Argentina\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Flag_of_Argentina.svg/40px-Flag_of_Argentina.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Flag_of_Argentina.svg/60px-Flag_of_Argentina.svg.png 2x\" data-file-width=\"800\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Argentina\" title=\"Argentina\">Argentina</a></span></td>\n<td>45,407,904</td>\n<td>45,538,401</td>\n<td><span style=\"display:none\" data-sort-value=\"6999290000000000000♠\"></span><span style=\"color:green\">+0.29%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Iraq\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Flag_of_Iraq.svg/40px-Flag_of_Iraq.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Flag_of_Iraq.svg/60px-Flag_of_Iraq.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Iraq\" title=\"Iraq\">Iraq</a></span></td>\n<td>44,070,551</td>\n<td>45,074,049</td>\n<td><span style=\"display:none\" data-sort-value=\"7000227999999999999♠\"></span><span style=\"color:green\">+2.28%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Afghanistan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_the_Taliban.svg/40px-Flag_of_the_Taliban.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_the_Taliban.svg/60px-Flag_of_the_Taliban.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Afghanistan\" title=\"Afghanistan\">Afghanistan</a></span></td>\n<td>40,578,842</td>\n<td>41,454,761</td>\n<td><span style=\"display:none\" data-sort-value=\"7000216000000000000♠\"></span><span style=\"color:green\">+2.16%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Yemen\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Yemen.svg/40px-Flag_of_Yemen.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Yemen.svg/60px-Flag_of_Yemen.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Yemen\" title=\"Yemen\">Yemen</a></span></td>\n<td>38,222,876</td>\n<td>39,390,799</td>\n<td><span style=\"display:none\" data-sort-value=\"7000306000000000000♠\"></span><span style=\"color:green\">+3.06%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Canada\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/40px-Flag_of_Canada_%28Pantone%29.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/60px-Flag_of_Canada_%28Pantone%29.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Canada\" title=\"Canada\">Canada</a></span></td>\n<td>38,821,259</td>\n<td>39,299,105</td>\n<td><span style=\"display:none\" data-sort-value=\"7000123000000000000♠\"></span><span style=\"color:green\">+1.23%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Northern_America\" title=\"Northern America\">Northern America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Poland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/1/12/Flag_of_Poland.svg/40px-Flag_of_Poland.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/1/12/Flag_of_Poland.svg/60px-Flag_of_Poland.svg.png 2x\" data-file-width=\"1280\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Poland\" title=\"Poland\">Poland</a></span></td>\n<td>38,385,739</td>\n<td>38,762,844</td>\n<td><span style=\"display:none\" data-sort-value=\"6999980000000000000♠\"></span><span style=\"color:green\">+0.98%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ukraine\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Ukraine.svg/40px-Flag_of_Ukraine.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Ukraine.svg/60px-Flag_of_Ukraine.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Ukraine\" title=\"Ukraine\">Ukraine</a></span><sup id=\"cite_ref-7\" class=\"reference\"><a href=\"#cite_note-7\"><span class=\"cite-bracket\">&#91;</span>e<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>41,048,766</td>\n<td>37,732,836</td>\n<td><span style=\"display:none\" data-sort-value=\"2999192000000000000♠\"></span><span style=\"color:red\">−8.08%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Morocco\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Flag_of_Morocco.svg/40px-Flag_of_Morocco.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Flag_of_Morocco.svg/60px-Flag_of_Morocco.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Morocco\" title=\"Morocco\">Morocco</a></span></td>\n<td>37,329,064</td>\n<td>37,712,505</td>\n<td><span style=\"display:none\" data-sort-value=\"7000103000000000000♠\"></span><span style=\"color:green\">+1.03%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Angola\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Flag_of_Angola.svg/40px-Flag_of_Angola.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Flag_of_Angola.svg/60px-Flag_of_Angola.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Angola\" title=\"Angola\">Angola</a></span></td>\n<td>35,635,029</td>\n<td>36,749,906</td>\n<td><span style=\"display:none\" data-sort-value=\"7000313000000000000♠\"></span><span style=\"color:green\">+3.13%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Uzbekistan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/84/Flag_of_Uzbekistan.svg/40px-Flag_of_Uzbekistan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/84/Flag_of_Uzbekistan.svg/60px-Flag_of_Uzbekistan.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Uzbekistan\" title=\"Uzbekistan\">Uzbekistan</a></span></td>\n<td>34,938,955</td>\n<td>35,652,307</td>\n<td><span style=\"display:none\" data-sort-value=\"7000204000000000000♠\"></span><span style=\"color:green\">+2.04%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Central_Asia\" title=\"Central Asia\">Central Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Malaysia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/66/Flag_of_Malaysia.svg/40px-Flag_of_Malaysia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/66/Flag_of_Malaysia.svg/60px-Flag_of_Malaysia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Malaysia\" title=\"Malaysia\">Malaysia</a></span></td>\n<td>34,695,493</td>\n<td>35,126,298</td>\n<td><span style=\"display:none\" data-sort-value=\"7000124000000000000♠\"></span><span style=\"color:green\">+1.24%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Peru\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Flag_of_Peru.svg/40px-Flag_of_Peru.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Flag_of_Peru.svg/60px-Flag_of_Peru.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Peru\" title=\"Peru\">Peru</a></span></td>\n<td>33,475,438</td>\n<td>33,845,617</td>\n<td><span style=\"display:none\" data-sort-value=\"7000111000000000000♠\"></span><span style=\"color:green\">+1.11%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ghana\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Ghana.svg/40px-Flag_of_Ghana.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Ghana.svg/60px-Flag_of_Ghana.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Ghana\" title=\"Ghana\">Ghana</a></span></td>\n<td>33,149,152</td>\n<td>33,787,914</td>\n<td><span style=\"display:none\" data-sort-value=\"7000193000000000000♠\"></span><span style=\"color:green\">+1.93%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mozambique\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Mozambique.svg/40px-Flag_of_Mozambique.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Mozambique.svg/60px-Flag_of_Mozambique.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Mozambique\" title=\"Mozambique\">Mozambique</a></span></td>\n<td>32,656,246</td>\n<td>33,635,160</td>\n<td><span style=\"display:none\" data-sort-value=\"7000300000000000000♠\"></span><span style=\"color:green\">+3.00%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Saudi Arabia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Flag_of_Saudi_Arabia.svg/40px-Flag_of_Saudi_Arabia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Flag_of_Saudi_Arabia.svg/60px-Flag_of_Saudi_Arabia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saudi_Arabia\" title=\"Saudi Arabia\">Saudi Arabia</a></span></td>\n<td>32,175,352</td>\n<td>32,264,292</td>\n<td><span style=\"display:none\" data-sort-value=\"6999280000000000000♠\"></span><span style=\"color:green\">+0.28%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Madagascar\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Madagascar.svg/40px-Flag_of_Madagascar.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Madagascar.svg/60px-Flag_of_Madagascar.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Madagascar\" title=\"Madagascar\">Madagascar</a></span></td>\n<td>30,437,261</td>\n<td>31,195,932</td>\n<td><span style=\"display:none\" data-sort-value=\"7000249000000000000♠\"></span><span style=\"color:green\">+2.49%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ivory Coast\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_C%C3%B4te_d%27Ivoire.svg/40px-Flag_of_C%C3%B4te_d%27Ivoire.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_C%C3%B4te_d%27Ivoire.svg/60px-Flag_of_C%C3%B4te_d%27Ivoire.svg.png 2x\" data-file-width=\"512\" data-file-height=\"341\" /></span></span>&#160;</span><a href=\"/wiki/Ivory_Coast\" title=\"Ivory Coast\">Ivory Coast</a></span></td>\n<td>30,395,002</td>\n<td>31,165,654</td>\n<td><span style=\"display:none\" data-sort-value=\"7000254000000000000♠\"></span><span style=\"color:green\">+2.54%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Nepal\"><span class=\"flagicon\"><span typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Flag_of_Nepal.svg/20px-Flag_of_Nepal.svg.png\" decoding=\"async\" width=\"16\" height=\"20\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Flag_of_Nepal.svg/40px-Flag_of_Nepal.svg.png 1.5x\" data-file-width=\"726\" data-file-height=\"885\" /></span></span>&#160;&#160;&#160;</span><a href=\"/wiki/Nepal\" title=\"Nepal\">Nepal</a></span></td>\n<td>29,715,436</td>\n<td>29,964,614</td>\n<td><span style=\"display:none\" data-sort-value=\"6999840000000000000♠\"></span><span style=\"color:green\">+0.84%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cameroon\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Flag_of_Cameroon.svg/40px-Flag_of_Cameroon.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Flag_of_Cameroon.svg/60px-Flag_of_Cameroon.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cameroon\" title=\"Cameroon\">Cameroon</a></span></td>\n<td>27,632,771</td>\n<td>28,372,687</td>\n<td><span style=\"display:none\" data-sort-value=\"7000268000000000000♠\"></span><span style=\"color:green\">+2.68%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Venezuela\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/06/Flag_of_Venezuela.svg/40px-Flag_of_Venezuela.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/06/Flag_of_Venezuela.svg/60px-Flag_of_Venezuela.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Venezuela\" title=\"Venezuela\">Venezuela</a></span></td>\n<td>28,213,017</td>\n<td>28,300,854</td>\n<td><span style=\"display:none\" data-sort-value=\"6999310000000000000♠\"></span><span style=\"color:green\">+0.31%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Australia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/88/Flag_of_Australia_%28converted%29.svg/40px-Flag_of_Australia_%28converted%29.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/88/Flag_of_Australia_%28converted%29.svg/60px-Flag_of_Australia_%28converted%29.svg.png 2x\" data-file-width=\"1280\" data-file-height=\"640\" /></span></span>&#160;</span><a href=\"/wiki/Australia\" title=\"Australia\">Australia</a></span><sup id=\"cite_ref-8\" class=\"reference\"><a href=\"#cite_note-8\"><span class=\"cite-bracket\">&#91;</span>f<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>26,200,984</td>\n<td>26,451,124</td>\n<td><span style=\"display:none\" data-sort-value=\"6999950000000000000♠\"></span><span style=\"color:green\">+0.95%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Australasia\" title=\"Australasia\">Australia and New Zealand</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"North Korea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/51/Flag_of_North_Korea.svg/40px-Flag_of_North_Korea.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/51/Flag_of_North_Korea.svg/60px-Flag_of_North_Korea.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/North_Korea\" title=\"North Korea\">North Korea</a></span></td>\n<td>26,329,845</td>\n<td>26,418,204</td>\n<td><span style=\"display:none\" data-sort-value=\"6999340000000000000♠\"></span><span style=\"color:green\">+0.34%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Niger\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f4/Flag_of_Niger.svg/20px-Flag_of_Niger.svg.png\" decoding=\"async\" width=\"18\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f4/Flag_of_Niger.svg/40px-Flag_of_Niger.svg.png 1.5x\" data-file-width=\"700\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Niger\" title=\"Niger\">Niger</a></span></td>\n<td>25,311,973</td>\n<td>26,159,867</td>\n<td><span style=\"display:none\" data-sort-value=\"7000335000000000000♠\"></span><span style=\"color:green\">+3.35%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mali\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_Mali.svg/40px-Flag_of_Mali.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_Mali.svg/60px-Flag_of_Mali.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Mali\" title=\"Mali\">Mali</a></span></td>\n<td>23,072,640</td>\n<td>23,769,127</td>\n<td><span style=\"display:none\" data-sort-value=\"7000302000000000000♠\"></span><span style=\"color:green\">+3.02%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span typeof=\"mw:File\"><a href=\"/wiki/File:Flag_of_Syria_(2025-).svg\" class=\"mw-file-description\"><img src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/54/Flag_of_Syria_%282025-%29.svg/40px-Flag_of_Syria_%282025-%29.svg.png\" decoding=\"async\" width=\"25\" height=\"17\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/54/Flag_of_Syria_%282025-%29.svg/60px-Flag_of_Syria_%282025-%29.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></a></span> <a href=\"/wiki/Syria\" title=\"Syria\">Syria</a></td>\n<td>22,462,173</td>\n<td>23,594,623</td>\n<td><span style=\"display:none\" data-sort-value=\"7000504000000000000♠\"></span><span style=\"color:green\">+5.04%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Taiwan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/40px-Flag_of_the_Republic_of_China.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/72/Flag_of_the_Republic_of_China.svg/60px-Flag_of_the_Republic_of_China.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Taiwan\" title=\"Taiwan\">Taiwan</a></span><sup id=\"cite_ref-9\" class=\"reference\"><a href=\"#cite_note-9\"><span class=\"cite-bracket\">&#91;</span>g<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>23,420,111</td>\n<td>23,317,145</td>\n<td><span style=\"display:none\" data-sort-value=\"3000560000000000000♠\"></span><span style=\"color:red\">−0.44%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Burkina Faso\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Burkina_Faso.svg/40px-Flag_of_Burkina_Faso.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Burkina_Faso.svg/60px-Flag_of_Burkina_Faso.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Burkina_Faso\" title=\"Burkina Faso\">Burkina Faso</a></span></td>\n<td>22,509,038</td>\n<td>23,025,776</td>\n<td><span style=\"display:none\" data-sort-value=\"7000229999999999999♠\"></span><span style=\"color:green\">+2.30%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Sri Lanka\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/11/Flag_of_Sri_Lanka.svg/40px-Flag_of_Sri_Lanka.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/11/Flag_of_Sri_Lanka.svg/60px-Flag_of_Sri_Lanka.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Sri_Lanka\" title=\"Sri Lanka\">Sri Lanka</a></span></td>\n<td>22,834,965</td>\n<td>22,971,617</td>\n<td><span style=\"display:none\" data-sort-value=\"6999600000000000000♠\"></span><span style=\"color:green\">+0.60%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Malawi\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Flag_of_Malawi.svg/40px-Flag_of_Malawi.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Flag_of_Malawi.svg/60px-Flag_of_Malawi.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Malawi\" title=\"Malawi\">Malawi</a></span></td>\n<td>20,568,728</td>\n<td>21,104,482</td>\n<td><span style=\"display:none\" data-sort-value=\"7000260000000000000♠\"></span><span style=\"color:green\">+2.60%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Zambia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/06/Flag_of_Zambia.svg/40px-Flag_of_Zambia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/06/Flag_of_Zambia.svg/60px-Flag_of_Zambia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Zambia\" title=\"Zambia\">Zambia</a></span></td>\n<td>20,152,938</td>\n<td>20,723,965</td>\n<td><span style=\"display:none\" data-sort-value=\"7000283000000000000♠\"></span><span style=\"color:green\">+2.83%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Kazakhstan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Kazakhstan.svg/40px-Flag_of_Kazakhstan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Kazakhstan.svg/60px-Flag_of_Kazakhstan.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Kazakhstan\" title=\"Kazakhstan\">Kazakhstan</a></span></td>\n<td>20,034,609</td>\n<td>20,330,104</td>\n<td><span style=\"display:none\" data-sort-value=\"7000147000000000000♠\"></span><span style=\"color:green\">+1.47%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Central_Asia\" title=\"Central Asia\">Central Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Chile\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/78/Flag_of_Chile.svg/40px-Flag_of_Chile.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/78/Flag_of_Chile.svg/60px-Flag_of_Chile.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Chile\" title=\"Chile\">Chile</a></span></td>\n<td>19,553,036</td>\n<td>19,658,835</td>\n<td><span style=\"display:none\" data-sort-value=\"6999540000000000000♠\"></span><span style=\"color:green\">+0.54%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Chad\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Flag_of_Chad.svg/40px-Flag_of_Chad.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Flag_of_Chad.svg/60px-Flag_of_Chad.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Chad\" title=\"Chad\">Chad</a></span></td>\n<td>18,455,316</td>\n<td>19,319,064</td>\n<td><span style=\"display:none\" data-sort-value=\"7000468000000000000♠\"></span><span style=\"color:green\">+4.68%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Romania\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Romania.svg/40px-Flag_of_Romania.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Romania.svg/60px-Flag_of_Romania.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Romania\" title=\"Romania\">Romania</a></span></td>\n<td>19,166,772</td>\n<td>19,118,479</td>\n<td><span style=\"display:none\" data-sort-value=\"3000750000000000000♠\"></span><span style=\"color:red\">−0.25%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Somalia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Flag_of_Somalia.svg/40px-Flag_of_Somalia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Flag_of_Somalia.svg/60px-Flag_of_Somalia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Somalia\" title=\"Somalia\">Somalia</a></span><sup id=\"cite_ref-10\" class=\"reference\"><a href=\"#cite_note-10\"><span class=\"cite-bracket\">&#91;</span>h<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>17,801,897</td>\n<td>18,358,615</td>\n<td><span style=\"display:none\" data-sort-value=\"7000313000000000000♠\"></span><span style=\"color:green\">+3.13%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Guatemala\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Flag_of_Guatemala.svg/40px-Flag_of_Guatemala.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Flag_of_Guatemala.svg/60px-Flag_of_Guatemala.svg.png 2x\" data-file-width=\"960\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Guatemala\" title=\"Guatemala\">Guatemala</a></span></td>\n<td>17,847,877</td>\n<td>18,124,838</td>\n<td><span style=\"display:none\" data-sort-value=\"7000155000000000000♠\"></span><span style=\"color:green\">+1.55%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Senegal\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fd/Flag_of_Senegal.svg/40px-Flag_of_Senegal.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fd/Flag_of_Senegal.svg/60px-Flag_of_Senegal.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Senegal\" title=\"Senegal\">Senegal</a></span></td>\n<td>17,651,103</td>\n<td>18,077,573</td>\n<td><span style=\"display:none\" data-sort-value=\"7000242000000000000♠\"></span><span style=\"color:green\">+2.42%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Netherlands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/40px-Flag_of_the_Netherlands.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/60px-Flag_of_the_Netherlands.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Netherlands\" title=\"Netherlands\">Netherlands</a></span><sup id=\"cite_ref-11\" class=\"reference\"><a href=\"#cite_note-11\"><span class=\"cite-bracket\">&#91;</span>i<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>17,904,421</td>\n<td>18,092,524</td>\n<td><span style=\"display:none\" data-sort-value=\"7000105000000000000♠\"></span><span style=\"color:green\">+1.05%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ecuador\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Flag_of_Ecuador.svg/40px-Flag_of_Ecuador.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Flag_of_Ecuador.svg/60px-Flag_of_Ecuador.svg.png 2x\" data-file-width=\"1440\" data-file-height=\"960\" /></span></span>&#160;</span><a href=\"/wiki/Ecuador\" title=\"Ecuador\">Ecuador</a></span></td>\n<td>17,823,897</td>\n<td>17,980,083</td>\n<td><span style=\"display:none\" data-sort-value=\"6999880000000000000♠\"></span><span style=\"color:green\">+0.88%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cambodia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/83/Flag_of_Cambodia.svg/40px-Flag_of_Cambodia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/83/Flag_of_Cambodia.svg/60px-Flag_of_Cambodia.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"640\" /></span></span>&#160;</span><a href=\"/wiki/Cambodia\" title=\"Cambodia\">Cambodia</a></span></td>\n<td>17,201,724</td>\n<td>17,423,880</td>\n<td><span style=\"display:none\" data-sort-value=\"7000129000000000000♠\"></span><span style=\"color:green\">+1.29%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Zimbabwe\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Flag_of_Zimbabwe.svg/40px-Flag_of_Zimbabwe.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Flag_of_Zimbabwe.svg/60px-Flag_of_Zimbabwe.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Zimbabwe\" title=\"Zimbabwe\">Zimbabwe</a></span></td>\n<td>16,069,056</td>\n<td>16,340,822</td>\n<td><span style=\"display:none\" data-sort-value=\"7000169000000000000♠\"></span><span style=\"color:green\">+1.69%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Guinea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ed/Flag_of_Guinea.svg/40px-Flag_of_Guinea.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ed/Flag_of_Guinea.svg/60px-Flag_of_Guinea.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Guinea\" title=\"Guinea\">Guinea</a></span></td>\n<td>14,055,137</td>\n<td>14,405,465</td>\n<td><span style=\"display:none\" data-sort-value=\"7000249000000000000♠\"></span><span style=\"color:green\">+2.49%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Benin\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_Benin.svg/40px-Flag_of_Benin.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_Benin.svg/60px-Flag_of_Benin.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Benin\" title=\"Benin\">Benin</a></span></td>\n<td>13,759,501</td>\n<td>14,111,034</td>\n<td><span style=\"display:none\" data-sort-value=\"7000254999999999999♠\"></span><span style=\"color:green\">+2.55%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Rwanda\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/17/Flag_of_Rwanda.svg/40px-Flag_of_Rwanda.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/17/Flag_of_Rwanda.svg/60px-Flag_of_Rwanda.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Rwanda\" title=\"Rwanda\">Rwanda</a></span></td>\n<td>13,651,030</td>\n<td>13,954,471</td>\n<td><span style=\"display:none\" data-sort-value=\"7000222000000000000♠\"></span><span style=\"color:green\">+2.22%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Burundi\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/50/Flag_of_Burundi.svg/40px-Flag_of_Burundi.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/50/Flag_of_Burundi.svg/60px-Flag_of_Burundi.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Burundi\" title=\"Burundi\">Burundi</a></span></td>\n<td>13,321,097</td>\n<td>13,689,450</td>\n<td><span style=\"display:none\" data-sort-value=\"7000277000000000000♠\"></span><span style=\"color:green\">+2.77%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bolivia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Bolivia.svg/40px-Flag_of_Bolivia.svg.png\" decoding=\"async\" width=\"22\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Bolivia.svg/60px-Flag_of_Bolivia.svg.png 2x\" data-file-width=\"1100\" data-file-height=\"750\" /></span></span>&#160;</span><a href=\"/wiki/Bolivia\" title=\"Bolivia\">Bolivia</a></span></td>\n<td>12,077,154</td>\n<td>12,244,159</td>\n<td><span style=\"display:none\" data-sort-value=\"7000137999999999999♠\"></span><span style=\"color:green\">+1.38%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Tunisia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Flag_of_Tunisia.svg/40px-Flag_of_Tunisia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Flag_of_Tunisia.svg/60px-Flag_of_Tunisia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Tunisia\" title=\"Tunisia\">Tunisia</a></span></td>\n<td>12,119,334</td>\n<td>12,200,431</td>\n<td><span style=\"display:none\" data-sort-value=\"6999670000000000000♠\"></span><span style=\"color:green\">+0.67%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Belgium\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_Belgium_%28civil%29.svg/40px-Flag_of_Belgium_%28civil%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_Belgium_%28civil%29.svg/60px-Flag_of_Belgium_%28civil%29.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Belgium\" title=\"Belgium\">Belgium</a></span></td>\n<td>11,641,820</td>\n<td>11,712,893</td>\n<td><span style=\"display:none\" data-sort-value=\"6999610000000000000♠\"></span><span style=\"color:green\">+0.61%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Haiti\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/56/Flag_of_Haiti.svg/40px-Flag_of_Haiti.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/56/Flag_of_Haiti.svg/60px-Flag_of_Haiti.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Haiti\" title=\"Haiti\">Haiti</a></span></td>\n<td>11,503,606</td>\n<td>11,637,398</td>\n<td><span style=\"display:none\" data-sort-value=\"7000115999999999999♠\"></span><span style=\"color:green\">+1.16%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"South Sudan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_South_Sudan.svg/40px-Flag_of_South_Sudan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_South_Sudan.svg/60px-Flag_of_South_Sudan.svg.png 2x\" data-file-width=\"1140\" data-file-height=\"570\" /></span></span>&#160;</span><a href=\"/wiki/South_Sudan\" title=\"South Sudan\">South Sudan</a></span></td>\n<td>11,021,177</td>\n<td>11,483,374</td>\n<td><span style=\"display:none\" data-sort-value=\"7000419000000000000♠\"></span><span style=\"color:green\">+4.19%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Jordan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Flag_of_Jordan.svg/40px-Flag_of_Jordan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Flag_of_Jordan.svg/60px-Flag_of_Jordan.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Jordan\" title=\"Jordan\">Jordan</a></span></td>\n<td>11,256,263</td>\n<td>11,439,213</td>\n<td><span style=\"display:none\" data-sort-value=\"7000163000000000000♠\"></span><span style=\"color:green\">+1.63%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Dominican Republic\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_the_Dominican_Republic.svg/40px-Flag_of_the_Dominican_Republic.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_the_Dominican_Republic.svg/60px-Flag_of_the_Dominican_Republic.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Dominican_Republic\" title=\"Dominican Republic\">Dominican Republic</a></span></td>\n<td>11,230,734</td>\n<td>11,331,265</td>\n<td><span style=\"display:none\" data-sort-value=\"6999900000000000000♠\"></span><span style=\"color:green\">+0.90%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cuba\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Flag_of_Cuba.svg/40px-Flag_of_Cuba.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Flag_of_Cuba.svg/60px-Flag_of_Cuba.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cuba\" title=\"Cuba\">Cuba</a></span></td>\n<td>11,059,820</td>\n<td>11,019,931</td>\n<td><span style=\"display:none\" data-sort-value=\"3000640000000000000♠\"></span><span style=\"color:red\">−0.36%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Czech Republic\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_Czech_Republic.svg/40px-Flag_of_the_Czech_Republic.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_Czech_Republic.svg/60px-Flag_of_the_Czech_Republic.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Czech_Republic\" title=\"Czech Republic\">Czechia</a></span></td>\n<td>10,673,213</td>\n<td>10,809,716</td>\n<td><span style=\"display:none\" data-sort-value=\"7000128000000000000♠\"></span><span style=\"color:green\">+1.28%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Honduras\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Flag_of_Honduras_%282022%E2%80%93present%29.svg/40px-Flag_of_Honduras_%282022%E2%80%93present%29.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Flag_of_Honduras_%282022%E2%80%93present%29.svg/60px-Flag_of_Honduras_%282022%E2%80%93present%29.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Honduras\" title=\"Honduras\">Honduras</a></span></td>\n<td>10,463,872</td>\n<td>10,644,851</td>\n<td><span style=\"display:none\" data-sort-value=\"7000173000000000000♠\"></span><span style=\"color:green\">+1.73%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"United Arab Emirates\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_United_Arab_Emirates.svg/40px-Flag_of_the_United_Arab_Emirates.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_United_Arab_Emirates.svg/60px-Flag_of_the_United_Arab_Emirates.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/United_Arab_Emirates\" title=\"United Arab Emirates\">United Arab Emirates</a></span></td>\n<td>10,242,086</td>\n<td>10,642,081</td>\n<td><span style=\"display:none\" data-sort-value=\"7000391000000000000♠\"></span><span style=\"color:green\">+3.91%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Sweden\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4c/Flag_of_Sweden.svg/40px-Flag_of_Sweden.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/4/4c/Flag_of_Sweden.svg/60px-Flag_of_Sweden.svg.png 2x\" data-file-width=\"1600\" data-file-height=\"1000\" /></span></span>&#160;</span><a href=\"/wiki/Sweden\" title=\"Sweden\">Sweden</a></span></td>\n<td>10,487,338</td>\n<td>10,551,494</td>\n<td><span style=\"display:none\" data-sort-value=\"6999610000000000000♠\"></span><span style=\"color:green\">+0.61%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Portugal\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Flag_of_Portugal_%28official%29.svg/40px-Flag_of_Portugal_%28official%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Flag_of_Portugal_%28official%29.svg/60px-Flag_of_Portugal_%28official%29.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Portugal\" title=\"Portugal\">Portugal</a></span><sup id=\"cite_ref-12\" class=\"reference\"><a href=\"#cite_note-12\"><span class=\"cite-bracket\">&#91;</span>j<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>10,417,073</td>\n<td>10,430,738</td>\n<td><span style=\"display:none\" data-sort-value=\"6999130000000000000♠\"></span><span style=\"color:green\">+0.13%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Tajikistan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Tajikistan.svg/40px-Flag_of_Tajikistan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Tajikistan.svg/60px-Flag_of_Tajikistan.svg.png 2x\" data-file-width=\"560\" data-file-height=\"280\" /></span></span>&#160;</span><a href=\"/wiki/Tajikistan\" title=\"Tajikistan\">Tajikistan</a></span></td>\n<td>10,182,222</td>\n<td>10,389,799</td>\n<td><span style=\"display:none\" data-sort-value=\"7000204000000000000♠\"></span><span style=\"color:green\">+2.04%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Central_Asia\" title=\"Central Asia\">Central Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Papua New Guinea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Flag_of_Papua_New_Guinea.svg/20px-Flag_of_Papua_New_Guinea.svg.png\" decoding=\"async\" width=\"20\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Flag_of_Papua_New_Guinea.svg/40px-Flag_of_Papua_New_Guinea.svg.png 1.5x\" data-file-width=\"600\" data-file-height=\"450\" /></span></span>&#160;</span><a href=\"/wiki/Papua_New_Guinea\" title=\"Papua New Guinea\">Papua New Guinea</a></span></td>\n<td>10,203,169</td>\n<td>10,389,635</td>\n<td><span style=\"display:none\" data-sort-value=\"7000183000000000000♠\"></span><span style=\"color:green\">+1.83%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Melanesia\" title=\"Melanesia\">Melanesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Azerbaijan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Flag_of_Azerbaijan.svg/40px-Flag_of_Azerbaijan.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Flag_of_Azerbaijan.svg/60px-Flag_of_Azerbaijan.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Azerbaijan\" title=\"Azerbaijan\">Azerbaijan</a></span></td>\n<td>10,295,304</td>\n<td>10,318,207</td>\n<td><span style=\"display:none\" data-sort-value=\"6999220000000000000♠\"></span><span style=\"color:green\">+0.22%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Greece\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Greece.svg/40px-Flag_of_Greece.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Greece.svg/60px-Flag_of_Greece.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Greece\" title=\"Greece\">Greece</a></span></td>\n<td>10,412,480</td>\n<td>10,242,908</td>\n<td><span style=\"display:none\" data-sort-value=\"2999837000000000000♠\"></span><span style=\"color:red\">−1.63%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Hungary\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Flag_of_Hungary.svg/40px-Flag_of_Hungary.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Flag_of_Hungary.svg/60px-Flag_of_Hungary.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Hungary\" title=\"Hungary\">Hungary</a></span></td>\n<td>9,964,306</td>\n<td>9,686,463</td>\n<td><span style=\"display:none\" data-sort-value=\"2999721000000000000♠\"></span><span style=\"color:red\">−2.79%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Togo\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_Togo_%283-2%29.svg/40px-Flag_of_Togo_%283-2%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_Togo_%283-2%29.svg/60px-Flag_of_Togo_%283-2%29.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Togo\" title=\"Togo\">Togo</a></span></td>\n<td>9,089,738</td>\n<td>9,304,337</td>\n<td><span style=\"display:none\" data-sort-value=\"7000236000000000000♠\"></span><span style=\"color:green\">+2.36%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Israel\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Israel.svg/40px-Flag_of_Israel.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Israel.svg/60px-Flag_of_Israel.svg.png 2x\" data-file-width=\"1100\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Israel\" title=\"Israel\">Israel</a></span></td>\n<td>9,103,151</td>\n<td>9,256,314</td>\n<td><span style=\"display:none\" data-sort-value=\"7000168000000000000♠\"></span><span style=\"color:green\">+1.68%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Austria\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/41/Flag_of_Austria.svg/40px-Flag_of_Austria.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/41/Flag_of_Austria.svg/60px-Flag_of_Austria.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Austria\" title=\"Austria\">Austria</a></span></td>\n<td>9,064,677</td>\n<td>9,130,429</td>\n<td><span style=\"display:none\" data-sort-value=\"6999730000000000000♠\"></span><span style=\"color:green\">+0.73%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Belarus\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/85/Flag_of_Belarus.svg/40px-Flag_of_Belarus.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/85/Flag_of_Belarus.svg/60px-Flag_of_Belarus.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Belarus\" title=\"Belarus\">Belarus</a></span></td>\n<td>9,173,237</td>\n<td>9,115,680</td>\n<td><span style=\"display:none\" data-sort-value=\"3000370000000000000♠\"></span><span style=\"color:red\">−0.63%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Switzerland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/Flag_of_Switzerland_%28Pantone%29.svg/20px-Flag_of_Switzerland_%28Pantone%29.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/Flag_of_Switzerland_%28Pantone%29.svg/40px-Flag_of_Switzerland_%28Pantone%29.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"512\" /></span></span>&#160;&#160;</span><a href=\"/wiki/Switzerland\" title=\"Switzerland\">Switzerland</a></span></td>\n<td>8,792,182</td>\n<td>8,870,561</td>\n<td><span style=\"display:none\" data-sort-value=\"6999890000000000000♠\"></span><span style=\"color:green\">+0.89%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Sierra Leone\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/17/Flag_of_Sierra_Leone.svg/40px-Flag_of_Sierra_Leone.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/17/Flag_of_Sierra_Leone.svg/60px-Flag_of_Sierra_Leone.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Sierra_Leone\" title=\"Sierra Leone\">Sierra Leone</a></span></td>\n<td>8,276,807</td>\n<td>8,460,512</td>\n<td><span style=\"display:none\" data-sort-value=\"7000222000000000000♠\"></span><span style=\"color:green\">+2.22%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Laos\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/56/Flag_of_Laos.svg/40px-Flag_of_Laos.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/56/Flag_of_Laos.svg/60px-Flag_of_Laos.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Laos\" title=\"Laos\">Laos</a></span></td>\n<td>7,559,007</td>\n<td>7,664,993</td>\n<td><span style=\"display:none\" data-sort-value=\"7000139999999999999♠\"></span><span style=\"color:green\">+1.40%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Hong Kong\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Flag_of_Hong_Kong.svg/40px-Flag_of_Hong_Kong.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Flag_of_Hong_Kong.svg/60px-Flag_of_Hong_Kong.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Hong_Kong\" title=\"Hong Kong\">Hong Kong</a></span> (<a href=\"/wiki/China\" title=\"China\">China</a>)<sup id=\"cite_ref-13\" class=\"reference\"><a href=\"#cite_note-13\"><span class=\"cite-bracket\">&#91;</span>k<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>7,465,915</td>\n<td>7,442,734</td>\n<td><span style=\"display:none\" data-sort-value=\"3000690000000000000♠\"></span><span style=\"color:red\">−0.31%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Turkmenistan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Turkmenistan.svg/40px-Flag_of_Turkmenistan.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Turkmenistan.svg/60px-Flag_of_Turkmenistan.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Turkmenistan\" title=\"Turkmenistan\">Turkmenistan</a></span></td>\n<td>7,230,193</td>\n<td>7,364,438</td>\n<td><span style=\"display:none\" data-sort-value=\"7000186000000000000♠\"></span><span style=\"color:green\">+1.86%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Central_Asia\" title=\"Central Asia\">Central Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Libya\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/05/Flag_of_Libya.svg/40px-Flag_of_Libya.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/05/Flag_of_Libya.svg/60px-Flag_of_Libya.svg.png 2x\" data-file-width=\"960\" data-file-height=\"480\" /></span></span>&#160;</span><a href=\"/wiki/Libya\" title=\"Libya\">Libya</a></span></td>\n<td>7,223,805</td>\n<td>7,305,659</td>\n<td><span style=\"display:none\" data-sort-value=\"7000112999999999999♠\"></span><span style=\"color:green\">+1.13%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Kyrgyzstan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Flag_of_Kyrgyzstan.svg/40px-Flag_of_Kyrgyzstan.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Flag_of_Kyrgyzstan.svg/60px-Flag_of_Kyrgyzstan.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Kyrgyzstan\" title=\"Kyrgyzstan\">Kyrgyzstan</a></span></td>\n<td>6,955,788</td>\n<td>7,073,516</td>\n<td><span style=\"display:none\" data-sort-value=\"7000169000000000000♠\"></span><span style=\"color:green\">+1.69%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Central_Asia\" title=\"Central Asia\">Central Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Paraguay\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag_of_Paraguay.svg/40px-Flag_of_Paraguay.svg.png\" decoding=\"async\" width=\"23\" height=\"13\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag_of_Paraguay.svg/60px-Flag_of_Paraguay.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"660\" /></span></span>&#160;</span><a href=\"/wiki/Paraguay\" title=\"Paraguay\">Paraguay</a></span></td>\n<td>6,760,464</td>\n<td>6,844,146</td>\n<td><span style=\"display:none\" data-sort-value=\"7000124000000000000♠\"></span><span style=\"color:green\">+1.24%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Nicaragua\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Nicaragua.svg/40px-Flag_of_Nicaragua.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Nicaragua.svg/60px-Flag_of_Nicaragua.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Nicaragua\" title=\"Nicaragua\">Nicaragua</a></span></td>\n<td>6,730,654</td>\n<td>6,823,613</td>\n<td><span style=\"display:none\" data-sort-value=\"7000137999999999999♠\"></span><span style=\"color:green\">+1.38%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bulgaria\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Flag_of_Bulgaria.svg/40px-Flag_of_Bulgaria.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Flag_of_Bulgaria.svg/60px-Flag_of_Bulgaria.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Bulgaria\" title=\"Bulgaria\">Bulgaria</a></span></td>\n<td>6,825,864</td>\n<td>6,795,803</td>\n<td><span style=\"display:none\" data-sort-value=\"3000560000000000000♠\"></span><span style=\"color:red\">−0.44%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Serbia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Flag_of_Serbia.svg/40px-Flag_of_Serbia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Flag_of_Serbia.svg/60px-Flag_of_Serbia.svg.png 2x\" data-file-width=\"1350\" data-file-height=\"900\" /></span></span>&#160;</span><a href=\"/wiki/Serbia\" title=\"Serbia\">Serbia</a></span><sup id=\"cite_ref-14\" class=\"reference\"><a href=\"#cite_note-14\"><span class=\"cite-bracket\">&#91;</span>l<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>6,791,213</td>\n<td>6,773,201</td>\n<td><span style=\"display:none\" data-sort-value=\"3000730000000000000♠\"></span><span style=\"color:red\">−0.27%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"El Salvador\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/34/Flag_of_El_Salvador.svg/40px-Flag_of_El_Salvador.svg.png\" decoding=\"async\" width=\"23\" height=\"13\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/34/Flag_of_El_Salvador.svg/60px-Flag_of_El_Salvador.svg.png 2x\" data-file-width=\"1005\" data-file-height=\"567\" /></span></span>&#160;</span><a href=\"/wiki/El_Salvador\" title=\"El Salvador\">El Salvador</a></span></td>\n<td>6,280,319</td>\n<td>6,309,624</td>\n<td><span style=\"display:none\" data-sort-value=\"6999470000000000000♠\"></span><span style=\"color:green\">+0.47%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Congo\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_the_Republic_of_the_Congo.svg/40px-Flag_of_the_Republic_of_the_Congo.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Flag_of_the_Republic_of_the_Congo.svg/60px-Flag_of_the_Republic_of_the_Congo.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Republic_of_the_Congo\" title=\"Republic of the Congo\">Congo</a></span></td>\n<td>6,035,104</td>\n<td>6,182,885</td>\n<td><span style=\"display:none\" data-sort-value=\"7000245000000000000♠\"></span><span style=\"color:green\">+2.45%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Denmark\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Flag_of_Denmark.svg/20px-Flag_of_Denmark.svg.png\" decoding=\"async\" width=\"20\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Flag_of_Denmark.svg/40px-Flag_of_Denmark.svg.png 1.5x\" data-file-width=\"512\" data-file-height=\"387\" /></span></span>&#160;</span><a href=\"/wiki/Denmark\" title=\"Denmark\">Denmark</a></span></td>\n<td>5,902,904</td>\n<td>5,948,136</td>\n<td><span style=\"display:none\" data-sort-value=\"6999770000000000000♠\"></span><span style=\"color:green\">+0.77%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Singapore\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Singapore.svg/40px-Flag_of_Singapore.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Singapore.svg/60px-Flag_of_Singapore.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Singapore\" title=\"Singapore\">Singapore</a></span></td>\n<td>5,649,885</td>\n<td>5,789,090</td>\n<td><span style=\"display:none\" data-sort-value=\"7000246000000000000♠\"></span><span style=\"color:green\">+2.46%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Lebanon\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/59/Flag_of_Lebanon.svg/40px-Flag_of_Lebanon.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/59/Flag_of_Lebanon.svg/60px-Flag_of_Lebanon.svg.png 2x\" data-file-width=\"2880\" data-file-height=\"1920\" /></span></span>&#160;</span><a href=\"/wiki/Lebanon\" title=\"Lebanon\">Lebanon</a></span></td>\n<td>5,744,489</td>\n<td>5,733,493</td>\n<td><span style=\"display:none\" data-sort-value=\"3000810000000000000♠\"></span><span style=\"color:red\">−0.19%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Finland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Finland.svg/40px-Flag_of_Finland.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Finland.svg/60px-Flag_of_Finland.svg.png 2x\" data-file-width=\"1800\" data-file-height=\"1100\" /></span></span>&#160;</span><a href=\"/wiki/Finland\" title=\"Finland\">Finland</a></span><sup id=\"cite_ref-15\" class=\"reference\"><a href=\"#cite_note-15\"><span class=\"cite-bracket\">&#91;</span>m<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>5,569,299</td>\n<td>5,601,185</td>\n<td><span style=\"display:none\" data-sort-value=\"6999570000000000000♠\"></span><span style=\"color:green\">+0.57%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Norway\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Norway.svg/40px-Flag_of_Norway.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Norway.svg/60px-Flag_of_Norway.svg.png 2x\" data-file-width=\"512\" data-file-height=\"372\" /></span></span>&#160;</span><a href=\"/wiki/Norway\" title=\"Norway\">Norway</a></span><sup id=\"cite_ref-16\" class=\"reference\"><a href=\"#cite_note-16\"><span class=\"cite-bracket\">&#91;</span>n<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>5,457,801</td>\n<td>5,519,167</td>\n<td><span style=\"display:none\" data-sort-value=\"7000112000000000000♠\"></span><span style=\"color:green\">+1.12%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Slovakia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Flag_of_Slovakia.svg/40px-Flag_of_Slovakia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Flag_of_Slovakia.svg/60px-Flag_of_Slovakia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Slovakia\" title=\"Slovakia\">Slovakia</a></span></td>\n<td>5,473,197</td>\n<td>5,518,055</td>\n<td><span style=\"display:none\" data-sort-value=\"6999820000000000000♠\"></span><span style=\"color:green\">+0.82%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Liberia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Flag_of_Liberia.svg/40px-Flag_of_Liberia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Flag_of_Liberia.svg/60px-Flag_of_Liberia.svg.png 2x\" data-file-width=\"1140\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Liberia\" title=\"Liberia\">Liberia</a></span></td>\n<td>5,373,294</td>\n<td>5,493,031</td>\n<td><span style=\"display:none\" data-sort-value=\"7000223000000000000♠\"></span><span style=\"color:green\">+2.23%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Palestine\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Palestine.svg/40px-Flag_of_Palestine.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Palestine.svg/60px-Flag_of_Palestine.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Palestine\" title=\"Palestine\">Palestine</a></span><sup id=\"cite_ref-17\" class=\"reference\"><a href=\"#cite_note-17\"><span class=\"cite-bracket\">&#91;</span>o<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>5,305,270</td>\n<td>5,409,202</td>\n<td><span style=\"display:none\" data-sort-value=\"7000196000000000000♠\"></span><span style=\"color:green\">+1.96%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Ireland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/45/Flag_of_Ireland.svg/40px-Flag_of_Ireland.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/45/Flag_of_Ireland.svg/60px-Flag_of_Ireland.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Republic_of_Ireland\" title=\"Republic of Ireland\">Ireland</a></span></td>\n<td>5,110,016</td>\n<td>5,196,630</td>\n<td><span style=\"display:none\" data-sort-value=\"7000169000000000000♠\"></span><span style=\"color:green\">+1.69%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"New Zealand\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Flag_of_New_Zealand.svg/40px-Flag_of_New_Zealand.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Flag_of_New_Zealand.svg/60px-Flag_of_New_Zealand.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/New_Zealand\" title=\"New Zealand\">New Zealand</a></span></td>\n<td>5,131,734</td>\n<td>5,172,836</td>\n<td><span style=\"display:none\" data-sort-value=\"6999800000000000000♠\"></span><span style=\"color:green\">+0.80%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Australasia\" title=\"Australasia\">Australia and New Zealand</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Central African Republic\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Flag_of_the_Central_African_Republic.svg/40px-Flag_of_the_Central_African_Republic.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Flag_of_the_Central_African_Republic.svg/60px-Flag_of_the_Central_African_Republic.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Central_African_Republic\" title=\"Central African Republic\">Central African Republic</a></span></td>\n<td>5,098,039</td>\n<td>5,152,421</td>\n<td><span style=\"display:none\" data-sort-value=\"7000107000000000000♠\"></span><span style=\"color:green\">+1.07%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Costa Rica\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f2/Flag_of_Costa_Rica.svg/40px-Flag_of_Costa_Rica.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f2/Flag_of_Costa_Rica.svg/60px-Flag_of_Costa_Rica.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Costa_Rica\" title=\"Costa Rica\">Costa Rica</a></span></td>\n<td>5,081,765</td>\n<td>5,105,525</td>\n<td><span style=\"display:none\" data-sort-value=\"6999470000000000000♠\"></span><span style=\"color:green\">+0.47%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Oman\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Flag_of_Oman.svg/40px-Flag_of_Oman.svg.png\" decoding=\"async\" width=\"23\" height=\"13\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Flag_of_Oman.svg/60px-Flag_of_Oman.svg.png 2x\" data-file-width=\"512\" data-file-height=\"293\" /></span></span>&#160;</span><a href=\"/wiki/Oman\" title=\"Oman\">Oman</a></span></td>\n<td>4,730,226</td>\n<td>5,049,269</td>\n<td><span style=\"display:none\" data-sort-value=\"7000674000000000000♠\"></span><span style=\"color:green\">+6.74%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mauritania\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/43/Flag_of_Mauritania.svg/40px-Flag_of_Mauritania.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/43/Flag_of_Mauritania.svg/60px-Flag_of_Mauritania.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Mauritania\" title=\"Mauritania\">Mauritania</a></span></td>\n<td>4,875,637</td>\n<td>5,022,441</td>\n<td><span style=\"display:none\" data-sort-value=\"7000301000000000000♠\"></span><span style=\"color:green\">+3.01%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Kuwait\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Flag_of_Kuwait.svg/40px-Flag_of_Kuwait.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Flag_of_Kuwait.svg/60px-Flag_of_Kuwait.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Kuwait\" title=\"Kuwait\">Kuwait</a></span></td>\n<td>4,589,511</td>\n<td>4,838,782</td>\n<td><span style=\"display:none\" data-sort-value=\"7000543000000000000♠\"></span><span style=\"color:green\">+5.43%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Panama\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Flag_of_Panama.svg/40px-Flag_of_Panama.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Flag_of_Panama.svg/60px-Flag_of_Panama.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Panama\" title=\"Panama\">Panama</a></span></td>\n<td>4,400,773</td>\n<td>4,458,759</td>\n<td><span style=\"display:none\" data-sort-value=\"7000132000000000000♠\"></span><span style=\"color:green\">+1.32%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Croatia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Croatia.svg/40px-Flag_of_Croatia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Croatia.svg/60px-Flag_of_Croatia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Croatia\" title=\"Croatia\">Croatia</a></span></td>\n<td>3,907,027</td>\n<td>3,896,023</td>\n<td><span style=\"display:none\" data-sort-value=\"3000720000000000000♠\"></span><span style=\"color:red\">−0.28%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Georgia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_Georgia.svg/40px-Flag_of_Georgia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_Georgia.svg/60px-Flag_of_Georgia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Georgia_(country)\" title=\"Georgia (country)\">Georgia</a></span><sup id=\"cite_ref-18\" class=\"reference\"><a href=\"#cite_note-18\"><span class=\"cite-bracket\">&#91;</span>p<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>3,794,784</td>\n<td>3,807,492</td>\n<td><span style=\"display:none\" data-sort-value=\"6999330000000000000♠\"></span><span style=\"color:green\">+0.33%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Eritrea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_Eritrea.svg/40px-Flag_of_Eritrea.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_Eritrea.svg/60px-Flag_of_Eritrea.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Eritrea\" title=\"Eritrea\">Eritrea</a></span></td>\n<td>3,409,447</td>\n<td>3,470,390</td>\n<td><span style=\"display:none\" data-sort-value=\"7000179000000000000♠\"></span><span style=\"color:green\">+1.79%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mongolia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Flag_of_Mongolia.svg/40px-Flag_of_Mongolia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Flag_of_Mongolia.svg/60px-Flag_of_Mongolia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Mongolia\" title=\"Mongolia\">Mongolia</a></span></td>\n<td>3,386,015</td>\n<td>3,431,932</td>\n<td><span style=\"display:none\" data-sort-value=\"7000136000000000000♠\"></span><span style=\"color:green\">+1.36%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Uruguay\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Uruguay.svg/40px-Flag_of_Uruguay.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Uruguay.svg/60px-Flag_of_Uruguay.svg.png 2x\" data-file-width=\"512\" data-file-height=\"341\" /></span></span>&#160;</span><a href=\"/wiki/Uruguay\" title=\"Uruguay\">Uruguay</a></span></td>\n<td>3,390,913</td>\n<td>3,388,081</td>\n<td><span style=\"display:none\" data-sort-value=\"3001200000000000000♠\"></span><span style=\"color:red\">−0.08%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Puerto Rico\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/28/Flag_of_Puerto_Rico.svg/40px-Flag_of_Puerto_Rico.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/28/Flag_of_Puerto_Rico.svg/60px-Flag_of_Puerto_Rico.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Puerto_Rico\" title=\"Puerto Rico\">Puerto Rico</a></span> (<a href=\"/wiki/United_States\" title=\"United States\">United States</a>)</td>\n<td>3,240,968</td>\n<td>3,242,023</td>\n<td><span style=\"display:none\" data-sort-value=\"6998300000000000000♠\"></span><span style=\"color:green\">+0.03%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bosnia and Herzegovina\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Flag_of_Bosnia_and_Herzegovina.svg/40px-Flag_of_Bosnia_and_Herzegovina.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Flag_of_Bosnia_and_Herzegovina.svg/60px-Flag_of_Bosnia_and_Herzegovina.svg.png 2x\" data-file-width=\"800\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Bosnia_and_Herzegovina\" title=\"Bosnia and Herzegovina\">Bosnia and Herzegovina</a></span></td>\n<td>3,204,802</td>\n<td>3,185,073</td>\n<td><span style=\"display:none\" data-sort-value=\"3000380000000000000♠\"></span><span style=\"color:red\">−0.62%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Moldova\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag_of_Moldova.svg/40px-Flag_of_Moldova.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag_of_Moldova.svg/60px-Flag_of_Moldova.svg.png 2x\" data-file-width=\"1800\" data-file-height=\"900\" /></span></span>&#160;</span><a href=\"/wiki/Moldova\" title=\"Moldova\">Moldova</a></span><sup id=\"cite_ref-19\" class=\"reference\"><a href=\"#cite_note-19\"><span class=\"cite-bracket\">&#91;</span>q<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>3,039,985</td>\n<td>3,067,070</td>\n<td><span style=\"display:none\" data-sort-value=\"6999890000000000000♠\"></span><span style=\"color:green\">+0.89%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Eastern_Europe\" title=\"Eastern Europe\">Eastern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Qatar\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Qatar.svg/40px-Flag_of_Qatar.svg.png\" decoding=\"async\" width=\"23\" height=\"9\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Qatar.svg/60px-Flag_of_Qatar.svg.png 2x\" data-file-width=\"1400\" data-file-height=\"550\" /></span></span>&#160;</span><a href=\"/wiki/Qatar\" title=\"Qatar\">Qatar</a></span></td>\n<td>2,892,455</td>\n<td>2,979,082</td>\n<td><span style=\"display:none\" data-sort-value=\"7000299000000000000♠\"></span><span style=\"color:green\">+2.99%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Namibia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Namibia.svg/40px-Flag_of_Namibia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Namibia.svg/60px-Flag_of_Namibia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Namibia\" title=\"Namibia\">Namibia</a></span></td>\n<td>2,889,662</td>\n<td>2,963,095</td>\n<td><span style=\"display:none\" data-sort-value=\"7000254000000000000♠\"></span><span style=\"color:green\">+2.54%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Southern_Africa\" title=\"Southern Africa\">Southern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Armenia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Flag_of_Armenia.svg/40px-Flag_of_Armenia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Flag_of_Armenia.svg/60px-Flag_of_Armenia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Armenia\" title=\"Armenia\">Armenia</a></span></td>\n<td>2,880,874</td>\n<td>2,943,393</td>\n<td><span style=\"display:none\" data-sort-value=\"7000217000000000000♠\"></span><span style=\"color:green\">+2.17%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Lithuania\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/11/Flag_of_Lithuania.svg/40px-Flag_of_Lithuania.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/11/Flag_of_Lithuania.svg/60px-Flag_of_Lithuania.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Lithuania\" title=\"Lithuania\">Lithuania</a></span></td>\n<td>2,816,919</td>\n<td>2,854,099</td>\n<td><span style=\"display:none\" data-sort-value=\"7000132000000000000♠\"></span><span style=\"color:green\">+1.32%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Jamaica\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_Jamaica.svg/40px-Flag_of_Jamaica.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_Jamaica.svg/60px-Flag_of_Jamaica.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Jamaica\" title=\"Jamaica\">Jamaica</a></span></td>\n<td>2,839,144</td>\n<td>2,839,786</td>\n<td><span style=\"display:none\" data-sort-value=\"6998200000000000000♠\"></span><span style=\"color:green\">+0.02%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Albania\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/36/Flag_of_Albania.svg/40px-Flag_of_Albania.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/36/Flag_of_Albania.svg/60px-Flag_of_Albania.svg.png 2x\" data-file-width=\"700\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Albania\" title=\"Albania\">Albania</a></span></td>\n<td>2,827,608</td>\n<td>2,811,655</td>\n<td><span style=\"display:none\" data-sort-value=\"3000439999999999999♠\"></span><span style=\"color:red\">−0.56%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Gambia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_The_Gambia.svg/40px-Flag_of_The_Gambia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_The_Gambia.svg/60px-Flag_of_The_Gambia.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/The_Gambia\" title=\"The Gambia\">Gambia</a></span></td>\n<td>2,636,470</td>\n<td>2,697,845</td>\n<td><span style=\"display:none\" data-sort-value=\"7000233000000000000♠\"></span><span style=\"color:green\">+2.33%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Gabon\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/04/Flag_of_Gabon.svg/20px-Flag_of_Gabon.svg.png\" decoding=\"async\" width=\"20\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/04/Flag_of_Gabon.svg/40px-Flag_of_Gabon.svg.png 1.5x\" data-file-width=\"1000\" data-file-height=\"750\" /></span></span>&#160;</span><a href=\"/wiki/Gabon\" title=\"Gabon\">Gabon</a></span></td>\n<td>2,430,747</td>\n<td>2,484,789</td>\n<td><span style=\"display:none\" data-sort-value=\"7000222000000000000♠\"></span><span style=\"color:green\">+2.22%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Botswana\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_Botswana.svg/40px-Flag_of_Botswana.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_Botswana.svg/60px-Flag_of_Botswana.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Botswana\" title=\"Botswana\">Botswana</a></span></td>\n<td>2,439,892</td>\n<td>2,480,244</td>\n<td><span style=\"display:none\" data-sort-value=\"7000165000000000000♠\"></span><span style=\"color:green\">+1.65%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Southern_Africa\" title=\"Southern Africa\">Southern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Lesotho\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Flag_of_Lesotho.svg/40px-Flag_of_Lesotho.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Flag_of_Lesotho.svg/60px-Flag_of_Lesotho.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Lesotho\" title=\"Lesotho\">Lesotho</a></span></td>\n<td>2,286,110</td>\n<td>2,311,472</td>\n<td><span style=\"display:none\" data-sort-value=\"7000111000000000000♠\"></span><span style=\"color:green\">+1.11%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Southern_Africa\" title=\"Southern Africa\">Southern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Guinea-Bissau\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Guinea-Bissau.svg/40px-Flag_of_Guinea-Bissau.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Guinea-Bissau.svg/60px-Flag_of_Guinea-Bissau.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Guinea-Bissau\" title=\"Guinea-Bissau\">Guinea-Bissau</a></span></td>\n<td>2,105,529</td>\n<td>2,153,339</td>\n<td><span style=\"display:none\" data-sort-value=\"7000227000000000000♠\"></span><span style=\"color:green\">+2.27%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Slovenia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Flag_of_Slovenia.svg/40px-Flag_of_Slovenia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Flag_of_Slovenia.svg/60px-Flag_of_Slovenia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Slovenia\" title=\"Slovenia\">Slovenia</a></span></td>\n<td>2,115,228</td>\n<td>2,118,396</td>\n<td><span style=\"display:none\" data-sort-value=\"6999150000000000000♠\"></span><span style=\"color:green\">+0.15%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Latvia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/84/Flag_of_Latvia.svg/40px-Flag_of_Latvia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/84/Flag_of_Latvia.svg/60px-Flag_of_Latvia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Latvia\" title=\"Latvia\">Latvia</a></span></td>\n<td>1,881,063</td>\n<td>1,882,396</td>\n<td><span style=\"display:none\" data-sort-value=\"6998700000000000000♠\"></span><span style=\"color:green\">+0.07%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Equatorial Guinea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Equatorial_Guinea.svg/40px-Flag_of_Equatorial_Guinea.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Equatorial_Guinea.svg/60px-Flag_of_Equatorial_Guinea.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Equatorial_Guinea\" title=\"Equatorial Guinea\">Equatorial Guinea</a></span></td>\n<td>1,803,545</td>\n<td>1,847,549</td>\n<td><span style=\"display:none\" data-sort-value=\"7000244000000000000♠\"></span><span style=\"color:green\">+2.44%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"North Macedonia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/79/Flag_of_North_Macedonia.svg/40px-Flag_of_North_Macedonia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/79/Flag_of_North_Macedonia.svg/60px-Flag_of_North_Macedonia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/North_Macedonia\" title=\"North Macedonia\">North Macedonia</a></span></td>\n<td>1,840,233</td>\n<td>1,831,802</td>\n<td><span style=\"display:none\" data-sort-value=\"3000540000000000000♠\"></span><span style=\"color:red\">−0.46%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Kosovo\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Flag_of_Kosovo.svg/40px-Flag_of_Kosovo.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Flag_of_Kosovo.svg/60px-Flag_of_Kosovo.svg.png 2x\" data-file-width=\"840\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Kosovo\" title=\"Kosovo\">Kosovo</a></span><sup id=\"cite_ref-20\" class=\"reference\"><a href=\"#cite_note-20\"><span class=\"cite-bracket\">&#91;</span>r<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>1,717,946</td>\n<td>1,700,031</td>\n<td><span style=\"display:none\" data-sort-value=\"2999896000000000000♠\"></span><span style=\"color:red\">−1.04%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bahrain\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Flag_of_Bahrain.svg/40px-Flag_of_Bahrain.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Flag_of_Bahrain.svg/60px-Flag_of_Bahrain.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Bahrain\" title=\"Bahrain\">Bahrain</a></span></td>\n<td>1,533,459</td>\n<td>1,569,666</td>\n<td><span style=\"display:none\" data-sort-value=\"7000236000000000000♠\"></span><span style=\"color:green\">+2.36%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Trinidad and Tobago\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/64/Flag_of_Trinidad_and_Tobago.svg/40px-Flag_of_Trinidad_and_Tobago.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/64/Flag_of_Trinidad_and_Tobago.svg/60px-Flag_of_Trinidad_and_Tobago.svg.png 2x\" data-file-width=\"800\" data-file-height=\"480\" /></span></span>&#160;</span><a href=\"/wiki/Trinidad_and_Tobago\" title=\"Trinidad and Tobago\">Trinidad and Tobago</a></span></td>\n<td>1,495,921</td>\n<td>1,502,932</td>\n<td><span style=\"display:none\" data-sort-value=\"6999470000000000000♠\"></span><span style=\"color:green\">+0.47%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Timor-Leste\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/26/Flag_of_East_Timor.svg/40px-Flag_of_East_Timor.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/26/Flag_of_East_Timor.svg/60px-Flag_of_East_Timor.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Timor-Leste\" title=\"Timor-Leste\">Timor-Leste</a></span></td>\n<td>1,369,295</td>\n<td>1,384,286</td>\n<td><span style=\"display:none\" data-sort-value=\"7000109000000000000♠\"></span><span style=\"color:green\">+1.09%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Estonia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Flag_of_Estonia.svg/40px-Flag_of_Estonia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Flag_of_Estonia.svg/60px-Flag_of_Estonia.svg.png 2x\" data-file-width=\"990\" data-file-height=\"630\" /></span></span>&#160;</span><a href=\"/wiki/Estonia\" title=\"Estonia\">Estonia</a></span></td>\n<td>1,350,091</td>\n<td>1,367,196</td>\n<td><span style=\"display:none\" data-sort-value=\"7000127000000000000♠\"></span><span style=\"color:green\">+1.27%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cyprus\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Cyprus.svg/40px-Flag_of_Cyprus.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Cyprus.svg/60px-Flag_of_Cyprus.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Cyprus\" title=\"Cyprus\">Cyprus</a></span><sup id=\"cite_ref-21\" class=\"reference\"><a href=\"#cite_note-21\"><span class=\"cite-bracket\">&#91;</span>s<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>1,331,370</td>\n<td>1,344,976</td>\n<td><span style=\"display:none\" data-sort-value=\"7000102000000000000♠\"></span><span style=\"color:green\">+1.02%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/West_Asia\" title=\"West Asia\">Western Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Mauritius\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_Mauritius.svg/40px-Flag_of_Mauritius.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_Mauritius.svg/60px-Flag_of_Mauritius.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Mauritius\" title=\"Mauritius\">Mauritius</a></span></td>\n<td>1,276,130</td>\n<td>1,273,588</td>\n<td><span style=\"display:none\" data-sort-value=\"3000800000000000000♠\"></span><span style=\"color:red\">−0.20%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Eswatini\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Flag_of_Eswatini.svg/40px-Flag_of_Eswatini.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Flag_of_Eswatini.svg/60px-Flag_of_Eswatini.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Eswatini\" title=\"Eswatini\">Eswatini</a></span></td>\n<td>1,218,917</td>\n<td>1,230,506</td>\n<td><span style=\"display:none\" data-sort-value=\"6999950000000000000♠\"></span><span style=\"color:green\">+0.95%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Southern_Africa\" title=\"Southern Africa\">Southern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Djibouti\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/34/Flag_of_Djibouti.svg/40px-Flag_of_Djibouti.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/34/Flag_of_Djibouti.svg/60px-Flag_of_Djibouti.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Djibouti\" title=\"Djibouti\">Djibouti</a></span></td>\n<td>1,137,096</td>\n<td>1,152,944</td>\n<td><span style=\"display:none\" data-sort-value=\"7000138999999999999♠\"></span><span style=\"color:green\">+1.39%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Fiji\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Fiji.svg/40px-Flag_of_Fiji.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Fiji.svg/60px-Flag_of_Fiji.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Fiji\" title=\"Fiji\">Fiji</a></span></td>\n<td>919,422</td>\n<td>924,145</td>\n<td><span style=\"display:none\" data-sort-value=\"6999510000000000000♠\"></span><span style=\"color:green\">+0.51%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Melanesia\" title=\"Melanesia\">Melanesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Réunion\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Proposed_flag_of_R%C3%A9union_%28VAR%29.svg/40px-Proposed_flag_of_R%C3%A9union_%28VAR%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Proposed_flag_of_R%C3%A9union_%28VAR%29.svg/60px-Proposed_flag_of_R%C3%A9union_%28VAR%29.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/R%C3%A9union\" title=\"Réunion\">Réunion</a></span> (<a href=\"/wiki/French_Fifth_Republic\" title=\"French Fifth Republic\">France</a>)</td>\n<td>871,540</td>\n<td>874,883</td>\n<td><span style=\"display:none\" data-sort-value=\"6999380000000000000♠\"></span><span style=\"color:green\">+0.38%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Comoros\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/94/Flag_of_the_Comoros.svg/40px-Flag_of_the_Comoros.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/94/Flag_of_the_Comoros.svg/60px-Flag_of_the_Comoros.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Comoros\" title=\"Comoros\">Comoros</a></span></td>\n<td>834,188</td>\n<td>850,387</td>\n<td><span style=\"display:none\" data-sort-value=\"7000194000000000000♠\"></span><span style=\"color:green\">+1.94%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Guyana\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/99/Flag_of_Guyana.svg/40px-Flag_of_Guyana.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/99/Flag_of_Guyana.svg/60px-Flag_of_Guyana.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Guyana\" title=\"Guyana\">Guyana</a></span></td>\n<td>821,637</td>\n<td>826,353</td>\n<td><span style=\"display:none\" data-sort-value=\"6999570000000000000♠\"></span><span style=\"color:green\">+0.57%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Solomon Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/74/Flag_of_the_Solomon_Islands.svg/40px-Flag_of_the_Solomon_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/74/Flag_of_the_Solomon_Islands.svg/60px-Flag_of_the_Solomon_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Solomon_Islands\" title=\"Solomon Islands\">Solomon Islands</a></span></td>\n<td>781,066</td>\n<td>800,005</td>\n<td><span style=\"display:none\" data-sort-value=\"7000242000000000000♠\"></span><span style=\"color:green\">+2.42%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Melanesia\" title=\"Melanesia\">Melanesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bhutan\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/91/Flag_of_Bhutan.svg/40px-Flag_of_Bhutan.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/91/Flag_of_Bhutan.svg/60px-Flag_of_Bhutan.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Bhutan\" title=\"Bhutan\">Bhutan</a></span></td>\n<td>780,914</td>\n<td>786,385</td>\n<td><span style=\"display:none\" data-sort-value=\"6999700000000000000♠\"></span><span style=\"color:green\">+0.70%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Macau\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/63/Flag_of_Macau.svg/40px-Flag_of_Macau.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/63/Flag_of_Macau.svg/60px-Flag_of_Macau.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Macau\" title=\"Macau\">Macao</a></span> (<a href=\"/wiki/China\" title=\"China\">China</a>)<sup id=\"cite_ref-22\" class=\"reference\"><a href=\"#cite_note-22\"><span class=\"cite-bracket\">&#91;</span>t<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>704,356</td>\n<td>713,912</td>\n<td><span style=\"display:none\" data-sort-value=\"7000136000000000000♠\"></span><span style=\"color:green\">+1.36%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/East_Asia\" title=\"East Asia\">Eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Luxembourg\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/da/Flag_of_Luxembourg.svg/40px-Flag_of_Luxembourg.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/da/Flag_of_Luxembourg.svg/60px-Flag_of_Luxembourg.svg.png 2x\" data-file-width=\"512\" data-file-height=\"307\" /></span></span>&#160;</span><a href=\"/wiki/Luxembourg\" title=\"Luxembourg\">Luxembourg</a></span></td>\n<td>653,313</td>\n<td>665,098</td>\n<td><span style=\"display:none\" data-sort-value=\"7000180000000000000♠\"></span><span style=\"color:green\">+1.80%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Montenegro\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/64/Flag_of_Montenegro.svg/40px-Flag_of_Montenegro.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/64/Flag_of_Montenegro.svg/60px-Flag_of_Montenegro.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Montenegro\" title=\"Montenegro\">Montenegro</a></span></td>\n<td>614,648</td>\n<td>633,552</td>\n<td><span style=\"display:none\" data-sort-value=\"7000308000000000000♠\"></span><span style=\"color:green\">+3.08%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Suriname\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/60/Flag_of_Suriname.svg/40px-Flag_of_Suriname.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/60/Flag_of_Suriname.svg/60px-Flag_of_Suriname.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Suriname\" title=\"Suriname\">Suriname</a></span></td>\n<td>623,164</td>\n<td>628,886</td>\n<td><span style=\"display:none\" data-sort-value=\"6999920000000000000♠\"></span><span style=\"color:green\">+0.92%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Flag_of_None.svg/40px-Flag_of_None.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Flag_of_None.svg/60px-Flag_of_None.svg.png 2x\" data-file-width=\"225\" data-file-height=\"150\" /></span></span></span> <a href=\"/wiki/Western_Sahara\" title=\"Western Sahara\">Western Sahara</a> <i>(<a href=\"/wiki/Western_Sahara_conflict\" title=\"Western Sahara conflict\">disputed</a>)</i></td>\n<td>568,739</td>\n<td>579,729</td>\n<td><span style=\"display:none\" data-sort-value=\"7000193000000000000♠\"></span><span style=\"color:green\">+1.93%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/North_Africa\" title=\"North Africa\">Northern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Malta\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Malta.svg/40px-Flag_of_Malta.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Malta.svg/60px-Flag_of_Malta.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Malta\" title=\"Malta\">Malta</a></span></td>\n<td>528,192</td>\n<td>532,956</td>\n<td><span style=\"display:none\" data-sort-value=\"6999900000000000000♠\"></span><span style=\"color:green\">+0.90%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Maldives\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_Maldives.svg/40px-Flag_of_Maldives.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_Maldives.svg/60px-Flag_of_Maldives.svg.png 2x\" data-file-width=\"720\" data-file-height=\"480\" /></span></span>&#160;</span><a href=\"/wiki/Maldives\" title=\"Maldives\">Maldives</a></span></td>\n<td>524,106</td>\n<td>525,994</td>\n<td><span style=\"display:none\" data-sort-value=\"6999360000000000000♠\"></span><span style=\"color:green\">+0.36%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/South_Asia\" title=\"South Asia\">Southern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cape Verde\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Cape_Verde.svg/40px-Flag_of_Cape_Verde.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Cape_Verde.svg/60px-Flag_of_Cape_Verde.svg.png 2x\" data-file-width=\"1020\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cape_Verde\" title=\"Cape Verde\">Cape Verde</a></span></td>\n<td>519,741</td>\n<td>522,331</td>\n<td><span style=\"display:none\" data-sort-value=\"6999500000000000000♠\"></span><span style=\"color:green\">+0.50%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Brunei\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Flag_of_Brunei.svg/40px-Flag_of_Brunei.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Flag_of_Brunei.svg/60px-Flag_of_Brunei.svg.png 2x\" data-file-width=\"1440\" data-file-height=\"720\" /></span></span>&#160;</span><a href=\"/wiki/Brunei\" title=\"Brunei\">Brunei</a></span></td>\n<td>455,370</td>\n<td>458,949</td>\n<td><span style=\"display:none\" data-sort-value=\"6999790000000000000♠\"></span><span style=\"color:green\">+0.79%</span></td>\n<td><a href=\"/wiki/Asia\" title=\"Asia\">Asia</a></td>\n<td><a href=\"/wiki/Southeast_Asia\" title=\"Southeast Asia\">South-eastern Asia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Belize\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Flag_of_Belize.svg/40px-Flag_of_Belize.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Flag_of_Belize.svg/60px-Flag_of_Belize.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Belize\" title=\"Belize\">Belize</a></span></td>\n<td>402,733</td>\n<td>411,106</td>\n<td><span style=\"display:none\" data-sort-value=\"7000208000000000000♠\"></span><span style=\"color:green\">+2.08%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Central_America\" title=\"Central America\">Central America</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Bahamas\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/93/Flag_of_the_Bahamas.svg/40px-Flag_of_the_Bahamas.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/93/Flag_of_the_Bahamas.svg/60px-Flag_of_the_Bahamas.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/The_Bahamas\" title=\"The Bahamas\">Bahamas</a></span></td>\n<td>397,538</td>\n<td>399,440</td>\n<td><span style=\"display:none\" data-sort-value=\"6999480000000000000♠\"></span><span style=\"color:green\">+0.48%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Iceland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Flag_of_Iceland.svg/40px-Flag_of_Iceland.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Flag_of_Iceland.svg/60px-Flag_of_Iceland.svg.png 2x\" data-file-width=\"1250\" data-file-height=\"900\" /></span></span>&#160;</span><a href=\"/wiki/Iceland\" title=\"Iceland\">Iceland</a></span></td>\n<td>380,356</td>\n<td>387,558</td>\n<td><span style=\"display:none\" data-sort-value=\"7000189000000000000♠\"></span><span style=\"color:green\">+1.89%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Guadeloupe\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Flag_of_Guadeloupe_%28local%29_variant.svg/40px-Flag_of_Guadeloupe_%28local%29_variant.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Flag_of_Guadeloupe_%28local%29_variant.svg/60px-Flag_of_Guadeloupe_%28local%29_variant.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Guadeloupe\" title=\"Guadeloupe\">Guadeloupe</a></span> (<a href=\"/wiki/French_Fifth_Republic\" title=\"French Fifth Republic\">France</a>)</td>\n<td>384,697</td>\n<td>376,517</td>\n<td><span style=\"display:none\" data-sort-value=\"2999787000000000000♠\"></span><span style=\"color:red\">−2.13%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Martinique\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag-of-Martinique.svg/40px-Flag-of-Martinique.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/27/Flag-of-Martinique.svg/60px-Flag-of-Martinique.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Martinique\" title=\"Martinique\">Martinique</a></span> (<a href=\"/wiki/French_Fifth_Republic\" title=\"French Fifth Republic\">France</a>)</td>\n<td>349,459</td>\n<td>346,002</td>\n<td><span style=\"display:none\" data-sort-value=\"3000010000000000000♠\"></span><span style=\"color:red\">−0.99%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Vanuatu\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Vanuatu.svg/40px-Flag_of_Vanuatu.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Vanuatu.svg/60px-Flag_of_Vanuatu.svg.png 2x\" data-file-width=\"600\" data-file-height=\"360\" /></span></span>&#160;</span><a href=\"/wiki/Vanuatu\" title=\"Vanuatu\">Vanuatu</a></span></td>\n<td>313,046</td>\n<td>320,409</td>\n<td><span style=\"display:none\" data-sort-value=\"7000235000000000000♠\"></span><span style=\"color:green\">+2.35%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Melanesia\" title=\"Melanesia\">Melanesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Mayotte\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Flag_of_Mayotte_%28Local%29.svg/40px-Flag_of_Mayotte_%28Local%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Flag_of_Mayotte_%28Local%29.svg/60px-Flag_of_Mayotte_%28Local%29.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Mayotte\" title=\"Mayotte\">Mayotte</a></span> (<a href=\"/wiki/French_Fifth_Republic\" title=\"French Fifth Republic\">France</a>)</td>\n<td>305,269</td>\n<td>316,015</td>\n<td><span style=\"display:none\" data-sort-value=\"7000352000000000000♠\"></span><span style=\"color:green\">+3.52%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"French Guiana\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_French_Guiana.svg/40px-Flag_of_French_Guiana.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_French_Guiana.svg/60px-Flag_of_French_Guiana.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/French_Guiana\" title=\"French Guiana\">French Guiana</a></span> (<a href=\"/wiki/French_Fifth_Republic\" title=\"French Fifth Republic\">France</a>)</td>\n<td>298,306</td>\n<td>303,402</td>\n<td><span style=\"display:none\" data-sort-value=\"7000171000000000000♠\"></span><span style=\"color:green\">+1.71%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"New Caledonia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/66/Flag_of_FLNKS.svg/40px-Flag_of_FLNKS.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/66/Flag_of_FLNKS.svg/60px-Flag_of_FLNKS.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/New_Caledonia\" title=\"New Caledonia\">New Caledonia</a></span> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>287,123</td>\n<td>289,870</td>\n<td><span style=\"display:none\" data-sort-value=\"6999960000000000000♠\"></span><span style=\"color:green\">+0.96%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Melanesia\" title=\"Melanesia\">Melanesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Barbados\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Flag_of_Barbados.svg/40px-Flag_of_Barbados.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Flag_of_Barbados.svg/60px-Flag_of_Barbados.svg.png 2x\" data-file-width=\"1500\" data-file-height=\"1000\" /></span></span>&#160;</span><a href=\"/wiki/Barbados\" title=\"Barbados\">Barbados</a></span></td>\n<td>288,318</td>\n<td>282,336</td>\n<td><span style=\"display:none\" data-sort-value=\"2999793000000000000♠\"></span><span style=\"color:red\">−2.07%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"French Polynesia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/db/Flag_of_French_Polynesia.svg/40px-Flag_of_French_Polynesia.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/db/Flag_of_French_Polynesia.svg/60px-Flag_of_French_Polynesia.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/French_Polynesia\" title=\"French Polynesia\">French Polynesia</a></span> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>280,378</td>\n<td>281,118</td>\n<td><span style=\"display:none\" data-sort-value=\"6999260000000000000♠\"></span><span style=\"color:green\">+0.26%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"São Tomé and Príncipe\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe.svg/40px-Flag_of_S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flag_of_S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe.svg/60px-Flag_of_S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe\" title=\"São Tomé and Príncipe\">São Tomé and Príncipe</a></span></td>\n<td>226,305</td>\n<td>230,871</td>\n<td><span style=\"display:none\" data-sort-value=\"7000202000000000000♠\"></span><span style=\"color:green\">+2.02%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/Central_Africa\" title=\"Central Africa\">Middle Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Samoa\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Samoa.svg/40px-Flag_of_Samoa.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/31/Flag_of_Samoa.svg/60px-Flag_of_Samoa.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Samoa\" title=\"Samoa\">Samoa</a></span></td>\n<td>215,261</td>\n<td>216,663</td>\n<td><span style=\"display:none\" data-sort-value=\"6999650000000000000♠\"></span><span style=\"color:green\">+0.65%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Curaçao\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Flag_of_Cura%C3%A7ao.svg/40px-Flag_of_Cura%C3%A7ao.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Flag_of_Cura%C3%A7ao.svg/60px-Flag_of_Cura%C3%A7ao.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cura%C3%A7ao\" title=\"Curaçao\">Curaçao</a></span> (<a href=\"/wiki/Kingdom_of_the_Netherlands\" title=\"Kingdom of the Netherlands\">Netherlands</a>)</td>\n<td>185,345</td>\n<td>185,427</td>\n<td><span style=\"display:none\" data-sort-value=\"6998400000000000000♠\"></span><span style=\"color:green\">+0.04%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Saint Lucia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_Saint_Lucia.svg/40px-Flag_of_Saint_Lucia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Flag_of_Saint_Lucia.svg/60px-Flag_of_Saint_Lucia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Lucia\" title=\"Saint Lucia\">Saint Lucia</a></span></td>\n<td>178,781</td>\n<td>179,285</td>\n<td><span style=\"display:none\" data-sort-value=\"6999280000000000000♠\"></span><span style=\"color:green\">+0.28%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Guam\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/07/Flag_of_Guam.svg/40px-Flag_of_Guam.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/07/Flag_of_Guam.svg/60px-Flag_of_Guam.svg.png 2x\" data-file-width=\"738\" data-file-height=\"396\" /></span></span>&#160;</span><a href=\"/wiki/Guam\" title=\"Guam\">Guam</a></span> (<a href=\"/wiki/United_States\" title=\"United States\">United States</a>)</td>\n<td>165,180</td>\n<td>166,506</td>\n<td><span style=\"display:none\" data-sort-value=\"6999800000000000000♠\"></span><span style=\"color:green\">+0.80%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Kiribati\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Kiribati.svg/40px-Flag_of_Kiribati.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Kiribati.svg/60px-Flag_of_Kiribati.svg.png 2x\" data-file-width=\"512\" data-file-height=\"256\" /></span></span>&#160;</span><a href=\"/wiki/Kiribati\" title=\"Kiribati\">Kiribati</a></span></td>\n<td>130,469</td>\n<td>132,530</td>\n<td><span style=\"display:none\" data-sort-value=\"7000158000000000000♠\"></span><span style=\"color:green\">+1.58%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Seychelles\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Seychelles.svg/40px-Flag_of_Seychelles.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Seychelles.svg/60px-Flag_of_Seychelles.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Seychelles\" title=\"Seychelles\">Seychelles</a></span></td>\n<td>125,522</td>\n<td>127,951</td>\n<td><span style=\"display:none\" data-sort-value=\"7000194000000000000♠\"></span><span style=\"color:green\">+1.94%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/East_Africa\" title=\"East Africa\">Eastern Africa</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Grenada\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Grenada.svg/40px-Flag_of_Grenada.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Grenada.svg/60px-Flag_of_Grenada.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Grenada\" title=\"Grenada\">Grenada</a></span></td>\n<td>116,913</td>\n<td>117,081</td>\n<td><span style=\"display:none\" data-sort-value=\"6999140000000000000♠\"></span><span style=\"color:green\">+0.14%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Micronesia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Flag_of_the_Federated_States_of_Micronesia.svg/40px-Flag_of_the_Federated_States_of_Micronesia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Flag_of_the_Federated_States_of_Micronesia.svg/60px-Flag_of_the_Federated_States_of_Micronesia.svg.png 2x\" data-file-width=\"760\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Federated_States_of_Micronesia\" title=\"Federated States of Micronesia\">Micronesia</a></span></td>\n<td>112,114</td>\n<td>112,630</td>\n<td><span style=\"display:none\" data-sort-value=\"6999460000000000000♠\"></span><span style=\"color:green\">+0.46%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Tonga\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Flag_of_Tonga.svg/40px-Flag_of_Tonga.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Flag_of_Tonga.svg/60px-Flag_of_Tonga.svg.png 2x\" data-file-width=\"960\" data-file-height=\"480\" /></span></span>&#160;</span><a href=\"/wiki/Tonga\" title=\"Tonga\">Tonga</a></span></td>\n<td>105,042</td>\n<td>104,597</td>\n<td><span style=\"display:none\" data-sort-value=\"3000580000000000000♠\"></span><span style=\"color:red\">−0.42%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Aruba\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Flag_of_Aruba.svg/40px-Flag_of_Aruba.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Flag_of_Aruba.svg/60px-Flag_of_Aruba.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Aruba\" title=\"Aruba\">Aruba</a></span> (<a href=\"/wiki/Kingdom_of_the_Netherlands\" title=\"Kingdom of the Netherlands\">Netherlands</a>)</td>\n<td>107,782</td>\n<td>107,939</td>\n<td><span style=\"display:none\" data-sort-value=\"6999150000000000000♠\"></span><span style=\"color:green\">+0.15%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Jersey\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1c/Flag_of_Jersey.svg/40px-Flag_of_Jersey.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1c/Flag_of_Jersey.svg/60px-Flag_of_Jersey.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Jersey\" title=\"Jersey\">Jersey</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>103,490</td>\n<td>103,674</td>\n<td><span style=\"display:none\" data-sort-value=\"6999180000000000000♠\"></span><span style=\"color:green\">+0.18%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Saint Vincent and the Grenadines\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Flag_of_Saint_Vincent_and_the_Grenadines.svg/40px-Flag_of_Saint_Vincent_and_the_Grenadines.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Flag_of_Saint_Vincent_and_the_Grenadines.svg/60px-Flag_of_Saint_Vincent_and_the_Grenadines.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Vincent_and_the_Grenadines\" title=\"Saint Vincent and the Grenadines\">Saint Vincent and the Grenadines</a></span></td>\n<td>102,046</td>\n<td>101,323</td>\n<td><span style=\"display:none\" data-sort-value=\"3000290000000000000♠\"></span><span style=\"color:red\">−0.71%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Antigua and Barbuda\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Antigua_and_Barbuda.svg/40px-Flag_of_Antigua_and_Barbuda.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Antigua_and_Barbuda.svg/60px-Flag_of_Antigua_and_Barbuda.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Antigua_and_Barbuda\" title=\"Antigua and Barbuda\">Antigua and Barbuda</a></span></td>\n<td>92,840</td>\n<td>93,316</td>\n<td><span style=\"display:none\" data-sort-value=\"6999510000000000000♠\"></span><span style=\"color:green\">+0.51%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"U.S. Virgin Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Flag_of_the_United_States_Virgin_Islands.svg/40px-Flag_of_the_United_States_Virgin_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Flag_of_the_United_States_Virgin_Islands.svg/60px-Flag_of_the_United_States_Virgin_Islands.svg.png 2x\" data-file-width=\"1275\" data-file-height=\"850\" /></span></span>&#160;</span><a href=\"/wiki/United_States_Virgin_Islands\" title=\"United States Virgin Islands\">U.S. Virgin Islands</a></span> (<a href=\"/wiki/United_States\" title=\"United States\">United States</a>)</td>\n<td>86,507</td>\n<td>85,701</td>\n<td><span style=\"display:none\" data-sort-value=\"3000069999999999999♠\"></span><span style=\"color:red\">−0.93%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Isle of Man\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_the_Isle_of_Man.svg/40px-Flag_of_the_Isle_of_Man.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_the_Isle_of_Man.svg/60px-Flag_of_the_Isle_of_Man.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Isle_of_Man\" title=\"Isle of Man\">Isle of Man</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>84,132</td>\n<td>84,165</td>\n<td><span style=\"display:none\" data-sort-value=\"6998400000000000000♠\"></span><span style=\"color:green\">+0.04%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Andorra\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Andorra.svg/40px-Flag_of_Andorra.svg.png\" decoding=\"async\" width=\"22\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Andorra.svg/60px-Flag_of_Andorra.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"700\" /></span></span>&#160;</span><a href=\"/wiki/Andorra\" title=\"Andorra\">Andorra</a></span></td>\n<td>79,705</td>\n<td>80,856</td>\n<td><span style=\"display:none\" data-sort-value=\"7000144000000000000♠\"></span><span style=\"color:green\">+1.44%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Cayman Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_the_Cayman_Islands.svg/40px-Flag_of_the_Cayman_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_the_Cayman_Islands.svg/60px-Flag_of_the_Cayman_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cayman_Islands\" title=\"Cayman Islands\">Cayman Islands</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>71,591</td>\n<td>73,038</td>\n<td><span style=\"display:none\" data-sort-value=\"7000202000000000000♠\"></span><span style=\"color:green\">+2.02%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Dominica\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Flag_of_Dominica.svg/40px-Flag_of_Dominica.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Flag_of_Dominica.svg/60px-Flag_of_Dominica.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Dominica\" title=\"Dominica\">Dominica</a></span></td>\n<td>66,826</td>\n<td>66,510</td>\n<td><span style=\"display:none\" data-sort-value=\"3000530000000000000♠\"></span><span style=\"color:red\">−0.47%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Bermuda\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Flag_of_Bermuda.svg/40px-Flag_of_Bermuda.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Flag_of_Bermuda.svg/60px-Flag_of_Bermuda.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Bermuda\" title=\"Bermuda\">Bermuda</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>64,749</td>\n<td>64,698</td>\n<td><span style=\"display:none\" data-sort-value=\"3001200000000000000♠\"></span><span style=\"color:red\">−0.08%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Northern_America\" title=\"Northern America\">Northern America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span class=\"flagicon nowrap\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_Guernsey.svg/40px-Flag_of_Guernsey.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flag_of_Guernsey.svg/60px-Flag_of_Guernsey.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span> </span><a href=\"/wiki/Bailiwick_of_Guernsey\" title=\"Bailiwick of Guernsey\">Guernsey</a> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>63,725</td>\n<td>64,017</td>\n<td><span style=\"display:none\" data-sort-value=\"6999460000000000000♠\"></span><span style=\"color:green\">+0.46%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Greenland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/09/Flag_of_Greenland.svg/40px-Flag_of_Greenland.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/09/Flag_of_Greenland.svg/60px-Flag_of_Greenland.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Greenland\" title=\"Greenland\">Greenland</a></span> (<a href=\"/wiki/Danish_Realm\" title=\"Danish Realm\">Denmark</a>)</td>\n<td>54,990</td>\n<td>55,922</td>\n<td><span style=\"display:none\" data-sort-value=\"7000169000000000000♠\"></span><span style=\"color:green\">+1.69%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Northern_America\" title=\"Northern America\">Northern America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Faroe Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Flag_of_the_Faroe_Islands.svg/40px-Flag_of_the_Faroe_Islands.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Flag_of_the_Faroe_Islands.svg/60px-Flag_of_the_Faroe_Islands.svg.png 2x\" data-file-width=\"1100\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Faroe_Islands\" title=\"Faroe Islands\">Faroe Islands</a></span> (<a href=\"/wiki/Danish_Realm\" title=\"Danish Realm\">Denmark</a>)</td>\n<td>54,011</td>\n<td>54,714</td>\n<td><span style=\"display:none\" data-sort-value=\"7000130000000000000♠\"></span><span style=\"color:green\">+1.30%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Northern_Europe\" title=\"Northern Europe\">Northern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"American Samoa\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/87/Flag_of_American_Samoa.svg/40px-Flag_of_American_Samoa.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/87/Flag_of_American_Samoa.svg/60px-Flag_of_American_Samoa.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/American_Samoa\" title=\"American Samoa\">American Samoa</a></span> (<a href=\"/wiki/United_States\" title=\"United States\">United States</a>)</td>\n<td>48,342</td>\n<td>47,521</td>\n<td><span style=\"display:none\" data-sort-value=\"2999830000000000000♠\"></span><span style=\"color:red\">−1.70%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Saint Kitts and Nevis\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Saint_Kitts_and_Nevis.svg/40px-Flag_of_Saint_Kitts_and_Nevis.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fe/Flag_of_Saint_Kitts_and_Nevis.svg/60px-Flag_of_Saint_Kitts_and_Nevis.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Kitts_and_Nevis\" title=\"Saint Kitts and Nevis\">Saint Kitts and Nevis</a></span></td>\n<td>46,709</td>\n<td>46,758</td>\n<td><span style=\"display:none\" data-sort-value=\"6999100000000000000♠\"></span><span style=\"color:green\">+0.10%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Turks and Caicos Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Flag_of_the_Turks_and_Caicos_Islands.svg/40px-Flag_of_the_Turks_and_Caicos_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Flag_of_the_Turks_and_Caicos_Islands.svg/60px-Flag_of_the_Turks_and_Caicos_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Turks_and_Caicos_Islands\" title=\"Turks and Caicos Islands\">Turks and Caicos Islands</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>45,847</td>\n<td>46,198</td>\n<td><span style=\"display:none\" data-sort-value=\"6999770000000000000♠\"></span><span style=\"color:green\">+0.77%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Northern Mariana Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Flag_of_the_Northern_Mariana_Islands.svg/40px-Flag_of_the_Northern_Mariana_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Flag_of_the_Northern_Mariana_Islands.svg/60px-Flag_of_the_Northern_Mariana_Islands.svg.png 2x\" data-file-width=\"1100\" data-file-height=\"550\" /></span></span>&#160;</span><a href=\"/wiki/Northern_Mariana_Islands\" title=\"Northern Mariana Islands\">Northern Mariana Islands</a></span> (<a href=\"/wiki/United_States\" title=\"United States\">United States</a>)</td>\n<td>46,078</td>\n<td>45,143</td>\n<td><span style=\"display:none\" data-sort-value=\"2999797000000000000♠\"></span><span style=\"color:red\">−2.03%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Sint Maarten\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Sint_Maarten.svg/40px-Flag_of_Sint_Maarten.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Sint_Maarten.svg/60px-Flag_of_Sint_Maarten.svg.png 2x\" data-file-width=\"675\" data-file-height=\"450\" /></span></span>&#160;</span><a href=\"/wiki/Sint_Maarten\" title=\"Sint Maarten\">Sint Maarten</a></span> (<a href=\"/wiki/Kingdom_of_the_Netherlands\" title=\"Kingdom of the Netherlands\">Netherlands</a>)</td>\n<td>42,139</td>\n<td>42,749</td>\n<td><span style=\"display:none\" data-sort-value=\"7000145000000000000♠\"></span><span style=\"color:green\">+1.45%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Liechtenstein\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/47/Flag_of_Liechtenstein.svg/40px-Flag_of_Liechtenstein.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/47/Flag_of_Liechtenstein.svg/60px-Flag_of_Liechtenstein.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Liechtenstein\" title=\"Liechtenstein\">Liechtenstein</a></span></td>\n<td>39,317</td>\n<td>39,598</td>\n<td><span style=\"display:none\" data-sort-value=\"6999710000000000000♠\"></span><span style=\"color:green\">+0.71%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"British Virgin Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/42/Flag_of_the_British_Virgin_Islands.svg/40px-Flag_of_the_British_Virgin_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/42/Flag_of_the_British_Virgin_Islands.svg/60px-Flag_of_the_British_Virgin_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/British_Virgin_Islands\" title=\"British Virgin Islands\">British Virgin Islands</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>38,319</td>\n<td>38,985</td>\n<td><span style=\"display:none\" data-sort-value=\"7000174000000000000♠\"></span><span style=\"color:green\">+1.74%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Monaco\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Flag_of_Monaco.svg/20px-Flag_of_Monaco.svg.png\" decoding=\"async\" width=\"19\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Flag_of_Monaco.svg/40px-Flag_of_Monaco.svg.png 1.5x\" data-file-width=\"1000\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Monaco\" title=\"Monaco\">Monaco</a></span></td>\n<td>38,931</td>\n<td>38,956</td>\n<td><span style=\"display:none\" data-sort-value=\"6998600000000000000♠\"></span><span style=\"color:green\">+0.06%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Western_Europe\" title=\"Western Europe\">Western Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Gibraltar\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/02/Flag_of_Gibraltar.svg/40px-Flag_of_Gibraltar.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/02/Flag_of_Gibraltar.svg/60px-Flag_of_Gibraltar.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Gibraltar\" title=\"Gibraltar\">Gibraltar</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>37,609</td>\n<td>38,471</td>\n<td><span style=\"display:none\" data-sort-value=\"7000229000000000000♠\"></span><span style=\"color:green\">+2.29%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Marshall Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Flag_of_the_Marshall_Islands.svg/40px-Flag_of_the_Marshall_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Flag_of_the_Marshall_Islands.svg/60px-Flag_of_the_Marshall_Islands.svg.png 2x\" data-file-width=\"1140\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Marshall_Islands\" title=\"Marshall Islands\">Marshall Islands</a></span></td>\n<td>40,077</td>\n<td>38,827</td>\n<td><span style=\"display:none\" data-sort-value=\"2999688000000000000♠\"></span><span style=\"color:red\">−3.12%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"San Marino\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Flag_of_San_Marino.svg/20px-Flag_of_San_Marino.svg.png\" decoding=\"async\" width=\"20\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Flag_of_San_Marino.svg/40px-Flag_of_San_Marino.svg.png 1.5x\" data-file-width=\"800\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/San_Marino\" title=\"San Marino\">San Marino</a></span></td>\n<td>34,090</td>\n<td>33,733</td>\n<td><span style=\"display:none\" data-sort-value=\"2999894999999999999♠\"></span><span style=\"color:red\">−1.05%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Caribbean Netherlands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/40px-Flag_of_the_Netherlands.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/60px-Flag_of_the_Netherlands.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Caribbean_Netherlands\" title=\"Caribbean Netherlands\">Caribbean Netherlands</a></span> (<a href=\"/wiki/Netherlands\" title=\"Netherlands\">Netherlands</a>)<sup id=\"cite_ref-23\" class=\"reference\"><a href=\"#cite_note-23\"><span class=\"cite-bracket\">&#91;</span>u<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>28,627</td>\n<td>29,898</td>\n<td><span style=\"display:none\" data-sort-value=\"7000444000000000000♠\"></span><span style=\"color:green\">+4.44%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Local_flag_of_the_Collectivity_of_Saint_Martin.svg/40px-Local_flag_of_the_Collectivity_of_Saint_Martin.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Local_flag_of_the_Collectivity_of_Saint_Martin.svg/60px-Local_flag_of_the_Collectivity_of_Saint_Martin.svg.png 2x\" data-file-width=\"324\" data-file-height=\"216\" /></span></span></span> <a href=\"/wiki/Collectivity_of_Saint_Martin\" title=\"Collectivity of Saint Martin\">Saint Martin</a> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>28,870</td>\n<td>27,515</td>\n<td><span style=\"display:none\" data-sort-value=\"2999530999999999999♠\"></span><span style=\"color:red\">−4.69%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Palau\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Palau.svg/40px-Flag_of_Palau.svg.png\" decoding=\"async\" width=\"23\" height=\"14\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Palau.svg/60px-Flag_of_Palau.svg.png 2x\" data-file-width=\"800\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Palau\" title=\"Palau\">Palau</a></span></td>\n<td>17,759</td>\n<td>17,727</td>\n<td><span style=\"display:none\" data-sort-value=\"3000820000000000000♠\"></span><span style=\"color:red\">−0.18%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Anguilla\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Anguilla.svg/40px-Flag_of_Anguilla.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Anguilla.svg/60px-Flag_of_Anguilla.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Anguilla\" title=\"Anguilla\">Anguilla</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>14,180</td>\n<td>14,410</td>\n<td><span style=\"display:none\" data-sort-value=\"7000162000000000000♠\"></span><span style=\"color:green\">+1.62%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Cook Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/35/Flag_of_the_Cook_Islands.svg/40px-Flag_of_the_Cook_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/35/Flag_of_the_Cook_Islands.svg/60px-Flag_of_the_Cook_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Cook_Islands\" title=\"Cook Islands\">Cook Islands</a></span> (<a href=\"/wiki/New_Zealand\" title=\"New Zealand\">New Zealand</a>)</td>\n<td>14,723</td>\n<td>14,222</td>\n<td><span style=\"display:none\" data-sort-value=\"2999660000000000000♠\"></span><span style=\"color:red\">−3.40%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Nauru\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/30/Flag_of_Nauru.svg/40px-Flag_of_Nauru.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/30/Flag_of_Nauru.svg/60px-Flag_of_Nauru.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Nauru\" title=\"Nauru\">Nauru</a></span></td>\n<td>11,801</td>\n<td>11,875</td>\n<td><span style=\"display:none\" data-sort-value=\"6999630000000000000♠\"></span><span style=\"color:green\">+0.63%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Micronesia\" title=\"Micronesia\">Micronesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Wallis and Futuna\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Flag_of_Wallis_and_Futuna.svg/40px-Flag_of_Wallis_and_Futuna.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Flag_of_Wallis_and_Futuna.svg/60px-Flag_of_Wallis_and_Futuna.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Wallis_and_Futuna\" title=\"Wallis and Futuna\">Wallis and Futuna</a></span> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>11,478</td>\n<td>11,370</td>\n<td><span style=\"display:none\" data-sort-value=\"3000060000000000000♠\"></span><span style=\"color:red\">−0.94%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Saint Barthélemy\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Flag_of_Saint_Barth%C3%A9lemy_%28Local%29.svg/40px-Flag_of_Saint_Barth%C3%A9lemy_%28Local%29.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Flag_of_Saint_Barth%C3%A9lemy_%28Local%29.svg/60px-Flag_of_Saint_Barth%C3%A9lemy_%28Local%29.svg.png 2x\" data-file-width=\"512\" data-file-height=\"341\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Barth%C3%A9lemy\" title=\"Saint Barthélemy\">Saint Barthélemy</a></span> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>10,920</td>\n<td>11,085</td>\n<td><span style=\"display:none\" data-sort-value=\"7000151000000000000♠\"></span><span style=\"color:green\">+1.51%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Tuvalu\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Tuvalu.svg/40px-Flag_of_Tuvalu.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/3/38/Flag_of_Tuvalu.svg/60px-Flag_of_Tuvalu.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Tuvalu\" title=\"Tuvalu\">Tuvalu</a></span></td>\n<td>9,992</td>\n<td>9,816</td>\n<td><span style=\"display:none\" data-sort-value=\"2999824000000000000♠\"></span><span style=\"color:red\">−1.76%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Saint Pierre and Miquelon\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/74/Flag_of_Saint-Pierre_and_Miquelon.svg/40px-Flag_of_Saint-Pierre_and_Miquelon.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/74/Flag_of_Saint-Pierre_and_Miquelon.svg/60px-Flag_of_Saint-Pierre_and_Miquelon.svg.png 2x\" data-file-width=\"450\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Pierre_and_Miquelon\" title=\"Saint Pierre and Miquelon\">Saint Pierre and Miquelon</a></span> (<a href=\"/wiki/France\" title=\"France\">France</a>)</td>\n<td>5,732</td>\n<td>5,681</td>\n<td><span style=\"display:none\" data-sort-value=\"3000109999999999999♠\"></span><span style=\"color:red\">−0.89%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Northern_America\" title=\"Northern America\">Northern America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Saint Helena, Ascension and Tristan da Cunha\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/en/thumb/a/ae/Flag_of_the_United_Kingdom.svg/40px-Flag_of_the_United_Kingdom.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/en/thumb/a/ae/Flag_of_the_United_Kingdom.svg/60px-Flag_of_the_United_Kingdom.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Helena,_Ascension_and_Tristan_da_Cunha\" title=\"Saint Helena, Ascension and Tristan da Cunha\">Saint Helena</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)<sup id=\"cite_ref-24\" class=\"reference\"><a href=\"#cite_note-24\"><span class=\"cite-bracket\">&#91;</span>v<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>5,343</td>\n<td>5,289</td>\n<td><span style=\"display:none\" data-sort-value=\"2999899000000000000♠\"></span><span style=\"color:red\">−1.01%</span></td>\n<td><a href=\"/wiki/Africa\" title=\"Africa\">Africa</a></td>\n<td><a href=\"/wiki/West_Africa\" title=\"West Africa\">Western Africa</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Montserrat\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Montserrat.svg/40px-Flag_of_Montserrat.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Flag_of_Montserrat.svg/60px-Flag_of_Montserrat.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Montserrat\" title=\"Montserrat\">Montserrat</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>4,453</td>\n<td>4,420</td>\n<td><span style=\"display:none\" data-sort-value=\"3000260000000000000♠\"></span><span style=\"color:red\">−0.74%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/Caribbean\" title=\"Caribbean\">Caribbean</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Falkland Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/83/Flag_of_the_Falkland_Islands.svg/40px-Flag_of_the_Falkland_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/83/Flag_of_the_Falkland_Islands.svg/60px-Flag_of_the_Falkland_Islands.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Falkland_Islands\" title=\"Falkland Islands\">Falkland Islands</a></span> (<a href=\"/wiki/United_Kingdom\" title=\"United Kingdom\">United Kingdom</a>)</td>\n<td>3,490</td>\n<td>3,477</td>\n<td><span style=\"display:none\" data-sort-value=\"3000630000000000000♠\"></span><span style=\"color:red\">−0.37%</span></td>\n<td><a href=\"/wiki/Americas\" title=\"Americas\">Americas</a></td>\n<td><a href=\"/wiki/South_America\" title=\"South America\">South America</a>\n</td></tr>\n<tr class=\"static-row-numbers-norank\">\n<td><span data-sort-value=\"Tokelau\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Flag_of_Tokelau.svg/40px-Flag_of_Tokelau.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Flag_of_Tokelau.svg/60px-Flag_of_Tokelau.svg.png 2x\" data-file-width=\"1800\" data-file-height=\"900\" /></span></span>&#160;</span><a href=\"/wiki/Tokelau\" title=\"Tokelau\">Tokelau</a></span> (<a href=\"/wiki/New_Zealand\" title=\"New Zealand\">New Zealand</a>)</td>\n<td>2,290</td>\n<td>2,397</td>\n<td><span style=\"display:none\" data-sort-value=\"7000467000000000000♠\"></span><span style=\"color:green\">+4.67%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Niue\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Niue.svg/40px-Flag_of_Niue.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/01/Flag_of_Niue.svg/60px-Flag_of_Niue.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Niue\" title=\"Niue\">Niue</a></span> (<a href=\"/wiki/New_Zealand\" title=\"New Zealand\">New Zealand</a>)</td>\n<td>1,821</td>\n<td>1,817</td>\n<td><span style=\"display:none\" data-sort-value=\"3000780000000000000♠\"></span><span style=\"color:red\">−0.22%</span></td>\n<td><a href=\"/wiki/Oceania\" title=\"Oceania\">Oceania</a></td>\n<td><a href=\"/wiki/Polynesia\" title=\"Polynesia\">Polynesia</a>\n</td></tr>\n<tr>\n<td><span data-sort-value=\"Vatican City\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b3/Flag_of_Vatican_City_%282023%E2%80%93present%29.svg/20px-Flag_of_Vatican_City_%282023%E2%80%93present%29.svg.png\" decoding=\"async\" width=\"16\" height=\"16\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b3/Flag_of_Vatican_City_%282023%E2%80%93present%29.svg/40px-Flag_of_Vatican_City_%282023%E2%80%93present%29.svg.png 1.5x\" data-file-width=\"1000\" data-file-height=\"1000\" /></span></span>&#160;</span><a href=\"/wiki/Vatican_City\" title=\"Vatican City\">Vatican City</a></span><sup id=\"cite_ref-25\" class=\"reference\"><a href=\"#cite_note-25\"><span class=\"cite-bracket\">&#91;</span>w<span class=\"cite-bracket\">&#93;</span></a></sup></td>\n<td>505</td>\n<td>496</td>\n<td><span style=\"display:none\" data-sort-value=\"2999822000000000000♠\"></span><span style=\"color:red\">−1.78%</span></td>\n<td><a href=\"/wiki/Europe\" title=\"Europe\">Europe</a></td>\n<td><a href=\"/wiki/Southern_Europe\" title=\"Southern Europe\">Southern Europe</a>\n</td></tr></tbody></table>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"See_also\">See also</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=2\" title=\"Edit section: See also\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"World\">World</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=3\" title=\"Edit section: World\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/List_of_countries_and_dependencies_by_population\" title=\"List of countries and dependencies by population\">List of countries and dependencies by population</a></li>\n<li><a href=\"/wiki/List_of_countries_by_past_and_projected_future_population\" title=\"List of countries by past and projected future population\">List of countries by past and projected future population</a>\n<ul><li><a href=\"/wiki/List_of_countries_by_population_in_1000\" title=\"List of countries by population in 1000\">List of countries by population in 1000</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1500\" title=\"List of countries by population in 1500\">List of countries by population in 1500</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1600\" title=\"List of countries by population in 1600\">List of countries by population in 1600</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1700\" title=\"List of countries by population in 1700\">List of countries by population in 1700</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1800\" title=\"List of countries by population in 1800\">List of countries by population in 1800</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1900\" title=\"List of countries by population in 1900\">List of countries by population in 1900</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1939\" title=\"List of countries by population in 1939\">List of countries by population in 1939</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1989\" title=\"List of countries by population in 1989\">List of countries by population in 1989</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2000\" title=\"List of countries by population in 2000\">List of countries by population in 2000</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2005\" title=\"List of countries by population in 2005\">List of countries by population in 2005</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2010\" title=\"List of countries by population in 2010\">List of countries by population in 2010</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2015\" title=\"List of countries by population in 2015\">List of countries by population in 2015</a></li></ul></li>\n<li><a href=\"/wiki/World_population\" title=\"World population\">World population</a></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Continental\">Continental</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=4\" title=\"Edit section: Continental\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/Demographics_of_Antarctica\" title=\"Demographics of Antarctica\">Demographics of Antarctica</a></li>\n<li><a href=\"/wiki/List_of_African_countries_by_population\" title=\"List of African countries by population\">List of African countries by population</a></li>\n<li><a href=\"/wiki/List_of_Asian_countries_by_population\" title=\"List of Asian countries by population\">List of Asian countries by population</a></li>\n<li><a href=\"/wiki/List_of_European_countries_by_population\" title=\"List of European countries by population\">List of European countries by population</a>\n<ul><li><a href=\"/wiki/List_of_European_countries_by_population_growth_rate\" title=\"List of European countries by population growth rate\">List of European countries by population growth rate</a></li></ul></li>\n<li><a href=\"/wiki/List_of_North_American_countries_by_population\" title=\"List of North American countries by population\">List of North American countries by population</a></li>\n<li><a href=\"/wiki/List_of_Oceanian_countries_by_population\" title=\"List of Oceanian countries by population\">List of Oceanian countries by population</a></li>\n<li><a href=\"/wiki/List_of_South_American_countries_by_population\" title=\"List of South American countries by population\">List of South American countries by population</a></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Transcontinental\">Transcontinental</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=5\" title=\"Edit section: Transcontinental\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/List_of_Arab_League_countries_by_population\" title=\"List of Arab League countries by population\">List of Arab League countries by population</a></li>\n<li><a href=\"/wiki/List_of_countries_in_the_Americas_by_population\" title=\"List of countries in the Americas by population\">List of countries in the Americas by population</a>\n<ul><li><a href=\"/wiki/List_of_Latin_American_countries_by_population\" title=\"List of Latin American countries by population\">List of Latin American countries by population</a></li></ul></li>\n<li><a href=\"/wiki/List_of_Eurasian_countries_by_population\" title=\"List of Eurasian countries by population\">List of Eurasian countries by population</a></li>\n<li><a href=\"/wiki/List_of_Middle_Eastern_countries_by_population\" title=\"List of Middle Eastern countries by population\">List of Middle Eastern countries by population</a></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Subregional\">Subregional</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=6\" title=\"Edit section: Subregional\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/List_of_Caribbean_countries_by_population\" title=\"List of Caribbean countries by population\">List of Caribbean countries by population</a></li></ul>\n<div class=\"mw-heading mw-heading3\"><h3 id=\"Others\">Others</h3><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=7\" title=\"Edit section: Others\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a href=\"/wiki/List_of_European_Union_member_states_by_population\" title=\"List of European Union member states by population\">List of European Union member states by population</a></li>\n<li><a href=\"/wiki/List_of_member_states_of_the_Commonwealth_of_Nations_by_population\" title=\"List of member states of the Commonwealth of Nations by population\">List of member states of the Commonwealth of Nations by population</a></li>\n<li><a href=\"/wiki/List_of_population_concern_organizations\" title=\"List of population concern organizations\">List of population concern organizations</a></li></ul>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"Explanatory_notes\">Explanatory notes</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=8\" title=\"Edit section: Explanatory notes\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<style data-mw-deduplicate=\"TemplateStyles:r1239543626\">.mw-parser-output .reflist{margin-bottom:0.5em;list-style-type:decimal}@media screen{.mw-parser-output .reflist{font-size:90%}}.mw-parser-output .reflist .references{font-size:100%;margin-bottom:0;list-style-type:inherit}.mw-parser-output .reflist-columns-2{column-width:30em}.mw-parser-output .reflist-columns-3{column-width:25em}.mw-parser-output .reflist-columns{margin-top:0.3em}.mw-parser-output .reflist-columns ol{margin-top:0}.mw-parser-output .reflist-columns li{page-break-inside:avoid;break-inside:avoid-column}.mw-parser-output .reflist-upper-alpha{list-style-type:upper-alpha}.mw-parser-output .reflist-upper-roman{list-style-type:upper-roman}.mw-parser-output .reflist-lower-alpha{list-style-type:lower-alpha}.mw-parser-output .reflist-lower-greek{list-style-type:lower-greek}.mw-parser-output .reflist-lower-roman{list-style-type:lower-roman}</style><div class=\"reflist reflist-lower-alpha\">\n<div class=\"mw-references-wrap mw-references-columns\"><ol class=\"references\" data-mw-group=\"lower-alpha\">\n<li id=\"cite_note-3\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-3\">^</a></b></span> <span class=\"reference-text\">Refers to <a href=\"/wiki/Mainland_China\" title=\"Mainland China\">mainland China</a> only. The UN source document states: \"For statistical purposes, the data for China do not include <a href=\"/wiki/Special_administrative_regions_of_China\" title=\"Special administrative regions of China\">special administrative regions (SAR) of China</a> (<a href=\"/wiki/Hong_Kong\" title=\"Hong Kong\">Hong Kong</a> and <a href=\"/wiki/Macau\" title=\"Macau\">Macau</a>) and <a href=\"/wiki/Taiwan\" title=\"Taiwan\">Taiwan</a>.\"</span>\n</li>\n<li id=\"cite_note-4\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-4\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Zanzibar\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Zanzibar.svg/40px-Flag_of_Zanzibar.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Zanzibar.svg/60px-Flag_of_Zanzibar.svg.png 2x\" data-file-width=\"512\" data-file-height=\"341\" /></span></span>&#160;</span><a href=\"/wiki/Zanzibar\" title=\"Zanzibar\">Zanzibar</a></span></i>.</span>\n</li>\n<li id=\"cite_note-5\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-5\">^</a></b></span> <span class=\"reference-text\">Refers to <a href=\"/wiki/Metropolitan_France\" title=\"Metropolitan France\">Metropolitan France</a> only.</span>\n</li>\n<li id=\"cite_note-6\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-6\">^</a></b></span> <span class=\"reference-text\">Including the <i><span data-sort-value=\"Canary Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Flag_of_the_Canary_Islands.svg/40px-Flag_of_the_Canary_Islands.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Flag_of_the_Canary_Islands.svg/60px-Flag_of_the_Canary_Islands.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Canary_Islands\" title=\"Canary Islands\">Canary Islands</a></span></i>, <i><span data-sort-value=\"Ceuta\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Ceuta.svg/40px-Flag_of_Ceuta.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Flag_of_Ceuta.svg/60px-Flag_of_Ceuta.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Ceuta\" title=\"Ceuta\">Ceuta</a></span></i>, and <i><span data-sort-value=\"Melilla\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Flag_of_Melilla.svg/40px-Flag_of_Melilla.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Flag_of_Melilla.svg/60px-Flag_of_Melilla.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Melilla\" title=\"Melilla\">Melilla</a></span></i>.</span>\n</li>\n<li id=\"cite_note-7\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-7\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Crimea\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Flag_of_Crimea.svg/40px-Flag_of_Crimea.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Flag_of_Crimea.svg/60px-Flag_of_Crimea.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Autonomous_Republic_of_Crimea\" title=\"Autonomous Republic of Crimea\">Crimea</a></span></i>, the <span data-sort-value=\"Donetsk People&#39;s Republic\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Donetsk_People%27s_Republic.svg/40px-Flag_of_Donetsk_People%27s_Republic.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Donetsk_People%27s_Republic.svg/60px-Flag_of_Donetsk_People%27s_Republic.svg.png 2x\" data-file-width=\"512\" data-file-height=\"341\" /></span></span>&#160;</span><a href=\"/wiki/Donetsk_People%27s_Republic\" title=\"Donetsk People&#39;s Republic\">Donetsk People's Republic</a></span>, the <span data-sort-value=\"Luhansk People&#39;s Republic\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/04/Flag_of_the_Luhansk_People%27s_Republic.svg/40px-Flag_of_the_Luhansk_People%27s_Republic.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/04/Flag_of_the_Luhansk_People%27s_Republic.svg/60px-Flag_of_the_Luhansk_People%27s_Republic.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Luhansk_People%27s_Republic\" title=\"Luhansk People&#39;s Republic\">Luhansk People's Republic</a></span>, and <i><span data-sort-value=\"Sevastopol\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/14/Flag_of_Sevastopol.svg/40px-Flag_of_Sevastopol.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/14/Flag_of_Sevastopol.svg/60px-Flag_of_Sevastopol.svg.png 2x\" data-file-width=\"600\" data-file-height=\"400\" /></span></span>&#160;</span><a href=\"/wiki/Sevastopol\" title=\"Sevastopol\">Sevastopol</a></span></i>.</span>\n</li>\n<li id=\"cite_note-8\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-8\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Christmas Island\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/67/Flag_of_Christmas_Island.svg/40px-Flag_of_Christmas_Island.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/67/Flag_of_Christmas_Island.svg/60px-Flag_of_Christmas_Island.svg.png 2x\" data-file-width=\"512\" data-file-height=\"256\" /></span></span>&#160;</span><a href=\"/wiki/Christmas_Island\" title=\"Christmas Island\">Christmas Island</a></span></i>, the <i><span data-sort-value=\"Cocos (Keeling) Islands\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1d/No_image.svg/15px-No_image.svg.png\" decoding=\"async\" width=\"15\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1d/No_image.svg/23px-No_image.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/1/1d/No_image.svg/30px-No_image.svg.png 2x\" data-file-width=\"1\" data-file-height=\"1\" /></span></span>&#160;</span><a href=\"/wiki/Cocos_(Keeling)_Islands\" title=\"Cocos (Keeling) Islands\">Cocos (Keeling) Islands</a></span></i>, and <i><span data-sort-value=\"Norfolk Island\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Norfolk_Island.svg/40px-Flag_of_Norfolk_Island.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Flag_of_Norfolk_Island.svg/60px-Flag_of_Norfolk_Island.svg.png 2x\" data-file-width=\"920\" data-file-height=\"460\" /></span></span>&#160;</span><a href=\"/wiki/Norfolk_Island\" title=\"Norfolk Island\">Norfolk Island</a></span></i>.</span>\n</li>\n<li id=\"cite_note-9\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-9\">^</a></b></span> <span class=\"reference-text\">Listed as <i><a href=\"/wiki/Taiwan,_China\" title=\"Taiwan, China\">China, Taiwan Province of China</a></i>.</span>\n</li>\n<li id=\"cite_note-10\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-10\">^</a></b></span> <span class=\"reference-text\">Including <span data-sort-value=\"Somaliland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Flag_of_Somaliland.svg/40px-Flag_of_Somaliland.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Flag_of_Somaliland.svg/60px-Flag_of_Somaliland.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Somaliland\" title=\"Somaliland\">Somaliland</a></span>.</span>\n</li>\n<li id=\"cite_note-11\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-11\">^</a></b></span> <span class=\"reference-text\">Refers to the <a href=\"/wiki/Netherlands\" title=\"Netherlands\">European Netherlands</a> only.</span>\n</li>\n<li id=\"cite_note-12\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-12\">^</a></b></span> <span class=\"reference-text\">Including the <i><span data-sort-value=\"Azores\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Flag_of_the_Azores.svg/40px-Flag_of_the_Azores.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Flag_of_the_Azores.svg/60px-Flag_of_the_Azores.svg.png 2x\" data-file-width=\"844\" data-file-height=\"562\" /></span></span>&#160;</span><a href=\"/wiki/Azores\" title=\"Azores\">Azores</a></span></i> and <i><span data-sort-value=\"Madeira\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Flag_of_Madeira.svg/40px-Flag_of_Madeira.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Flag_of_Madeira.svg/60px-Flag_of_Madeira.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"800\" /></span></span>&#160;</span><a href=\"/wiki/Madeira\" title=\"Madeira\">Madeira</a></span></i>.</span>\n</li>\n<li id=\"cite_note-13\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-13\">^</a></b></span> <span class=\"reference-text\">Listed as <i><a href=\"/wiki/Special_administrative_regions_of_China\" title=\"Special administrative regions of China\">China, Hong Kong SAR</a></i>.</span>\n</li>\n<li id=\"cite_note-14\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-14\">^</a></b></span> <span class=\"reference-text\">Excluding <span data-sort-value=\"Kosovo\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Flag_of_Kosovo.svg/40px-Flag_of_Kosovo.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Flag_of_Kosovo.svg/60px-Flag_of_Kosovo.svg.png 2x\" data-file-width=\"840\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Kosovo\" title=\"Kosovo\">Kosovo</a></span>.</span>\n</li>\n<li id=\"cite_note-15\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-15\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Åland\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/52/Flag_of_%C3%85land.svg/40px-Flag_of_%C3%85land.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/52/Flag_of_%C3%85land.svg/60px-Flag_of_%C3%85land.svg.png 2x\" data-file-width=\"520\" data-file-height=\"340\" /></span></span>&#160;</span><a href=\"/wiki/%C3%85land\" title=\"Åland\">Åland</a></span></i>.</span>\n</li>\n<li id=\"cite_note-16\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-16\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Svalbard and Jan Mayen\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Norway.svg/40px-Flag_of_Norway.svg.png\" decoding=\"async\" width=\"21\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Norway.svg/60px-Flag_of_Norway.svg.png 2x\" data-file-width=\"512\" data-file-height=\"372\" /></span></span>&#160;</span><a href=\"/wiki/Svalbard_and_Jan_Mayen\" title=\"Svalbard and Jan Mayen\">Svalbard and Jan Mayen</a></span></i>.</span>\n</li>\n<li id=\"cite_note-17\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-17\">^</a></b></span> <span class=\"reference-text\">Including <i><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><a href=\"/wiki/Palestine\" title=\"Palestine\"><img alt=\"Palestine\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Palestine.svg/40px-Flag_of_Palestine.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Palestine.svg/60px-Flag_of_Palestine.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></a></span></span> <a href=\"/wiki/East_Jerusalem\" title=\"East Jerusalem\">East Jerusalem</a></i>.</span>\n</li>\n<li id=\"cite_note-18\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-18\">^</a></b></span> <span class=\"reference-text\">Including <span data-sort-value=\"Abkhazia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_the_Republic_of_Abkhazia.svg/40px-Flag_of_the_Republic_of_Abkhazia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Flag_of_the_Republic_of_Abkhazia.svg/60px-Flag_of_the_Republic_of_Abkhazia.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Abkhazia\" title=\"Abkhazia\">Abkhazia</a></span> and <span data-sort-value=\"South Ossetia\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/12/Flag_of_South_Ossetia.svg/40px-Flag_of_South_Ossetia.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/12/Flag_of_South_Ossetia.svg/60px-Flag_of_South_Ossetia.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/South_Ossetia\" title=\"South Ossetia\">South Ossetia</a></span>.</span>\n</li>\n<li id=\"cite_note-19\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-19\">^</a></b></span> <span class=\"reference-text\">Including <span data-sort-value=\"Transnistria\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Transnistria_%28state%29.svg/40px-Flag_of_Transnistria_%28state%29.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_Transnistria_%28state%29.svg/60px-Flag_of_Transnistria_%28state%29.svg.png 2x\" data-file-width=\"600\" data-file-height=\"300\" /></span></span>&#160;</span><a href=\"/wiki/Transnistria\" title=\"Transnistria\">Transnistria</a></span>.</span>\n</li>\n<li id=\"cite_note-20\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-20\">^</a></b></span> <span class=\"reference-text\">Listed as <i><a href=\"/wiki/Kosovo\" title=\"Kosovo\">Kosovo (under UNSC res. 1244)</a></i>.</span>\n</li>\n<li id=\"cite_note-21\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-21\">^</a></b></span> <span class=\"reference-text\">Including <span data-sort-value=\"Northern Cyprus\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Flag_of_the_Turkish_Republic_of_Northern_Cyprus.svg/40px-Flag_of_the_Turkish_Republic_of_Northern_Cyprus.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Flag_of_the_Turkish_Republic_of_Northern_Cyprus.svg/60px-Flag_of_the_Turkish_Republic_of_Northern_Cyprus.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Northern_Cyprus\" title=\"Northern Cyprus\">Northern Cyprus</a></span>.</span>\n</li>\n<li id=\"cite_note-22\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-22\">^</a></b></span> <span class=\"reference-text\">Listed as <i><a href=\"/wiki/Special_administrative_regions_of_China\" title=\"Special administrative regions of China\">China, Macao SAR</a></i>.</span>\n</li>\n<li id=\"cite_note-23\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-23\">^</a></b></span> <span class=\"reference-text\">Listed as <i><span data-sort-value=\"Bonaire\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Flag_of_Bonaire.svg/40px-Flag_of_Bonaire.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Flag_of_Bonaire.svg/60px-Flag_of_Bonaire.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Bonaire\" title=\"Bonaire\">Bonaire</a></span></i>, <i><span data-sort-value=\"Sint Eustatius\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/Flag_of_Sint_Eustatius.svg/40px-Flag_of_Sint_Eustatius.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/08/Flag_of_Sint_Eustatius.svg/60px-Flag_of_Sint_Eustatius.svg.png 2x\" data-file-width=\"900\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Sint_Eustatius\" title=\"Sint Eustatius\">Sint Eustatius</a></span></i> and <i><span data-sort-value=\"Saba\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Flag_of_Saba.svg/40px-Flag_of_Saba.svg.png\" decoding=\"async\" width=\"23\" height=\"15\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Flag_of_Saba.svg/60px-Flag_of_Saba.svg.png 2x\" data-file-width=\"750\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Saba_(island)\" title=\"Saba (island)\">Saba</a></span></i>.</span>\n</li>\n<li id=\"cite_note-24\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-24\">^</a></b></span> <span class=\"reference-text\">Including <i><span data-sort-value=\"Saint Helena\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Saint_Helena.svg/40px-Flag_of_Saint_Helena.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/0/00/Flag_of_Saint_Helena.svg/60px-Flag_of_Saint_Helena.svg.png 2x\" data-file-width=\"1200\" data-file-height=\"600\" /></span></span>&#160;</span><a href=\"/wiki/Saint_Helena\" title=\"Saint Helena\">Saint Helena</a></span></i>, <i><span data-sort-value=\"Ascension Island\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Ascension_Island.svg/40px-Flag_of_Ascension_Island.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Ascension_Island.svg/60px-Flag_of_Ascension_Island.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Ascension_Island\" title=\"Ascension Island\">Ascension</a></span></i>, and <i><span data-sort-value=\"Tristan da Cunha\"><span class=\"flagicon\"><span class=\"mw-image-border\" typeof=\"mw:File\"><span><img alt=\"\" src=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Tristan_da_Cunha.svg/40px-Flag_of_Tristan_da_Cunha.svg.png\" decoding=\"async\" width=\"23\" height=\"12\" class=\"mw-file-element\" srcset=\"//upload.wikimedia.org/wikipedia/commons/thumb/8/89/Flag_of_Tristan_da_Cunha.svg/60px-Flag_of_Tristan_da_Cunha.svg.png 2x\" data-file-width=\"1000\" data-file-height=\"500\" /></span></span>&#160;</span><a href=\"/wiki/Tristan_da_Cunha\" title=\"Tristan da Cunha\">Tristan da Cunha</a></span></i>.</span>\n</li>\n<li id=\"cite_note-25\"><span class=\"mw-cite-backlink\"><b><a href=\"#cite_ref-25\">^</a></b></span> <span class=\"reference-text\">Listed as <i><a href=\"/wiki/Holy_See\" title=\"Holy See\">Holy See</a></i>.</span>\n</li>\n</ol></div></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"References\">References</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=9\" title=\"Edit section: References\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1239543626\" /><div class=\"reflist\">\n<div class=\"mw-references-wrap\"><ol class=\"references\">\n<li id=\"cite_note-UNregions-1\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-UNregions_1-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-UNregions_1-1\"><sup><i><b>b</b></i></sup></a> <a href=\"#cite_ref-UNregions_1-2\"><sup><i><b>c</b></i></sup></a></span> <span class=\"reference-text\"><style data-mw-deduplicate=\"TemplateStyles:r1238218222\">.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}.mw-parser-output .citation q{quotes:\"\\\"\"\"\\\"\"\"'\"\"'\"}.mw-parser-output .citation:target{background-color:rgba(0,127,255,0.133)}.mw-parser-output .id-lock-free.id-lock-free a{background:url(\"//upload.wikimedia.org/wikipedia/commons/6/65/Lock-green.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-limited.id-lock-limited a,.mw-parser-output .id-lock-registration.id-lock-registration a{background:url(\"//upload.wikimedia.org/wikipedia/commons/d/d6/Lock-gray-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .id-lock-subscription.id-lock-subscription a{background:url(\"//upload.wikimedia.org/wikipedia/commons/a/aa/Lock-red-alt-2.svg\")right 0.1em center/9px no-repeat}.mw-parser-output .cs1-ws-icon a{background:url(\"//upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg\")right 0.1em center/12px no-repeat}body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-free a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-limited a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-registration a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .id-lock-subscription a,body:not(.skin-timeless):not(.skin-minerva) .mw-parser-output .cs1-ws-icon a{background-size:contain;padding:0 1em 0 0}.mw-parser-output .cs1-code{color:inherit;background:inherit;border:none;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;color:var(--color-error,#d33)}.mw-parser-output .cs1-visible-error{color:var(--color-error,#d33)}.mw-parser-output .cs1-maint{display:none;color:#085;margin-left:0.3em}.mw-parser-output .cs1-kern-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right{padding-right:0.2em}.mw-parser-output .citation .mw-selflink{font-weight:inherit}@media screen{.mw-parser-output .cs1-format{font-size:95%}html.skin-theme-clientpref-night .mw-parser-output .cs1-maint{color:#18911f}}@media screen and (prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .cs1-maint{color:#18911f}}</style><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://unstats.un.org/unsd/methodology/m49/\">\"Standard country or area codes for statistical use (M49)\"</a>. <a href=\"/wiki/United_Nations_Statistics_Division\" title=\"United Nations Statistics Division\">United Nations Statistics Division</a>.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=Standard+country+or+area+codes+for+statistical+use+%28M49%29&amp;rft.pub=United+Nations+Statistics+Division&amp;rft_id=https%3A%2F%2Funstats.un.org%2Funsd%2Fmethodology%2Fm49%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AList+of+countries+and+dependencies+by+population+%28United+Nations%29\" class=\"Z3988\"></span></span>\n</li>\n<li id=\"cite_note-UN-2\"><span class=\"mw-cite-backlink\">^ <a href=\"#cite_ref-UN_2-0\"><sup><i><b>a</b></i></sup></a> <a href=\"#cite_ref-UN_2-1\"><sup><i><b>b</b></i></sup></a></span> <span class=\"reference-text\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1238218222\" /><cite class=\"citation web cs1\"><a rel=\"nofollow\" class=\"external text\" href=\"https://population.un.org/wpp/\">\"World Population Prospects, 2024 Revision\"</a>. <a href=\"/wiki/United_Nations_Department_of_Economic_and_Social_Affairs\" title=\"United Nations Department of Economic and Social Affairs\">United Nations Department of Economic and Social Affairs</a>, Population Division.</cite><span title=\"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&amp;rft.genre=unknown&amp;rft.btitle=World+Population+Prospects%2C+2024+Revision&amp;rft.pub=United+Nations+Department+of+Economic+and+Social+Affairs%2C+Population+Division&amp;rft_id=https%3A%2F%2Fpopulation.un.org%2Fwpp%2F&amp;rfr_id=info%3Asid%2Fen.wikipedia.org%3AList+of+countries+and+dependencies+by+population+%28United+Nations%29\" class=\"Z3988\"></span> \"It presents population estimates from 1950 to the present\".</span>\n</li>\n</ol></div></div>\n<div class=\"mw-heading mw-heading2\"><h2 id=\"External_links\">External links</h2><span class=\"mw-editsection\"><span class=\"mw-editsection-bracket\">[</span><a href=\"/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;action=edit&amp;section=10\" title=\"Edit section: External links\"><span>edit</span></a><span class=\"mw-editsection-bracket\">]</span></span></div>\n<ul><li><a rel=\"nofollow\" class=\"external text\" href=\"https://web.archive.org/web/20150816232627/http://esa.un.org/unpd/wpp/index.htm\"><i>World Population Prospects</i>, the 2019 Revision</a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://population.un.org/wpp/data-sources\">Data sources for the <i>World Population Prospects</i></a></li>\n<li><a rel=\"nofollow\" class=\"external text\" href=\"https://population.un.org/wpp/methodology\">Methodology of the United Nations Population Estimates and Projections</a></li></ul>\n<div class=\"navbox-styles\"><style data-mw-deduplicate=\"TemplateStyles:r1129693374\">.mw-parser-output .hlist dl,.mw-parser-output .hlist ol,.mw-parser-output .hlist ul{margin:0;padding:0}.mw-parser-output .hlist dd,.mw-parser-output .hlist dt,.mw-parser-output .hlist li{margin:0;display:inline}.mw-parser-output .hlist.inline,.mw-parser-output .hlist.inline dl,.mw-parser-output .hlist.inline ol,.mw-parser-output .hlist.inline ul,.mw-parser-output .hlist dl dl,.mw-parser-output .hlist dl ol,.mw-parser-output .hlist dl ul,.mw-parser-output .hlist ol dl,.mw-parser-output .hlist ol ol,.mw-parser-output .hlist ol ul,.mw-parser-output .hlist ul dl,.mw-parser-output .hlist ul ol,.mw-parser-output .hlist ul ul{display:inline}.mw-parser-output .hlist .mw-empty-li{display:none}.mw-parser-output .hlist dt::after{content:\": \"}.mw-parser-output .hlist dd::after,.mw-parser-output .hlist li::after{content:\" · \";font-weight:bold}.mw-parser-output .hlist dd:last-child::after,.mw-parser-output .hlist dt:last-child::after,.mw-parser-output .hlist li:last-child::after{content:none}.mw-parser-output .hlist dd dd:first-child::before,.mw-parser-output .hlist dd dt:first-child::before,.mw-parser-output .hlist dd li:first-child::before,.mw-parser-output .hlist dt dd:first-child::before,.mw-parser-output .hlist dt dt:first-child::before,.mw-parser-output .hlist dt li:first-child::before,.mw-parser-output .hlist li dd:first-child::before,.mw-parser-output .hlist li dt:first-child::before,.mw-parser-output .hlist li li:first-child::before{content:\" (\";font-weight:normal}.mw-parser-output .hlist dd dd:last-child::after,.mw-parser-output .hlist dd dt:last-child::after,.mw-parser-output .hlist dd li:last-child::after,.mw-parser-output .hlist dt dd:last-child::after,.mw-parser-output .hlist dt dt:last-child::after,.mw-parser-output .hlist dt li:last-child::after,.mw-parser-output .hlist li dd:last-child::after,.mw-parser-output .hlist li dt:last-child::after,.mw-parser-output .hlist li li:last-child::after{content:\")\";font-weight:normal}.mw-parser-output .hlist ol{counter-reset:listitem}.mw-parser-output .hlist ol>li{counter-increment:listitem}.mw-parser-output .hlist ol>li::before{content:\" \"counter(listitem)\"\\a0 \"}.mw-parser-output .hlist dd ol>li:first-child::before,.mw-parser-output .hlist dt ol>li:first-child::before,.mw-parser-output .hlist li ol>li:first-child::before{content:\" (\"counter(listitem)\"\\a0 \"}</style><style data-mw-deduplicate=\"TemplateStyles:r1236075235\">.mw-parser-output .navbox{box-sizing:border-box;border:1px solid #a2a9b1;width:100%;clear:both;font-size:88%;text-align:center;padding:1px;margin:1em auto 0}.mw-parser-output .navbox .navbox{margin-top:0}.mw-parser-output .navbox+.navbox,.mw-parser-output .navbox+.navbox-styles+.navbox{margin-top:-1px}.mw-parser-output .navbox-inner,.mw-parser-output .navbox-subgroup{width:100%}.mw-parser-output .navbox-group,.mw-parser-output .navbox-title,.mw-parser-output .navbox-abovebelow{padding:0.25em 1em;line-height:1.5em;text-align:center}.mw-parser-output .navbox-group{white-space:nowrap;text-align:right}.mw-parser-output .navbox,.mw-parser-output .navbox-subgroup{background-color:#fdfdfd}.mw-parser-output .navbox-list{line-height:1.5em;border-color:#fdfdfd}.mw-parser-output .navbox-list-with-group{text-align:left;border-left-width:2px;border-left-style:solid}.mw-parser-output tr+tr>.navbox-abovebelow,.mw-parser-output tr+tr>.navbox-group,.mw-parser-output tr+tr>.navbox-image,.mw-parser-output tr+tr>.navbox-list{border-top:2px solid #fdfdfd}.mw-parser-output .navbox-title{background-color:#ccf}.mw-parser-output .navbox-abovebelow,.mw-parser-output .navbox-group,.mw-parser-output .navbox-subgroup .navbox-title{background-color:#ddf}.mw-parser-output .navbox-subgroup .navbox-group,.mw-parser-output .navbox-subgroup .navbox-abovebelow{background-color:#e6e6ff}.mw-parser-output .navbox-even{background-color:#f7f7f7}.mw-parser-output .navbox-odd{background-color:transparent}.mw-parser-output .navbox .hlist td dl,.mw-parser-output .navbox .hlist td ol,.mw-parser-output .navbox .hlist td ul,.mw-parser-output .navbox td.hlist dl,.mw-parser-output .navbox td.hlist ol,.mw-parser-output .navbox td.hlist ul{padding:0.125em 0}.mw-parser-output .navbox .navbar{display:block;font-size:100%}.mw-parser-output .navbox-title .navbar{float:left;text-align:left;margin-right:0.5em}body.skin--responsive .mw-parser-output .navbox-image img{max-width:none!important}@media print{body.ns-0 .mw-parser-output .navbox{display:none!important}}</style><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /></div><div role=\"navigation\" class=\"navbox\" aria-labelledby=\"Lists_of_countries_by_population_statistics8319\" style=\"padding:3px\"><table class=\"nowraplinks hlist mw-collapsible autocollapse navbox-inner\" style=\"border-spacing:0;background:transparent;color:inherit\"><tbody><tr><th scope=\"col\" class=\"navbox-title\" colspan=\"2\"><link rel=\"mw-deduplicated-inline-style\" href=\"mw-data:TemplateStyles:r1129693374\" /><style data-mw-deduplicate=\"TemplateStyles:r1239400231\">.mw-parser-output .navbar{display:inline;font-size:88%;font-weight:normal}.mw-parser-output .navbar-collapse{float:left;text-align:left}.mw-parser-output .navbar-boxtext{word-spacing:0}.mw-parser-output .navbar ul{display:inline-block;white-space:nowrap;line-height:inherit}.mw-parser-output .navbar-brackets::before{margin-right:-0.125em;content:\"[ \"}.mw-parser-output .navbar-brackets::after{margin-left:-0.125em;content:\" ]\"}.mw-parser-output .navbar li{word-spacing:-0.125em}.mw-parser-output .navbar a>span,.mw-parser-output .navbar a>abbr{text-decoration:inherit}.mw-parser-output .navbar-mini abbr{font-variant:small-caps;border-bottom:none;text-decoration:none;cursor:inherit}.mw-parser-output .navbar-ct-full{font-size:114%;margin:0 7em}.mw-parser-output .navbar-ct-mini{font-size:114%;margin:0 4em}html.skin-theme-clientpref-night .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}@media(prefers-color-scheme:dark){html.skin-theme-clientpref-os .mw-parser-output .navbar li a abbr{color:var(--color-base)!important}}@media print{.mw-parser-output .navbar{display:none!important}}</style><div class=\"navbar plainlinks hlist navbar-mini\"><ul><li class=\"nv-view\"><a href=\"/wiki/Template:Lists_of_countries_by_population_statistics\" title=\"Template:Lists of countries by population statistics\"><abbr title=\"View this template\">v</abbr></a></li><li class=\"nv-talk\"><a href=\"/wiki/Template_talk:Lists_of_countries_by_population_statistics\" title=\"Template talk:Lists of countries by population statistics\"><abbr title=\"Discuss this template\">t</abbr></a></li><li class=\"nv-edit\"><a href=\"/wiki/Special:EditPage/Template:Lists_of_countries_by_population_statistics\" title=\"Special:EditPage/Template:Lists of countries by population statistics\"><abbr title=\"Edit this template\">e</abbr></a></li></ul></div><div id=\"Lists_of_countries_by_population_statistics8319\" style=\"font-size:114%;margin:0 4em\"><a href=\"/wiki/Lists_of_sovereign_states_and_dependent_territories\" title=\"Lists of sovereign states and dependent territories\">Lists of countries</a> by <a href=\"/wiki/Demographic_statistics\" title=\"Demographic statistics\">population statistics</a></div></th></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/World_population\" title=\"World population\">Global</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_and_dependencies_by_population\" title=\"List of countries and dependencies by population\">Current population</a>\n<ul><li><a href=\"/wiki/List_of_countries_by_population_(United_Nations)\" class=\"mw-redirect\" title=\"List of countries by population (United Nations)\">United Nations</a></li></ul></li>\n<li><a href=\"/wiki/Demographics_of_the_world\" title=\"Demographics of the world\">Demographics of the world</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/List_of_continents_and_continental_subregions_by_population\" title=\"List of continents and continental subregions by population\">Continents/subregions</a></th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_African_countries_by_population\" title=\"List of African countries by population\">Africa</a></li>\n<li><a href=\"/wiki/Demographics_of_Antarctica\" title=\"Demographics of Antarctica\">Antarctica</a></li>\n<li><a href=\"/wiki/List_of_Asian_countries_by_population\" title=\"List of Asian countries by population\">Asia</a></li>\n<li><a href=\"/wiki/List_of_European_countries_by_population\" title=\"List of European countries by population\">Europe</a></li>\n<li><a href=\"/wiki/List_of_North_American_countries_by_population\" title=\"List of North American countries by population\">North America</a>\n<ul><li><a href=\"/wiki/List_of_Caribbean_countries_by_population\" title=\"List of Caribbean countries by population\">Caribbean</a></li></ul></li>\n<li><a href=\"/wiki/List_of_Oceanian_countries_by_population\" title=\"List of Oceanian countries by population\">Oceania</a></li>\n<li><a href=\"/wiki/List_of_South_American_countries_by_population\" title=\"List of South American countries by population\">South America</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Intercontinental</th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_in_the_Americas_by_population\" title=\"List of countries in the Americas by population\">Americas</a></li>\n<li><a href=\"/wiki/List_of_Arab_countries_by_population\" class=\"mw-redirect\" title=\"List of Arab countries by population\">Arab world</a></li>\n<li><a href=\"/wiki/List_of_member_states_of_the_Commonwealth_of_Nations_by_population\" title=\"List of member states of the Commonwealth of Nations by population\">Commonwealth of Nations</a></li>\n<li><a href=\"/wiki/List_of_Eurasian_countries_by_population\" title=\"List of Eurasian countries by population\">Eurasia</a></li>\n<li><a href=\"/wiki/List_of_European_Union_member_states_by_population\" title=\"List of European Union member states by population\">European Union</a></li>\n<li><a href=\"/wiki/List_of_islands_by_population\" title=\"List of islands by population\">Islands</a></li>\n<li><a href=\"/wiki/List_of_Latin_American_countries_by_population\" title=\"List of Latin American countries by population\">Latin America</a></li>\n<li><a href=\"/wiki/List_of_Middle_Eastern_countries_by_population\" title=\"List of Middle Eastern countries by population\">Middle East</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Cities/urban areas</th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_largest_cities\" title=\"List of largest cities\">World cities</a></li>\n<li><a href=\"/wiki/List_of_national_capitals_by_population\" title=\"List of national capitals by population\">National capitals</a></li>\n<li><a href=\"/wiki/Megacity\" title=\"Megacity\">Megacities</a></li>\n<li><a href=\"/wiki/Megalopolis\" title=\"Megalopolis\">Megalopolises</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Past and future</th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_past_and_projected_future_population\" title=\"List of countries by past and projected future population\">Past and future population</a></li>\n<li><a href=\"/wiki/Estimates_of_historical_world_population\" title=\"Estimates of historical world population\">Estimates of historical world population</a></li>\n<li><a href=\"/wiki/Human_population_projections\" title=\"Human population projections\">Human population projections</a></li>\n<li><a href=\"/wiki/List_of_states_by_population_in_1_CE\" title=\"List of states by population in 1 CE\">1</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1000\" title=\"List of countries by population in 1000\">1000</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1500\" title=\"List of countries by population in 1500\">1500</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1600\" title=\"List of countries by population in 1600\">1600</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1700\" title=\"List of countries by population in 1700\">1700</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1800\" title=\"List of countries by population in 1800\">1800</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1900\" title=\"List of countries by population in 1900\">1900</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1939\" title=\"List of countries by population in 1939\">1939</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_1989\" title=\"List of countries by population in 1989\">1989</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2000\" title=\"List of countries by population in 2000\">2000</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2005\" title=\"List of countries by population in 2005\">2005</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2010\" title=\"List of countries by population in 2010\">2010</a></li>\n<li><a href=\"/wiki/List_of_countries_by_population_in_2015\" title=\"List of countries by population in 2015\">2015</a></li>\n<li><a href=\"/wiki/List_of_population_milestones_by_country\" title=\"List of population milestones by country\">Population milestones</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Population_density\" title=\"Population density\">Population density</a></th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_and_dependencies_by_population_density\" title=\"List of countries and dependencies by population density\">Current density</a></li>\n<li><a href=\"/wiki/Past_and_future_population_density_by_country\" class=\"mw-redirect\" title=\"Past and future population density by country\">Past and future population density</a></li>\n<li><a href=\"/wiki/List_of_countries_by_arable_land_density\" title=\"List of countries by arable land density\">Person-to-arable land ratio</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Growth indicators</th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_population_growth_rate\" title=\"List of countries by population growth rate\">Population growth rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_natural_increase\" class=\"mw-redirect\" title=\"List of countries by natural increase\">Natural increase</a></li>\n<li><a href=\"/wiki/List_of_countries_by_net_reproduction_rate\" title=\"List of countries by net reproduction rate\">Net reproduction rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_number_of_births\" title=\"List of countries by number of births\">Number of births</a></li>\n<li><a href=\"/wiki/List_of_countries_by_number_of_deaths\" title=\"List of countries by number of deaths\">Number of deaths</a></li>\n<li><a href=\"/wiki/List_of_countries_by_birth_rate\" title=\"List of countries by birth rate\">Birth rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_mortality_rate\" title=\"List of countries by mortality rate\">Mortality rate</a></li>\n<li><a href=\"/wiki/List_of_sovereign_states_and_dependencies_by_total_fertility_rate\" class=\"mw-redirect\" title=\"List of sovereign states and dependencies by total fertility rate\">Fertility rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_past_fertility_rate\" title=\"List of countries by past fertility rate\">Past fertility rate</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Life_expectancy\" title=\"Life expectancy\">Life expectancy</a></th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_life_expectancy\" title=\"List of countries by life expectancy\">World</a></li>\n<li><a href=\"/wiki/List_of_African_countries_by_life_expectancy\" title=\"List of African countries by life expectancy\">Africa</a></li>\n<li><a href=\"/wiki/List_of_Asian_countries_by_life_expectancy\" title=\"List of Asian countries by life expectancy\">Asia</a></li>\n<li><a href=\"/wiki/List_of_European_countries_by_life_expectancy\" title=\"List of European countries by life expectancy\">Europe</a></li>\n<li><a href=\"/wiki/List_of_North_American_countries_by_life_expectancy\" title=\"List of North American countries by life expectancy\">North America</a></li>\n<li><a href=\"/wiki/List_of_Oceanian_countries_by_life_expectancy\" title=\"List of Oceanian countries by life expectancy\">Oceania</a></li>\n<li><a href=\"/wiki/List_of_South_American_countries_by_life_expectancy\" title=\"List of South American countries by life expectancy\">South America</a></li>\n<li><a href=\"/wiki/List_of_world_regions_by_life_expectancy\" title=\"List of world regions by life expectancy\">world regions</a></li>\n<li><a href=\"/wiki/List_of_countries_by_past_life_expectancy\" title=\"List of countries by past life expectancy\">past life expectancy</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Other <a href=\"/wiki/Demography\" title=\"Demography\">demographics</a></th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_mean_age_at_childbearing\" title=\"List of countries by mean age at childbearing\">Age at childbearing</a></li>\n<li><a href=\"/wiki/List_of_countries_by_age_at_first_marriage\" title=\"List of countries by age at first marriage\">Age at first marriage</a></li>\n<li><a href=\"/wiki/List_of_countries_by_age_structure\" title=\"List of countries by age structure\">Age structure</a></li>\n<li><a href=\"/wiki/List_of_countries_by_dependency_ratio\" title=\"List of countries by dependency ratio\">Dependency ratio</a></li>\n<li><a href=\"/wiki/Divorce_demography\" class=\"mw-redirect\" title=\"Divorce demography\">Divorce rate</a></li>\n<li><a href=\"/wiki/List_of_countries_ranked_by_ethnic_and_cultural_diversity_level\" class=\"mw-redirect\" title=\"List of countries ranked by ethnic and cultural diversity level\">Ethnic and cultural diversity level</a></li>\n<li><a href=\"/wiki/List_of_countries_by_ethnic_groups\" title=\"List of countries by ethnic groups\">Ethnic composition</a></li>\n<li><a href=\"/wiki/List_of_sovereign_states_by_immigrant_and_emigrant_population\" title=\"List of sovereign states by immigrant and emigrant population\">Immigrant population</a></li>\n<li><a href=\"/wiki/Linguistic_diversity_index\" title=\"Linguistic diversity index\">Linguistic diversity</a></li>\n<li><a href=\"/wiki/List_of_countries_by_median_age\" title=\"List of countries by median age\">Median age</a></li>\n<li><a href=\"/wiki/List_of_sovereign_states_by_net_migration_rate\" class=\"mw-redirect\" title=\"List of sovereign states by net migration rate\">Net migration rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_number_of_households\" title=\"List of countries by number of households\">Number of households</a></li>\n<li><a href=\"/wiki/List_of_religious_populations\" title=\"List of religious populations\">Religion</a> / <a href=\"/wiki/List_of_countries_by_irreligion\" title=\"List of countries by irreligion\">Irreligion</a></li>\n<li><a href=\"/wiki/List_of_countries_by_sex_ratio\" title=\"List of countries by sex ratio\">Sex ratio</a></li>\n<li><a href=\"/wiki/List_of_countries_by_urban_population\" class=\"mw-redirect\" title=\"List of countries by urban population\">Urban population</a></li>\n<li><a href=\"/wiki/Urbanization_by_country\" class=\"mw-redirect\" title=\"Urbanization by country\">Urbanization</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Health\" title=\"Health\">Health</a></th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_antidepressant_consumption\" title=\"List of countries by antidepressant consumption\">Antidepressant consumption</a></li>\n<li><a href=\"/wiki/Stockpiling_antiviral_medications_for_pandemic_influenza\" title=\"Stockpiling antiviral medications for pandemic influenza\">Antiviral medications for pandemic influenza</a></li>\n<li><a href=\"/wiki/List_of_countries_by_HIV/AIDS_adult_prevalence_rate\" class=\"mw-redirect\" title=\"List of countries by HIV/AIDS adult prevalence rate\">HIV/AIDS adult prevalence rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_infant_and_under-five_mortality_rates\" title=\"List of countries by infant and under-five mortality rates\">Infant and under-five mortality rates</a></li>\n<li><a href=\"/wiki/List_of_countries_by_maternal_mortality_ratio\" title=\"List of countries by maternal mortality ratio\">Maternal mortality rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_obesity_rate\" title=\"List of countries by obesity rate\">Obesity rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_percentage_of_population_suffering_from_undernourishment\" class=\"mw-redirect\" title=\"List of countries by percentage of population suffering from undernourishment\">Percentage suffering from undernourishment</a></li>\n<li><a href=\"/wiki/List_of_OECD_health_expenditure_by_country_by_type_of_financing\" class=\"mw-redirect\" title=\"List of OECD health expenditure by country by type of financing\">Health expenditure by country by type of financing</a></li>\n<li><a href=\"/wiki/List_of_countries_by_suicide_rate\" title=\"List of countries by suicide rate\">Suicide rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_total_health_expenditure_per_capita\" title=\"List of countries by total health expenditure per capita\">Total health expenditure per capita</a></li>\n<li><a href=\"/wiki/Health_spending_as_percent_of_gross_domestic_product_(GDP)_by_country\" title=\"Health spending as percent of gross domestic product (GDP) by country\">Health spending as&#160;% of GDP</a></li>\n<li><a href=\"/wiki/List_of_countries_by_body_mass_index\" title=\"List of countries by body mass index\">Body mass index</a> (BMI)</li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\">Education and innovation</th><td class=\"navbox-list-with-group navbox-list navbox-odd wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/Bloomberg_Innovation_Index\" class=\"mw-redirect\" title=\"Bloomberg Innovation Index\">Bloomberg Innovation Index</a></li>\n<li><a href=\"/wiki/Education_Index\" title=\"Education Index\">Education Index</a></li>\n<li><a href=\"/wiki/Global_Innovation_Index\" title=\"Global Innovation Index\">Global Innovation Index</a></li>\n<li><a href=\"/wiki/International_Innovation_Index\" title=\"International Innovation Index\">International Innovation Index</a></li>\n<li><a href=\"/wiki/List_of_countries_by_literacy_rate\" title=\"List of countries by literacy rate\">Literacy rate</a></li>\n<li><a href=\"/wiki/Programme_for_the_International_Assessment_of_Adult_Competencies\" title=\"Programme for the International Assessment of Adult Competencies\">Programme for the International Assessment of Adult Competencies</a></li>\n<li><a href=\"/wiki/Programme_for_International_Student_Assessment\" title=\"Programme for International Student Assessment\">Programme for International Student Assessment</a> (PISA)</li>\n<li><a href=\"/wiki/Progress_in_International_Reading_Literacy_Study\" title=\"Progress in International Reading Literacy Study\">Progress in International Reading Literacy Study</a> (PIRLS)</li>\n<li><a href=\"/wiki/Trends_in_International_Mathematics_and_Science_Study\" title=\"Trends in International Mathematics and Science Study\">Trends in International Mathematics and Science Study</a> (TIMSS)</li>\n<li><a href=\"/wiki/List_of_countries_by_tertiary_education_attainment\" title=\"List of countries by tertiary education attainment\">Tertiary education attainment</a></li>\n<li><a href=\"/wiki/World_Intellectual_Property_Indicators\" title=\"World Intellectual Property Indicators\">World Intellectual Property Indicators</a></li></ul>\n</div></td></tr><tr><th scope=\"row\" class=\"navbox-group\" style=\"width:1%\"><a href=\"/wiki/Demographic_economics\" title=\"Demographic economics\">Economic</a></th><td class=\"navbox-list-with-group navbox-list navbox-even wraplinks\" style=\"width:100%;padding:0\"><div style=\"padding:0 0.25em\">\n<ul><li><a href=\"/wiki/List_of_countries_by_share_of_population_with_access_to_financial_services\" class=\"mw-redirect\" title=\"List of countries by share of population with access to financial services\">Access to financial services</a></li>\n<li><a href=\"/wiki/List_of_development_aid_sovereign_state_donors\" title=\"List of development aid sovereign state donors\">Development aid donors</a>\n<ul><li><a href=\"/wiki/List_of_sovereign_states_by_Official_Development_Assistance_received\" class=\"mw-redirect\" title=\"List of sovereign states by Official Development Assistance received\">Official Development Assistance received</a></li></ul></li>\n<li><a href=\"/wiki/List_of_countries_by_employment_rate\" class=\"mw-redirect\" title=\"List of countries by employment rate\">Employment rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_irrigated_land_area\" title=\"List of countries by irrigated land area\">Irrigated land area</a></li>\n<li><a href=\"/wiki/Human_Development_Index\" title=\"Human Development Index\">Human Development Index</a>\n<ul><li><a href=\"/wiki/List_of_countries_by_Human_Development_Index\" title=\"List of countries by Human Development Index\">by country</a></li>\n<li><a href=\"/wiki/List_of_countries_by_inequality-adjusted_Human_Development_Index\" title=\"List of countries by inequality-adjusted Human Development Index\">inequality-adjusted</a></li>\n<li><a href=\"/wiki/List_of_countries_by_planetary_pressures%E2%80%93adjusted_Human_Development_Index\" title=\"List of countries by planetary pressures–adjusted Human Development Index\">planetary pressures–adjusted HDI</a></li></ul></li>\n<li><a href=\"/wiki/Human_Poverty_Index\" title=\"Human Poverty Index\">Human Poverty Index</a></li>\n<li><a href=\"/wiki/List_of_countries_by_imports\" title=\"List of countries by imports\">Imports</a></li>\n<li><a href=\"/wiki/List_of_countries_by_exports\" title=\"List of countries by exports\">Exports</a></li>\n<li><a href=\"/wiki/List_of_countries_by_income_inequality\" title=\"List of countries by income inequality\">Income inequality</a></li>\n<li><a href=\"/wiki/List_of_countries_by_labour_force\" title=\"List of countries by labour force\">Labour force</a></li>\n<li><a href=\"/wiki/List_of_countries_by_share_of_income_of_the_richest_one_percent\" class=\"mw-redirect\" title=\"List of countries by share of income of the richest one percent\">Share of income of top 1%</a></li>\n<li><a href=\"/wiki/List_of_countries_by_number_of_millionaires\" title=\"List of countries by number of millionaires\">Number of millionaires (US dollars)</a></li>\n<li><a href=\"/wiki/List_of_countries_by_number_of_billionaires\" title=\"List of countries by number of billionaires\">Number of billionaires (US dollars)</a></li>\n<li><a href=\"/wiki/List_of_countries_by_percentage_of_population_living_in_poverty\" title=\"List of countries by percentage of population living in poverty\">Percentage living in poverty</a></li>\n<li><a href=\"/wiki/List_of_countries_by_public_sector_size\" title=\"List of countries by public sector size\">Public sector</a></li>\n<li><a href=\"/wiki/List_of_countries_by_unemployment_rate\" title=\"List of countries by unemployment rate\">Unemployment rate</a></li>\n<li><a href=\"/wiki/List_of_countries_by_wealth_inequality\" class=\"mw-redirect\" title=\"List of countries by wealth inequality\">Wealth inequality</a></li></ul>\n</div></td></tr><tr><td class=\"navbox-abovebelow\" colspan=\"2\"><div><div class=\"hlist\" style=\"text-align:center\">\n<ul><li><a href=\"/wiki/List_of_international_rankings\" title=\"List of international rankings\">List of international rankings</a></li>\n<li><a href=\"/wiki/Lists_by_country\" title=\"Lists by country\">Lists by country</a></li></ul>\n</div></div></td></tr></tbody></table></div>\n<!--\nNewPP limit report\nParsed by mw‐web.codfw.main‐84d6dc54bf‐xzm5g\nCached time: 20250924025126\nCache expiry: 2592000\nReduced expiry: false\nComplications: [vary‐revision‐sha1, show‐toc]\nCPU time usage: 5.431 seconds\nReal time usage: 6.239 seconds\nPreprocessor visited node count: 1000580/1000000\nRevision size: 76467/2097152 bytes\nPost‐expand include size: 356536/2097152 bytes\nTemplate argument size: 66427/2097152 bytes\nHighest expansion depth: 13/100\nExpensive parser function count: 1/500\nUnstrip recursion depth: 1/20\nUnstrip post‐expand size: 64028/5000000 bytes\nLua time usage: 0.520/10.000 seconds\nLua memory usage: 3729126/52428800 bytes\nNumber of Wikibase entities loaded: 0/500\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 5550.382      1 -total\n 62.75% 3482.604    238 Template:Change\n 24.19% 1342.578    259 Template:Flagcountry\n  5.17%  287.104      2 Template:Reflist\n  2.64%  146.796      2 Template:Cite_web\n  2.52%  139.820    259 Template:Flag_country/core\n  2.50%  138.658      1 Template:Notelist\n  1.97%  109.082      1 Template:Short_description\n  1.53%   84.938      1 Template:Lists_of_countries_by_population_statistics\n  1.49%   82.833      1 Template:Navbox\n-->\n\n<!-- Saved in parser cache with key enwiki:pcache:39707994:|#|:idhash:canonical and timestamp 20250924025126 and revision id 1311341016. Rendering was triggered because: page_view\n -->\n</div><noscript><img src=\"https://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1&amp;usesul3=1\" alt=\"\" width=\"1\" height=\"1\" style=\"border: none; position: absolute;\"></noscript>\n<div class=\"printfooter\" data-nosnippet=\"\">Retrieved from \"<a dir=\"ltr\" href=\"https://en.wikipedia.org/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;oldid=1311341016\">https://en.wikipedia.org/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;oldid=1311341016</a>\"</div></div>\n\t\t\t\t\t<div id=\"catlinks\" class=\"catlinks\" data-mw=\"interface\"><div id=\"mw-normal-catlinks\" class=\"mw-normal-catlinks\"><a href=\"/wiki/Help:Category\" title=\"Help:Category\">Categories</a>: <ul><li><a href=\"/wiki/Category:Lists_of_countries_by_past_and_future_population_(United_Nations)\" title=\"Category:Lists of countries by past and future population (United Nations)\">Lists of countries by past and future population (United Nations)</a></li><li><a href=\"/wiki/Category:Lists_of_countries_by_continent\" title=\"Category:Lists of countries by continent\">Lists of countries by continent</a></li></ul></div><div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks mw-hidden-cats-hidden\">Hidden categories: <ul><li><a href=\"/wiki/Category:Articles_with_short_description\" title=\"Category:Articles with short description\">Articles with short description</a></li><li><a href=\"/wiki/Category:Short_description_is_different_from_Wikidata\" title=\"Category:Short description is different from Wikidata\">Short description is different from Wikidata</a></li><li><a href=\"/wiki/Category:Pages_where_node_count_is_exceeded\" title=\"Category:Pages where node count is exceeded\">Pages where node count is exceeded</a></li></ul></div></div>\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t</div>\n\t\t<div class=\"mw-footer-container\">\n\n<footer id=\"footer\" class=\"mw-footer\" >\n\t<ul id=\"footer-info\">\n\t<li id=\"footer-info-lastmod\"> This page was last edited on 14 September 2025, at 20:30<span class=\"anonymous-show\">&#160;(UTC)</span>.</li>\n\t<li id=\"footer-info-copyright\">Text is available under the <a href=\"/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License\" title=\"Wikipedia:Text of the Creative Commons Attribution-ShareAlike 4.0 International License\">Creative Commons Attribution-ShareAlike 4.0 License</a>;\nadditional terms may apply. By using this site, you agree to the <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Terms of Use\">Terms of Use</a> and <a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\" class=\"extiw\" title=\"foundation:Special:MyLanguage/Policy:Privacy policy\">Privacy Policy</a>. Wikipedia® is a registered trademark of the <a rel=\"nofollow\" class=\"external text\" href=\"https://wikimediafoundation.org/\">Wikimedia Foundation, Inc.</a>, a non-profit organization.</li>\n</ul>\n\n\t<ul id=\"footer-places\">\n\t<li id=\"footer-places-privacy\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy\">Privacy policy</a></li>\n\t<li id=\"footer-places-about\"><a href=\"/wiki/Wikipedia:About\">About Wikipedia</a></li>\n\t<li id=\"footer-places-disclaimers\"><a href=\"/wiki/Wikipedia:General_disclaimer\">Disclaimers</a></li>\n\t<li id=\"footer-places-contact\"><a href=\"//en.wikipedia.org/wiki/Wikipedia:Contact_us\">Contact Wikipedia</a></li>\n\t<li id=\"footer-places-wm-codeofconduct\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Universal_Code_of_Conduct\">Code of Conduct</a></li>\n\t<li id=\"footer-places-developers\"><a href=\"https://developer.wikimedia.org\">Developers</a></li>\n\t<li id=\"footer-places-statslink\"><a href=\"https://stats.wikimedia.org/#/en.wikipedia.org\">Statistics</a></li>\n\t<li id=\"footer-places-cookiestatement\"><a href=\"https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Cookie_statement\">Cookie statement</a></li>\n\t<li id=\"footer-places-mobileview\"><a href=\"//en.m.wikipedia.org/w/index.php?title=List_of_countries_and_dependencies_by_population_(United_Nations)&amp;mobileaction=toggle_view_mobile\" class=\"noprint stopMobileRedirectToggle\">Mobile view</a></li>\n</ul>\n\n\t<ul id=\"footer-icons\" class=\"noprint\">\n\t<li id=\"footer-copyrightico\"><a href=\"https://www.wikimedia.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/static/images/footer/wikimedia-button.svg\" width=\"84\" height=\"29\"><img src=\"/static/images/footer/wikimedia.svg\" width=\"25\" height=\"25\" alt=\"Wikimedia Foundation\" lang=\"en\" loading=\"lazy\"></picture></a></li>\n\t<li id=\"footer-poweredbyico\"><a href=\"https://www.mediawiki.org/\" class=\"cdx-button cdx-button--fake-button cdx-button--size-large cdx-button--fake-button--enabled\"><picture><source media=\"(min-width: 500px)\" srcset=\"/w/resources/assets/poweredby_mediawiki.svg\" width=\"88\" height=\"31\"><img src=\"/w/resources/assets/mediawiki_compact.svg\" alt=\"Powered by MediaWiki\" lang=\"en\" width=\"25\" height=\"25\" loading=\"lazy\"></picture></a></li>\n</ul>\n\n</footer>\n\n\t\t</div>\n\t</div>\n</div>\n<div class=\"vector-header-container vector-sticky-header-container no-font-mode-scale\">\n\t<div id=\"vector-sticky-header\" class=\"vector-sticky-header\">\n\t\t<div class=\"vector-sticky-header-start\">\n\t\t\t<div class=\"vector-sticky-header-icon-start vector-button-flush-left vector-button-flush-right\" aria-hidden=\"true\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-sticky-header-search-toggle\" tabindex=\"-1\" data-event-name=\"ui.vector-sticky-search-form.icon\"><span class=\"vector-icon mw-ui-icon-search mw-ui-icon-wikimedia-search\"></span>\n\n<span>Search</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div role=\"search\" class=\"vector-search-box-vue  vector-search-box-show-thumbnail vector-search-box\">\n\t\t\t<div class=\"vector-typeahead-search-container\">\n\t\t\t\t<div class=\"cdx-typeahead-search cdx-typeahead-search--show-thumbnail\">\n\t\t\t\t\t<form action=\"/w/index.php\" id=\"vector-sticky-search-form\" class=\"cdx-search-input cdx-search-input--has-end-button\">\n\t\t\t\t\t\t<div  class=\"cdx-search-input__input-wrapper\"  data-search-loc=\"header-moved\">\n\t\t\t\t\t\t\t<div class=\"cdx-text-input cdx-text-input--has-start-icon\">\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tclass=\"cdx-text-input__input mw-searchInput\" autocomplete=\"off\"\n\n\t\t\t\t\t\t\t\t\ttype=\"search\" name=\"search\" placeholder=\"Search Wikipedia\">\n\t\t\t\t\t\t\t\t<span class=\"cdx-text-input__icon cdx-text-input__start-icon\"></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"title\" value=\"Special:Search\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"cdx-button cdx-search-input__end-button\">Search</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-context-bar\">\n\t\t\t\t<nav aria-label=\"Contents\" class=\"vector-toc-landmark\">\n\n\t\t\t\t\t<div id=\"vector-sticky-header-toc\" class=\"vector-dropdown mw-portlet mw-portlet-sticky-header-toc vector-sticky-header-toc vector-button-flush-left\"  >\n\t\t\t\t\t\t<input type=\"checkbox\" id=\"vector-sticky-header-toc-checkbox\" role=\"button\" aria-haspopup=\"true\" data-event-name=\"ui.dropdown-vector-sticky-header-toc\" class=\"vector-dropdown-checkbox \"  aria-label=\"Toggle the table of contents\"  >\n\t\t\t\t\t\t<label id=\"vector-sticky-header-toc-label\" for=\"vector-sticky-header-toc-checkbox\" class=\"vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only \" aria-hidden=\"true\"  ><span class=\"vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet\"></span>\n\n<span class=\"vector-dropdown-label-text\">Toggle the table of contents</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<div class=\"vector-dropdown-content\">\n\n\t\t\t\t\t\t<div id=\"vector-sticky-header-toc-unpinned-container\" class=\"vector-unpinned-container\">\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t</nav>\n\t\t\t\t<div class=\"vector-sticky-header-context-bar-primary\" aria-hidden=\"true\" ><span class=\"mw-page-title-main\">List of countries and dependencies by population (United Nations)</span></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"vector-sticky-header-end\" aria-hidden=\"true\">\n\t\t\t<div class=\"vector-sticky-header-icons\">\n\t\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-talk-sticky-header\" tabindex=\"-1\" data-event-name=\"talk-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbles mw-ui-icon-wikimedia-speechBubbles\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-subject-sticky-header\" tabindex=\"-1\" data-event-name=\"subject-sticky-header\"><span class=\"vector-icon mw-ui-icon-article mw-ui-icon-wikimedia-article\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-history-sticky-header\" tabindex=\"-1\" data-event-name=\"history-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-history mw-ui-icon-wikimedia-wikimedia-history\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only mw-watchlink\" id=\"ca-watchstar-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-star mw-ui-icon-wikimedia-wikimedia-star\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only reading-lists-bookmark\" id=\"ca-bookmark-sticky-header\" tabindex=\"-1\" data-event-name=\"watch-sticky-bookmark\"><span class=\"vector-icon mw-ui-icon-wikimedia-bookmarkOutline mw-ui-icon-wikimedia-wikimedia-bookmarkOutline\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"wikitext-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-wikiText mw-ui-icon-wikimedia-wikimedia-wikiText\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-ve-edit-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-edit mw-ui-icon-wikimedia-wikimedia-edit\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only\" id=\"ca-viewsource-sticky-header\" tabindex=\"-1\" data-event-name=\"ve-edit-protected-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-editLock mw-ui-icon-wikimedia-wikimedia-editLock\"></span>\n\n<span></span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-buttons\">\n\t\t\t\t<button class=\"cdx-button cdx-button--weight-quiet mw-interlanguage-selector\" id=\"p-lang-btn-sticky-header\" tabindex=\"-1\" data-event-name=\"ui.dropdown-p-lang-btn-sticky-header\"><span class=\"vector-icon mw-ui-icon-wikimedia-language mw-ui-icon-wikimedia-wikimedia-language\"></span>\n\n<span>13 languages</span>\n\t\t\t</button>\n\t\t\t<a href=\"#\" class=\"cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--action-progressive\" id=\"ca-addsection-sticky-header\" tabindex=\"-1\" data-event-name=\"addsection-sticky-header\"><span class=\"vector-icon mw-ui-icon-speechBubbleAdd-progressive mw-ui-icon-wikimedia-speechBubbleAdd-progressive\"></span>\n\n<span>Add topic</span>\n\t\t\t</a>\n\t\t</div>\n\t\t\t<div class=\"vector-sticky-header-icon-end\">\n\t\t\t\t<div class=\"vector-user-links\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n<div class=\"mw-portlet mw-portlet-dock-bottom emptyPortlet\" id=\"p-dock-bottom\">\n\t<ul>\n\n\t</ul>\n</div>\n<script>(RLQ=window.RLQ||[]).push(function(){mw.config.set({\"wgHostname\":\"mw-web.codfw.main-5b846fd6fc-dd77d\",\"wgBackendResponseTime\":184,\"wgPageParseReport\":{\"limitreport\":{\"cputime\":\"5.431\",\"walltime\":\"6.239\",\"ppvisitednodes\":{\"value\":1000580,\"limit\":1000000},\"revisionsize\":{\"value\":76467,\"limit\":2097152},\"postexpandincludesize\":{\"value\":356536,\"limit\":2097152},\"templateargumentsize\":{\"value\":66427,\"limit\":2097152},\"expansiondepth\":{\"value\":13,\"limit\":100},\"expensivefunctioncount\":{\"value\":1,\"limit\":500},\"unstrip-depth\":{\"value\":1,\"limit\":20},\"unstrip-size\":{\"value\":64028,\"limit\":5000000},\"entityaccesscount\":{\"value\":0,\"limit\":500},\"timingprofile\":[\"100.00% 5550.382      1 -total\",\" 62.75% 3482.604    238 Template:Change\",\" 24.19% 1342.578    259 Template:Flagcountry\",\"  5.17%  287.104      2 Template:Reflist\",\"  2.64%  146.796      2 Template:Cite_web\",\"  2.52%  139.820    259 Template:Flag_country/core\",\"  2.50%  138.658      1 Template:Notelist\",\"  1.97%  109.082      1 Template:Short_description\",\"  1.53%   84.938      1 Template:Lists_of_countries_by_population_statistics\",\"  1.49%   82.833      1 Template:Navbox\"]},\"scribunto\":{\"limitreport-timeusage\":{\"value\":\"0.520\",\"limit\":\"10.000\"},\"limitreport-memusage\":{\"value\":3729126,\"limit\":52428800}},\"cachereport\":{\"origin\":\"mw-web.codfw.main-84d6dc54bf-xzm5g\",\"timestamp\":\"20250924025126\",\"ttl\":2592000,\"transientcontent\":false}}});});</script>\n<script type=\"application/ld+json\">{\"@context\":\"https:\\/\\/schema.org\",\"@type\":\"Article\",\"name\":\"List of countries and dependencies by population (United Nations)\",\"url\":\"https:\\/\\/en.wikipedia.org\\/wiki\\/List_of_countries_and_dependencies_by_population_(United_Nations)\",\"sameAs\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q14940491\",\"mainEntity\":\"http:\\/\\/www.wikidata.org\\/entity\\/Q14940491\",\"author\":{\"@type\":\"Organization\",\"name\":\"Contributors to Wikimedia projects\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Wikimedia Foundation, Inc.\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/www.wikimedia.org\\/static\\/images\\/wmf-hor-googpub.png\"}},\"datePublished\":\"2013-06-18T08:11:41Z\",\"dateModified\":\"2025-09-14T20:30:07Z\",\"image\":\"https:\\/\\/upload.wikimedia.org\\/wikipedia\\/commons\\/0\\/08\\/United_Nations_geographical_subregions.png\",\"headline\":\"Wikimedia list article\"}</script>\n</body>\n</html>\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ESNext\"],\n    \"moduleResolution\": \"bundler\",\n\n    // Strict type checking\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictBindCallApply\": true,\n    \"strictPropertyInitialization\": true,\n    \"noImplicitThis\": true,\n    \"useUnknownInCatchVariables\": true,\n    \"alwaysStrict\": true,\n\n    // Additional strict checks\n    \"exactOptionalPropertyTypes\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noPropertyAccessFromIndexSignature\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"allowUnusedLabels\": false,\n    \"allowUnreachableCode\": false,\n\n    // Module and interop\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n\n    // Output\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "zensical.toml",
    "content": "[project]\nsite_name = \"html-to-markdown\"\nsite_description = \"High-performance HTML to Markdown conversion powered by Rust\"\nsite_url = \"https://docs.html-to-markdown.kreuzberg.dev\"\nsite_author = \"kreuzberg.dev\"\nrepo_name = \"kreuzberg-dev/html-to-markdown\"\nrepo_url = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nedit_uri = \"edit/main/docs/\"\ncopyright = \"Copyright &copy; 2023-2026 Kreuzberg.dev\"\n\nextra_css = [\"css/extra.css\"]\n\nnav = [\n  { \"Home\" = \"index.md\" },\n  { \"Get started\" = [\n    { \"Installation\" = \"installation.md\" },\n    { \"Usage\" = \"usage.md\" },\n    { \"CLI\" = \"cli.md\" },\n  ] },\n  { \"Configuration\" = [\n    { \"Options\" = \"configuration.md\" },\n    { \"Visitor pattern\" = \"visitor.md\" },\n    { \"Table extraction\" = \"tables.md\" },\n  ] },\n  { \"Reference\" = [\n    { \"API reference\" = \"api-reference.md\" },\n    { \"Language APIs\" = [\n      { \"Rust\" = \"reference/api-rust.md\" },\n      { \"Python\" = \"reference/api-python.md\" },\n      { \"TypeScript\" = \"reference/api-typescript.md\" },\n      { \"Go\" = \"reference/api-go.md\" },\n      { \"Ruby\" = \"reference/api-ruby.md\" },\n      { \"PHP\" = \"reference/api-php.md\" },\n      { \"Java\" = \"reference/api-java.md\" },\n      { \"C#\" = \"reference/api-csharp.md\" },\n      { \"Elixir\" = \"reference/api-elixir.md\" },\n      { \"R\" = \"reference/api-r.md\" },\n      { \"C\" = \"reference/api-c.md\" },\n      { \"WebAssembly\" = \"reference/api-wasm.md\" },\n    ] },\n    { \"Types\" = \"reference/types.md\" },\n    { \"Configuration (generated)\" = \"reference/configuration.md\" },\n    { \"Error types (generated)\" = \"reference/errors.md\" },\n    { \"Language guides\" = \"language-guides.md\" },\n    { \"Error handling\" = \"errors.md\" },\n    { \"Architecture\" = \"architecture.md\" },\n  ] },\n  { \"Migration\" = \"migration.md\" },\n  { \"Contributing\" = \"contributing.md\" },\n  { \"Changelog\" = \"https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CHANGELOG.md\" },\n]\n\n[project.theme]\nname = \"material\"\ncustom_dir = \"docs/overrides\"\nlogo = \"assets/logo.svg\"\nfavicon = \"assets/favicon.ico\"\nlanguage = \"en\"\nfeatures = [\n  \"content.code.copy\",\n  \"content.code.annotate\",\n  \"content.tabs.link\",\n  \"navigation.instant\",\n  \"navigation.instant.progress\",\n  \"navigation.path\",\n  \"navigation.prune\",\n  \"navigation.indexes\",\n  \"navigation.sections\",\n  \"navigation.tabs\",\n  \"navigation.tabs.sticky\",\n  \"navigation.tracking\",\n  \"navigation.top\",\n  \"navigation.footer\",\n  \"search.highlight\",\n  \"search.share\",\n  \"search.suggest\",\n  \"toc.follow\",\n  \"toc.integrate\",\n  \"announce.dismiss\",\n  \"content.action.edit\",\n  \"content.action.view\",\n]\n\n[project.theme.font]\ntext = \"Inter\"\ncode = \"JetBrains Mono\"\n\n[project.theme.icon]\nrepo = \"github\"\nsearch = \"search\"\n\n[[project.theme.palette]]\nmedia = \"(prefers-color-scheme: dark)\"\nscheme = \"slate\"\nprimary = \"custom\"\naccent = \"custom\"\n\n[project.theme.palette.toggle]\nicon = \"sun\"\nname = \"Switch to light mode\"\n\n[[project.theme.palette]]\nmedia = \"(prefers-color-scheme: light)\"\nscheme = \"default\"\nprimary = \"custom\"\naccent = \"custom\"\n\n[project.theme.palette.toggle]\nicon = \"moon\"\nname = \"Switch to dark mode\"\n\n# --- Markdown Extensions ---\n\n[project.markdown_extensions.admonition]\n\n[project.markdown_extensions.attr_list]\n\n[project.markdown_extensions.def_list]\n\n[project.markdown_extensions.footnotes]\n\n[project.markdown_extensions.md_in_html]\n\n[project.markdown_extensions.tables]\n\n[project.markdown_extensions.toc]\npermalink = true\ntoc_depth = 3\n\n[project.markdown_extensions.pymdownx.details]\n\n[project.markdown_extensions.pymdownx.emoji]\nemoji_index = \"zensical.extensions.emoji.twemoji\"\nemoji_generator = \"zensical.extensions.emoji.to_svg\"\noptions = { custom_icons = [\"docs/overrides/.icons\"] }\n\n[project.markdown_extensions.pymdownx.highlight]\nanchor_linenums = true\nline_spans = \"__span\"\npygments_lang_class = true\nlinenums = false\nuse_pygments = true\n\n[project.markdown_extensions.pymdownx.inlinehilite]\n\n[project.markdown_extensions.pymdownx.keys]\n\n[project.markdown_extensions.pymdownx.mark]\n\n[project.markdown_extensions.pymdownx.smartsymbols]\n\n[project.markdown_extensions.pymdownx.snippets]\nbase_path = [\"docs\", \".\"]\ncheck_paths = true\n\n[project.markdown_extensions.pymdownx.superfences]\ncustom_fences = [\n  { name = \"mermaid\", class = \"mermaid\", format = \"pymdownx.superfences.fence_code_format\" },\n]\n\n[project.markdown_extensions.pymdownx.tabbed]\nalternate_style = true\ncombine_header_slug = true\n\n[project.markdown_extensions.pymdownx.tabbed.slugify]\ncase = \"lower\"\n\n[project.markdown_extensions.pymdownx.tasklist]\ncustom_checkbox = true\n\n# --- Plugins ---\n\n[project.plugins.search]\n\n# --- Extra ---\n\n[project.extra]\n\n[project.extra.status]\nnew = \"Recently added\"\ndeprecated = \"Deprecated\"\n\n[project.extra.repo]\nstars = true\nforks = true\n\n[[project.extra.social]]\nicon = \"fontawesome/brands/github\"\nlink = \"https://github.com/kreuzberg-dev/html-to-markdown\"\nname = \"View on GitHub\"\n\n[[project.extra.social]]\nicon = \"fontawesome/brands/python\"\nlink = \"https://pypi.org/project/html-to-markdown/\"\nname = \"View on PyPI\"\n\n[[project.extra.social]]\nicon = \"fontawesome/brands/npm\"\nlink = \"https://www.npmjs.com/package/@kreuzberg/html-to-markdown\"\nname = \"View on npm\"\n\n[[project.extra.social]]\nicon = \"fontawesome/brands/rust\"\nlink = \"https://crates.io/crates/html-to-markdown-rs\"\nname = \"View on crates.io\"\n"
  }
]